Initial commit
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { describe, test, expect, beforeEach } from 'bun:test';
|
||||
import { Hono } from 'hono';
|
||||
import { ResourceRegistry } from '../src/services/registry.ts';
|
||||
import { createNovelRouter } from '../src/routes/novel.ts';
|
||||
import type { ChapterView } from '../src/types.ts';
|
||||
import type { AppConfig } from '../src/config/types.ts';
|
||||
|
||||
// Cache is not exercised by route tests — pass a stub that satisfies the type
|
||||
const stubCache = {} as import('../src/services/cache.ts').Cache;
|
||||
|
||||
const stubAppConfig: AppConfig = {
|
||||
server: { port: 8080 },
|
||||
selenium: { remoteUrl: 'http://selenium:4444/wd/hub', browser: 'firefox' },
|
||||
cache: { basePath: '/tmp' },
|
||||
addons: { directory: './addons' },
|
||||
};
|
||||
|
||||
function makeView(id: number, min: number, max: number): ChapterView {
|
||||
return { id, min, max, chapters: [] };
|
||||
}
|
||||
|
||||
let registry: ResourceRegistry;
|
||||
let app: Hono;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ResourceRegistry();
|
||||
const router = createNovelRouter(registry, stubCache, stubAppConfig);
|
||||
app = new Hono();
|
||||
app.route('/qa/requests/novel', router);
|
||||
});
|
||||
|
||||
describe('GET /qa/requests/novel/available', () => {
|
||||
test('returns empty object when nothing registered', async () => {
|
||||
const res = await app.request('/qa/requests/novel/available');
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
test('returns registered resources (without fetch fn)', async () => {
|
||||
registry.register({
|
||||
hash: 'abc123',
|
||||
slug: 'my-novel',
|
||||
startsAt: 0,
|
||||
endsAt: 100,
|
||||
fetch: async () => makeView(0, 0, 0),
|
||||
});
|
||||
const res = await app.request('/qa/requests/novel/available');
|
||||
const body = await res.json() as Record<string, unknown>;
|
||||
expect(Object.keys(body)).toHaveLength(1);
|
||||
expect((body['0'] as Record<string, unknown>)['slug']).toBe('my-novel');
|
||||
expect((body['0'] as Record<string, unknown>)['fetch']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /qa/requests/novel/:novel', () => {
|
||||
beforeEach(() => {
|
||||
registry.register({
|
||||
hash: 'deadbeef',
|
||||
slug: 'test-novel',
|
||||
startsAt: 0,
|
||||
endsAt: 9,
|
||||
fetch: async (from, to) => makeView(0, from, to),
|
||||
});
|
||||
});
|
||||
|
||||
test('returns 400 when r param is missing', async () => {
|
||||
const res = await app.request('/qa/requests/novel/0');
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json() as { error: string };
|
||||
expect(body.error).toContain('Missing');
|
||||
});
|
||||
|
||||
test('returns 400 for malformed r param', async () => {
|
||||
const res = await app.request('/qa/requests/novel/0?r=notanumber');
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('returns 400 when min > max', async () => {
|
||||
const res = await app.request('/qa/requests/novel/0?r=10,5');
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('returns 422 for unknown novel id', async () => {
|
||||
const res = await app.request('/qa/requests/novel/999?r=0,5');
|
||||
expect(res.status).toBe(422);
|
||||
const body = await res.json() as { error: string };
|
||||
expect(body.error).toContain('Unknown');
|
||||
});
|
||||
|
||||
test('returns 400 for non-integer novel id', async () => {
|
||||
const res = await app.request('/qa/requests/novel/abc?r=0,5');
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('returns 200 html by default', async () => {
|
||||
const res = await app.request('/qa/requests/novel/0?r=0,0');
|
||||
// fetch returns empty chapters so HTML template would be rendered (template read from disk)
|
||||
// We just verify it doesn't 5xx from the route layer itself
|
||||
expect([200, 500]).toContain(res.status);
|
||||
});
|
||||
|
||||
test('returns 200 json when f=json', async () => {
|
||||
const res = await app.request('/qa/requests/novel/0?r=0,0&f=json');
|
||||
expect([200, 500]).toContain(res.status);
|
||||
if (res.status === 200) {
|
||||
expect(res.headers.get('content-type')).toContain('application/json');
|
||||
}
|
||||
});
|
||||
|
||||
test('returns 500 when fetch throws', async () => {
|
||||
registry.register({
|
||||
hash: 'badfetch',
|
||||
slug: 'broken-novel',
|
||||
startsAt: 0,
|
||||
endsAt: 0,
|
||||
fetch: async () => { throw new Error('something went wrong internally'); },
|
||||
});
|
||||
const res = await app.request('/qa/requests/novel/1?r=0,0');
|
||||
expect(res.status).toBe(500);
|
||||
const body = await res.json() as { error: string; detail: string };
|
||||
expect(body.error).toBeDefined();
|
||||
expect(body.detail).toBe('something went wrong internally');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /qa/requests/novel/new', () => {
|
||||
test('returns 400 for non-JSON body', async () => {
|
||||
const res = await app.request('/qa/requests/novel/new', {
|
||||
method: 'POST',
|
||||
body: 'not json',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('returns 400 when type is missing', async () => {
|
||||
const res = await app.request('/qa/requests/novel/new', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ slug: 'test', urls: ['http://x.com/book.epub'] }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('returns 400 for unsupported type', async () => {
|
||||
const res = await app.request('/qa/requests/novel/new', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type: 'novel', slug: 'test', urls: ['http://x.com'] }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json() as { error: string };
|
||||
expect(body.error).toContain('Unsupported');
|
||||
});
|
||||
|
||||
test('registers an epub and returns numeric id', async () => {
|
||||
const res = await app.request('/qa/requests/novel/new', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type: 'epub', slug: 'my-book', urls: ['http://x.com/book.epub'] }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
expect(parseInt(text, 10)).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('returns 409 on duplicate registration', async () => {
|
||||
const body = JSON.stringify({ type: 'epub', slug: 'dup', urls: ['http://x.com/dup.epub'] });
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
await app.request('/qa/requests/novel/new', { method: 'POST', body, headers });
|
||||
const res = await app.request('/qa/requests/novel/new', { method: 'POST', body, headers });
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user