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