Skip to content

Commit 685397e

Browse files
committed
test: add tests for auth, rate-limit, and config endpoint
Improve test coverage for three low-coverage files: - auth.ts: 40% → 100% (JWT sign/verify, getCurrentUser, slugify) - rate-limit.ts: 59% → 96% (sliding window, cleanup, retryAfter) - config/+server.ts: 71% → 100% (shell, macOS prefs, error branches) Overall line coverage: 80.69% → 90.40% (+9.71%)
1 parent f44fe59 commit 685397e

3 files changed

Lines changed: 1072 additions & 0 deletions

File tree

src/lib/server/auth.test.ts

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/**
2+
* Tests for auth.ts - JWT signing/verification, getCurrentUser, slugify, generateId
3+
*/
4+
5+
import { describe, it, expect, vi, beforeEach } from 'vitest';
6+
import { signToken, verifyToken, getCookie, getCurrentUser, slugify, generateId } from './auth';
7+
import { createMockDB } from '$lib/test/db-mock';
8+
import { mockUser, mockApiToken, createMockCookies, createMockRequest } from '$lib/test/fixtures';
9+
10+
const TEST_SECRET = 'test-jwt-secret-key-32-chars-long';
11+
12+
describe('signToken / verifyToken', () => {
13+
it('should sign and verify a valid token', async () => {
14+
const payload = { userId: 'u1', username: 'alice', exp: Date.now() + 60_000 };
15+
const token = await signToken(payload, TEST_SECRET);
16+
17+
expect(token).toContain('.');
18+
const parts = token.split('.');
19+
expect(parts).toHaveLength(2);
20+
21+
const result = await verifyToken(token, TEST_SECRET);
22+
expect(result).toEqual(payload);
23+
});
24+
25+
it('should reject token signed with wrong secret', async () => {
26+
const payload = { userId: 'u1', username: 'alice', exp: Date.now() + 60_000 };
27+
const token = await signToken(payload, TEST_SECRET);
28+
29+
const result = await verifyToken(token, 'wrong-secret-key-32-chars-long!!');
30+
expect(result).toBeNull();
31+
});
32+
33+
it('should reject expired token', async () => {
34+
const payload = { userId: 'u1', username: 'alice', exp: Date.now() - 1000 };
35+
const token = await signToken(payload, TEST_SECRET);
36+
37+
const result = await verifyToken(token, TEST_SECRET);
38+
expect(result).toBeNull();
39+
});
40+
41+
it('should reject token without exp', async () => {
42+
const payload = { userId: 'u1', username: 'alice', exp: 0 };
43+
const token = await signToken(payload, TEST_SECRET);
44+
45+
const result = await verifyToken(token, TEST_SECRET);
46+
expect(result).toBeNull();
47+
});
48+
49+
it('should reject malformed token - no dot', async () => {
50+
const result = await verifyToken('nodottoken', TEST_SECRET);
51+
expect(result).toBeNull();
52+
});
53+
54+
it('should reject malformed token - empty parts', async () => {
55+
const result = await verifyToken('.', TEST_SECRET);
56+
expect(result).toBeNull();
57+
});
58+
59+
it('should reject token with tampered data', async () => {
60+
const payload = { userId: 'u1', username: 'alice', exp: Date.now() + 60_000 };
61+
const token = await signToken(payload, TEST_SECRET);
62+
const [, sig] = token.split('.');
63+
64+
// Tamper with data portion
65+
const tampered = { userId: 'u1', username: 'evil', exp: Date.now() + 60_000 };
66+
const tamperedData = btoa(JSON.stringify(tampered));
67+
68+
const result = await verifyToken(`${tamperedData}.${sig}`, TEST_SECRET);
69+
expect(result).toBeNull();
70+
});
71+
72+
it('should reject token with invalid base64', async () => {
73+
const result = await verifyToken('not!valid!base64.also!not!valid', TEST_SECRET);
74+
expect(result).toBeNull();
75+
});
76+
});
77+
78+
describe('getCookie', () => {
79+
it('should return cookie value when present', () => {
80+
const cookies = createMockCookies({ session: 'abc123' });
81+
expect(getCookie(cookies, 'session')).toBe('abc123');
82+
});
83+
84+
it('should return undefined when cookie missing', () => {
85+
const cookies = createMockCookies({});
86+
expect(getCookie(cookies, 'session')).toBeUndefined();
87+
});
88+
});
89+
90+
describe('getCurrentUser', () => {
91+
it('should authenticate via Bearer obt_ API token', async () => {
92+
const db = createMockDB({
93+
users: [mockUser],
94+
api_tokens: [mockApiToken]
95+
});
96+
97+
const request = createMockRequest({
98+
headers: { Authorization: `Bearer ${mockApiToken.token}` }
99+
});
100+
const cookies = createMockCookies({});
101+
102+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
103+
expect(user).toBeDefined();
104+
expect((user as any).username).toBe('testuser');
105+
});
106+
107+
it('should update last_used_at on valid API token auth', async () => {
108+
const db = createMockDB({
109+
users: [mockUser],
110+
api_tokens: [mockApiToken]
111+
});
112+
113+
const request = createMockRequest({
114+
headers: { Authorization: `Bearer ${mockApiToken.token}` }
115+
});
116+
const cookies = createMockCookies({});
117+
118+
await getCurrentUser(request, cookies, db as any, TEST_SECRET);
119+
// If it got here without error, the UPDATE ran successfully
120+
expect(true).toBe(true);
121+
});
122+
123+
it('should return null for invalid API token', async () => {
124+
const db = createMockDB({
125+
users: [mockUser],
126+
api_tokens: [mockApiToken]
127+
});
128+
129+
const request = createMockRequest({
130+
headers: { Authorization: 'Bearer obt_nonexistent_token_value_here' }
131+
});
132+
const cookies = createMockCookies({});
133+
134+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
135+
expect(user).toBeNull();
136+
});
137+
138+
it('should return null for expired API token', async () => {
139+
const expiredToken = {
140+
...mockApiToken,
141+
id: 'tok_expired',
142+
token: 'obt_expired1234567890abcdefghijklmnopqr',
143+
expires_at: '2020-01-01T00:00:00Z'
144+
};
145+
const db = createMockDB({
146+
users: [mockUser],
147+
api_tokens: [expiredToken]
148+
});
149+
150+
const request = createMockRequest({
151+
headers: { Authorization: `Bearer ${expiredToken.token}` }
152+
});
153+
const cookies = createMockCookies({});
154+
155+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
156+
expect(user).toBeNull();
157+
});
158+
159+
it('should return null for API token with non-existent user', async () => {
160+
const orphanToken = {
161+
...mockApiToken,
162+
user_id: 'nonexistent_user_id'
163+
};
164+
const db = createMockDB({
165+
users: [mockUser],
166+
api_tokens: [orphanToken]
167+
});
168+
169+
const request = createMockRequest({
170+
headers: { Authorization: `Bearer ${orphanToken.token}` }
171+
});
172+
const cookies = createMockCookies({});
173+
174+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
175+
expect(user).toBeNull();
176+
});
177+
178+
it('should authenticate via session cookie with valid JWT', async () => {
179+
const payload = { userId: mockUser.id, username: mockUser.username, exp: Date.now() + 60_000 };
180+
const jwt = await signToken(payload, TEST_SECRET);
181+
182+
const db = createMockDB({ users: [mockUser] });
183+
const request = createMockRequest({});
184+
const cookies = createMockCookies({ session: jwt });
185+
186+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
187+
expect(user).toBeDefined();
188+
expect((user as any).username).toBe('testuser');
189+
});
190+
191+
it('should return null when no session cookie', async () => {
192+
const db = createMockDB({ users: [mockUser] });
193+
const request = createMockRequest({});
194+
const cookies = createMockCookies({});
195+
196+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
197+
expect(user).toBeNull();
198+
});
199+
200+
it('should return null for invalid session JWT', async () => {
201+
const db = createMockDB({ users: [mockUser] });
202+
const request = createMockRequest({});
203+
const cookies = createMockCookies({ session: 'invalid.jwt.token' });
204+
205+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
206+
expect(user).toBeNull();
207+
});
208+
209+
it('should return null for expired session JWT', async () => {
210+
const payload = { userId: mockUser.id, username: mockUser.username, exp: Date.now() - 1000 };
211+
const jwt = await signToken(payload, TEST_SECRET);
212+
213+
const db = createMockDB({ users: [mockUser] });
214+
const request = createMockRequest({});
215+
const cookies = createMockCookies({ session: jwt });
216+
217+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
218+
expect(user).toBeNull();
219+
});
220+
221+
it('should prefer API token over session cookie', async () => {
222+
const payload = { userId: mockUser.id, username: mockUser.username, exp: Date.now() + 60_000 };
223+
const jwt = await signToken(payload, TEST_SECRET);
224+
225+
const db = createMockDB({
226+
users: [mockUser],
227+
api_tokens: [mockApiToken]
228+
});
229+
230+
const request = createMockRequest({
231+
headers: { Authorization: `Bearer ${mockApiToken.token}` }
232+
});
233+
const cookies = createMockCookies({ session: jwt });
234+
235+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
236+
expect(user).toBeDefined();
237+
expect((user as any).username).toBe('testuser');
238+
});
239+
240+
it('should skip non-obt Bearer tokens and fall through to cookie', async () => {
241+
const db = createMockDB({ users: [mockUser] });
242+
const request = createMockRequest({
243+
headers: { Authorization: 'Bearer some-regular-jwt-token' }
244+
});
245+
const cookies = createMockCookies({});
246+
247+
const user = await getCurrentUser(request, cookies, db as any, TEST_SECRET);
248+
expect(user).toBeNull();
249+
});
250+
});
251+
252+
describe('slugify', () => {
253+
it('should lowercase and replace non-alphanumeric chars', () => {
254+
expect(slugify('Hello World')).toBe('hello-world');
255+
});
256+
257+
it('should collapse multiple hyphens', () => {
258+
expect(slugify('a---b')).toBe('a-b');
259+
});
260+
261+
it('should trim leading/trailing hyphens', () => {
262+
expect(slugify('--hello--')).toBe('hello');
263+
});
264+
265+
it('should handle special characters', () => {
266+
expect(slugify('My Config! @2024')).toBe('my-config-2024');
267+
});
268+
269+
it('should truncate to 50 characters', () => {
270+
const long = 'a'.repeat(60);
271+
expect(slugify(long).length).toBe(50);
272+
});
273+
274+
it('should handle empty string', () => {
275+
expect(slugify('')).toBe('');
276+
});
277+
});
278+
279+
describe('generateId', () => {
280+
it('should return a valid UUID', () => {
281+
const id = generateId();
282+
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
283+
});
284+
285+
it('should generate unique IDs', () => {
286+
const ids = new Set(Array.from({ length: 10 }, () => generateId()));
287+
expect(ids.size).toBe(10);
288+
});
289+
});

0 commit comments

Comments
 (0)