Skip to content

Commit 7194d05

Browse files
committed
test: add tests
1 parent c8c8db4 commit 7194d05

4 files changed

Lines changed: 645 additions & 0 deletions

File tree

test/github.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'
2+
import fs from 'node:fs/promises'
3+
import path from 'node:path'
4+
import os from 'node:os'
5+
import {
6+
getProjectIgnoreFile,
7+
getProjectKeepFile,
8+
getAuthorization,
9+
reloadOverrides,
10+
projectIgnore,
11+
projectKeep,
12+
} from '../src/github'
13+
14+
describe('github', () => {
15+
describe('getProjectIgnoreFile', () => {
16+
test('returns path to .projectignore in overridesDir', () => {
17+
const result = getProjectIgnoreFile({ overridesDir: '/some/dir' })
18+
expect(result).toBe(path.resolve('/some/dir', '.projectignore'))
19+
})
20+
})
21+
22+
describe('getProjectKeepFile', () => {
23+
test('returns path to .projectkeep in overridesDir', () => {
24+
const result = getProjectKeepFile({ overridesDir: '/some/dir' })
25+
expect(result).toBe(path.resolve('/some/dir', '.projectkeep'))
26+
})
27+
})
28+
29+
describe('getAuthorization', () => {
30+
test('returns headers with bearer token', () => {
31+
const headers = getAuthorization({ apiToken: 'test-token' } as any)
32+
expect(headers.get('Authorization')).toBe('Bearer test-token')
33+
})
34+
35+
test('returns Headers instance', () => {
36+
const headers = getAuthorization({ apiToken: 'abc' } as any)
37+
expect(headers).toBeInstanceOf(Headers)
38+
})
39+
})
40+
41+
describe('reloadOverrides', () => {
42+
let tmpDir: string
43+
44+
beforeEach(async () => {
45+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ghrepo-test-'))
46+
})
47+
48+
afterEach(async () => {
49+
await fs.rm(tmpDir, { recursive: true })
50+
})
51+
52+
test('loads projectignore and projectkeep files', async () => {
53+
await fs.writeFile(path.join(tmpDir, '.projectignore'), 'ignored-repo\nanother-ignored\n')
54+
await fs.writeFile(path.join(tmpDir, '.projectkeep'), 'kept-repo\n')
55+
56+
await reloadOverrides({ overridesDir: tmpDir } as any)
57+
58+
expect(projectIgnore).toEqual(['ignored-repo', 'another-ignored'])
59+
expect(projectKeep).toEqual(['kept-repo'])
60+
})
61+
62+
test('ignores comments and blank lines', async () => {
63+
await fs.writeFile(
64+
path.join(tmpDir, '.projectignore'),
65+
'# comment\nrepo1\n\n# another comment\nrepo2\n',
66+
)
67+
await fs.writeFile(path.join(tmpDir, '.projectkeep'), '# keep these\nrepo3\n')
68+
69+
await reloadOverrides({ overridesDir: tmpDir } as any)
70+
71+
expect(projectIgnore).toEqual(['repo1', 'repo2'])
72+
expect(projectKeep).toEqual(['repo3'])
73+
})
74+
75+
test('trims whitespace from entries', async () => {
76+
await fs.writeFile(path.join(tmpDir, '.projectignore'), ' spaced-repo \n')
77+
await fs.writeFile(path.join(tmpDir, '.projectkeep'), ' kept \n')
78+
79+
await reloadOverrides({ overridesDir: tmpDir } as any)
80+
81+
expect(projectIgnore).toEqual(['spaced-repo'])
82+
expect(projectKeep).toEqual(['kept'])
83+
})
84+
})
85+
})

test/loader.test.ts

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { describe, expect, test, vi, beforeEach } from 'vitest'
2+
import path from 'node:path'
3+
import { githubProjectsLoader } from '../src/loader'
4+
import type { LoaderOptionsType } from '../src/types'
5+
6+
// Mock the parser module
7+
vi.mock('../src/parser', () => ({
8+
getProjectsList: vi.fn().mockResolvedValue([
9+
{
10+
name: 'mock-project',
11+
title: 'Mock Project',
12+
description: 'desc',
13+
url: 'https://github.com/user/mock-project',
14+
stars: 5,
15+
order: -5,
16+
links: [],
17+
featured: false,
18+
raw: {},
19+
readmeHtml: '<p>hello</p>',
20+
},
21+
]),
22+
}))
23+
24+
// Mock github module
25+
vi.mock('../src/github', () => ({
26+
reloadOverrides: vi.fn().mockResolvedValue(undefined),
27+
}))
28+
29+
// Suppress logger
30+
vi.mock('../src/logger', () => ({
31+
logger: { enabled: false, log: vi.fn(), error: vi.fn(), warn: vi.fn() },
32+
}))
33+
34+
import { getProjectsList } from '../src/parser'
35+
import { reloadOverrides } from '../src/github'
36+
37+
function makeMeta() {
38+
const data = new Map<string, string>()
39+
return {
40+
get: (key: string) => data.get(key),
41+
set: (key: string, value: string) => data.set(key, value),
42+
delete: (key: string) => data.delete(key),
43+
has: (key: string) => data.has(key),
44+
_data: data,
45+
}
46+
}
47+
48+
function makeStore() {
49+
const entries: any[] = []
50+
return {
51+
set: vi.fn((entry: any) => entries.push(entry)),
52+
entries,
53+
}
54+
}
55+
56+
function makeWatcher() {
57+
const handlers: Record<string, Function[]> = {}
58+
return {
59+
on: vi.fn((event: string, handler: Function) => {
60+
handlers[event] = handlers[event] || []
61+
handlers[event].push(handler)
62+
}),
63+
emit: (event: string, ...args: any[]) => {
64+
for (const handler of handlers[event] ?? []) {
65+
handler(...args)
66+
}
67+
},
68+
handlers,
69+
}
70+
}
71+
72+
const defaultOpts: LoaderOptionsType = {
73+
username: 'testuser',
74+
apiToken: 'test-token',
75+
debug: false,
76+
force: false,
77+
filter: () => true,
78+
}
79+
80+
describe('loader', () => {
81+
beforeEach(() => {
82+
vi.clearAllMocks()
83+
})
84+
85+
describe('githubProjectsLoader', () => {
86+
test('returns loader with correct name and schema', () => {
87+
const loader = githubProjectsLoader(defaultOpts)
88+
expect(loader.name).toBe('github-repos-loader')
89+
expect(loader.schema).toBeDefined()
90+
})
91+
92+
test('load calls reloadOverrides and getProjectsList', async () => {
93+
const loader = githubProjectsLoader(defaultOpts)
94+
const meta = makeMeta()
95+
const store = makeStore()
96+
97+
await loader.load({ store, meta, watcher: undefined } as any)
98+
99+
expect(reloadOverrides).toHaveBeenCalled()
100+
expect(getProjectsList).toHaveBeenCalled()
101+
})
102+
103+
test('populates store with projects', async () => {
104+
const loader = githubProjectsLoader(defaultOpts)
105+
const meta = makeMeta()
106+
const store = makeStore()
107+
108+
await loader.load({ store, meta, watcher: undefined } as any)
109+
110+
expect(store.set).toHaveBeenCalledWith(
111+
expect.objectContaining({
112+
id: 'mock-project',
113+
data: expect.objectContaining({ name: 'mock-project' }),
114+
rendered: { html: '<p>hello</p>' },
115+
}),
116+
)
117+
})
118+
119+
test('sets lastUpdated in meta after loading', async () => {
120+
const loader = githubProjectsLoader(defaultOpts)
121+
const meta = makeMeta()
122+
const store = makeStore()
123+
124+
await loader.load({ store, meta, watcher: undefined } as any)
125+
126+
expect(meta.has('lastUpdated')).toBe(true)
127+
})
128+
129+
test('uses cached lastUpdated on subsequent loads', async () => {
130+
const loader = githubProjectsLoader(defaultOpts)
131+
const meta = makeMeta()
132+
const store = makeStore()
133+
134+
// First load
135+
await loader.load({ store, meta, watcher: undefined } as any)
136+
const firstCallOptions = vi.mocked(getProjectsList).mock.calls[0][0]
137+
138+
// Second load (simulated by calling load again)
139+
await loader.load({ store, meta, watcher: undefined } as any)
140+
const secondCallOptions = vi.mocked(getProjectsList).mock.calls[1][0]
141+
142+
// Second call should have a more recent lastUpdated than the first
143+
expect(secondCallOptions.lastUpdated.getTime()).toBeGreaterThan(
144+
firstCallOptions.lastUpdated.getTime(),
145+
)
146+
})
147+
148+
test('force option ignores cached lastUpdated', async () => {
149+
const loader = githubProjectsLoader({ ...defaultOpts, force: true })
150+
const meta = makeMeta()
151+
meta.set('lastUpdated', '2025-06-01T00:00:00Z')
152+
const store = makeStore()
153+
154+
await loader.load({ store, meta, watcher: undefined } as any)
155+
156+
const callOptions = vi.mocked(getProjectsList).mock.calls[0][0]
157+
// Should be epoch, not the cached value
158+
expect(callOptions.lastUpdated.getFullYear()).toBe(1970)
159+
})
160+
})
161+
162+
describe('watcher', () => {
163+
test('registers change handler on watcher', async () => {
164+
const loader = githubProjectsLoader(defaultOpts)
165+
const meta = makeMeta()
166+
const store = makeStore()
167+
const watcher = makeWatcher()
168+
169+
await loader.load({ store, meta, watcher } as any)
170+
171+
expect(watcher.on).toHaveBeenCalledWith('change', expect.any(Function))
172+
})
173+
174+
test('clears cache when override file changes', async () => {
175+
const overridesDir = path.join(process.cwd(), 'src', 'content', 'project-overrides')
176+
const loader = githubProjectsLoader(defaultOpts)
177+
const meta = makeMeta()
178+
const store = makeStore()
179+
const watcher = makeWatcher()
180+
181+
await loader.load({ store, meta, watcher } as any)
182+
183+
// Meta should have lastUpdated from initial load
184+
expect(meta.has('lastUpdated')).toBe(true)
185+
186+
vi.clearAllMocks()
187+
188+
// Simulate override file change
189+
await watcher.emit('change', path.join(overridesDir, 'some-project.md'))
190+
191+
// Should have cleared and re-set lastUpdated
192+
expect(getProjectsList).toHaveBeenCalled()
193+
// The lastUpdated passed should be epoch (cache was cleared)
194+
const callOptions = vi.mocked(getProjectsList).mock.calls[0][0]
195+
expect(callOptions.lastUpdated.getFullYear()).toBe(1970)
196+
})
197+
198+
test('ignores changes outside overrides directory', async () => {
199+
const loader = githubProjectsLoader(defaultOpts)
200+
const meta = makeMeta()
201+
const store = makeStore()
202+
const watcher = makeWatcher()
203+
204+
await loader.load({ store, meta, watcher } as any)
205+
vi.clearAllMocks()
206+
207+
// Simulate change in a different directory
208+
await watcher.emit('change', '/some/other/dir/file.md')
209+
210+
expect(getProjectsList).not.toHaveBeenCalled()
211+
})
212+
213+
test('uses custom overridesDir for watcher filtering', async () => {
214+
const customDir = '/custom/overrides'
215+
const loader = githubProjectsLoader({ ...defaultOpts, overridesDir: customDir })
216+
const meta = makeMeta()
217+
const store = makeStore()
218+
const watcher = makeWatcher()
219+
220+
await loader.load({ store, meta, watcher } as any)
221+
vi.clearAllMocks()
222+
223+
// Change in custom dir should trigger reload
224+
await watcher.emit('change', path.join(customDir, 'project.md'))
225+
expect(getProjectsList).toHaveBeenCalled()
226+
227+
vi.clearAllMocks()
228+
229+
// Change in default dir should NOT trigger reload
230+
const defaultDir = path.join(process.cwd(), 'src', 'content', 'project-overrides')
231+
await watcher.emit('change', path.join(defaultDir, 'project.md'))
232+
expect(getProjectsList).not.toHaveBeenCalled()
233+
})
234+
})
235+
236+
describe('rendered content', () => {
237+
test('omits rendered when readmeHtml is empty', async () => {
238+
vi.mocked(getProjectsList).mockResolvedValueOnce([
239+
{
240+
name: 'no-readme',
241+
title: 'No Readme',
242+
description: null,
243+
url: 'https://github.com/user/no-readme',
244+
stars: 0,
245+
order: 0,
246+
links: [],
247+
featured: false,
248+
raw: {} as any,
249+
},
250+
])
251+
252+
const loader = githubProjectsLoader(defaultOpts)
253+
const meta = makeMeta()
254+
const store = makeStore()
255+
256+
await loader.load({ store, meta, watcher: undefined } as any)
257+
258+
expect(store.set).toHaveBeenCalledWith(
259+
expect.objectContaining({
260+
id: 'no-readme',
261+
rendered: undefined,
262+
}),
263+
)
264+
})
265+
})
266+
})

0 commit comments

Comments
 (0)