sveltekit-auth-example 5.8.3 → 5.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.prettierignore +1 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +1 -1
  4. package/eslint.config.mjs +19 -1
  5. package/package.json +2 -1
  6. package/src/app.d.ts +0 -2
  7. package/src/hooks.server.unit.test.ts +1 -2
  8. package/src/lib/Turnstile.svelte +1 -0
  9. package/src/lib/app-state.svelte.unit.test.ts +14 -4
  10. package/src/lib/fetch-interceptor.unit.test.ts +8 -1
  11. package/src/lib/focus.ts +1 -1
  12. package/src/lib/google.unit.test.ts +24 -16
  13. package/src/lib/server/brevo.unit.test.ts +4 -1
  14. package/src/lib/server/db.ts +2 -4
  15. package/src/lib/server/db.unit.test.ts +4 -1
  16. package/src/lib/server/email/password-reset.unit.test.ts +3 -1
  17. package/src/lib/server/email/verify-email.unit.test.ts +3 -1
  18. package/src/lib/server/turnstile.unit.test.ts +1 -4
  19. package/src/routes/api/v1/user/+server.ts +1 -1
  20. package/src/routes/api/v1/user/{+server.unit.test.ts → server.unit.test.ts} +7 -13
  21. package/src/routes/auth/forgot/{+server.unit.test.ts → server.unit.test.ts} +3 -1
  22. package/src/routes/auth/google/{+server.unit.test.ts → server.unit.test.ts} +48 -22
  23. package/src/routes/auth/login/+server.ts +1 -1
  24. package/src/routes/auth/login/{+server.unit.test.ts → server.unit.test.ts} +50 -15
  25. package/src/routes/auth/logout/{+server.unit.test.ts → server.unit.test.ts} +8 -5
  26. package/src/routes/auth/mfa/{+server.unit.test.ts → server.unit.test.ts} +48 -26
  27. package/src/routes/auth/register/+server.ts +1 -1
  28. package/src/routes/auth/register/{+server.unit.test.ts → server.unit.test.ts} +32 -15
  29. package/src/routes/auth/reset/+server.ts +2 -2
  30. package/src/routes/auth/reset/{+server.unit.test.ts → server.unit.test.ts} +12 -13
  31. package/src/routes/auth/verify/[token]/{+server.unit.test.ts → server.unit.test.ts} +20 -12
  32. package/src/routes/register/+page.svelte +1 -1
  33. package/src/service-worker.unit.test.ts +6 -4
  34. /package/src/routes/auth/[slug]/{+server.unit.test.ts → server.unit.test.ts} +0 -0
package/.prettierignore CHANGED
@@ -4,6 +4,7 @@
4
4
  .yarn
5
5
  node_modules
6
6
  coverage
7
+ build
7
8
  .env
8
9
  !.env.example
9
10
  yarn.lock
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  - Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
4
4
 
5
+ # 5.8.5
6
+
7
+ - Fix lint errors
8
+
9
+ # 5.8.4
10
+
11
+ - Fix README: replace outdated SendGrid references with Brevo; update env var names (`BREVO_KEY`, `EMAIL`) and source file paths
12
+
5
13
  # 5.8.3
6
14
 
7
15
  - Add unit tests for all auth route handlers: `/auth/[slug]`, `/auth/forgot`, `/auth/google`, `/auth/login`, `/auth/logout`, `/auth/mfa`, `/auth/register`, `/auth/reset`, `/auth/verify/[token]`
package/README.md CHANGED
@@ -118,4 +118,4 @@ The website supports two types of authentication:
118
118
 
119
119
  > There is some overhead to checking the user session in a database each time versus using a JWT; however, validating each request avoids problems discussed in [this article](https://redis.com/blog/json-web-tokens-jwt-are-dangerous-for-user-sessions/). For a high-volume website, I would use Redis or the equivalent.
120
120
 
121
- The forgot password / password reset functionality uses a JWT and [**SendGrid**](https://www.sendgrid.com) to send the email. You would need to have a **SendGrid** account and set two environmental variables. Email sending is in /src/routes/auth/forgot/+server.ts. This code could easily be replaced by nodemailer or something similar. Note: I have no affiliation with **SendGrid** (used their API in another project).
121
+ The forgot password / password reset functionality uses a JWT and [**Brevo**](https://brevo.com) to send the email. You would need to have a **Brevo** account and set the `BREVO_KEY` and `EMAIL` environment variables (see setup instructions above). Email sending is in `src/lib/server/brevo.ts` and the email templates are in `src/lib/server/email/`. This code could easily be replaced by nodemailer or something similar.
package/eslint.config.mjs CHANGED
@@ -27,7 +27,18 @@ export default defineConfig(
27
27
  rules: {
28
28
  // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
29
29
  // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
30
- 'no-undef': 'off'
30
+ 'no-undef': 'off',
31
+ // Standard SvelteKit patterns (goto(), hrefs) don't need resolve()
32
+ 'svelte/no-navigation-without-resolve': 'off',
33
+ // Allow underscore-prefixed names as conventional "discard" variables
34
+ '@typescript-eslint/no-unused-vars': [
35
+ 'error',
36
+ {
37
+ argsIgnorePattern: '^_',
38
+ varsIgnorePattern: '^_',
39
+ caughtErrorsIgnorePattern: '^_'
40
+ }
41
+ ]
31
42
  }
32
43
  },
33
44
  {
@@ -44,5 +55,12 @@ export default defineConfig(
44
55
  'svelte/no-at-html-tags': 'warn',
45
56
  'svelte/require-each-key': 'warn'
46
57
  }
58
+ },
59
+ {
60
+ // Test files routinely need `as any` to mock typed return values
61
+ files: ['**/*.unit.test.ts'],
62
+ rules: {
63
+ '@typescript-eslint/no-explicit-any': 'off'
64
+ }
47
65
  }
48
66
  )
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sveltekit-auth-example",
3
3
  "description": "SvelteKit Authentication Example",
4
- "version": "5.8.3",
4
+ "version": "5.8.5",
5
5
  "author": "Nate Stuyvesant",
6
6
  "license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
7
7
  "repository": {
@@ -44,6 +44,7 @@
44
44
  "pg": "^8.20.0"
45
45
  },
46
46
  "devDependencies": {
47
+ "@eslint/compat": "^1.0.3",
47
48
  "@eslint/js": "^10.0.1",
48
49
  "@playwright/test": "^1.58.2",
49
50
  "@sveltejs/adapter-node": "^5.5.4",
package/src/app.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
-
3
1
  // See https://kit.svelte.dev/docs/types#app
4
2
  // for information about these interfaces
5
3
  // and what to do when importing types
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import { error } from '@sveltejs/kit'
3
2
 
4
3
  vi.mock('$lib/server/db', () => ({ query: vi.fn() }))
5
4
 
@@ -28,7 +27,7 @@ function makeEvent({
28
27
  } as unknown as Parameters<typeof handle>[0]['event']
29
28
  }
30
29
 
31
- const resolve = vi.fn(async (event: unknown) => new Response('ok', { status: 200 }))
30
+ const resolve = vi.fn(async (_event: unknown) => new Response('ok', { status: 200 }))
32
31
 
33
32
  beforeEach(() => {
34
33
  vi.resetAllMocks()
@@ -7,6 +7,7 @@
7
7
  theme?: 'light' | 'dark' | 'auto'
8
8
  }
9
9
 
10
+ // eslint-disable-next-line no-useless-assignment
10
11
  let { token = $bindable(''), theme = 'auto' }: Props = $props()
11
12
 
12
13
  let container: HTMLDivElement | undefined = $state()
@@ -31,8 +31,7 @@ describe('appState', () => {
31
31
  email: 'admin@example.com',
32
32
  firstName: 'Jane',
33
33
  lastName: 'Doe',
34
- phone: '412-555-1212',
35
- optOut: false
34
+ phone: '412-555-1212'
36
35
  }
37
36
 
38
37
  appState.user = user
@@ -41,7 +40,14 @@ describe('appState', () => {
41
40
  })
42
41
 
43
42
  it('can be cleared back to undefined', () => {
44
- appState.user = { id: 1, role: 'student', email: 'a@b.com', firstName: 'A', lastName: 'B', phone: '', optOut: false }
43
+ appState.user = {
44
+ id: 1,
45
+ role: 'student',
46
+ email: 'a@b.com',
47
+ firstName: 'A',
48
+ lastName: 'B',
49
+ phone: ''
50
+ }
45
51
  appState.user = undefined
46
52
  expect(appState.user).toBeUndefined()
47
53
  })
@@ -51,7 +57,11 @@ describe('appState', () => {
51
57
  it('can be updated to show a notification', () => {
52
58
  appState.toast = { title: 'Success', body: 'Your changes were saved.', isOpen: true }
53
59
 
54
- expect(appState.toast).toEqual({ title: 'Success', body: 'Your changes were saved.', isOpen: true })
60
+ expect(appState.toast).toEqual({
61
+ title: 'Success',
62
+ body: 'Your changes were saved.',
63
+ isOpen: true
64
+ })
55
65
  })
56
66
 
57
67
  it('isOpen can be toggled independently', () => {
@@ -24,7 +24,14 @@ describe('setupFetchInterceptor', () => {
24
24
  // Save and restore window.fetch around each test
25
25
  originalFetch = window.fetch
26
26
  mockGoto.mockReset()
27
- appState.user = { id: 1, role: 'admin', email: 'a@b.com', firstName: 'A', lastName: 'B', phone: '', optOut: false }
27
+ appState.user = {
28
+ id: 1,
29
+ role: 'admin',
30
+ email: 'a@b.com',
31
+ firstName: 'A',
32
+ lastName: 'B',
33
+ phone: ''
34
+ }
28
35
  setupFetchInterceptor()
29
36
  })
30
37
 
package/src/lib/focus.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * @param form - The form element to search for invalid inputs.
8
8
  */
9
9
  export const focusOnFirstError = (form: HTMLFormElement) => {
10
- for (const field of form.elements) {
10
+ for (const field of Array.from(form.elements)) {
11
11
  if (field instanceof HTMLInputElement && !field.checkValidity()) {
12
12
  field.focus()
13
13
  break
@@ -2,7 +2,9 @@
2
2
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
3
3
 
4
4
  vi.mock('$env/static/public', () => ({ PUBLIC_GOOGLE_CLIENT_ID: 'test-client-id' }))
5
- vi.mock('$lib/app-state.svelte', () => ({ appState: { googleInitialized: false, user: undefined } }))
5
+ vi.mock('$lib/app-state.svelte', () => ({
6
+ appState: { googleInitialized: false, user: undefined }
7
+ }))
6
8
  vi.mock('$lib/auth-redirect', () => ({ redirectAfterLogin: vi.fn() }))
7
9
 
8
10
  import { renderGoogleButton, initializeGoogleAccounts } from './google'
@@ -47,11 +49,14 @@ describe('renderGoogleButton', () => {
47
49
  renderGoogleButton()
48
50
 
49
51
  expect(mock.renderButton).toHaveBeenCalledOnce()
50
- expect(mock.renderButton).toHaveBeenCalledWith(btn, expect.objectContaining({
51
- type: 'standard',
52
- theme: 'outline',
53
- size: 'large'
54
- }))
52
+ expect(mock.renderButton).toHaveBeenCalledWith(
53
+ btn,
54
+ expect.objectContaining({
55
+ type: 'standard',
56
+ theme: 'outline',
57
+ size: 'large'
58
+ })
59
+ )
55
60
  })
56
61
 
57
62
  it('falls back to 400 when the button has no width', () => {
@@ -118,9 +123,11 @@ describe('initializeGoogleAccounts', () => {
118
123
  initializeGoogleAccounts()
119
124
 
120
125
  expect(mock.initialize).toHaveBeenCalledOnce()
121
- expect(mock.initialize).toHaveBeenCalledWith(expect.objectContaining({
122
- client_id: 'test-client-id'
123
- }))
126
+ expect(mock.initialize).toHaveBeenCalledWith(
127
+ expect.objectContaining({
128
+ client_id: 'test-client-id'
129
+ })
130
+ )
124
131
  })
125
132
 
126
133
  it('sets appState.googleInitialized to true', () => {
@@ -153,19 +160,20 @@ describe('initializeGoogleAccounts', () => {
153
160
  const { callback } = mock.initialize.mock.calls[0][0]
154
161
  await callback({ credential: 'test-credential' })
155
162
 
156
- expect(fetch).toHaveBeenCalledWith('/auth/google', expect.objectContaining({
157
- method: 'POST',
158
- body: JSON.stringify({ token: 'test-credential' })
159
- }))
163
+ expect(fetch).toHaveBeenCalledWith(
164
+ '/auth/google',
165
+ expect.objectContaining({
166
+ method: 'POST',
167
+ body: JSON.stringify({ token: 'test-credential' })
168
+ })
169
+ )
160
170
  expect(appState.user).toEqual(user)
161
171
  expect(mockRedirectAfterLogin).toHaveBeenCalledWith(user)
162
172
  })
163
173
 
164
174
  it('callback does not update state when the response is not ok', async () => {
165
175
  const mock = installGoogleMock()
166
- vi.spyOn(globalThis, 'fetch').mockResolvedValue(
167
- new Response(null, { status: 401 })
168
- )
176
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 401 }))
169
177
 
170
178
  initializeGoogleAccounts()
171
179
 
@@ -32,6 +32,7 @@ describe('sendMessage', () => {
32
32
  it('throws if BREVO_KEY is missing', async () => {
33
33
  vi.doMock('$env/dynamic/private', () => ({ env: {} }))
34
34
  // Re-import to pick up the new mock
35
+ // @ts-expect-error Vite query string not recognized by tsc
35
36
  const { sendMessage: sm } = await import('./brevo?nocache-1')
36
37
  await expect(sm(validMessage)).rejects.toThrow('Brevo API key is missing')
37
38
  })
@@ -160,7 +161,9 @@ describe('sendMessage', () => {
160
161
  vi.useFakeTimers()
161
162
  vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('Network failure'))
162
163
 
163
- const rejection = expect(sendMessage(validMessage)).rejects.toThrow('Failed to send email after retries')
164
+ const rejection = expect(sendMessage(validMessage)).rejects.toThrow(
165
+ 'Failed to send email after retries'
166
+ )
164
167
  await vi.runAllTimersAsync()
165
168
  await rejection
166
169
  vi.useRealTimers()
@@ -16,8 +16,6 @@ type QueryFunction = <T extends QueryResultRow>(
16
16
  name?: string
17
17
  ) => Promise<QueryResult<T>>
18
18
 
19
- let queryFn: QueryFunction
20
-
21
19
  const pool = new pg.Pool({
22
20
  max: 10, // default
23
21
  idleTimeoutMillis: 10000,
@@ -39,7 +37,7 @@ pool.on('error', (err: Error) => {
39
37
  * @param name - Optional name for the query to use a prepared statement.
40
38
  * @returns A promise resolving to the typed QueryResult.
41
39
  */
42
- queryFn = <T extends QueryResultRow>(
40
+ const queryFn: QueryFunction = <T extends QueryResultRow>(
43
41
  sql: string,
44
42
  params?: (string | number | boolean | object | null)[],
45
43
  name?: string
@@ -71,7 +69,7 @@ queryFn = <T extends QueryResultRow>(
71
69
  * );
72
70
  * ```
73
71
  */
74
- export const query = <T extends QueryResultRow = any>(
72
+ export const query = <T extends QueryResultRow = QueryResultRow>(
75
73
  sql: string,
76
74
  params?: (string | number | boolean | object | null)[],
77
75
  name?: string
@@ -38,7 +38,10 @@ describe('query', () => {
38
38
  await query('SELECT $1::text AS val', ['hello'])
39
39
 
40
40
  expect(mockPoolQuery).toHaveBeenCalledOnce()
41
- expect(mockPoolQuery).toHaveBeenCalledWith({ text: 'SELECT $1::text AS val', values: ['hello'] })
41
+ expect(mockPoolQuery).toHaveBeenCalledWith({
42
+ text: 'SELECT $1::text AS val',
43
+ values: ['hello']
44
+ })
42
45
  })
43
46
 
44
47
  it('passes SQL without params to pool.query', async () => {
@@ -1,6 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
 
3
- vi.mock('$env/dynamic/private', () => ({ env: { EMAIL: 'no-reply@example.com', DOMAIN: 'https://example.com' } }))
3
+ vi.mock('$env/dynamic/private', () => ({
4
+ env: { EMAIL: 'no-reply@example.com', DOMAIN: 'https://example.com' }
5
+ }))
4
6
 
5
7
  vi.mock('$lib/server/brevo', () => ({ sendMessage: vi.fn() }))
6
8
 
@@ -1,6 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
 
3
- vi.mock('$env/dynamic/private', () => ({ env: { EMAIL: 'no-reply@example.com', DOMAIN: 'https://example.com' } }))
3
+ vi.mock('$env/dynamic/private', () => ({
4
+ env: { EMAIL: 'no-reply@example.com', DOMAIN: 'https://example.com' }
5
+ }))
4
6
 
5
7
  vi.mock('$lib/server/brevo', () => ({ sendMessage: vi.fn() }))
6
8
 
@@ -94,10 +94,7 @@ describe('verifyTurnstileToken', () => {
94
94
  const result = await verifyTurnstileToken('some-token')
95
95
 
96
96
  expect(result).toBe(false)
97
- expect(consoleSpy).toHaveBeenCalledWith(
98
- 'Turnstile verification error:',
99
- expect.any(Error)
100
- )
97
+ expect(consoleSpy).toHaveBeenCalledWith('Turnstile verification error:', expect.any(Error))
101
98
  })
102
99
 
103
100
  it('returns false when the response body is not valid JSON', async () => {
@@ -20,7 +20,7 @@ export const PUT: RequestHandler = async event => {
20
20
  try {
21
21
  const userUpdate = await event.request.json()
22
22
  await query(`CALL update_user($1, $2);`, [user.id, JSON.stringify(userUpdate)])
23
- } catch (err) {
23
+ } catch {
24
24
  error(503, 'Could not communicate with database.')
25
25
  }
26
26
 
@@ -9,10 +9,7 @@ const mockQuery = vi.mocked(query)
9
9
 
10
10
  const adminUser: UserProperties = { id: 42, role: 'admin', email: 'admin@example.com' }
11
11
 
12
- function makeEvent({
13
- noUser = false,
14
- body = {} as unknown
15
- } = {}) {
12
+ function makeEvent({ noUser = false, body = {} as unknown } = {}) {
16
13
  return {
17
14
  locals: { user: noUser ? undefined : adminUser },
18
15
  request: { json: vi.fn().mockResolvedValue(body) },
@@ -43,10 +40,10 @@ describe('PUT /api/v1/user', () => {
43
40
 
44
41
  await PUT(makeEvent({ body: update }))
45
42
 
46
- expect(mockQuery).toHaveBeenCalledWith(
47
- expect.stringContaining('update_user'),
48
- [adminUser.id, JSON.stringify(update)]
49
- )
43
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('update_user'), [
44
+ adminUser.id,
45
+ JSON.stringify(update)
46
+ ])
50
47
  })
51
48
 
52
49
  it('throws 401 when user is not authenticated', async () => {
@@ -78,10 +75,7 @@ describe('DELETE /api/v1/user', () => {
78
75
 
79
76
  await DELETE(makeEvent())
80
77
 
81
- expect(mockQuery).toHaveBeenCalledWith(
82
- expect.stringContaining('delete_user'),
83
- [adminUser.id]
84
- )
78
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('delete_user'), [adminUser.id])
85
79
  })
86
80
 
87
81
  it('deletes the session cookie on success', async () => {
@@ -107,7 +101,7 @@ describe('DELETE /api/v1/user', () => {
107
101
  mockQuery.mockRejectedValue(new Error('db down'))
108
102
  const event = makeEvent()
109
103
 
110
- await DELETE(event).catch(() => {})
104
+ await Promise.resolve(DELETE(event)).catch(() => {})
111
105
 
112
106
  expect(event.cookies.delete).not.toHaveBeenCalled()
113
107
  })
@@ -17,7 +17,9 @@ const mockVerifyTurnstileToken = vi.mocked(verifyTurnstileToken)
17
17
  function makeEvent(body: Record<string, unknown> = {}) {
18
18
  return {
19
19
  request: {
20
- json: vi.fn().mockResolvedValue({ turnstileToken: 'tok', email: 'user@example.com', ...body }),
20
+ json: vi
21
+ .fn()
22
+ .mockResolvedValue({ turnstileToken: 'tok', email: 'user@example.com', ...body }),
21
23
  headers: { get: vi.fn().mockReturnValue(null) }
22
24
  },
23
25
  getClientAddress: vi.fn().mockReturnValue('127.0.0.1')
@@ -13,7 +13,13 @@ import { OAuth2Client } from 'google-auth-library'
13
13
  const mockQuery = vi.mocked(query)
14
14
  const MockOAuth2Client = vi.mocked(OAuth2Client)
15
15
 
16
- const mockUser: User = { id: 1, email: 'jane@example.com', firstName: 'Jane', lastName: 'Doe', role: 'user' }
16
+ const mockUser: UserProperties = {
17
+ id: 1,
18
+ email: 'jane@example.com',
19
+ firstName: 'Jane',
20
+ lastName: 'Doe',
21
+ role: 'student'
22
+ }
17
23
  const mockUserSession: UserSession = { id: 'session-abc', user: mockUser }
18
24
 
19
25
  function makeVerifyIdToken(payload: Record<string, unknown> | null) {
@@ -23,7 +29,7 @@ function makeVerifyIdToken(payload: Record<string, unknown> | null) {
23
29
  function setupOAuth2Mock(verifyIdToken: ReturnType<typeof vi.fn>) {
24
30
  MockOAuth2Client.mockImplementation(function () {
25
31
  return { verifyIdToken }
26
- } as unknown as new (clientId: string) => OAuth2Client)
32
+ } as any)
27
33
  }
28
34
 
29
35
  function makeEvent(body: Record<string, unknown> = { token: 'google-jwt' }) {
@@ -40,7 +46,9 @@ beforeEach(() => {
40
46
 
41
47
  describe('POST /auth/google', () => {
42
48
  it('returns 200 with user data on successful sign-in', async () => {
43
- setupOAuth2Mock(makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' }))
49
+ setupOAuth2Mock(
50
+ makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' })
51
+ )
44
52
  mockQuery.mockResolvedValue({ rows: [{ user_session: mockUserSession }] } as any)
45
53
 
46
54
  const res = await POST(makeEvent())
@@ -52,22 +60,30 @@ describe('POST /auth/google', () => {
52
60
  })
53
61
 
54
62
  it('sets an httpOnly session cookie on success', async () => {
55
- setupOAuth2Mock(makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' }))
63
+ setupOAuth2Mock(
64
+ makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' })
65
+ )
56
66
  mockQuery.mockResolvedValue({ rows: [{ user_session: mockUserSession }] } as any)
57
67
  const event = makeEvent()
58
68
 
59
69
  await POST(event)
60
70
 
61
- expect(event.cookies.set).toHaveBeenCalledWith('session', 'session-abc', expect.objectContaining({
62
- httpOnly: true,
63
- sameSite: 'lax',
64
- secure: true,
65
- path: '/'
66
- }))
71
+ expect(event.cookies.set).toHaveBeenCalledWith(
72
+ 'session',
73
+ 'session-abc',
74
+ expect.objectContaining({
75
+ httpOnly: true,
76
+ sameSite: 'lax',
77
+ secure: true,
78
+ path: '/'
79
+ })
80
+ )
67
81
  })
68
82
 
69
83
  it('sets event.locals.user on success', async () => {
70
- setupOAuth2Mock(makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' }))
84
+ setupOAuth2Mock(
85
+ makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' })
86
+ )
71
87
  mockQuery.mockResolvedValue({ rows: [{ user_session: mockUserSession }] } as any)
72
88
  const event = makeEvent()
73
89
 
@@ -77,7 +93,11 @@ describe('POST /auth/google', () => {
77
93
  })
78
94
 
79
95
  it('uses PUBLIC_GOOGLE_CLIENT_ID when verifying the token', async () => {
80
- const verifyIdToken = makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' })
96
+ const verifyIdToken = makeVerifyIdToken({
97
+ given_name: 'Jane',
98
+ family_name: 'Doe',
99
+ email: 'jane@example.com'
100
+ })
81
101
  setupOAuth2Mock(verifyIdToken)
82
102
  mockQuery.mockResolvedValue({ rows: [{ user_session: mockUserSession }] } as any)
83
103
 
@@ -88,15 +108,16 @@ describe('POST /auth/google', () => {
88
108
  })
89
109
 
90
110
  it('passes the Google user data to the DB upsert', async () => {
91
- setupOAuth2Mock(makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' }))
111
+ setupOAuth2Mock(
112
+ makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' })
113
+ )
92
114
  mockQuery.mockResolvedValue({ rows: [{ user_session: mockUserSession }] } as any)
93
115
 
94
116
  await POST(makeEvent())
95
117
 
96
- expect(mockQuery).toHaveBeenCalledWith(
97
- expect.stringContaining('start_gmail_user_session'),
98
- [JSON.stringify({ firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com' })]
99
- )
118
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('start_gmail_user_session'), [
119
+ JSON.stringify({ firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com' })
120
+ ])
100
121
  })
101
122
 
102
123
  it('falls back to placeholder names when given_name/family_name are missing', async () => {
@@ -105,10 +126,13 @@ describe('POST /auth/google', () => {
105
126
 
106
127
  await POST(makeEvent())
107
128
 
108
- expect(mockQuery).toHaveBeenCalledWith(
109
- expect.any(String),
110
- [JSON.stringify({ firstName: 'UnknownFirstName', lastName: 'UnknownLastName', email: 'noname@example.com' })]
111
- )
129
+ expect(mockQuery).toHaveBeenCalledWith(expect.any(String), [
130
+ JSON.stringify({
131
+ firstName: 'UnknownFirstName',
132
+ lastName: 'UnknownLastName',
133
+ email: 'noname@example.com'
134
+ })
135
+ ])
112
136
  })
113
137
 
114
138
  it('throws 401 when the Google token is invalid', async () => {
@@ -118,7 +142,9 @@ describe('POST /auth/google', () => {
118
142
  })
119
143
 
120
144
  it('throws 401 when the DB upsert fails', async () => {
121
- setupOAuth2Mock(makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' }))
145
+ setupOAuth2Mock(
146
+ makeVerifyIdToken({ given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com' })
147
+ )
122
148
  mockQuery.mockRejectedValue(new Error('db error'))
123
149
 
124
150
  await expect(POST(makeEvent())).rejects.toMatchObject({ status: 401 })
@@ -69,7 +69,7 @@ export const POST: RequestHandler = async event => {
69
69
  error(503, 'Could not communicate with database.')
70
70
  }
71
71
 
72
- const { authenticationResult }: { authenticationResult: AuthenticationResult } = result.rows[0]
72
+ const { authenticationResult } = result.rows[0] as { authenticationResult: AuthenticationResult }
73
73
 
74
74
  if (!authenticationResult.user) {
75
75
  // Track failed attempt for lockout
@@ -15,7 +15,13 @@ const mockQuery = vi.mocked(query)
15
15
  const mockSendMfaCodeEmail = vi.mocked(sendMfaCodeEmail)
16
16
  const mockVerifyTurnstileToken = vi.mocked(verifyTurnstileToken)
17
17
 
18
- const mockUser: User = { id: 7, email: 'user@example.com', firstName: 'Jane', lastName: 'Doe', role: 'user' }
18
+ const mockUser: UserProperties = {
19
+ id: 7,
20
+ email: 'user@example.com',
21
+ firstName: 'Jane',
22
+ lastName: 'Doe',
23
+ role: 'student'
24
+ }
19
25
 
20
26
  const successResult: AuthenticationResult = {
21
27
  user: mockUser,
@@ -32,7 +38,10 @@ const failResult: AuthenticationResult = {
32
38
  }
33
39
 
34
40
  function makeEvent({
35
- body = { email: 'user@example.com', password: 'Password1!', turnstileToken: 'tok' } as Record<string, unknown>,
41
+ body = { email: 'user@example.com', password: 'Password1!', turnstileToken: 'tok' } as Record<
42
+ string,
43
+ unknown
44
+ >,
36
45
  mfaTrustedCookie = undefined as string | undefined
37
46
  } = {}) {
38
47
  return {
@@ -61,8 +70,8 @@ describe('POST /auth/login — MFA flow', () => {
61
70
  beforeEach(() => {
62
71
  mockQuery
63
72
  .mockResolvedValueOnce({ rows: [{ authenticationResult: successResult }] } as any) // authenticate
64
- .mockResolvedValueOnce({ rows: [] } as any) // delete_session
65
- .mockResolvedValueOnce({ rows: [{ code: '123456' }] } as any) // create_mfa_code
73
+ .mockResolvedValueOnce({ rows: [] } as any) // delete_session
74
+ .mockResolvedValueOnce({ rows: [{ code: '123456' }] } as any) // create_mfa_code
66
75
  })
67
76
 
68
77
  it('returns { mfaRequired: true } when no trusted cookie is present', async () => {
@@ -114,12 +123,16 @@ describe('POST /auth/login — MFA trusted device', () => {
114
123
 
115
124
  await POST(event)
116
125
 
117
- expect(event.cookies.set).toHaveBeenCalledWith('session', 'sess-123', expect.objectContaining({
118
- httpOnly: true,
119
- sameSite: 'lax',
120
- secure: true,
121
- path: '/'
122
- }))
126
+ expect(event.cookies.set).toHaveBeenCalledWith(
127
+ 'session',
128
+ 'sess-123',
129
+ expect.objectContaining({
130
+ httpOnly: true,
131
+ sameSite: 'lax',
132
+ secure: true,
133
+ path: '/'
134
+ })
135
+ )
123
136
  })
124
137
 
125
138
  it('sets event.locals.user when the trusted cookie is valid', async () => {
@@ -145,7 +158,9 @@ describe('POST /auth/login — MFA trusted device', () => {
145
158
  })
146
159
 
147
160
  it('falls through to MFA and deletes cookie when trusted token is expired', async () => {
148
- const expiredToken = jwt.sign({ userId: mockUser.id, purpose: 'mfa-trusted' }, 'test-secret', { expiresIn: -1 })
161
+ const expiredToken = jwt.sign({ userId: mockUser.id, purpose: 'mfa-trusted' }, 'test-secret', {
162
+ expiresIn: -1
163
+ })
149
164
  mockQuery
150
165
  .mockResolvedValueOnce({ rows: [{ authenticationResult: successResult }] } as any)
151
166
  .mockResolvedValueOnce({ rows: [] } as any)
@@ -195,18 +210,34 @@ describe('POST /auth/login — brute-force lockout', () => {
195
210
 
196
211
  // 5 failures to trigger lockout
197
212
  for (let i = 0; i < 5; i++) {
198
- await POST(makeEvent({ body: { email: 'lockout@example.com', password: 'wrong', turnstileToken: 'tok' } })).catch(() => {})
213
+ await Promise.resolve(
214
+ POST(
215
+ makeEvent({
216
+ body: { email: 'lockout@example.com', password: 'wrong', turnstileToken: 'tok' }
217
+ })
218
+ )
219
+ ).catch(() => {})
199
220
  }
200
221
 
201
222
  await expect(
202
- POST(makeEvent({ body: { email: 'lockout@example.com', password: 'wrong', turnstileToken: 'tok' } }))
223
+ POST(
224
+ makeEvent({
225
+ body: { email: 'lockout@example.com', password: 'wrong', turnstileToken: 'tok' }
226
+ })
227
+ )
203
228
  ).rejects.toMatchObject({ status: 429 })
204
229
  })
205
230
 
206
231
  it('clears the lockout tracker on successful login', async () => {
207
232
  // Register a failed attempt first
208
233
  mockQuery.mockResolvedValueOnce({ rows: [{ authenticationResult: failResult }] } as any)
209
- await POST(makeEvent({ body: { email: 'clear@example.com', password: 'wrong', turnstileToken: 'tok' } })).catch(() => {})
234
+ await Promise.resolve(
235
+ POST(
236
+ makeEvent({
237
+ body: { email: 'clear@example.com', password: 'wrong', turnstileToken: 'tok' }
238
+ })
239
+ )
240
+ ).catch(() => {})
210
241
 
211
242
  // Then succeed — mfa flow needs 3 query responses
212
243
  mockQuery
@@ -215,7 +246,11 @@ describe('POST /auth/login — brute-force lockout', () => {
215
246
  .mockResolvedValueOnce({ rows: [{ code: '000000' }] } as any)
216
247
 
217
248
  // Should not throw 429
218
- const res = await POST(makeEvent({ body: { email: 'clear@example.com', password: 'Password1!', turnstileToken: 'tok' } }))
249
+ const res = await POST(
250
+ makeEvent({
251
+ body: { email: 'clear@example.com', password: 'Password1!', turnstileToken: 'tok' }
252
+ })
253
+ )
219
254
  expect((await res.json()).mfaRequired).toBe(true)
220
255
  })
221
256
  })
@@ -7,7 +7,13 @@ import { query } from '$lib/server/db'
7
7
 
8
8
  const mockQuery = vi.mocked(query)
9
9
 
10
- const mockUser: User = { id: 3, email: 'user@example.com', firstName: 'Jane', lastName: 'Doe', role: 'user' }
10
+ const mockUser: UserProperties = {
11
+ id: 3,
12
+ email: 'user@example.com',
13
+ firstName: 'Jane',
14
+ lastName: 'Doe',
15
+ role: 'student'
16
+ }
11
17
 
12
18
  function makeEvent({ noUser = false } = {}) {
13
19
  return {
@@ -44,10 +50,7 @@ describe('POST /auth/logout', () => {
44
50
 
45
51
  await POST(makeEvent())
46
52
 
47
- expect(mockQuery).toHaveBeenCalledWith(
48
- expect.stringContaining('delete_session'),
49
- [mockUser.id]
50
- )
53
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('delete_session'), [mockUser.id])
51
54
  })
52
55
 
53
56
  it('skips the DB call when no user is authenticated', async () => {
@@ -12,9 +12,21 @@ import jwt from 'jsonwebtoken'
12
12
  const mockQuery = vi.mocked(query)
13
13
  const mockVerifyTurnstileToken = vi.mocked(verifyTurnstileToken)
14
14
 
15
- const mockUser: User = { id: 5, email: 'user@example.com', firstName: 'Jane', lastName: 'Doe', role: 'user' }
15
+ const mockUser: UserProperties = {
16
+ id: 5,
17
+ email: 'user@example.com',
18
+ firstName: 'Jane',
19
+ lastName: 'Doe',
20
+ role: 'student'
21
+ }
16
22
 
17
- function makeEvent(body: Record<string, unknown> = { email: 'user@example.com', code: '123456', turnstileToken: 'tok' }) {
23
+ function makeEvent(
24
+ body: Record<string, unknown> = {
25
+ email: 'user@example.com',
26
+ code: '123456',
27
+ turnstileToken: 'tok'
28
+ }
29
+ ) {
18
30
  return {
19
31
  request: {
20
32
  json: vi.fn().mockResolvedValue(body),
@@ -28,9 +40,9 @@ function makeEvent(body: Record<string, unknown> = { email: 'user@example.com',
28
40
  /** Set up the three DB calls needed for a successful MFA verification. */
29
41
  function setupSuccessQueries() {
30
42
  mockQuery
31
- .mockResolvedValueOnce({ rows: [{ userId: mockUser.id }] } as any) // verify_mfa_code
32
- .mockResolvedValueOnce({ rows: [{ sessionId: 'sess-xyz' }] } as any) // create_session
33
- .mockResolvedValueOnce({ rows: [{ get_session: mockUser }] } as any) // get_session
43
+ .mockResolvedValueOnce({ rows: [{ userId: mockUser.id }] } as any) // verify_mfa_code
44
+ .mockResolvedValueOnce({ rows: [{ sessionId: 'sess-xyz' }] } as any) // create_session
45
+ .mockResolvedValueOnce({ rows: [{ get_session: mockUser }] } as any) // get_session
34
46
  }
35
47
 
36
48
  beforeEach(() => {
@@ -56,12 +68,16 @@ describe('POST /auth/mfa', () => {
56
68
 
57
69
  await POST(event)
58
70
 
59
- expect(event.cookies.set).toHaveBeenCalledWith('session', 'sess-xyz', expect.objectContaining({
60
- httpOnly: true,
61
- sameSite: 'lax',
62
- secure: true,
63
- path: '/'
64
- }))
71
+ expect(event.cookies.set).toHaveBeenCalledWith(
72
+ 'session',
73
+ 'sess-xyz',
74
+ expect.objectContaining({
75
+ httpOnly: true,
76
+ sameSite: 'lax',
77
+ secure: true,
78
+ path: '/'
79
+ })
80
+ )
65
81
  })
66
82
 
67
83
  it('sets an mfa_trusted cookie on success', async () => {
@@ -70,13 +86,17 @@ describe('POST /auth/mfa', () => {
70
86
 
71
87
  await POST(event)
72
88
 
73
- expect(event.cookies.set).toHaveBeenCalledWith('mfa_trusted', expect.any(String), expect.objectContaining({
74
- httpOnly: true,
75
- sameSite: 'lax',
76
- secure: true,
77
- path: '/',
78
- maxAge: 30 * 24 * 60 * 60
79
- }))
89
+ expect(event.cookies.set).toHaveBeenCalledWith(
90
+ 'mfa_trusted',
91
+ expect.any(String),
92
+ expect.objectContaining({
93
+ httpOnly: true,
94
+ sameSite: 'lax',
95
+ secure: true,
96
+ path: '/',
97
+ maxAge: 30 * 24 * 60 * 60
98
+ })
99
+ )
80
100
  })
81
101
 
82
102
  it('mfa_trusted cookie is a valid JWT with correct payload', async () => {
@@ -85,7 +105,9 @@ describe('POST /auth/mfa', () => {
85
105
 
86
106
  await POST(event)
87
107
 
88
- const [, trustedToken] = vi.mocked(event.cookies.set).mock.calls.find(([name]) => name === 'mfa_trusted')!
108
+ const [, trustedToken] = vi
109
+ .mocked(event.cookies.set)
110
+ .mock.calls.find(([name]) => name === 'mfa_trusted')!
89
111
  const payload = jwt.verify(trustedToken as string, 'test-secret') as Record<string, unknown>
90
112
  expect(payload.userId).toBe(mockUser.id)
91
113
  expect(payload.purpose).toBe('mfa-trusted')
@@ -96,10 +118,10 @@ describe('POST /auth/mfa', () => {
96
118
 
97
119
  await POST(makeEvent({ email: 'User@Example.COM', code: '123456', turnstileToken: 'tok' }))
98
120
 
99
- expect(mockQuery).toHaveBeenCalledWith(
100
- expect.stringContaining('verify_mfa_code'),
101
- ['user@example.com', '123456']
102
- )
121
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('verify_mfa_code'), [
122
+ 'user@example.com',
123
+ '123456'
124
+ ])
103
125
  })
104
126
 
105
127
  it('throws 401 when the MFA code is invalid or expired', async () => {
@@ -109,9 +131,9 @@ describe('POST /auth/mfa', () => {
109
131
  })
110
132
 
111
133
  it('throws 400 when email is missing', async () => {
112
- await expect(
113
- POST(makeEvent({ code: '123456', turnstileToken: 'tok' }))
114
- ).rejects.toMatchObject({ status: 400 })
134
+ await expect(POST(makeEvent({ code: '123456', turnstileToken: 'tok' }))).rejects.toMatchObject({
135
+ status: 400
136
+ })
115
137
  })
116
138
 
117
139
  it('throws 400 when code is missing', async () => {
@@ -59,7 +59,7 @@ export const POST: RequestHandler = async event => {
59
59
  error(503, 'Could not communicate with database.')
60
60
  }
61
61
 
62
- const { authenticationResult }: { authenticationResult: AuthenticationResult } = result.rows[0]
62
+ const { authenticationResult } = result.rows[0] as { authenticationResult: AuthenticationResult }
63
63
 
64
64
  if (!authenticationResult.user)
65
65
  error(authenticationResult.statusCode, authenticationResult.status)
@@ -15,7 +15,13 @@ const mockQuery = vi.mocked(query)
15
15
  const mockSendVerificationEmail = vi.mocked(sendVerificationEmail)
16
16
  const mockVerifyTurnstileToken = vi.mocked(verifyTurnstileToken)
17
17
 
18
- const mockUser: User = { id: 9, email: 'new@example.com', firstName: 'Jane', lastName: 'Doe', role: 'user' }
18
+ const mockUser: UserProperties = {
19
+ id: 9,
20
+ email: 'new@example.com',
21
+ firstName: 'Jane',
22
+ lastName: 'Doe',
23
+ role: 'student'
24
+ }
19
25
 
20
26
  const successResult: AuthenticationResult = {
21
27
  user: mockUser,
@@ -46,7 +52,7 @@ function makeEvent(body: Record<string, unknown> = validBody) {
46
52
  function setupSuccessQueries() {
47
53
  mockQuery
48
54
  .mockResolvedValueOnce({ rows: [{ authenticationResult: successResult }] } as any) // register
49
- .mockResolvedValueOnce({ rows: [] } as any) // delete_session
55
+ .mockResolvedValueOnce({ rows: [] } as any) // delete_session
50
56
  }
51
57
 
52
58
  beforeEach(() => {
@@ -92,10 +98,9 @@ describe('POST /auth/register', () => {
92
98
 
93
99
  await POST(makeEvent())
94
100
 
95
- expect(mockQuery).toHaveBeenCalledWith(
96
- expect.stringContaining('delete_session'),
97
- [successResult.sessionId]
98
- )
101
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('delete_session'), [
102
+ successResult.sessionId
103
+ ])
99
104
  })
100
105
 
101
106
  it('calls the register SQL function with the full body', async () => {
@@ -103,10 +108,9 @@ describe('POST /auth/register', () => {
103
108
 
104
109
  await POST(makeEvent())
105
110
 
106
- expect(mockQuery).toHaveBeenCalledWith(
107
- expect.stringContaining('register'),
108
- [JSON.stringify(validBody)]
109
- )
111
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('register'), [
112
+ JSON.stringify(validBody)
113
+ ])
110
114
  })
111
115
 
112
116
  it('still sends the verification email when delete_session fails', async () => {
@@ -143,19 +147,27 @@ describe('POST /auth/register — validation', () => {
143
147
  })
144
148
 
145
149
  it('throws 400 when password has no uppercase letter', async () => {
146
- await expect(POST(makeEvent({ ...validBody, password: 'password1!' }))).rejects.toMatchObject({ status: 400 })
150
+ await expect(POST(makeEvent({ ...validBody, password: 'password1!' }))).rejects.toMatchObject({
151
+ status: 400
152
+ })
147
153
  })
148
154
 
149
155
  it('throws 400 when password has no number', async () => {
150
- await expect(POST(makeEvent({ ...validBody, password: 'Password!' }))).rejects.toMatchObject({ status: 400 })
156
+ await expect(POST(makeEvent({ ...validBody, password: 'Password!' }))).rejects.toMatchObject({
157
+ status: 400
158
+ })
151
159
  })
152
160
 
153
161
  it('throws 400 when password has no special character', async () => {
154
- await expect(POST(makeEvent({ ...validBody, password: 'Password1' }))).rejects.toMatchObject({ status: 400 })
162
+ await expect(POST(makeEvent({ ...validBody, password: 'Password1' }))).rejects.toMatchObject({
163
+ status: 400
164
+ })
155
165
  })
156
166
 
157
167
  it('throws 400 when password is too short', async () => {
158
- await expect(POST(makeEvent({ ...validBody, password: 'P1!' }))).rejects.toMatchObject({ status: 400 })
168
+ await expect(POST(makeEvent({ ...validBody, password: 'P1!' }))).rejects.toMatchObject({
169
+ status: 400
170
+ })
159
171
  })
160
172
 
161
173
  it('throws 400 when Turnstile verification fails', async () => {
@@ -175,7 +187,12 @@ describe('POST /auth/register — validation', () => {
175
187
  })
176
188
 
177
189
  it('throws the DB status code on registration failure (e.g. duplicate email)', async () => {
178
- const dupResult: AuthenticationResult = { user: null, sessionId: '', status: 'Email already registered.', statusCode: 409 }
190
+ const dupResult: AuthenticationResult = {
191
+ user: null,
192
+ sessionId: '',
193
+ status: 'Email already registered.',
194
+ statusCode: 409
195
+ }
179
196
  mockQuery.mockResolvedValueOnce({ rows: [{ authenticationResult: dupResult }] } as any)
180
197
  await expect(POST(makeEvent())).rejects.toMatchObject({ status: 409 })
181
198
  })
@@ -50,8 +50,8 @@ export const PUT: RequestHandler = async event => {
50
50
  return json({
51
51
  message: 'Password successfully reset.'
52
52
  })
53
- } catch (error) {
54
- // Technically, I should check error.message to make sure it's not a DB issue
53
+ } catch {
54
+ // Technically, we should check if it's a DB issue vs an invalid/expired token
55
55
  return json(
56
56
  {
57
57
  message: 'Password reset token expired.'
@@ -13,7 +13,9 @@ const mockQuery = vi.mocked(query)
13
13
  const mockVerifyTurnstileToken = vi.mocked(verifyTurnstileToken)
14
14
 
15
15
  function makeResetToken(overrides: Record<string, unknown> = {}) {
16
- return jwt.sign({ subject: 42, purpose: 'reset-password', ...overrides }, 'test-secret', { expiresIn: '30m' })
16
+ return jwt.sign({ subject: 42, purpose: 'reset-password', ...overrides }, 'test-secret', {
17
+ expiresIn: '30m'
18
+ })
17
19
  }
18
20
 
19
21
  function makeEvent(body: Record<string, unknown> = {}) {
@@ -58,10 +60,10 @@ describe('PUT /auth/reset', () => {
58
60
 
59
61
  await PUT(makeEvent({ password: 'NewPassword1!' }))
60
62
 
61
- expect(mockQuery).toHaveBeenCalledWith(
62
- expect.stringContaining('reset_password'),
63
- [42, 'NewPassword1!']
64
- )
63
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('reset_password'), [
64
+ 42,
65
+ 'NewPassword1!'
66
+ ])
65
67
  })
66
68
 
67
69
  it('invalidates existing sessions after a successful reset', async () => {
@@ -69,16 +71,11 @@ describe('PUT /auth/reset', () => {
69
71
 
70
72
  await PUT(makeEvent())
71
73
 
72
- expect(mockQuery).toHaveBeenCalledWith(
73
- expect.stringContaining('delete_session'),
74
- [42]
75
- )
74
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('delete_session'), [42])
76
75
  })
77
76
 
78
77
  it('still returns 200 when session invalidation fails', async () => {
79
- mockQuery
80
- .mockResolvedValueOnce({ rows: [] } as any)
81
- .mockRejectedValueOnce(new Error('db down'))
78
+ mockQuery.mockResolvedValueOnce({ rows: [] } as any).mockRejectedValueOnce(new Error('db down'))
82
79
 
83
80
  const res = await PUT(makeEvent())
84
81
 
@@ -86,7 +83,9 @@ describe('PUT /auth/reset', () => {
86
83
  })
87
84
 
88
85
  it('returns 403 when the token is expired', async () => {
89
- const expiredToken = jwt.sign({ subject: 42, purpose: 'reset-password' }, 'test-secret', { expiresIn: -1 })
86
+ const expiredToken = jwt.sign({ subject: 42, purpose: 'reset-password' }, 'test-secret', {
87
+ expiresIn: -1
88
+ })
90
89
 
91
90
  const res = await PUT(makeEvent({ token: expiredToken }))
92
91
 
@@ -10,7 +10,9 @@ import jwt from 'jsonwebtoken'
10
10
  const mockQuery = vi.mocked(query)
11
11
 
12
12
  function makeVerifyEmailToken(overrides: Record<string, unknown> = {}) {
13
- return jwt.sign({ subject: '7', purpose: 'verify-email', ...overrides }, 'test-secret', { expiresIn: '24h' })
13
+ return jwt.sign({ subject: '7', purpose: 'verify-email', ...overrides }, 'test-secret', {
14
+ expiresIn: '24h'
15
+ })
14
16
  }
15
17
 
16
18
  function makeEvent(token: string) {
@@ -38,20 +40,24 @@ describe('GET /auth/verify/[token]', () => {
38
40
  mockQuery.mockResolvedValue({ rows: [{ verify_email_and_create_session: 'sess-abc' }] } as any)
39
41
  const event = makeEvent(makeVerifyEmailToken())
40
42
 
41
- await GET(event).catch(() => {})
42
-
43
- expect(event.cookies.set).toHaveBeenCalledWith('session', 'sess-abc', expect.objectContaining({
44
- httpOnly: true,
45
- sameSite: 'lax',
46
- secure: true,
47
- path: '/'
48
- }))
43
+ await Promise.resolve(GET(event)).catch(() => {})
44
+
45
+ expect(event.cookies.set).toHaveBeenCalledWith(
46
+ 'session',
47
+ 'sess-abc',
48
+ expect.objectContaining({
49
+ httpOnly: true,
50
+ sameSite: 'lax',
51
+ secure: true,
52
+ path: '/'
53
+ })
54
+ )
49
55
  })
50
56
 
51
57
  it('calls verify_email_and_create_session with the userId from the token', async () => {
52
58
  mockQuery.mockResolvedValue({ rows: [{ verify_email_and_create_session: 'sess-abc' }] } as any)
53
59
 
54
- await GET(makeEvent(makeVerifyEmailToken())).catch(() => {})
60
+ await Promise.resolve(GET(makeEvent(makeVerifyEmailToken()))).catch(() => {})
55
61
 
56
62
  expect(mockQuery).toHaveBeenCalledWith(
57
63
  expect.stringContaining('verify_email_and_create_session'),
@@ -60,7 +66,9 @@ describe('GET /auth/verify/[token]', () => {
60
66
  })
61
67
 
62
68
  it('redirects to /login?error=invalid-token when the token is expired', async () => {
63
- const expiredToken = jwt.sign({ subject: '7', purpose: 'verify-email' }, 'test-secret', { expiresIn: -1 })
69
+ const expiredToken = jwt.sign({ subject: '7', purpose: 'verify-email' }, 'test-secret', {
70
+ expiresIn: -1
71
+ })
64
72
 
65
73
  await expect(GET(makeEvent(expiredToken))).rejects.toMatchObject({
66
74
  location: '/login?error=invalid-token',
@@ -117,7 +125,7 @@ describe('GET /auth/verify/[token]', () => {
117
125
  mockQuery.mockRejectedValue(new Error('db down'))
118
126
  const event = makeEvent(makeVerifyEmailToken())
119
127
 
120
- await GET(event).catch(() => {})
128
+ await Promise.resolve(GET(event)).catch(() => {})
121
129
 
122
130
  expect(event.cookies.set).not.toHaveBeenCalled()
123
131
  })
@@ -101,7 +101,7 @@
101
101
  } catch (err) {
102
102
  console.error('Register error', err)
103
103
  if (err instanceof Error) {
104
- throw new Error(err.message)
104
+ throw new Error(err.message, { cause: err })
105
105
  }
106
106
  } finally {
107
107
  loading = false
@@ -22,15 +22,17 @@ const mockCaches = {
22
22
  delete: vi.fn()
23
23
  }
24
24
 
25
- const swHandlers: Record<string, Function> = {}
25
+ const swHandlers: Record<string, (event: any) => void> = {}
26
26
 
27
27
  beforeAll(async () => {
28
28
  vi.stubGlobal('caches', mockCaches)
29
29
 
30
30
  // Spy on addEventListener to capture the handlers the SW registers
31
- vi.spyOn(window, 'addEventListener').mockImplementation((type: string, handler: any) => {
32
- swHandlers[type] = handler
33
- })
31
+ vi.spyOn(window, 'addEventListener').mockImplementation(
32
+ (type: string, handler: EventListener) => {
33
+ swHandlers[type] = handler
34
+ }
35
+ )
34
36
 
35
37
  // Reset module registry so the SW file re-evaluates and re-registers its listeners
36
38
  vi.resetModules()