Refs #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
85 lines
3.6 KiB
TypeScript
85 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { DEFAULT_OPENAI, OpenAiTranscriber, redact } from '../src/core/openai';
|
|
import type { HttpRequest } from '../src/core/ports';
|
|
import { FakeHttp } from './fakes';
|
|
|
|
const KEY = 'sk-test-123';
|
|
|
|
describe('redact', () => {
|
|
it('replaces every occurrence of the key', () => {
|
|
expect(redact(`a ${KEY} b ${KEY}`, { apiKey: KEY })).toBe('a *** b ***');
|
|
});
|
|
|
|
it('masks any Bearer token', () => {
|
|
expect(redact('Authorization: Bearer abc.def-1', { apiKey: KEY })).toBe('Authorization: Bearer ***');
|
|
});
|
|
|
|
it('leaves the message alone for an empty key', () => {
|
|
expect(redact('HTTP 401: nope', { apiKey: '' })).toBe('HTTP 401: nope');
|
|
});
|
|
|
|
it('cuts at the multipart boundary and trims trailing ": "', () => {
|
|
const b = '----voice-note-0000';
|
|
expect(redact(`HTTP 400: echo --${b}\r\nContent-Disposition: form-data`, { apiKey: KEY, boundary: b })).toBe(
|
|
'HTTP 400: echo',
|
|
);
|
|
expect(redact(`HTTP 400: --${b}\r\n`, { apiKey: KEY, boundary: b })).toBe('HTTP 400');
|
|
});
|
|
|
|
it('cuts at Content-Disposition regardless of case', () => {
|
|
expect(redact('HTTP 400: got content-disposition: form-data; name="file"', { apiKey: KEY })).toBe('HTTP 400: got');
|
|
});
|
|
|
|
it('caps the result at 300 chars', () => {
|
|
expect(redact('x'.repeat(500), { apiKey: KEY })).toHaveLength(300);
|
|
});
|
|
|
|
it('replaces the key before capping, so no key fragment survives the cut', () => {
|
|
const out = redact(`${'y'.repeat(295)}${KEY}`, { apiKey: KEY });
|
|
expect(out).toBe(`${'y'.repeat(295)}***`);
|
|
expect(out).not.toContain('sk-');
|
|
});
|
|
});
|
|
|
|
describe('secrets never reach the reason (end to end)', () => {
|
|
const audio = () => new Uint8Array([1, 2, 3]).buffer;
|
|
const run = async (reply: ConstructorParameters<typeof FakeHttp>[0]) => {
|
|
const http = new FakeHttp(reply);
|
|
const t = new OpenAiTranscriber(http, () => ({ ...DEFAULT_OPENAI, apiKey: KEY }), () => 0);
|
|
const result = await t.transcribe(audio(), 'audio/mp4', new AbortController().signal);
|
|
if (result.ok) throw new Error('expected an error');
|
|
return { message: result.message, req: http.requests[0] };
|
|
};
|
|
const echo = (status: number) => (req: HttpRequest) => ({ status, text: new TextDecoder().decode(req.body) });
|
|
|
|
it('401 JSON echoing the key and Authorization header', async () => {
|
|
const body = JSON.stringify({
|
|
error: { message: `Incorrect API key provided: ${KEY}. Authorization: Bearer ${KEY}` },
|
|
});
|
|
const { message } = await run(() => ({ status: 401, text: body }));
|
|
expect(message).toBe('HTTP 401: Incorrect API key provided: ***. Authorization: Bearer ***');
|
|
});
|
|
|
|
it('401 plain text echoing the Authorization header', async () => {
|
|
const { message } = await run(() => ({ status: 401, text: `Authorization: Bearer ${KEY}` }));
|
|
expect(message).not.toContain(KEY);
|
|
expect(message).toBe('HTTP 401: Authorization: Bearer ***');
|
|
});
|
|
|
|
it('network exception mentioning the key', async () => {
|
|
const { message } = await run(() =>
|
|
Promise.reject(new Error(`connect failed for Bearer ${KEY} at https://x/?key=${KEY}`)),
|
|
);
|
|
expect(message).not.toContain(KEY);
|
|
expect(message).toBe('сеть: connect failed for Bearer *** at https://x/?key=***');
|
|
});
|
|
|
|
it.each([200, 400, 401, 500])('service echoing the request body with status %i', async (status) => {
|
|
const { message, req } = await run(echo(status));
|
|
const boundary = /boundary=(.+)$/.exec(req.contentType)![1];
|
|
expect(message).not.toContain(KEY);
|
|
expect(message).not.toContain(boundary);
|
|
expect(message.toLowerCase()).not.toContain('content-disposition');
|
|
});
|
|
});
|