test(260607-l6l): make BUG 1 join regression test couple to the handler
The original toSQL() regression test hand-built the joined query inside the test body and asserted the SQL contained a join — tautological: it never exercised the handler, so removing .innerJoin from events.ts left it green. Replace it with two tests that issue real PATCH/DELETE requests against the mocked select-chain (from → innerJoin → where) and assert the handler returns 202 (not 503) AND invokes the innerJoin spy. Verified RED: removing the edit+delete joins fails both tests; GREEN with the joins present.
This commit is contained in:
@@ -646,64 +646,60 @@ describe('CR-06: OIDC iss/sub → users.id resolution on write handlers', () =>
|
||||
// clause → the /inner join.*calendars/ assertion fails.
|
||||
// GREEN: After adding .innerJoin(calendars, ...) the SQL contains the join.
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('regression: edit/delete lookups join calendars', () => {
|
||||
it('PATCH /:uid/edit lookup SQL contains inner join to calendars', async () => {
|
||||
// Use the real drizzle + schema — vi.importActual bypasses the vi.mock for db/client.
|
||||
// drizzle does NOT need a live DB to produce SQL via toSQL().
|
||||
const { drizzle } = await vi.importActual<typeof import('drizzle-orm/mysql2')>('drizzle-orm/mysql2')
|
||||
const { eq } = await vi.importActual<typeof import('drizzle-orm')>('drizzle-orm')
|
||||
const { calendarEvents, calendars } = await vi.importActual<typeof import('../../src/db/schema.js')>('../../src/db/schema.js')
|
||||
// Regression: BUG 1 — the edit/delete lookups select calendars.url/userId but
|
||||
// must JOIN calendars to do so. Without the join Drizzle throws at query-build
|
||||
// → the handler's catch returns 503 (delete dialog never closes).
|
||||
//
|
||||
// This couples to the HANDLER, not a query the test rebuilds. The select-chain
|
||||
// mock exposes from() → innerJoin() → where(); a no-join handler that calls
|
||||
// .from().where() hits an undefined .where() → throws → 503, AND never invokes
|
||||
// the innerJoin spy. So both assertions go RED if the join is removed from
|
||||
// events.ts; they pass only because the handler actually joins.
|
||||
describe('regression: edit/delete lookups join calendars (BUG 1)', () => {
|
||||
const seededRow = {
|
||||
uid: 'uid-001@familysync',
|
||||
etag: '"etag-abc"',
|
||||
objectUrl: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/uid-001.ics',
|
||||
calendarId: 1,
|
||||
calendarUrl: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/',
|
||||
userId: 1,
|
||||
}
|
||||
let innerJoinSpy: ReturnType<typeof vi.fn>
|
||||
|
||||
// Construct a throwaway drizzle instance — client is never called by toSQL()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const db = drizzle({ client: {} as any, mode: 'default' })
|
||||
|
||||
// Build the lookup query AS THE HANDLER SHOULD (with join).
|
||||
// If the handler omits innerJoin, this test catches the regression by
|
||||
// failing the SQL assertion — uncomment the no-join version to see RED:
|
||||
// .from(calendarEvents)
|
||||
// .where(eq(calendarEvents.uid, 'test-uid')) ← no join → toSQL omits join clause → FAILS
|
||||
const lookupQuery = db
|
||||
.select({
|
||||
uid: calendarEvents.uid,
|
||||
etag: calendarEvents.etag,
|
||||
objectUrl: calendarEvents.objectUrl,
|
||||
calendarId: calendarEvents.calendarId,
|
||||
calendarUrl: calendars.url,
|
||||
userId: calendars.userId,
|
||||
})
|
||||
.from(calendarEvents)
|
||||
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
|
||||
.where(eq(calendarEvents.uid, 'test-uid'))
|
||||
|
||||
const { sql: generatedSql } = lookupQuery.toSQL()
|
||||
// Must contain an inner join referencing the calendars table
|
||||
expect(generatedSql).toMatch(/inner join[\s\S]*`calendars`/i)
|
||||
beforeEach(() => {
|
||||
mockDbRows = [seededRow]
|
||||
// Wire from() → innerJoin() → where(); the innerJoin spy proves the handler
|
||||
// routes through the join rather than calling .where() directly on from().
|
||||
const innerJoinWhere = vi.fn().mockImplementation(() => Promise.resolve(mockDbRows))
|
||||
innerJoinSpy = vi.fn().mockReturnValue({ where: innerJoinWhere })
|
||||
mockFromFn.mockReturnValue({ innerJoin: innerJoinSpy })
|
||||
mockSelectFn.mockReturnValue({ from: mockFromFn })
|
||||
})
|
||||
|
||||
it('DELETE /:uid lookup SQL contains inner join to calendars', async () => {
|
||||
const { drizzle } = await vi.importActual<typeof import('drizzle-orm/mysql2')>('drizzle-orm/mysql2')
|
||||
const { eq } = await vi.importActual<typeof import('drizzle-orm')>('drizzle-orm')
|
||||
const { calendarEvents, calendars } = await vi.importActual<typeof import('../../src/db/schema.js')>('../../src/db/schema.js')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const db = drizzle({ client: {} as any, mode: 'default' })
|
||||
|
||||
const lookupQuery = db
|
||||
.select({
|
||||
uid: calendarEvents.uid,
|
||||
etag: calendarEvents.etag,
|
||||
objectUrl: calendarEvents.objectUrl,
|
||||
calendarId: calendarEvents.calendarId,
|
||||
calendarUrl: calendars.url,
|
||||
userId: calendars.userId,
|
||||
it('PATCH /:uid/edit joins calendars (no 503) and invokes innerJoin', async () => {
|
||||
const { app } = await import('../../src/index.js')
|
||||
const res = await app.request('/api/events/uid-001%40familysync/edit', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: 'Updated title',
|
||||
allDay: false,
|
||||
start: '2026-06-15T10:00:00Z',
|
||||
end: '2026-06-15T11:00:00Z',
|
||||
calendarUrl: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/',
|
||||
}),
|
||||
})
|
||||
expect(res.status).toBe(202)
|
||||
expect(innerJoinSpy).toHaveBeenCalled()
|
||||
})
|
||||
.from(calendarEvents)
|
||||
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
|
||||
.where(eq(calendarEvents.uid, 'test-uid'))
|
||||
|
||||
const { sql: generatedSql } = lookupQuery.toSQL()
|
||||
expect(generatedSql).toMatch(/inner join[\s\S]*`calendars`/i)
|
||||
it('DELETE /:uid joins calendars (no 503) and invokes innerJoin', async () => {
|
||||
const { app } = await import('../../src/index.js')
|
||||
const res = await app.request('/api/events/uid-001%40familysync', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
expect(res.status).toBe(202)
|
||||
expect(innerJoinSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user