Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/routes/feedback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { CallableError } from '../lib/callable';
import type { Env } from '../types';
import { handleFeedback } from './feedback';

jest.mock('../lib/auth/session', () => ({
verifySessionToken: jest.fn(async () => 'verified-uid'),
}));

const send = jest.fn(async (_message: unknown) => undefined);
const env = { FEEDBACK_QUEUE: { send } } as unknown as Env;

const buildRequest = (report: unknown): Request =>
new Request('https://example.com/postFeedback', {
method: 'POST',
headers: {
'content-type': 'application/json; charset=UTF-8',
Authorization: 'Bearer dummy',
},
body: JSON.stringify({ data: { report } }),
});

const baseReport = {
id: 'feedback-id',
reportType: 'feedback',
description: '遅い',
reporterUid: 'client-claimed-uid',
};

describe('handleFeedback', () => {
afterEach(() => {
jest.clearAllMocks();
});

// 文字数の下限は撤廃したため、短い本文もそのままキューへ流す
it('queues a short report', async () => {
const res = await handleFeedback(buildRequest(baseReport), env);

expect(res.status).toBe(200);
expect(send).toHaveBeenCalledTimes(1);
expect(send.mock.calls[0][0]).toMatchObject({
id: 'feedback-id',
report: { description: '遅い', reporterUid: 'verified-uid' },
});
});

// code は HTTP ステータスへ直結する(invalid-argument なら 400)。CallableError で
// あることしか見ていないと、internal へ変わって 500 を返すようになっても気づけない。
// アプリは 4xx と 5xx で扱いを変えられるため、コードまで固定する。
it.each([
['empty', ''],
['whitespace only', ' \n\t '],
['number', 1],
['null', null],
])(
'rejects a %s description without queueing',
async (_label, description) => {
const rejected = handleFeedback(
buildRequest({ ...baseReport, description }),
env
);

await expect(rejected).rejects.toThrow(CallableError);
await expect(rejected).rejects.toMatchObject({
code: 'invalid-argument',
});
expect(send).not.toHaveBeenCalled();
}
);

it('rejects a missing description without queueing', async () => {
const rejected = handleFeedback(buildRequest({ id: 'feedback-id' }), env);

await expect(rejected).rejects.toThrow(/report.description required/);
await expect(rejected).rejects.toMatchObject({ code: 'invalid-argument' });
expect(send).not.toHaveBeenCalled();
});
Comment thread
TinyKitten marked this conversation as resolved.
});
5 changes: 5 additions & 0 deletions src/routes/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export const handleFeedback = async (
if (!report?.id) {
throw new CallableError('invalid-argument', 'report.id required');
}
// 本文が空白のみのフィードバックはトリアージへ流さない。文字数の下限は設けず、
// 短い本文やクラッシュレポートの短いエラーメッセージはそのまま受け付ける。
if (typeof report.description !== 'string' || !report.description.trim()) {
throw new CallableError('invalid-argument', 'report.description required');
}

// reporterUid はクライアント申告を信用せず、検証済みトークンの sub で上書きする。
// (他ユーザーの UID を名乗って Issue/Discord に載せるなりすましを防ぐ)
Expand Down
Loading