openreader/tests/unit/admin-providers-validation.spec.ts
Richard R 913c6d5d76 refactor(tts,config,admin): update provider resolution logic and ordering for consistency
Remove fallbackProviderRef usage from settings and config context to streamline
provider resolution. Adjust normalizeLegacyProviderRef to retain 'default-openai'
unless a non-default fallback is explicitly provided. Update provider listing to
enforce deterministic ordering by updatedAt, createdAt, and slug. Expand unit tests
to verify provider ordering, legacy ref normalization, and selection logic.
2026-05-13 12:08:39 -06:00

138 lines
4 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { inArray } from 'drizzle-orm';
import { db } from '../../src/db';
import { adminProviders } from '../../src/db/schema';
import {
AdminProviderError,
listAdminProviders,
toMasked,
validateProviderType,
validateSlug,
type AdminProviderRecord,
} from '../../src/lib/server/admin/providers';
test.describe('admin provider validation', () => {
test('normalizes valid slugs to lowercase', () => {
expect(validateSlug('My-Provider-1')).toBe('my-provider-1');
});
test('rejects built-in provider ids as slugs', () => {
expect(() => validateSlug('openai')).toThrow(AdminProviderError);
expect(() => validateSlug('custom-openai')).toThrow('reserved');
});
test('rejects malformed slug values', () => {
expect(() => validateSlug('-bad-')).toThrow(AdminProviderError);
expect(() => validateSlug('bad_slug')).toThrow(AdminProviderError);
expect(() => validateSlug('')).toThrow(AdminProviderError);
});
test('accepts only known provider types', () => {
expect(validateProviderType('openai')).toBe('openai');
expect(() => validateProviderType('unknown')).toThrow(AdminProviderError);
});
test('masks api key from last4 in list responses', () => {
const record: AdminProviderRecord = {
id: 'id-1',
slug: 'shared-one',
displayName: 'Shared One',
providerType: 'openai',
baseUrl: null,
apiKeyCiphertext: 'ciphertext',
apiKeyIv: 'iv',
apiKeyLast4: 'abcd',
defaultModel: 'gpt-4o-mini-tts',
defaultInstructions: 'warm tone',
enabled: true,
createdAt: 1,
updatedAt: 1,
};
expect(toMasked(record).apiKeyMask).toBe('••••abcd');
});
test('lists providers in deterministic updated/created/slug order', async () => {
const suffix = `${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
const inserted = [
{
id: `prov-aaa-${suffix}`,
slug: `ord-aaa-${suffix}`,
displayName: 'Order AAA',
providerType: 'openai' as const,
baseUrl: null,
apiKeyCiphertext: 'cipher',
apiKeyIv: 'iv',
apiKeyLast4: '1111',
defaultModel: null,
defaultInstructions: null,
enabled: 1,
createdAt: 200,
updatedAt: 300,
},
{
id: `prov-zzz-${suffix}`,
slug: `ord-zzz-${suffix}`,
displayName: 'Order ZZZ',
providerType: 'openai' as const,
baseUrl: null,
apiKeyCiphertext: 'cipher',
apiKeyIv: 'iv',
apiKeyLast4: '2222',
defaultModel: null,
defaultInstructions: null,
enabled: 1,
createdAt: 200,
updatedAt: 300,
},
{
id: `prov-mid-${suffix}`,
slug: `ord-mid-${suffix}`,
displayName: 'Order MID',
providerType: 'openai' as const,
baseUrl: null,
apiKeyCiphertext: 'cipher',
apiKeyIv: 'iv',
apiKeyLast4: '3333',
defaultModel: null,
defaultInstructions: null,
enabled: 1,
createdAt: 150,
updatedAt: 300,
},
{
id: `prov-old-${suffix}`,
slug: `ord-old-${suffix}`,
displayName: 'Order OLD',
providerType: 'openai' as const,
baseUrl: null,
apiKeyCiphertext: 'cipher',
apiKeyIv: 'iv',
apiKeyLast4: '4444',
defaultModel: null,
defaultInstructions: null,
enabled: 1,
createdAt: 999,
updatedAt: 200,
},
];
try {
await db.insert(adminProviders).values(inserted);
const listed = await listAdminProviders();
const ordered = listed
.filter((row) => row.slug.endsWith(suffix))
.map((row) => row.slug);
expect(ordered).toEqual([
`ord-aaa-${suffix}`,
`ord-zzz-${suffix}`,
`ord-mid-${suffix}`,
`ord-old-${suffix}`,
]);
} finally {
await db.delete(adminProviders).where(inArray(adminProviders.id, inserted.map((row) => row.id)));
}
});
});