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, repoMapping } from '../../server/db/schema' 5import { clearDb, setDb, useDb } from '../../server/utils/db' 6import { clearEncryptionKeyCache } from '../../server/utils/encryption' 7import { enrollRepo } from '../../server/utils/tangled-repo' 8import { createTestDb } from '../utils/db' 9 10const ORIGINAL_ENC_KEY = process.env.NUXT_ENCRYPTION_KEY 11 12interface GithubRepoLike { 13 id: number 14 full_name: string 15 private: boolean 16 fork: boolean 17 default_branch: string 18} 19 20const githubGet = vi.fn<(input: { repository_id: number }) => Promise<{ data: GithubRepoLike }>>() 21const getServiceAuthMock = vi.fn<(input: { aud: string, lxm: string, exp: number }) => Promise<{ data: { token: string } }>>() 22const putRecordMock = vi.fn<(input: { repo: string, collection: string, rkey: string, record: Record<string, unknown> }) => Promise<unknown>>() 23 24vi.mock('@atproto/api', () => ({ 25 Agent: class { 26 com = { 27 atproto: { 28 server: { getServiceAuth: getServiceAuthMock }, 29 repo: { putRecord: putRecordMock }, 30 }, 31 } 32 }, 33})) 34 35vi.mock('../../server/utils/github-app', () => ({ 36 installationOctokit: async () => ({ 37 request: githubGet, 38 }), 39 clearGitHubAppCache: () => {}, 40})) 41 42const fakeFetch = vi.fn<(url: string, init: RequestInit) => Promise<Response>>() 43const ORIGINAL_FETCH = globalThis.fetch 44 45describe('enrollRepo', () => { 46 beforeEach(async () => { 47 process.env.NUXT_ENCRYPTION_KEY = crypto.randomBytes(32).toString('base64') 48 clearEncryptionKeyCache() 49 50 setDb(await createTestDb()) 51 await useDb().insert(installation).values({ 52 id: 1, accountLogin: 'alice', accountId: 100, accountType: 'User', 53 }) 54 55 githubGet.mockReset() 56 getServiceAuthMock.mockReset() 57 putRecordMock.mockReset() 58 fakeFetch.mockReset() 59 globalThis.fetch = fakeFetch as unknown as typeof fetch 60 61 getServiceAuthMock.mockResolvedValue({ data: { token: 'service-auth-jwt' } }) 62 putRecordMock.mockResolvedValue({ data: { uri: 'at://did:plc:abc/sh.tangled.repo/whatever', cid: 'bafy' } }) 63 }) 64 65 afterEach(() => { 66 globalThis.fetch = ORIGINAL_FETCH 67 if (ORIGINAL_ENC_KEY === undefined) delete process.env.NUXT_ENCRYPTION_KEY 68 else process.env.NUXT_ENCRYPTION_KEY = ORIGINAL_ENC_KEY 69 clearEncryptionKeyCache() 70 clearDb() 71 }) 72 73 function fakeOauthSession(did: string) { 74 return { did } as never 75 } 76 77 function ghRepo(over: Partial<GithubRepoLike> = {}): GithubRepoLike { 78 return { 79 id: 9001, 80 full_name: 'alice/my-project', 81 private: false, 82 fork: false, 83 default_branch: 'main', 84 ...over, 85 } 86 } 87 88 it('enrolls a public, non-fork repo end to end', async () => { 89 githubGet.mockResolvedValue({ data: ghRepo() }) 90 fakeFetch.mockResolvedValue(new Response( 91 JSON.stringify({ repoDid: 'did:plc:repo-xyz' }), 92 { status: 200 }, 93 )) 94 95 const result = await enrollRepo({ 96 oauthSession: fakeOauthSession('did:plc:abc'), 97 installationId: 1, 98 githubRepoId: 9001, 99 }) 100 expect(result.status).toBe('enrolled') 101 102 // Service auth requested with the right shape. 103 expect(getServiceAuthMock).toHaveBeenCalledTimes(1) 104 const sa = getServiceAuthMock.mock.calls[0]?.[0] 105 expect(sa?.aud).toBe('did:web:knot1.tangled.sh') 106 expect(sa?.lxm).toBe('sh.tangled.repo.create') 107 108 // Knot procedure invoked with source URL and rkey. 109 expect(fakeFetch).toHaveBeenCalledTimes(1) 110 const fetchCall = fakeFetch.mock.calls[0] 111 const url = fetchCall?.[0] 112 const init = fetchCall?.[1] 113 expect(url).toBe('https://knot1.tangled.sh/xrpc/sh.tangled.repo.create') 114 expect((init!.headers as Record<string, string>).authorization).toBe('Bearer service-auth-jwt') 115 const body = JSON.parse(init!.body as string) as Record<string, unknown> 116 expect(body.name).toBe('my-project') 117 expect(body.source).toBe('https://github.com/alice/my-project') 118 expect(body.defaultBranch).toBe('main') 119 expect(typeof body.rkey).toBe('string') 120 121 // PDS record written with the same rkey. 122 expect(putRecordMock).toHaveBeenCalledTimes(1) 123 const put = putRecordMock.mock.calls[0]?.[0] 124 expect(put?.rkey).toBe(body.rkey) 125 expect(put?.record.repoDid).toBe('did:plc:repo-xyz') 126 expect(put?.record.knot).toBe('knot1.tangled.sh') 127 128 // Mapping persisted. 129 const rows = await useDb().select().from(repoMapping) 130 .where(sql`${repoMapping.installationId} = 1`) 131 expect(rows).toHaveLength(1) 132 expect(rows[0]!.tangledRepoDid).toBe('did:plc:repo-xyz') 133 expect(rows[0]!.knot).toBe('knot1.tangled.sh') 134 expect(rows[0]!.status).toBe('active') 135 }) 136 137 it('skips private repos', async () => { 138 githubGet.mockResolvedValue({ data: ghRepo({ private: true }) }) 139 140 const result = await enrollRepo({ 141 oauthSession: fakeOauthSession('did:plc:abc'), 142 installationId: 1, 143 githubRepoId: 9001, 144 }) 145 expect(result).toEqual({ status: 'skipped', reason: 'private' }) 146 expect(fakeFetch).not.toHaveBeenCalled() 147 expect(putRecordMock).not.toHaveBeenCalled() 148 expect(await useDb().select().from(repoMapping)).toHaveLength(0) 149 }) 150 151 it('skips forks', async () => { 152 githubGet.mockResolvedValue({ data: ghRepo({ fork: true }) }) 153 154 const result = await enrollRepo({ 155 oauthSession: fakeOauthSession('did:plc:abc'), 156 installationId: 1, 157 githubRepoId: 9001, 158 }) 159 expect(result).toEqual({ status: 'skipped', reason: 'fork' }) 160 expect(fakeFetch).not.toHaveBeenCalled() 161 }) 162 163 it('no-ops if a mapping already exists', async () => { 164 await useDb().insert(repoMapping).values({ 165 installationId: 1, 166 githubRepoId: 9001, 167 githubFullName: 'alice/my-project', 168 status: 'active', 169 }) 170 171 const result = await enrollRepo({ 172 oauthSession: fakeOauthSession('did:plc:abc'), 173 installationId: 1, 174 githubRepoId: 9001, 175 }) 176 expect(result).toEqual({ status: 'already' }) 177 expect(githubGet).not.toHaveBeenCalled() 178 }) 179 180 it('throws and writes nothing if the knot rejects the procedure', async () => { 181 githubGet.mockResolvedValue({ data: ghRepo() }) 182 fakeFetch.mockResolvedValue(new Response('nope', { status: 500 })) 183 184 await expect(enrollRepo({ 185 oauthSession: fakeOauthSession('did:plc:abc'), 186 installationId: 1, 187 githubRepoId: 9001, 188 })).rejects.toThrow(/knot1\.tangled\.sh returned 500/) 189 190 expect(putRecordMock).not.toHaveBeenCalled() 191 expect(await useDb().select().from(repoMapping)).toHaveLength(0) 192 }) 193})