Skip to content

Commit 5dbef4c

Browse files
sweetmantechclaude
andauthored
feat: add PATCH /api/chats endpoint to update chat topic (#191)
* feat: add PATCH /api/chats endpoint to update chat topic Migrate Recoup-Chat/app/api/room/update/route.ts to PATCH /api/chats following the new API docs at developers.recoupable.com/api-reference/chat/update - Add PATCH method to app/api/chats/route.ts - Add lib/chats/updateChatHandler.ts for business logic - Add lib/chats/validateUpdateChatBody.ts for Zod validation - Add lib/supabase/rooms/updateRoom.ts for database operations The endpoint: - Accepts chatId (UUID) and topic (3-50 chars) in request body - Supports both x-api-key and Authorization Bearer token auth - Validates access using buildGetChatsParams (same as GET /api/chats) - Returns 404 if chat not found, 403 if no access, 200 on success Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: correct access control in updateChatHandler + add TDD requirement Bug fix: - Remove target_account_id from buildGetChatsParams call which was causing "Personal API keys cannot filter by account_id" error for users updating their own chats - Access control now correctly checks if room's account_id is in the user's allowed account_ids Tests: - Add comprehensive test suite for updateChatHandler covering: - Successful updates (personal key, org key) - Validation errors (invalid UUID, topic length) - Auth errors (401) - Not found errors (404) - Access denied errors (403) Documentation: - Add TDD section to CLAUDE.md requiring tests before implementation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: move JSON parsing into validateUpdateChatBody - validateUpdateChatBody now takes NextRequest instead of unknown body - Handles JSON parsing internally with try/catch - Remove safeParseJson import from updateChatHandler - Add comprehensive tests for validateUpdateChatBody (10 tests) - Update handler tests to mock validateUpdateChatBody instead of safeParseJson Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: move auth and room validation into validateUpdateChatBody - validateUpdateChatBody now handles: - JSON parsing - Body schema validation - Authentication via validateAuthContext - Room existence check via selectRoom - Returns ValidatedUpdateChat with chatId, topic, room, accountId, orgId - Handler now only handles access control and update logic - Updated tests for both validation (13 tests) and handler (7 tests) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: move access control into validateUpdateChatBody - validateUpdateChatBody now handles complete request validation: - JSON parsing - Body schema validation (chatId UUID, topic 3-50 chars) - Authentication via validateAuthContext - Room existence check via selectRoom - Access control via buildGetChatsParams - Returns only { chatId, topic } on success (simpler interface) - Handler now only calls updateRoom and formats response - Validation tests: 16 tests (including 2 new access denied tests) - Handler tests: 7 tests (simplified, validation errors pass through) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c1eeb0f commit 5dbef4c

File tree

7 files changed

+856
-0
lines changed

7 files changed

+856
-0
lines changed

CLAUDE.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,74 @@ export async function selectTableName({
144144
- All API routes should have JSDoc comments
145145
- Run `pnpm lint` before committing
146146

147+
## Test-Driven Development (TDD)
148+
149+
**CRITICAL: Always write tests BEFORE implementing new features or fixing bugs.**
150+
151+
### TDD Workflow
152+
153+
1. **Write failing tests first** - Create tests in `lib/[domain]/__tests__/[filename].test.ts` that describe the expected behavior
154+
2. **Run tests to verify they fail** - `pnpm test path/to/test.ts`
155+
3. **Implement the code** - Write the minimum code needed to make tests pass
156+
4. **Run tests to verify they pass** - All tests should be green
157+
5. **Refactor if needed** - Clean up while keeping tests green
158+
159+
### Test File Location
160+
161+
Tests live alongside the code they test:
162+
```
163+
lib/
164+
├── chats/
165+
│ ├── __tests__/
166+
│ │ └── updateChatHandler.test.ts
167+
│ ├── updateChatHandler.ts
168+
│ └── validateUpdateChatBody.ts
169+
```
170+
171+
### Test Pattern
172+
173+
```typescript
174+
import { describe, it, expect, vi, beforeEach } from "vitest";
175+
import { NextRequest, NextResponse } from "next/server";
176+
177+
// Mock dependencies
178+
vi.mock("@/lib/networking/getCorsHeaders", () => ({
179+
getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })),
180+
}));
181+
182+
describe("functionName", () => {
183+
beforeEach(() => {
184+
vi.clearAllMocks();
185+
});
186+
187+
describe("successful cases", () => {
188+
it("does something when condition is met", async () => {
189+
// Arrange
190+
vi.mocked(dependency).mockResolvedValue(mockData);
191+
192+
// Act
193+
const result = await functionName(input);
194+
195+
// Assert
196+
expect(result.status).toBe(200);
197+
});
198+
});
199+
200+
describe("error cases", () => {
201+
it("returns 400 when validation fails", async () => {
202+
// Test error handling
203+
});
204+
});
205+
});
206+
```
207+
208+
### When to Write Tests
209+
210+
- **New API endpoints**: Write tests for all success and error paths
211+
- **New handlers**: Test business logic with mocked dependencies
212+
- **Bug fixes**: Write a failing test that reproduces the bug, then fix it
213+
- **Validation functions**: Test all valid and invalid input combinations
214+
147215
## Authentication
148216

149217
**Never use `account_id` in request bodies or tool schemas.** Always derive the account ID from authentication:

app/api/chats/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
33
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
44
import { createChatHandler } from "@/lib/chats/createChatHandler";
55
import { getChatsHandler } from "@/lib/chats/getChatsHandler";
6+
import { updateChatHandler } from "@/lib/chats/updateChatHandler";
67

78
/**
89
* OPTIONS handler for CORS preflight requests.
@@ -52,3 +53,21 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
5253
export async function POST(request: NextRequest): Promise<NextResponse> {
5354
return createChatHandler(request);
5455
}
56+
57+
/**
58+
* PATCH /api/chats
59+
*
60+
* Update a chat room's topic (display name).
61+
*
62+
* Authentication: x-api-key header or Authorization Bearer token required.
63+
*
64+
* Body parameters:
65+
* - chatId (required): UUID of the chat room to update
66+
* - topic (required): New display name for the chat (3-50 characters)
67+
*
68+
* @param request - The request object
69+
* @returns A NextResponse with the updated chat or an error
70+
*/
71+
export async function PATCH(request: NextRequest): Promise<NextResponse> {
72+
return updateChatHandler(request);
73+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { NextRequest, NextResponse } from "next/server";
3+
import { updateChatHandler } from "../updateChatHandler";
4+
5+
vi.mock("@/lib/networking/getCorsHeaders", () => ({
6+
getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })),
7+
}));
8+
9+
vi.mock("@/lib/chats/validateUpdateChatBody", () => ({
10+
validateUpdateChatBody: vi.fn(),
11+
}));
12+
13+
vi.mock("@/lib/supabase/rooms/updateRoom", () => ({
14+
updateRoom: vi.fn(),
15+
}));
16+
17+
import { validateUpdateChatBody } from "@/lib/chats/validateUpdateChatBody";
18+
import { updateRoom } from "@/lib/supabase/rooms/updateRoom";
19+
20+
describe("updateChatHandler", () => {
21+
const mockRequest = () => {
22+
return new NextRequest("http://localhost/api/chats", {
23+
method: "PATCH",
24+
headers: { "x-api-key": "test-key", "Content-Type": "application/json" },
25+
body: JSON.stringify({}),
26+
});
27+
};
28+
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
});
32+
33+
describe("successful update", () => {
34+
it("updates chat topic and returns success response", async () => {
35+
const chatId = "123e4567-e89b-12d3-a456-426614174001";
36+
const accountId = "123e4567-e89b-12d3-a456-426614174000";
37+
const newTopic = "My Updated Chat";
38+
39+
vi.mocked(validateUpdateChatBody).mockResolvedValue({
40+
chatId,
41+
topic: newTopic,
42+
});
43+
44+
vi.mocked(updateRoom).mockResolvedValue({
45+
id: chatId,
46+
account_id: accountId,
47+
artist_id: null,
48+
topic: newTopic,
49+
updated_at: "2024-01-02T00:00:00Z",
50+
});
51+
52+
const request = mockRequest();
53+
const response = await updateChatHandler(request);
54+
55+
expect(response.status).toBe(200);
56+
const body = await response.json();
57+
expect(body).toEqual({
58+
status: "success",
59+
chat: {
60+
id: chatId,
61+
account_id: accountId,
62+
topic: newTopic,
63+
updated_at: "2024-01-02T00:00:00Z",
64+
artist_id: null,
65+
},
66+
});
67+
68+
expect(updateRoom).toHaveBeenCalledWith(chatId, { topic: newTopic });
69+
});
70+
});
71+
72+
describe("validation errors", () => {
73+
it("returns 400 from validation when body is invalid", async () => {
74+
vi.mocked(validateUpdateChatBody).mockResolvedValue(
75+
NextResponse.json(
76+
{ status: "error", error: "chatId must be a valid UUID" },
77+
{ status: 400 },
78+
),
79+
);
80+
81+
const request = mockRequest();
82+
const response = await updateChatHandler(request);
83+
84+
expect(response.status).toBe(400);
85+
expect(updateRoom).not.toHaveBeenCalled();
86+
});
87+
88+
it("returns 401 from validation when auth fails", async () => {
89+
vi.mocked(validateUpdateChatBody).mockResolvedValue(
90+
NextResponse.json(
91+
{ status: "error", error: "Unauthorized" },
92+
{ status: 401 },
93+
),
94+
);
95+
96+
const request = mockRequest();
97+
const response = await updateChatHandler(request);
98+
99+
expect(response.status).toBe(401);
100+
expect(updateRoom).not.toHaveBeenCalled();
101+
});
102+
103+
it("returns 404 from validation when chat not found", async () => {
104+
vi.mocked(validateUpdateChatBody).mockResolvedValue(
105+
NextResponse.json(
106+
{ status: "error", error: "Chat room not found" },
107+
{ status: 404 },
108+
),
109+
);
110+
111+
const request = mockRequest();
112+
const response = await updateChatHandler(request);
113+
114+
expect(response.status).toBe(404);
115+
expect(updateRoom).not.toHaveBeenCalled();
116+
});
117+
118+
it("returns 403 from validation when access denied", async () => {
119+
vi.mocked(validateUpdateChatBody).mockResolvedValue(
120+
NextResponse.json(
121+
{ status: "error", error: "Access denied to this chat" },
122+
{ status: 403 },
123+
),
124+
);
125+
126+
const request = mockRequest();
127+
const response = await updateChatHandler(request);
128+
129+
expect(response.status).toBe(403);
130+
expect(updateRoom).not.toHaveBeenCalled();
131+
});
132+
});
133+
134+
describe("update errors", () => {
135+
it("returns 500 when updateRoom fails", async () => {
136+
vi.mocked(validateUpdateChatBody).mockResolvedValue({
137+
chatId: "123e4567-e89b-12d3-a456-426614174001",
138+
topic: "New Topic",
139+
});
140+
141+
vi.mocked(updateRoom).mockResolvedValue(null);
142+
143+
const request = mockRequest();
144+
const response = await updateChatHandler(request);
145+
146+
expect(response.status).toBe(500);
147+
const body = await response.json();
148+
expect(body.error).toContain("Failed to update");
149+
});
150+
151+
it("returns 500 when updateRoom throws", async () => {
152+
vi.mocked(validateUpdateChatBody).mockResolvedValue({
153+
chatId: "123e4567-e89b-12d3-a456-426614174001",
154+
topic: "New Topic",
155+
});
156+
157+
vi.mocked(updateRoom).mockRejectedValue(new Error("Database error"));
158+
159+
const request = mockRequest();
160+
const response = await updateChatHandler(request);
161+
162+
expect(response.status).toBe(500);
163+
const body = await response.json();
164+
expect(body.error).toBe("Database error");
165+
});
166+
});
167+
});

0 commit comments

Comments
 (0)