mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

1import crypto from 'node:crypto' 2import { sql } from 'drizzle-orm' 3import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 4import { installation, sshKey } from '../../server/db/schema' 5import { clearDb, setDb, useDb } from '../../server/utils/db' 6import { clearEncryptionKeyCache, decrypt } from '../../server/utils/encryption' 7import { generateAndPublishKey } from '../../server/utils/tangled-pubkey' 8import { createTestDb } from '../utils/db' 9 10const ORIGINAL_ENC_KEY = process.env.NUXT_ENCRYPTION_KEY 11 12const createRecordMock = vi.fn<(input: { repo: string, collection: string, record: Record<string, unknown> }) => Promise<{ data: { uri: string, cid: string } }>>() 13 14vi.mock('@atproto/api', () => ({ 15 Agent: class { 16 com = { 17 atproto: { 18 repo: { 19 createRecord: createRecordMock, 20 }, 21 }, 22 } 23 }, 24})) 25 26describe('generateAndPublishKey', () => { 27 beforeEach(async () => { 28 process.env.NUXT_ENCRYPTION_KEY = crypto.randomBytes(32).toString('base64') 29 clearEncryptionKeyCache() 30 31 setDb(await createTestDb()) 32 const db = useDb() 33 await db.insert(installation).values({ 34 id: 1, 35 accountLogin: 'alice', 36 accountId: 100, 37 accountType: 'User', 38 }) 39 40 createRecordMock.mockReset() 41 createRecordMock.mockResolvedValue({ 42 data: { uri: 'at://did:plc:abc/sh.tangled.publicKey/3kh2y4xq2lk2v', cid: 'bafy' }, 43 }) 44 }) 45 46 afterEach(() => { 47 if (ORIGINAL_ENC_KEY === undefined) delete process.env.NUXT_ENCRYPTION_KEY 48 else process.env.NUXT_ENCRYPTION_KEY = ORIGINAL_ENC_KEY 49 clearEncryptionKeyCache() 50 clearDb() 51 }) 52 53 function fakeOauthSession(did: string) { 54 // The Agent mock above ignores its constructor argument, so we only need 55 // a `.did` field for the helper itself. 56 return { did } as never 57 } 58 59 it('generates a key, publishes to PDS, and stores the encrypted private half', async () => { 60 const result = await generateAndPublishKey({ 61 oauthSession: fakeOauthSession('did:plc:abc'), 62 installationId: 1, 63 }) 64 65 expect(result.created).toBe(true) 66 expect(createRecordMock).toHaveBeenCalledTimes(1) 67 const call = createRecordMock.mock.calls[0]![0] 68 expect(call.repo).toBe('did:plc:abc') 69 expect(call.collection).toBe('sh.tangled.publicKey') 70 expect(call.record.$type).toBe('sh.tangled.publicKey') 71 expect(call.record.key).toMatch(/^ssh-ed25519 /) 72 expect(call.record.name).toBe('synchub.to/1') 73 74 const db = useDb() 75 const rows = await db.select().from(sshKey) 76 .where(sql`${sshKey.installationId} = 1 AND ${sshKey.did} = 'did:plc:abc'`) 77 expect(rows).toHaveLength(1) 78 const row = rows[0]! 79 expect(row.publicKey).toMatch(/^ssh-ed25519 /) 80 expect(row.tangledKeyRkey).toBe('3kh2y4xq2lk2v') 81 82 const decrypted = decrypt(row.privateKeyCiphertext, row.privateKeyNonce) 83 expect(decrypted).toMatch(/^-----BEGIN PRIVATE KEY-----/) 84 expect(decrypted).toContain('-----END PRIVATE KEY-----') 85 }) 86 87 it('no-ops if a key already exists for (installation, did)', async () => { 88 await generateAndPublishKey({ 89 oauthSession: fakeOauthSession('did:plc:abc'), 90 installationId: 1, 91 }) 92 expect(createRecordMock).toHaveBeenCalledTimes(1) 93 94 const result = await generateAndPublishKey({ 95 oauthSession: fakeOauthSession('did:plc:abc'), 96 installationId: 1, 97 }) 98 expect(result.created).toBe(false) 99 expect(createRecordMock).toHaveBeenCalledTimes(1) // not called again 100 101 const db = useDb() 102 const rows = await db.select().from(sshKey) 103 expect(rows).toHaveLength(1) 104 }) 105 106 it('does not write a row if the PDS publish fails', async () => { 107 createRecordMock.mockRejectedValueOnce(new Error('pds is sad')) 108 109 await expect(generateAndPublishKey({ 110 oauthSession: fakeOauthSession('did:plc:abc'), 111 installationId: 1, 112 })).rejects.toThrow(/pds is sad/) 113 114 const db = useDb() 115 const rows = await db.select().from(sshKey) 116 expect(rows).toHaveLength(0) 117 }) 118})