Mocking bandcamp call and adding articles test with mocks.
Some checks failed
Run_Svelte_Unit_on_PRs / test (push) Waiting to run
Run_Svelte_Integration_on_PRs / e2e-tests (push) Has been cancelled

This commit is contained in:
Bradley Shellnut 2025-08-23 16:04:22 -07:00
parent 2714d05917
commit de0155e0db
2 changed files with 185 additions and 10 deletions

145
src/lib/api.test.ts Normal file
View file

@ -0,0 +1,145 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Hoisted mocks to satisfy Vitest's module mock hoisting
const hoisted = vi.hoisted(() => {
return {
redisMock: {
get: vi.fn(),
set: vi.fn(),
ttl: vi.fn(),
},
} as const;
});
// Mock env constants used by fetchArticlesApi
vi.mock('$env/static/private', () => ({
PAGE_SIZE: '10',
USE_REDIS_CACHE: 'true', // enable cache to test both hit/miss paths with stubs
WALLABAG_CLIENT_ID: 'client-id',
WALLABAG_CLIENT_SECRET: 'client-secret',
WALLABAG_PASSWORD: 'password',
WALLABAG_URL: 'https://wallabag.example',
WALLABAG_USERNAME: 'username',
}));
// Mock redis client so no real connection is used
vi.mock('$lib/server/redis', () => ({
redis: hoisted.redisMock,
}));
// Helper to mock global fetch responses
function makeJsonResponse<T>(data: T, headers: Record<string, string> = {}) {
return {
ok: true,
status: 200,
statusText: 'OK',
headers: { get: (k: string) => headers[k.toLowerCase()] ?? null },
json: async () => data,
} as const;
}
// Import after mocks are set up
import { fetchArticlesApi } from './api';
describe('fetchArticlesApi (unit, mocked)', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('fetches and maps articles on cache miss, then stores in redis', async () => {
// Cache miss setup
hoisted.redisMock.get.mockResolvedValueOnce(null);
hoisted.redisMock.ttl.mockResolvedValueOnce(0);
// Mock token fetch
const token = { access_token: 'access-token' } as const;
// Mock entries fetch
const wallabagResponse = {
_embedded: {
items: [
{
title: 'Great Post',
url: 'https://example.com/post',
domain_name: 'www.example.com',
hashed_url: 'hash123',
reading_time: 7,
preview_picture: 'https://example.com/img.jpg',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-02T00:00:00Z',
archived_at: null,
tags: [{ slug: 'programming' }],
},
],
},
page: 1,
pages: 5,
total: 100,
limit: 10,
} as const;
const fetchMock = vi.fn(async (input: unknown) => {
const url = String(input);
if (url.endsWith('/oauth/v2/token')) {
return makeJsonResponse(token);
}
if (url.startsWith('https://wallabag.example/api/entries.json')) {
return makeJsonResponse(wallabagResponse, { 'cache-control': 'max-age=60' });
}
throw new Error('Unexpected fetch to ' + url);
});
// @ts-expect-error assign to global
global.fetch = fetchMock;
const result = await fetchArticlesApi('GET', 'entries', { page: '1', limit: '10' });
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(result.currentPage).toBe(1);
expect(result.totalPages).toBe(5);
expect(result.limit).toBe(10);
expect(result.totalArticles).toBe(100);
expect(result.cacheControl).toBe('max-age=60');
expect(result.articles.length).toBe(1);
const article = result.articles[0];
expect(article.title).toBe('Great Post');
expect(article.url).toBeInstanceOf(URL);
expect(article.url.hostname).toBe('example.com');
expect(article.domain_name).toBe('example.com');
// Stored in Redis with EX for 12 hours
expect(hoisted.redisMock.set).toHaveBeenCalled();
const setArgs = (hoisted.redisMock.set as unknown as { mock: { calls: unknown[][] } }).mock
.calls[0] as [string, string, 'EX', number];
expect(setArgs[0]).toContain('perPage=10');
expect(setArgs[0]).toContain('page=1');
expect(setArgs[2]).toBe('EX');
expect(setArgs[3]).toBe(43200);
});
it('returns cached response and cacheControl when redis has value (cache hit)', async () => {
const cached = {
articles: [],
currentPage: 2,
totalPages: 3,
limit: 10,
totalArticles: 20,
};
hoisted.redisMock.get.mockResolvedValueOnce(JSON.stringify(cached));
hoisted.redisMock.ttl.mockResolvedValueOnce(321);
const fetchMock = vi.fn();
// @ts-expect-error assign to global
global.fetch = fetchMock;
const result = await fetchArticlesApi('GET', 'entries', { page: '2', limit: '10' });
// No network calls on cache hit
expect(fetchMock).not.toHaveBeenCalled();
expect(result.currentPage).toBe(2);
expect(result.totalPages).toBe(3);
expect(result.limit).toBe(10);
expect(result.totalArticles).toBe(20);
expect(result.cacheControl).toBe('max-age=321');
});
});

View file

@ -1,17 +1,47 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
// Mock env to avoid relying on GitHub secrets and to disable Redis usage
vi.mock('$env/static/private', () => ({
BANDCAMP_USERNAME: 'testuser',
USE_REDIS_CACHE: 'false',
}));
// Stub Redis client so no real connection is attempted
vi.mock('$lib/server/redis', () => ({
redis: {
get: vi.fn(async () => null),
set: vi.fn(async () => 'OK'),
ttl: vi.fn(async () => 0),
},
}));
// Mock scrape-it to avoid real network calls to Bandcamp
vi.mock('scrape-it', () => ({
default: vi.fn(async () => ({
data: {
collectionItems: [
{
url: 'https://bandcamp.com/album/123',
artwork: 'https://img.bandcamp.com/art.jpg',
title: 'Test Album',
artist: 'Test Artist',
},
],
},
})),
}));
import { fetchBandcampAlbums } from './fetchBandcampAlbums'; import { fetchBandcampAlbums } from './fetchBandcampAlbums';
describe('test fetchBandcampAlbums', () => { describe('fetchBandcampAlbums (mocked)', () => {
it('fetches bandcamp albums', async () => { it('returns albums from mocked scrape-it', async () => {
const albums = await fetchBandcampAlbums(); const albums = await fetchBandcampAlbums();
expect(albums).not.toBeNull();
expect(albums).toBeTruthy(); expect(albums).toBeTruthy();
expect(albums?.length).toBeGreaterThan(0); expect(albums?.length).toBeGreaterThan(0);
for (const album of albums) { const [album] = albums;
expect(album?.artist).toHaveLength; expect(album.artist).toBe('Test Artist');
expect(album?.artwork).toHaveLength; expect(album.title).toBe('Test Album');
expect(album?.title).toHaveLength; expect(album.url).toBe('https://bandcamp.com/album/123');
expect(album?.url).toHaveLength; expect(album.artwork).toBe('https://img.bandcamp.com/art.jpg');
}
}); });
}); });