+116
src/lib/db.test.ts
+116
src/lib/db.test.ts
···
1
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
2
+
import {
3
+
claimCustomDomain,
4
+
getCustomDomainInfo,
5
+
deleteCustomDomain,
6
+
updateCustomDomainVerification,
7
+
db
8
+
} from './db'
9
+
10
+
describe('custom domain claiming', () => {
11
+
const testDid1 = 'did:plc:testuser1'
12
+
const testDid2 = 'did:plc:testuser2'
13
+
const testDomain = 'example-test-domain.com'
14
+
const hash1 = 'testhash12345678'
15
+
const hash2 = 'testhash87654321'
16
+
const hash3 = 'testhash11111111'
17
+
18
+
beforeAll(async () => {
19
+
// Clean up any existing test data
20
+
try {
21
+
await db`DELETE FROM custom_domains WHERE domain = ${testDomain}`
22
+
} catch (err) {
23
+
// Ignore errors if table doesn't exist or other issues
24
+
}
25
+
})
26
+
27
+
afterAll(async () => {
28
+
// Clean up test data
29
+
try {
30
+
await db`DELETE FROM custom_domains WHERE domain = ${testDomain}`
31
+
} catch (err) {
32
+
// Ignore cleanup errors
33
+
}
34
+
})
35
+
36
+
test('should allow first user to claim a domain', async () => {
37
+
const result = await claimCustomDomain(testDid1, testDomain, hash1)
38
+
expect(result.success).toBe(true)
39
+
expect(result.hash).toBe(hash1)
40
+
41
+
const domainInfo = await getCustomDomainInfo(testDomain)
42
+
expect(domainInfo).toBeTruthy()
43
+
expect(domainInfo!.domain).toBe(testDomain)
44
+
expect(domainInfo!.did).toBe(testDid1)
45
+
expect(domainInfo!.verified).toBe(false)
46
+
expect(domainInfo!.id).toBe(hash1)
47
+
})
48
+
49
+
test('should allow second user to claim an unverified domain', async () => {
50
+
const result = await claimCustomDomain(testDid2, testDomain, hash2)
51
+
expect(result.success).toBe(true)
52
+
expect(result.hash).toBe(hash2)
53
+
54
+
const domainInfo = await getCustomDomainInfo(testDomain)
55
+
expect(domainInfo).toBeTruthy()
56
+
expect(domainInfo!.domain).toBe(testDomain)
57
+
expect(domainInfo!.did).toBe(testDid2) // Should have changed
58
+
expect(domainInfo!.verified).toBe(false)
59
+
expect(domainInfo!.id).toBe(hash2) // Should have changed
60
+
})
61
+
62
+
test('should prevent claiming a verified domain', async () => {
63
+
// First verify the domain for testDid2
64
+
await updateCustomDomainVerification(hash2, true)
65
+
66
+
// Now try to claim it with testDid1 - should fail
67
+
try {
68
+
await claimCustomDomain(testDid1, testDomain, hash3)
69
+
expect.fail('Should have thrown an error when trying to claim a verified domain')
70
+
} catch (err) {
71
+
expect(err.message).toBe('conflict')
72
+
}
73
+
74
+
// Verify the domain is still owned by testDid2 and verified
75
+
const domainInfo = await getCustomDomainInfo(testDomain)
76
+
expect(domainInfo).toBeTruthy()
77
+
expect(domainInfo!.did).toBe(testDid2)
78
+
expect(domainInfo!.verified).toBe(true)
79
+
expect(domainInfo!.id).toBe(hash2)
80
+
})
81
+
82
+
test('should allow claiming after unverification', async () => {
83
+
// Unverify the domain
84
+
await updateCustomDomainVerification(hash2, false)
85
+
86
+
// Now should be claimable again
87
+
const result = await claimCustomDomain(testDid1, testDomain, hash3)
88
+
expect(result.success).toBe(true)
89
+
expect(result.hash).toBe(hash3)
90
+
91
+
const domainInfo = await getCustomDomainInfo(testDomain)
92
+
expect(domainInfo).toBeTruthy()
93
+
expect(domainInfo!.did).toBe(testDid1) // Should have changed back
94
+
expect(domainInfo!.verified).toBe(false)
95
+
expect(domainInfo!.id).toBe(hash3)
96
+
})
97
+
98
+
test('should handle concurrent claims gracefully', async () => {
99
+
// Both users try to claim at the same time - one should win
100
+
const promise1 = claimCustomDomain(testDid1, testDomain, hash1)
101
+
const promise2 = claimCustomDomain(testDid2, testDomain, hash2)
102
+
103
+
const [result1, result2] = await Promise.allSettled([promise1, promise2])
104
+
105
+
// At least one should succeed
106
+
const successCount = [result1, result2].filter(r => r.status === 'fulfilled').length
107
+
expect(successCount).toBeGreaterThan(0)
108
+
expect(successCount).toBeLessThanOrEqual(2)
109
+
110
+
// Final state should be consistent
111
+
const domainInfo = await getCustomDomainInfo(testDomain)
112
+
expect(domainInfo).toBeTruthy()
113
+
expect(domainInfo!.verified).toBe(false)
114
+
expect([hash1, hash2]).toContain(domainInfo!.id)
115
+
})
116
+
})
+16
-1
src/lib/db.ts
+16
-1
src/lib/db.ts
···
595
595
export const claimCustomDomain = async (did: string, domain: string, hash: string, rkey: string | null = null) => {
596
596
const domainLower = domain.toLowerCase();
597
597
try {
598
-
await db`
598
+
// Use UPSERT with ON CONFLICT to handle existing pending domains
599
+
const result = await db`
599
600
INSERT INTO custom_domains (id, domain, did, rkey, verified, created_at)
600
601
VALUES (${hash}, ${domainLower}, ${did}, ${rkey}, false, EXTRACT(EPOCH FROM NOW()))
602
+
ON CONFLICT (domain) DO UPDATE SET
603
+
id = EXCLUDED.id,
604
+
did = EXCLUDED.did,
605
+
rkey = EXCLUDED.rkey,
606
+
verified = EXCLUDED.verified,
607
+
created_at = EXCLUDED.created_at
608
+
WHERE custom_domains.verified = false
609
+
RETURNING *
601
610
`;
611
+
612
+
if (result.length === 0) {
613
+
// No rows were updated, meaning the domain exists and is verified
614
+
throw new Error('conflict');
615
+
}
616
+
602
617
return { success: true, hash };
603
618
} catch (err) {
604
619
console.error('Failed to claim custom domain', err);
+3
-3
src/routes/domain.ts
+3
-3
src/routes/domain.ts
···
241
241
}
242
242
}
243
243
244
-
// Check if already exists
244
+
// Check if already exists and is verified
245
245
const existing = await getCustomDomainInfo(domainLower);
246
-
if (existing) {
247
-
throw new Error('Domain already claimed');
246
+
if (existing && existing.verified) {
247
+
throw new Error('Domain already verified and claimed');
248
248
}
249
249
250
250
// Create hash for ID