203 lines
5.4 KiB
TypeScript
203 lines
5.4 KiB
TypeScript
import {
|
|
afterAll,
|
|
beforeAll,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it
|
|
} from 'bun:test';
|
|
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { Database } from 'bun:sqlite';
|
|
import type { CompanyAnalysis } from '@/lib/types';
|
|
|
|
const TEST_USER_ID = 'overview-cache-user';
|
|
|
|
let tempDir: string | null = null;
|
|
let sqliteClient: Database | null = null;
|
|
let overviewCacheRepo: typeof import('./company-overview-cache') | null = null;
|
|
|
|
function resetDbSingletons() {
|
|
const globalState = globalThis as typeof globalThis & {
|
|
__fiscalSqliteClient?: Database;
|
|
__fiscalDrizzleDb?: unknown;
|
|
};
|
|
|
|
globalState.__fiscalSqliteClient?.close();
|
|
globalState.__fiscalSqliteClient = undefined;
|
|
globalState.__fiscalDrizzleDb = undefined;
|
|
}
|
|
|
|
function applyMigration(client: Database, fileName: string) {
|
|
const sql = readFileSync(join(process.cwd(), 'drizzle', fileName), 'utf8');
|
|
client.exec(sql);
|
|
}
|
|
|
|
function ensureUser(client: Database) {
|
|
const now = Date.now();
|
|
client.exec(`
|
|
INSERT OR REPLACE INTO user (id, name, email, emailVerified, image, createdAt, updatedAt, role, banned, banReason, banExpires)
|
|
VALUES ('${TEST_USER_ID}', 'Overview Cache User', 'overview-cache@example.com', 1, NULL, ${now}, ${now}, NULL, 0, NULL, NULL);
|
|
`);
|
|
}
|
|
|
|
function clearCache(client: Database) {
|
|
client.exec('DELETE FROM company_overview_cache;');
|
|
}
|
|
|
|
function buildAnalysisPayload(companyName: string): CompanyAnalysis {
|
|
return {
|
|
company: {
|
|
ticker: 'MSFT',
|
|
companyName,
|
|
sector: null,
|
|
category: null,
|
|
tags: [],
|
|
cik: null
|
|
},
|
|
quote: 100,
|
|
position: null,
|
|
priceHistory: [],
|
|
benchmarkHistory: [],
|
|
financials: [],
|
|
filings: [],
|
|
aiReports: [],
|
|
coverage: null,
|
|
journalPreview: [],
|
|
recentAiReports: [],
|
|
latestFilingSummary: null,
|
|
keyMetrics: {
|
|
referenceDate: null,
|
|
revenue: null,
|
|
netIncome: null,
|
|
totalAssets: null,
|
|
cash: null,
|
|
debt: null,
|
|
netMargin: null
|
|
},
|
|
companyProfile: {
|
|
description: null,
|
|
exchange: null,
|
|
industry: null,
|
|
country: null,
|
|
website: null,
|
|
fiscalYearEnd: null,
|
|
employeeCount: null,
|
|
source: 'unavailable'
|
|
},
|
|
valuationSnapshot: {
|
|
sharesOutstanding: null,
|
|
marketCap: null,
|
|
enterpriseValue: null,
|
|
trailingPe: null,
|
|
evToRevenue: null,
|
|
evToEbitda: null,
|
|
source: 'unavailable'
|
|
},
|
|
bullBear: {
|
|
source: 'unavailable',
|
|
bull: [],
|
|
bear: [],
|
|
updatedAt: null
|
|
},
|
|
recentDevelopments: {
|
|
status: 'unavailable',
|
|
items: [],
|
|
weeklySnapshot: null
|
|
}
|
|
};
|
|
}
|
|
|
|
describe('company overview cache repo', () => {
|
|
beforeAll(async () => {
|
|
tempDir = mkdtempSync(join(tmpdir(), 'fiscal-overview-cache-'));
|
|
const env = process.env as Record<string, string | undefined>;
|
|
env.DATABASE_URL = `file:${join(tempDir, 'repo.sqlite')}`;
|
|
env.NODE_ENV = 'test';
|
|
|
|
resetDbSingletons();
|
|
|
|
sqliteClient = new Database(join(tempDir, 'repo.sqlite'), { create: true });
|
|
sqliteClient.exec('PRAGMA foreign_keys = ON;');
|
|
applyMigration(sqliteClient, '0000_cold_silver_centurion.sql');
|
|
applyMigration(sqliteClient, '0012_company_overview_cache.sql');
|
|
ensureUser(sqliteClient);
|
|
|
|
const globalState = globalThis as typeof globalThis & {
|
|
__fiscalSqliteClient?: Database;
|
|
__fiscalDrizzleDb?: unknown;
|
|
};
|
|
globalState.__fiscalSqliteClient = sqliteClient;
|
|
globalState.__fiscalDrizzleDb = undefined;
|
|
|
|
overviewCacheRepo = await import('./company-overview-cache');
|
|
});
|
|
|
|
afterAll(() => {
|
|
sqliteClient?.close();
|
|
resetDbSingletons();
|
|
if (tempDir) {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
beforeEach(() => {
|
|
if (!sqliteClient) {
|
|
throw new Error('sqlite client not initialized');
|
|
}
|
|
|
|
clearCache(sqliteClient);
|
|
});
|
|
|
|
it('upserts and reloads cached overview payloads', async () => {
|
|
if (!overviewCacheRepo) {
|
|
throw new Error('overview cache repo not initialized');
|
|
}
|
|
|
|
const first = await overviewCacheRepo.upsertCompanyOverviewCache({
|
|
userId: TEST_USER_ID,
|
|
ticker: 'msft',
|
|
sourceSignature: 'sig-1',
|
|
payload: buildAnalysisPayload('Microsoft Corporation')
|
|
});
|
|
|
|
const loaded = await overviewCacheRepo.getCompanyOverviewCache({
|
|
userId: TEST_USER_ID,
|
|
ticker: 'MSFT'
|
|
});
|
|
|
|
expect(first.ticker).toBe('MSFT');
|
|
expect(loaded?.source_signature).toBe('sig-1');
|
|
expect(loaded?.payload.company.companyName).toBe('Microsoft Corporation');
|
|
});
|
|
|
|
it('updates existing cached rows in place', async () => {
|
|
if (!overviewCacheRepo) {
|
|
throw new Error('overview cache repo not initialized');
|
|
}
|
|
|
|
const first = await overviewCacheRepo.upsertCompanyOverviewCache({
|
|
userId: TEST_USER_ID,
|
|
ticker: 'MSFT',
|
|
sourceSignature: 'sig-1',
|
|
payload: buildAnalysisPayload('Old Name')
|
|
});
|
|
const second = await overviewCacheRepo.upsertCompanyOverviewCache({
|
|
userId: TEST_USER_ID,
|
|
ticker: 'MSFT',
|
|
sourceSignature: 'sig-2',
|
|
payload: buildAnalysisPayload('New Name')
|
|
});
|
|
|
|
const loaded = await overviewCacheRepo.getCompanyOverviewCache({
|
|
userId: TEST_USER_ID,
|
|
ticker: 'MSFT'
|
|
});
|
|
|
|
expect(second.id).toBe(first.id);
|
|
expect(loaded?.source_signature).toBe('sig-2');
|
|
expect(loaded?.payload.company.companyName).toBe('New Name');
|
|
});
|
|
});
|