Files
LivingDexTracker/tests/unit/pokedexExportService.test.ts
T
Josh Creek 92d6460765 fix(export): restrict provider endpoint overrides to loopback test servers
The endpoint overrides are read through `$env/dynamic/private`, so they are
evaluated per request in production, not baked in at build time. That made a
single injected environment variable enough to redirect the authorization-code
and refresh-token POSTs - which carry the OAuth client secret and the user's
refresh token - to an arbitrary host, and to redirect the user's authorize hop
to an arbitrary URL.

Overrides are now ignored unless ALLOW_PROVIDER_ENDPOINT_OVERRIDES is exactly
"true" and the value is a loopback URL. `npm run test:bdd` sets the flag;
nothing else should. resolveProviderEndpoints is pure so the refusals are unit
tested, including near-miss hosts such as http://127.0.0.1.example.

Also drops the unused `pokedex` parameter from buildCsv rather than silencing it
with `void`, and the dead hasGigantamaxed field from its fallback record.
2026-09-13 17:38:42 +01:00

73 lines
2.1 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
buildCsv,
csvEscape,
sanitizeFileName,
shouldRefreshToken
} from '$lib/services/PokedexExportFormatting';
describe('Pokédex export formatting', () => {
it.each([
[null, ''],
[undefined, ''],
['plain', 'plain'],
['comma,value', '"comma,value"'],
['a "quote"', '"a ""quote"""'],
['two\nlines', '"two\nlines"']
])('escapes CSV value %j', (value, expected) => {
expect(csvEscape(value)).toBe(expected);
});
it('sanitizes provider filenames while preserving a CSV suffix', () => {
expect(sanitizeFileName(' My: Dex? ', 'fallback')).toBe('My- Dex-.csv');
expect(sanitizeFileName('already.csv', 'fallback')).toBe('already.csv');
expect(sanitizeFileName('***', 'fallback')).toBe('-.csv');
expect(sanitizeFileName(' ', 'fallback')).toBe('fallback');
});
it('builds a stable, escaped CSV with defaults for missing catch records', () => {
const csv = buildCsv([
{
pokedexEntry: {
_id: '25',
pokedexNumber: 25,
pokemon: 'Pikachu',
form: null
},
catchRecord: {
caught: true,
haveToEvolve: false,
inHome: true,
hasGigantamaxed: false,
personalNotes: 'Comma, and "quote"'
}
},
{
pokedexEntry: {
_id: '26',
pokedexNumber: 26,
pokemon: 'Raichu',
form: 'Alolan'
},
catchRecord: null
}
] as never);
expect(csv.split('\r\n')).toEqual([
'pokemonId,pokedexNumber,pokemon,form,caught,haveToEvolve,inHome,personalNotes',
'25,25,Pikachu,,true,false,true,"Comma, and ""quote"""',
'26,26,Raichu,Alolan,false,false,false,'
]);
});
it('refreshes only finite expiries within the next minute', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-09-13T12:00:00Z'));
expect(shouldRefreshToken(null)).toBe(false);
expect(shouldRefreshToken('not-a-date')).toBe(false);
expect(shouldRefreshToken('2026-09-13T12:02:00Z')).toBe(false);
expect(shouldRefreshToken('2026-09-13T12:00:30Z')).toBe(true);
expect(shouldRefreshToken('2026-09-13T11:59:00Z')).toBe(true);
vi.useRealTimers();
});
});