Skip to content

feat: add tests for routes #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -70,3 +72,12 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 7

- name: Process test result
if: always()
run: |
if [[ "${{ steps.run-tests.outputs.tests-passed }}" == "true" ]]; then
echo "All tests passed!"
else
echo "Some tests failed"
fi
70 changes: 42 additions & 28 deletions app/(chat)/api/document/route.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,53 @@
import { auth } from '@/app/(auth)/auth';
import { ArtifactKind } from '@/components/artifact';
import type { ArtifactKind } from '@/components/artifact';
import {
deleteDocumentsByIdAfterTimestamp,
getDocumentsById,
saveDocument,
} from '@/lib/db/queries';
import { apiErrors, successResponse } from '@/lib/responses';

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

if (!id) {
return new Response('Missing id', { status: 400 });
return apiErrors.missingParameter();
}

const session = await auth();

if (!session || !session.user) {
return new Response('Unauthorized', { status: 401 });
if (!session?.user?.id) {
return apiErrors.unauthorized();
}

const documents = await getDocumentsById({ id });

const [document] = documents;

if (!document) {
return new Response('Not Found', { status: 404 });
return apiErrors.documentNotFound();
}

if (document.userId !== session.user.id) {
return new Response('Unauthorized', { status: 401 });
return apiErrors.documentForbidden();
}

return Response.json(documents, { status: 200 });
return successResponse(documents);
}

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

if (!id) {
return new Response('Missing id', { status: 400 });
return apiErrors.missingParameter();
}

const session = await auth();

if (!session) {
return new Response('Unauthorized', { status: 401 });
return apiErrors.unauthorized();
}

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

if (session.user?.id) {
const document = await saveDocument({
id,
content,
title,
kind,
userId: session.user.id,
});
if (!session?.user?.id) {
return apiErrors.unauthorized();
}

const documents = await getDocumentsById({ id: id });

if (documents.length > 0) {
const [document] = documents;

return Response.json(document, { status: 200 });
if (document.userId !== session.user.id) {
return apiErrors.documentForbidden();
}
}

return new Response('Unauthorized', { status: 401 });
const [createdDocument] = await saveDocument({
id,
content,
title,
kind,
userId: session.user.id,
});

return successResponse(createdDocument);
}

export async function PATCH(request: Request) {
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');

const { timestamp }: { timestamp: string } = await request.json();
const timestamp = searchParams.get('timestamp');

if (!id) {
return new Response('Missing id', { status: 400 });
return apiErrors.missingParameter();
}

if (!timestamp) {
return apiErrors.missingParameter();
}

const session = await auth();

if (!session || !session.user) {
return new Response('Unauthorized', { status: 401 });
if (!session?.user?.id) {
return apiErrors.unauthorized();
}

const documents = await getDocumentsById({ id });

const [document] = documents;

if (document.userId !== session.user.id) {
return new Response('Unauthorized', { status: 401 });
return apiErrors.documentForbidden();
}

await deleteDocumentsByIdAfterTimestamp({
const deletedDocuments = await deleteDocumentsByIdAfterTimestamp({
id,
timestamp: new Date(timestamp),
});

return new Response('Deleted', { status: 200 });
return successResponse(deletedDocuments);
}
2 changes: 1 addition & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"formatter": { "enabled": false },
"linter": { "enabled": false }
},
"organizeImports": { "enabled": false },
"organizeImports": { "enabled": true },
"overrides": [
// Playwright requires an object destructure, even if empty
// https://github.com/microsoft/playwright/issues/30007
Expand Down
18 changes: 9 additions & 9 deletions components/version-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ export const VersionFooter = ({

mutate(
`/api/document?id=${artifact.documentId}`,
await fetch(`/api/document?id=${artifact.documentId}`, {
method: 'PATCH',
body: JSON.stringify({
timestamp: getDocumentTimestampByIndex(
documents,
currentVersionIndex,
),
}),
}),
await fetch(
`/api/document?id=${artifact.documentId}&timestamp=${getDocumentTimestampByIndex(
documents,
currentVersionIndex,
)}`,
{
method: 'DELETE',
},
),
{
optimisticData: documents
? [
Expand Down
38 changes: 26 additions & 12 deletions lib/db/queries.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import 'server-only';

import { genSaltSync, hashSync } from 'bcrypt-ts';
import { and, asc, desc, eq, gt, gte, inArray, lt, SQL } from 'drizzle-orm';
import {
and,
asc,
desc,
eq,
gt,
gte,
inArray,
lt,
type SQL,
} from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

Expand All @@ -15,9 +25,9 @@ import {
message,
vote,
type DBMessage,
Chat,
type Chat,
} from './schema';
import { ArtifactKind } from '@/components/artifact';
import type { ArtifactKind } from '@/components/artifact';

// Optionally, if not using email/pass login, you can
// use the Drizzle adapter for Auth.js / NextAuth
Expand Down Expand Up @@ -241,14 +251,17 @@ export async function saveDocument({
userId: string;
}) {
try {
return await db.insert(document).values({
id,
title,
kind,
content,
userId,
createdAt: new Date(),
});
return await db
.insert(document)
.values({
id,
title,
kind,
content,
userId,
createdAt: new Date(),
})
.returning();
} catch (error) {
console.error('Failed to save document in database');
throw error;
Expand Down Expand Up @@ -304,7 +317,8 @@ export async function deleteDocumentsByIdAfterTimestamp({

return await db
.delete(document)
.where(and(eq(document.id, id), gt(document.createdAt, timestamp)));
.where(and(eq(document.id, id), gt(document.createdAt, timestamp)))
.returning();
} catch (error) {
console.error(
'Failed to delete documents by id after timestamp from database',
Expand Down
19 changes: 19 additions & 0 deletions lib/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const ERRORS = {
MISSING_PARAMETER: {
type: 'missing_parameter',
message: 'Missing parameter',
},
UNAUTHORIZED: {
type: 'unauthorized',
message: 'Unauthorized',
},
DOCUMENT_NOT_FOUND: {
type: 'document_not_found',
message: 'Document not found',
},
DOCUMENT_FORBIDDEN: {
type: 'document_forbidden',
message:
'Access to this document is forbidden. You may not have the required permissions.',
},
};
19 changes: 19 additions & 0 deletions lib/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ERRORS } from './errors';

export function successResponse(data: any) {
return Response.json({ data, error: null }, { status: 200 });
}

export function errorResponse(
error: { type: string; message: string },
status: number,
) {
return Response.json({ data: null, error }, { status });
}

export const apiErrors = {
missingParameter: () => errorResponse(ERRORS.MISSING_PARAMETER, 400),
unauthorized: () => errorResponse(ERRORS.UNAUTHORIZED, 401),
documentNotFound: () => errorResponse(ERRORS.DOCUMENT_NOT_FOUND, 404),
documentForbidden: () => errorResponse(ERRORS.DOCUMENT_FORBIDDEN, 403),
};
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"db:pull": "drizzle-kit pull",
"db:check": "drizzle-kit check",
"db:up": "drizzle-kit up",
"test": "export PLAYWRIGHT=True && pnpm exec playwright test --workers=4"
"test": "pnpm tsx tests/run-tests"
},
"dependencies": {
"@ai-sdk/react": "^1.2.8",
Expand Down Expand Up @@ -92,6 +92,7 @@
"@types/pdf-parse": "^1.1.4",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/wait-on": "^5.3.4",
"drizzle-kit": "^0.25.0",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.5",
Expand All @@ -101,7 +102,8 @@
"postcss": "^8",
"tailwindcss": "^3.4.1",
"tsx": "^4.19.1",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"wait-on": "^8.0.3"
},
"packageManager": "[email protected]"
}
18 changes: 9 additions & 9 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: [['line']],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
Expand Down Expand Up @@ -91,6 +91,14 @@ export default defineConfig({
storageState: 'playwright/.auth/session.json',
},
},
{
name: 'routes',
testMatch: /routes\/.*\.test.ts/,
dependencies: [],
use: {
...devices['Desktop Chrome'],
},
},

// {
// name: 'firefox',
Expand Down Expand Up @@ -122,12 +130,4 @@ export default defineConfig({
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
webServer: {
command: 'pnpm dev',
url: baseURL,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
});
Loading