馃 distributed transcription service
thistle.dunkirk.sh
1import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2import db from "../db/schema";
3import {
4 consumePasswordResetToken,
5 createEmailVerificationToken,
6 createPasswordResetToken,
7 createUser,
8 isEmailVerified,
9 verifyEmailToken,
10 verifyPasswordResetToken,
11} from "./auth";
12
13describe("Email Verification", () => {
14 let userId: number;
15 const testEmail = `test-verify-${Date.now()}@example.com`;
16
17 beforeEach(async () => {
18 // Create test user
19 const user = await createUser(testEmail, "a".repeat(64), "Test User");
20 userId = user.id;
21 });
22
23 afterEach(() => {
24 // Cleanup
25 db.run("DELETE FROM users WHERE email = ?", [testEmail]);
26 db.run("DELETE FROM email_verification_tokens WHERE user_id = ?", [userId]);
27 });
28
29 test("creates verification token", () => {
30 const result = createEmailVerificationToken(userId);
31 expect(result).toBeDefined();
32 expect(typeof result).toBe("object");
33 expect(typeof result.code).toBe("string");
34 expect(typeof result.token).toBe("string");
35 expect(typeof result.sentAt).toBe("number");
36 expect(result.code.length).toBe(6);
37 });
38
39 test("verifies valid token", () => {
40 const { token } = createEmailVerificationToken(userId);
41 const result = verifyEmailToken(token);
42
43 expect(result).not.toBeNull();
44 expect(result?.userId).toBe(userId);
45 expect(result?.email).toBe(testEmail);
46 expect(isEmailVerified(userId)).toBe(true);
47 });
48
49 test("rejects invalid token", () => {
50 const result = verifyEmailToken("invalid-token-12345");
51 expect(result).toBeNull();
52 expect(isEmailVerified(userId)).toBe(false);
53 });
54
55 test("token is one-time use", () => {
56 const { token } = createEmailVerificationToken(userId);
57
58 // First use succeeds
59 const firstResult = verifyEmailToken(token);
60 expect(firstResult).not.toBeNull();
61
62 // Second use fails
63 const secondResult = verifyEmailToken(token);
64 expect(secondResult).toBeNull();
65 });
66
67 test("rejects expired token", () => {
68 const { token } = createEmailVerificationToken(userId);
69
70 // Manually expire the token
71 db.run(
72 "UPDATE email_verification_tokens SET expires_at = ? WHERE token = ?",
73 [Math.floor(Date.now() / 1000) - 100, token],
74 );
75
76 const result = verifyEmailToken(token);
77 expect(result).toBeNull();
78 });
79
80 test("replaces existing token when creating new one", () => {
81 const { token: token1 } = createEmailVerificationToken(userId);
82 const { token: token2 } = createEmailVerificationToken(userId);
83
84 // First token should be invalidated
85 expect(verifyEmailToken(token1)).toBeNull();
86
87 // Second token should work
88 expect(verifyEmailToken(token2)).not.toBeNull();
89 });
90});
91
92describe("Password Reset", () => {
93 let userId: number;
94 const testEmail = `test-reset-${Date.now()}@example.com`;
95
96 beforeEach(async () => {
97 const user = await createUser(testEmail, "a".repeat(64), "Test User");
98 userId = user.id;
99 });
100
101 afterEach(() => {
102 db.run("DELETE FROM users WHERE email = ?", [testEmail]);
103 db.run("DELETE FROM password_reset_tokens WHERE user_id = ?", [userId]);
104 });
105
106 test("creates reset token", () => {
107 const token = createPasswordResetToken(userId);
108 expect(token).toBeDefined();
109 expect(typeof token).toBe("string");
110 expect(token.length).toBeGreaterThan(0);
111 });
112
113 test("verifies valid reset token", () => {
114 const token = createPasswordResetToken(userId);
115 const verifiedUserId = verifyPasswordResetToken(token);
116
117 expect(verifiedUserId).toBe(userId);
118 });
119
120 test("rejects invalid reset token", () => {
121 const verifiedUserId = verifyPasswordResetToken("invalid-token-12345");
122 expect(verifiedUserId).toBeNull();
123 });
124
125 test("consumes reset token", () => {
126 const token = createPasswordResetToken(userId);
127
128 // Token works before consumption
129 expect(verifyPasswordResetToken(token)).toBe(userId);
130
131 // Consume token
132 consumePasswordResetToken(token);
133
134 // Token no longer works
135 expect(verifyPasswordResetToken(token)).toBeNull();
136 });
137
138 test("rejects expired reset token", () => {
139 const token = createPasswordResetToken(userId);
140
141 // Manually expire the token
142 db.run("UPDATE password_reset_tokens SET expires_at = ? WHERE token = ?", [
143 Math.floor(Date.now() / 1000) - 100,
144 token,
145 ]);
146
147 const verifiedUserId = verifyPasswordResetToken(token);
148 expect(verifiedUserId).toBeNull();
149 });
150
151 test("replaces existing reset token when creating new one", () => {
152 const token1 = createPasswordResetToken(userId);
153 const token2 = createPasswordResetToken(userId);
154
155 // First token should be invalidated
156 expect(verifyPasswordResetToken(token1)).toBeNull();
157
158 // Second token should work
159 expect(verifyPasswordResetToken(token2)).toBe(userId);
160 });
161});