274 lines
9.8 KiB
TypeScript
274 lines
9.8 KiB
TypeScript
/**
|
|
* Unit tests for VersionService.
|
|
*
|
|
* Uses an in-memory SQLite database seeded from the canonical migration file.
|
|
* Returned rows have snake_case column names matching the raw better-sqlite3
|
|
* output — the same pattern used by repository.service.test.ts.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import Database from 'better-sqlite3';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { VersionService } from './version.service';
|
|
import { RepositoryService } from './repository.service';
|
|
import { AlreadyExistsError, NotFoundError } from '$lib/server/utils/validation';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test DB factory
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function createTestDb(): Database.Database {
|
|
const client = new Database(':memory:');
|
|
client.pragma('foreign_keys = ON');
|
|
|
|
const migrationsFolder = join(import.meta.dirname, '../db/migrations');
|
|
|
|
// Apply all migration files in order
|
|
const migration0 = readFileSync(join(migrationsFolder, '0000_large_master_chief.sql'), 'utf-8');
|
|
const migration1 = readFileSync(join(migrationsFolder, '0001_quick_nighthawk.sql'), 'utf-8');
|
|
|
|
// Apply first migration
|
|
const statements0 = migration0
|
|
.split('--> statement-breakpoint')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
for (const stmt of statements0) {
|
|
client.exec(stmt);
|
|
}
|
|
|
|
// Apply second migration
|
|
const statements1 = migration1
|
|
.split('--> statement-breakpoint')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
for (const stmt of statements1) {
|
|
client.exec(stmt);
|
|
}
|
|
|
|
return client;
|
|
}
|
|
|
|
// Raw row shape returned by better-sqlite3 SELECT * FROM repository_versions.
|
|
interface RawVersion {
|
|
id: string;
|
|
repository_id: string;
|
|
tag: string;
|
|
title: string | null;
|
|
state: string;
|
|
total_snippets: number;
|
|
indexed_at: number | null;
|
|
created_at: number;
|
|
}
|
|
|
|
function setup() {
|
|
const client = createTestDb();
|
|
const repoService = new RepositoryService(client);
|
|
const versionService = new VersionService(client);
|
|
|
|
// Add a parent repository used across most tests.
|
|
repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
|
|
|
|
return { client, repoService, versionService };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// list()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('VersionService.list()', () => {
|
|
it('returns an empty array when no versions exist', () => {
|
|
const { versionService } = setup();
|
|
expect(versionService.list('/facebook/react')).toEqual([]);
|
|
});
|
|
|
|
it('returns all versions for a repository', () => {
|
|
const { versionService } = setup();
|
|
versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0');
|
|
versionService.add('/facebook/react', 'v17.0.2', 'React v17.0.2');
|
|
const versions = versionService.list('/facebook/react');
|
|
expect(versions).toHaveLength(2);
|
|
});
|
|
|
|
it('returns newest versions first', () => {
|
|
const { client, versionService } = setup();
|
|
const now = Math.floor(Date.now() / 1000);
|
|
// Insert with explicit timestamps to guarantee ordering.
|
|
client
|
|
.prepare(
|
|
`INSERT INTO repository_versions (id, repository_id, tag, state, total_snippets, created_at)
|
|
VALUES (?, '/facebook/react', ?, 'pending', 0, ?)`
|
|
)
|
|
.run('/facebook/react/v17.0.2', 'v17.0.2', now - 10);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO repository_versions (id, repository_id, tag, state, total_snippets, created_at)
|
|
VALUES (?, '/facebook/react', ?, 'pending', 0, ?)`
|
|
)
|
|
.run('/facebook/react/v18.3.0', 'v18.3.0', now);
|
|
const versions = versionService.list('/facebook/react') as unknown as RawVersion[];
|
|
// Newest created_at first.
|
|
expect(versions[0].tag).toBe('v18.3.0');
|
|
});
|
|
|
|
it('returns empty array for an unknown repository (no FK check on list)', () => {
|
|
const { versionService } = setup();
|
|
expect(versionService.list('/unknown/repo')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// add()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('VersionService.add()', () => {
|
|
it('creates a version with the correct ID format', () => {
|
|
const { versionService } = setup();
|
|
const version = versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0');
|
|
expect(version.id).toBe('/facebook/react/v18.3.0');
|
|
expect(version.repositoryId).toBe('/facebook/react');
|
|
expect(version.tag).toBe('v18.3.0');
|
|
expect(version.title).toBe('React v18.3.0');
|
|
expect(version.state).toBe('pending');
|
|
expect(version.totalSnippets).toBe(0);
|
|
expect(version.indexedAt).toBeNull();
|
|
});
|
|
|
|
it('creates a version without a title', () => {
|
|
const { versionService } = setup();
|
|
const version = versionService.add('/facebook/react', 'v18.3.0');
|
|
expect(version.title).toBeNull();
|
|
});
|
|
|
|
it('throws NotFoundError when the parent repository does not exist', () => {
|
|
const { versionService } = setup();
|
|
expect(() => versionService.add('/unknown/repo', 'v1.0.0')).toThrow(NotFoundError);
|
|
});
|
|
|
|
it('throws AlreadyExistsError when adding a duplicate tag', () => {
|
|
const { versionService } = setup();
|
|
versionService.add('/facebook/react', 'v18.3.0');
|
|
expect(() => versionService.add('/facebook/react', 'v18.3.0')).toThrow(AlreadyExistsError);
|
|
});
|
|
|
|
it('allows the same tag for different repositories', () => {
|
|
const { repoService, versionService } = setup();
|
|
// Use a repo name without dots so resolveGitHubId produces a predictable ID.
|
|
repoService.add({ source: 'github', sourceUrl: 'https://github.com/vercel/nextjs' });
|
|
versionService.add('/facebook/react', 'v18.3.0');
|
|
const v = versionService.add('/vercel/nextjs', 'v18.3.0');
|
|
expect(v.id).toBe('/vercel/nextjs/v18.3.0');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// remove()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('VersionService.remove()', () => {
|
|
it('removes an existing version', () => {
|
|
const { versionService } = setup();
|
|
versionService.add('/facebook/react', 'v18.3.0');
|
|
versionService.remove('/facebook/react', 'v18.3.0');
|
|
expect(versionService.getByTag('/facebook/react', 'v18.3.0')).toBeNull();
|
|
});
|
|
|
|
it('throws NotFoundError when the version does not exist', () => {
|
|
const { versionService } = setup();
|
|
expect(() => versionService.remove('/facebook/react', 'v99.0.0')).toThrow(NotFoundError);
|
|
});
|
|
|
|
it('cascades to documents and snippets on delete', () => {
|
|
const { client, versionService } = setup();
|
|
versionService.add('/facebook/react', 'v18.3.0');
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const docId = crypto.randomUUID();
|
|
client
|
|
.prepare(
|
|
`INSERT INTO documents (id, repository_id, version_id, file_path, checksum, indexed_at)
|
|
VALUES (?, '/facebook/react', '/facebook/react/v18.3.0', 'README.md', 'abc', ?)`
|
|
)
|
|
.run(docId, now);
|
|
|
|
versionService.remove('/facebook/react', 'v18.3.0');
|
|
|
|
const doc = client.prepare(`SELECT id FROM documents WHERE id = ?`).get(docId);
|
|
expect(doc).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getByTag()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('VersionService.getByTag()', () => {
|
|
it('returns null when the version does not exist', () => {
|
|
const { versionService } = setup();
|
|
expect(versionService.getByTag('/facebook/react', 'v99.0.0')).toBeNull();
|
|
});
|
|
|
|
it('returns the version record when it exists', () => {
|
|
const { versionService } = setup();
|
|
versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0');
|
|
const version = versionService.getByTag('/facebook/react', 'v18.3.0');
|
|
expect(version).not.toBeNull();
|
|
if (!version) throw new Error('Expected version to exist');
|
|
expect(version.tag).toBe('v18.3.0');
|
|
expect(version?.repositoryId).toBe('/facebook/react');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// registerFromConfig()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('VersionService.registerFromConfig()', () => {
|
|
it('registers multiple versions from config', () => {
|
|
const { versionService } = setup();
|
|
const result = versionService.registerFromConfig('/facebook/react', [
|
|
{ tag: 'v18.3.0', title: 'React v18.3.0' },
|
|
{ tag: 'v17.0.2', title: 'React v17.0.2' }
|
|
]);
|
|
expect(result).toHaveLength(2);
|
|
expect(versionService.list('/facebook/react')).toHaveLength(2);
|
|
});
|
|
|
|
it('skips already-registered tags idempotently', () => {
|
|
const { versionService } = setup();
|
|
versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0');
|
|
const result = versionService.registerFromConfig('/facebook/react', [
|
|
{ tag: 'v18.3.0', title: 'React v18.3.0 (duplicate)' },
|
|
{ tag: 'v17.0.2', title: 'React v17.0.2' }
|
|
]);
|
|
// Both entries are returned but only one new row is created.
|
|
expect(result).toHaveLength(2);
|
|
expect(versionService.list('/facebook/react')).toHaveLength(2);
|
|
});
|
|
|
|
it('returns an empty array when given an empty previousVersions list', () => {
|
|
const { versionService } = setup();
|
|
const result = versionService.registerFromConfig('/facebook/react', []);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('throws NotFoundError when the parent repository does not exist', () => {
|
|
const { versionService } = setup();
|
|
expect(() =>
|
|
versionService.registerFromConfig('/unknown/repo', [{ tag: 'v1.0.0', title: 'v1' }])
|
|
).toThrow(NotFoundError);
|
|
});
|
|
|
|
it('sets all registered versions to state pending', () => {
|
|
const { versionService } = setup();
|
|
versionService.registerFromConfig('/facebook/react', [
|
|
{ tag: 'v18.3.0', title: 'React v18.3.0' }
|
|
]);
|
|
const version = versionService.getByTag('/facebook/react', 'v18.3.0');
|
|
if (!version) throw new Error('Expected version to exist');
|
|
expect(version.state).toBe('pending');
|
|
});
|
|
});
|