- Implemented `normalizeMigrationInput` to standardize database migration input for sqlite, mysql, and postgres. - Added `maskConnectionString` to obscure sensitive credentials in connection strings. - Created comprehensive tests for migration input normalization and connection string masking. - Established database schema creation logic for multiple dialects (sqlite, mysql, postgres). - Developed functions for managing database transactions, including begin, commit, and rollback. - Implemented data insertion logic from backup snapshots into the target database.
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { mkdtempSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { eq } from 'drizzle-orm';
|
|
|
|
const getApiTokenMock = vi.fn();
|
|
const getModelsMock = vi.fn();
|
|
|
|
vi.mock('./platforms/index.js', () => ({
|
|
getAdapter: () => ({
|
|
getApiToken: (...args: unknown[]) => getApiTokenMock(...args),
|
|
getModels: (...args: unknown[]) => getModelsMock(...args),
|
|
}),
|
|
}));
|
|
|
|
type DbModule = typeof import('../db/index.js');
|
|
type ModelServiceModule = typeof import('./modelService.js');
|
|
|
|
describe('refreshModelsForAccount credential discovery', () => {
|
|
let db: DbModule['db'];
|
|
let schema: DbModule['schema'];
|
|
let refreshModelsForAccount: ModelServiceModule['refreshModelsForAccount'];
|
|
let dataDir = '';
|
|
|
|
beforeAll(async () => {
|
|
dataDir = mkdtempSync(join(tmpdir(), 'metapi-model-discovery-'));
|
|
process.env.DATA_DIR = dataDir;
|
|
|
|
await import('../db/migrate.js');
|
|
const dbModule = await import('../db/index.js');
|
|
const modelService = await import('./modelService.js');
|
|
|
|
db = dbModule.db;
|
|
schema = dbModule.schema;
|
|
refreshModelsForAccount = modelService.refreshModelsForAccount;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
getApiTokenMock.mockReset();
|
|
getModelsMock.mockReset();
|
|
|
|
await db.delete(schema.routeChannels).run();
|
|
await db.delete(schema.tokenRoutes).run();
|
|
await db.delete(schema.tokenModelAvailability).run();
|
|
await db.delete(schema.modelAvailability).run();
|
|
await db.delete(schema.accountTokens).run();
|
|
await db.delete(schema.accounts).run();
|
|
await db.delete(schema.sites).run();
|
|
});
|
|
|
|
afterAll(() => {
|
|
delete process.env.DATA_DIR;
|
|
});
|
|
|
|
it('discovers models from account session credential without account_tokens', async () => {
|
|
getApiTokenMock.mockResolvedValue(null);
|
|
getModelsMock.mockImplementation(async (_baseUrl: string, token: string) => (
|
|
token === 'session-token' ? ['claude-sonnet-4-5-20250929', 'claude-opus-4-6'] : []
|
|
));
|
|
|
|
const site = await db.insert(schema.sites).values({
|
|
name: 'site-a',
|
|
url: 'https://site-a.example.com',
|
|
platform: 'new-api',
|
|
status: 'active',
|
|
}).returning().get();
|
|
|
|
const account = await db.insert(schema.accounts).values({
|
|
siteId: site.id,
|
|
username: 'alice',
|
|
accessToken: 'session-token',
|
|
apiToken: null,
|
|
status: 'active',
|
|
}).returning().get();
|
|
|
|
const result = await refreshModelsForAccount(account.id);
|
|
|
|
expect(result).toMatchObject({
|
|
accountId: account.id,
|
|
refreshed: true,
|
|
modelCount: 2,
|
|
tokenScanned: 0,
|
|
discoveredByCredential: true,
|
|
});
|
|
|
|
const rows = await db.select().from(schema.modelAvailability)
|
|
.where(eq(schema.modelAvailability.accountId, account.id))
|
|
.all();
|
|
expect(rows.map((row) => row.modelName).sort()).toEqual([
|
|
'claude-opus-4-6',
|
|
'claude-sonnet-4-5-20250929',
|
|
]);
|
|
|
|
const tokenRows = await db.select().from(schema.tokenModelAvailability).all();
|
|
expect(tokenRows).toHaveLength(0);
|
|
});
|
|
});
|