From 856797aceb0f8482f78fc0020ce2237271530cce Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:56:00 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9C=AC=E6=96=87=E3=81=8C=E7=A9=BA?= =?UTF-8?q?=E7=99=BD=E3=81=AE=E3=81=BF=E3=81=AE=E3=83=95=E3=82=A3=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=83=90=E3=83=83=E3=82=AF=E3=82=92postFeedback?= =?UTF-8?q?=E3=81=A7=E5=BC=BE=E3=81=8F=E3=82=88=E3=81=86=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit アプリ側の50文字下限を撤廃したため、空白のみを弾く条件はサーバでも持ちます。 文字数の下限は設けません。短い本文やクラッシュレポートの短いエラーメッセージは これまでどおり受け付けます。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018a5y38tYxDTfNPq95txKzf --- src/routes/feedback.test.ts | 65 +++++++++++++++++++++++++++++++++++++ src/routes/feedback.ts | 5 +++ 2 files changed, 70 insertions(+) create mode 100644 src/routes/feedback.test.ts diff --git a/src/routes/feedback.test.ts b/src/routes/feedback.test.ts new file mode 100644 index 0000000..a030eeb --- /dev/null +++ b/src/routes/feedback.test.ts @@ -0,0 +1,65 @@ +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' }, + }); + }); + + it.each([ + ['empty', ''], + ['whitespace only', ' \n\t '], + ])( + 'rejects a %s description without queueing', + async (_label, description) => { + await expect( + handleFeedback(buildRequest({ ...baseReport, description }), env) + ).rejects.toThrow(CallableError); + expect(send).not.toHaveBeenCalled(); + } + ); + + it('rejects a missing description without queueing', async () => { + await expect( + handleFeedback(buildRequest({ id: 'feedback-id' }), env) + ).rejects.toThrow(/report.description required/); + expect(send).not.toHaveBeenCalled(); + }); +}); diff --git a/src/routes/feedback.ts b/src/routes/feedback.ts index 9971299..5a9cccc 100644 --- a/src/routes/feedback.ts +++ b/src/routes/feedback.ts @@ -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 に載せるなりすましを防ぐ) From ef984e3d20d65a30c2794c1d057b8ba587134de6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 11:58:40 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=E6=9C=AC=E6=96=87=E6=A4=9C=E8=A8=BC?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=81=A7=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=82=B3=E3=83=BC=E3=83=89=E3=81=A8=E9=9D=9E=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=88=97=E3=82=82=E5=9B=BA=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CallableError であることしか見ていなかったため、code を internal へ変えても テストが通っていました。code は HTTP ステータスへ直結するので、 invalid-argument であることまで確かめます。数値と null のケースも足して、 typeof の検証を欠落値以外でも固定します。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018a5y38tYxDTfNPq95txKzf --- src/routes/feedback.test.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/routes/feedback.test.ts b/src/routes/feedback.test.ts index a030eeb..6dbf8e6 100644 --- a/src/routes/feedback.test.ts +++ b/src/routes/feedback.test.ts @@ -43,23 +43,35 @@ describe('handleFeedback', () => { }); }); + // 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) => { - await expect( - handleFeedback(buildRequest({ ...baseReport, description }), env) - ).rejects.toThrow(CallableError); + 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 () => { - await expect( - handleFeedback(buildRequest({ id: 'feedback-id' }), env) - ).rejects.toThrow(/report.description required/); + 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(); }); });