Skip to content

Commit b604a48

Browse files
feat: add tests for routes and support db branching
1 parent dfda911 commit b604a48

File tree

15 files changed

+723
-54
lines changed

15 files changed

+723
-54
lines changed

.github/workflows/playwright.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ jobs:
1313
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
1414
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
1515
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
16+
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
17+
NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}
1618

1719
steps:
1820
- uses: actions/checkout@v4

app/(chat)/api/document/route.ts

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,53 @@
11
import { auth } from '@/app/(auth)/auth';
2-
import { ArtifactKind } from '@/components/artifact';
2+
import type { ArtifactKind } from '@/components/artifact';
33
import {
44
deleteDocumentsByIdAfterTimestamp,
55
getDocumentsById,
66
saveDocument,
77
} from '@/lib/db/queries';
8+
import { apiErrors, successResponse } from '@/lib/responses';
89

910
export async function GET(request: Request) {
1011
const { searchParams } = new URL(request.url);
1112
const id = searchParams.get('id');
1213

1314
if (!id) {
14-
return new Response('Missing id', { status: 400 });
15+
return apiErrors.missingParameter();
1516
}
1617

1718
const session = await auth();
1819

19-
if (!session || !session.user) {
20-
return new Response('Unauthorized', { status: 401 });
20+
if (!session?.user?.id) {
21+
return apiErrors.unauthorized();
2122
}
2223

2324
const documents = await getDocumentsById({ id });
2425

2526
const [document] = documents;
2627

2728
if (!document) {
28-
return new Response('Not Found', { status: 404 });
29+
return apiErrors.documentNotFound();
2930
}
3031

3132
if (document.userId !== session.user.id) {
32-
return new Response('Unauthorized', { status: 401 });
33+
return apiErrors.documentForbidden();
3334
}
3435

35-
return Response.json(documents, { status: 200 });
36+
return successResponse(documents);
3637
}
3738

3839
export async function POST(request: Request) {
3940
const { searchParams } = new URL(request.url);
4041
const id = searchParams.get('id');
4142

4243
if (!id) {
43-
return new Response('Missing id', { status: 400 });
44+
return apiErrors.missingParameter();
4445
}
4546

4647
const session = await auth();
4748

4849
if (!session) {
49-
return new Response('Unauthorized', { status: 401 });
50+
return apiErrors.unauthorized();
5051
}
5152

5253
const {
@@ -56,49 +57,62 @@ export async function POST(request: Request) {
5657
}: { content: string; title: string; kind: ArtifactKind } =
5758
await request.json();
5859

59-
if (session.user?.id) {
60-
const document = await saveDocument({
61-
id,
62-
content,
63-
title,
64-
kind,
65-
userId: session.user.id,
66-
});
60+
if (!session?.user?.id) {
61+
return apiErrors.unauthorized();
62+
}
63+
64+
const documents = await getDocumentsById({ id: id });
65+
66+
if (documents.length > 0) {
67+
const [document] = documents;
6768

68-
return Response.json(document, { status: 200 });
69+
if (document.userId !== session.user.id) {
70+
return apiErrors.documentForbidden();
71+
}
6972
}
7073

71-
return new Response('Unauthorized', { status: 401 });
74+
const [createdDocument] = await saveDocument({
75+
id,
76+
content,
77+
title,
78+
kind,
79+
userId: session.user.id,
80+
});
81+
82+
return successResponse(createdDocument);
7283
}
7384

74-
export async function PATCH(request: Request) {
85+
export async function DELETE(request: Request) {
7586
const { searchParams } = new URL(request.url);
7687
const id = searchParams.get('id');
77-
78-
const { timestamp }: { timestamp: string } = await request.json();
88+
const timestamp = searchParams.get('timestamp');
7989

8090
if (!id) {
81-
return new Response('Missing id', { status: 400 });
91+
return apiErrors.missingParameter();
92+
}
93+
94+
if (!timestamp) {
95+
return apiErrors.missingParameter();
8296
}
8397

8498
const session = await auth();
8599

86-
if (!session || !session.user) {
87-
return new Response('Unauthorized', { status: 401 });
100+
if (!session?.user?.id) {
101+
return apiErrors.unauthorized();
88102
}
89103

90104
const documents = await getDocumentsById({ id });
91105

92106
const [document] = documents;
93107

94108
if (document.userId !== session.user.id) {
95-
return new Response('Unauthorized', { status: 401 });
109+
return apiErrors.documentForbidden();
96110
}
97111

98-
await deleteDocumentsByIdAfterTimestamp({
112+
const deletedDocuments = await deleteDocumentsByIdAfterTimestamp({
99113
id,
100114
timestamp: new Date(timestamp),
101115
});
102116

103-
return new Response('Deleted', { status: 200 });
117+
return successResponse(deletedDocuments);
104118
}

biome.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
"formatter": { "enabled": false },
117117
"linter": { "enabled": false }
118118
},
119-
"organizeImports": { "enabled": false },
119+
"organizeImports": { "enabled": true },
120120
"overrides": [
121121
// Playwright requires an object destructure, even if empty
122122
// https://github.com/microsoft/playwright/issues/30007

components/version-footer.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ export const VersionFooter = ({
5757

5858
mutate(
5959
`/api/document?id=${artifact.documentId}`,
60-
await fetch(`/api/document?id=${artifact.documentId}`, {
61-
method: 'PATCH',
62-
body: JSON.stringify({
63-
timestamp: getDocumentTimestampByIndex(
64-
documents,
65-
currentVersionIndex,
66-
),
67-
}),
68-
}),
60+
await fetch(
61+
`/api/document?id=${artifact.documentId}&timestamp=${getDocumentTimestampByIndex(
62+
documents,
63+
currentVersionIndex,
64+
)}`,
65+
{
66+
method: 'DELETE',
67+
},
68+
),
6969
{
7070
optimisticData: documents
7171
? [

lib/db/queries.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import 'server-only';
22

33
import { genSaltSync, hashSync } from 'bcrypt-ts';
4-
import { and, asc, desc, eq, gt, gte, inArray, lt, SQL } from 'drizzle-orm';
4+
import {
5+
and,
6+
asc,
7+
desc,
8+
eq,
9+
gt,
10+
gte,
11+
inArray,
12+
lt,
13+
type SQL,
14+
} from 'drizzle-orm';
515
import { drizzle } from 'drizzle-orm/postgres-js';
616
import postgres from 'postgres';
717

@@ -15,9 +25,9 @@ import {
1525
message,
1626
vote,
1727
type DBMessage,
18-
Chat,
28+
type Chat,
1929
} from './schema';
20-
import { ArtifactKind } from '@/components/artifact';
30+
import type { ArtifactKind } from '@/components/artifact';
2131

2232
// Optionally, if not using email/pass login, you can
2333
// use the Drizzle adapter for Auth.js / NextAuth
@@ -241,14 +251,17 @@ export async function saveDocument({
241251
userId: string;
242252
}) {
243253
try {
244-
return await db.insert(document).values({
245-
id,
246-
title,
247-
kind,
248-
content,
249-
userId,
250-
createdAt: new Date(),
251-
});
254+
return await db
255+
.insert(document)
256+
.values({
257+
id,
258+
title,
259+
kind,
260+
content,
261+
userId,
262+
createdAt: new Date(),
263+
})
264+
.returning();
252265
} catch (error) {
253266
console.error('Failed to save document in database');
254267
throw error;
@@ -304,7 +317,8 @@ export async function deleteDocumentsByIdAfterTimestamp({
304317

305318
return await db
306319
.delete(document)
307-
.where(and(eq(document.id, id), gt(document.createdAt, timestamp)));
320+
.where(and(eq(document.id, id), gt(document.createdAt, timestamp)))
321+
.returning();
308322
} catch (error) {
309323
console.error(
310324
'Failed to delete documents by id after timestamp from database',

lib/errors.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const ERRORS = {
2+
MISSING_PARAMETER: {
3+
type: 'missing_parameter',
4+
message: 'Missing parameter',
5+
},
6+
UNAUTHORIZED: {
7+
type: 'unauthorized',
8+
message: 'Unauthorized',
9+
},
10+
DOCUMENT_NOT_FOUND: {
11+
type: 'document_not_found',
12+
message: 'Document not found',
13+
},
14+
DOCUMENT_FORBIDDEN: {
15+
type: 'document_forbidden',
16+
message:
17+
'Access to this document is forbidden. You may not have the required permissions.',
18+
},
19+
};

lib/responses.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ERRORS } from './errors';
2+
3+
export function successResponse(data: any) {
4+
return Response.json({ data, error: null }, { status: 200 });
5+
}
6+
7+
export function errorResponse(
8+
error: { type: string; message: string },
9+
status: number,
10+
) {
11+
return Response.json({ data: null, error }, { status });
12+
}
13+
14+
export const apiErrors = {
15+
missingParameter: () => errorResponse(ERRORS.MISSING_PARAMETER, 400),
16+
unauthorized: () => errorResponse(ERRORS.UNAUTHORIZED, 401),
17+
documentNotFound: () => errorResponse(ERRORS.DOCUMENT_NOT_FOUND, 404),
18+
documentForbidden: () => errorResponse(ERRORS.DOCUMENT_FORBIDDEN, 403),
19+
};

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"db:pull": "drizzle-kit pull",
1717
"db:check": "drizzle-kit check",
1818
"db:up": "drizzle-kit up",
19-
"test": "export PLAYWRIGHT=True && pnpm exec playwright test --workers=4"
19+
"test": "pnpm tsx tests/run-tests"
2020
},
2121
"dependencies": {
2222
"@ai-sdk/react": "^1.2.8",
@@ -92,6 +92,7 @@
9292
"@types/pdf-parse": "^1.1.4",
9393
"@types/react": "^18",
9494
"@types/react-dom": "^18",
95+
"@types/wait-on": "^5.3.4",
9596
"drizzle-kit": "^0.25.0",
9697
"eslint": "^8.57.0",
9798
"eslint-config-next": "14.2.5",
@@ -101,7 +102,8 @@
101102
"postcss": "^8",
102103
"tailwindcss": "^3.4.1",
103104
"tsx": "^4.19.1",
104-
"typescript": "^5.6.3"
105+
"typescript": "^5.6.3",
106+
"wait-on": "^8.0.3"
105107
},
106108
"packageManager": "[email protected]"
107109
}

playwright.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ export default defineConfig({
9191
storageState: 'playwright/.auth/session.json',
9292
},
9393
},
94+
{
95+
name: 'routes',
96+
testMatch: /routes\/.*\.test.ts/,
97+
dependencies: [],
98+
use: {
99+
...devices['Desktop Chrome'],
100+
},
101+
},
94102

95103
// {
96104
// name: 'firefox',

0 commit comments

Comments
 (0)