// src/sources/free/pointfree.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import PointFreeSource from '../pointfree.js';
const mockFetch = vi.hoisted(() => vi.fn());
vi.mock('../../../utils/fetch.js', () => ({
fetch: (...args: unknown[]) => mockFetch(...args),
}));
vi.mock('../../../utils/cache.js', () => ({
rssCache: {
get: vi.fn(async () => undefined),
set: vi.fn(async () => undefined),
},
articleCache: {
get: vi.fn(async () => undefined),
set: vi.fn(async () => undefined),
},
}));
function createResponse(body: string | Record<string, unknown>, ok = true) {
return {
ok,
status: ok ? 200 : 500,
json: async () => body,
text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
};
}
describe('PointFreeSource', () => {
beforeEach(() => {
mockFetch.mockReset();
});
afterEach(() => {
mockFetch.mockReset();
});
it('fetches and parses patterns from GitHub content', async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes('/repos/pointfreeco/pointfreeco')) {
if (url.includes('/git/trees/')) {
return Promise.resolve(createResponse({
tree: [
{ path: 'README.md', type: 'blob' },
{ path: 'Sources/PointFree/Episodes/001-Intro.md', type: 'blob' },
{ path: 'Sources/PointFree/Utilities/Helper.swift', type: 'blob' },
],
}));
}
return Promise.resolve(createResponse({ default_branch: 'main' }));
}
if (url.includes('README.md')) {
return Promise.resolve(createResponse('# Point-Free\n\nSwiftUI patterns and architecture tips.'));
}
if (url.includes('001-Intro.md')) {
return Promise.resolve(createResponse('# Episode 1\n\nLearn composable architecture.\n\n```swift\nstruct App {}\n```'));
}
return Promise.resolve(createResponse(''));
});
const source = new PointFreeSource();
const patterns = await source.fetchPatterns();
expect(patterns).toHaveLength(2);
expect(patterns[0].title).toBe('Point-Free');
expect(patterns[1].title).toContain('Episode 1');
expect(patterns[1].topics).toContain('architecture');
expect(patterns[1].hasCode).toBe(true);
});
it('searchPatterns returns relevant results', async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes('/repos/pointfreeco/pointfreeco')) {
if (url.includes('/git/trees/')) {
return Promise.resolve(createResponse({
tree: [
{ path: 'README.md', type: 'blob' },
],
}));
}
return Promise.resolve(createResponse({ default_branch: 'main' }));
}
if (url.includes('README.md')) {
return Promise.resolve(createResponse('# Point-Free\n\nSwiftUI guide for reducers.'));
}
return Promise.resolve(createResponse(''));
});
const source = new PointFreeSource();
const results = await source.searchPatterns('swiftui');
expect(results[0].title).toMatch(/Point-Free/i);
expect(results[0].topics).toContain('swiftui');
});
it('returns [] when repo tree fetch fails', async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes('/repos/pointfreeco/pointfreeco') && !url.includes('/git/trees/')) {
return Promise.resolve(createResponse({ default_branch: 'main' }));
}
return Promise.resolve(createResponse('tree failed', false));
});
const source = new PointFreeSource();
await expect(source.fetchPatterns()).resolves.toEqual([]);
});
it('falls back to default branch when repo metadata request fails', async () => {
mockFetch.mockImplementation((url: string) => {
if (url.endsWith('/repos/pointfreeco/pointfreeco')) {
return Promise.resolve(createResponse('repo failed', false));
}
if (url.includes('/git/trees/main?recursive=1')) {
return Promise.resolve(createResponse({ tree: [{ path: 'README.md', type: 'blob' }] }));
}
if (url.includes('/README.md')) {
return Promise.resolve(createResponse('# Point-Free fallback branch test'));
}
return Promise.resolve(createResponse(''));
});
const source = new PointFreeSource();
const patterns = await source.fetchPatterns();
expect(patterns).toHaveLength(1);
expect(patterns[0].title).toBe('Point-Free fallback branch test');
expect(patterns[0].url).toContain('/blob/main/README.md');
});
it('continues processing when one content file fetch fails', async () => {
mockFetch.mockImplementation((url: string) => {
if (url.endsWith('/repos/pointfreeco/pointfreeco')) {
return Promise.resolve(createResponse({ default_branch: 'main' }));
}
if (url.includes('/git/trees/main?recursive=1')) {
return Promise.resolve(createResponse({
tree: [
{ path: 'README.md', type: 'blob' },
{ path: 'Sources/PointFree/Episodes/002-Fails.md', type: 'blob' },
],
}));
}
if (url.includes('/README.md')) {
return Promise.resolve(createResponse('# Point-Free\n\nStable pattern content'));
}
if (url.includes('/002-Fails.md')) {
return Promise.resolve(createResponse('not found', false));
}
return Promise.resolve(createResponse(''));
});
const source = new PointFreeSource();
const patterns = await source.fetchPatterns();
expect(patterns).toHaveLength(1);
expect(patterns[0].title).toBe('Point-Free');
});
it('returns [] when tree has no content paths', async () => {
mockFetch.mockImplementation((url: string) => {
if (url.endsWith('/repos/pointfreeco/pointfreeco')) {
return Promise.resolve(createResponse({ default_branch: 'main' }));
}
if (url.includes('/git/trees/main?recursive=1')) {
return Promise.resolve(createResponse({
tree: [
{ path: 'scripts/build.sh', type: 'blob' },
{ path: '.github/workflows/ci.yml', type: 'blob' },
],
}));
}
return Promise.resolve(createResponse(''));
});
const source = new PointFreeSource();
await expect(source.fetchPatterns()).resolves.toEqual([]);
});
});