feat: add tests for /api/document (#932)

This commit is contained in:
Jeremy 2025-04-14 10:34:11 -07:00 committed by GitHub
parent 5b4ad941d3
commit 020494f63b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 316 additions and 19 deletions

View file

@ -16,7 +16,7 @@ export async function GET(request: Request) {
const session = await auth();
if (!session || !session.user) {
if (!session?.user?.id) {
return new Response('Unauthorized', { status: 401 });
}
@ -25,11 +25,11 @@ export async function GET(request: Request) {
const [document] = documents;
if (!document) {
return new Response('Not Found', { status: 404 });
return new Response('Not found', { status: 404 });
}
if (document.userId !== session.user.id) {
return new Response('Unauthorized', { status: 401 });
return new Response('Forbidden', { status: 403 });
}
return Response.json(documents, { status: 200 });
@ -56,7 +56,7 @@ export async function POST(request: Request) {
}: { content: string; title: string; kind: ArtifactKind } =
await request.json();
const documents = await getDocumentsById({ id: id });
const documents = await getDocumentsById({ id });
if (documents.length > 0) {
const [document] = documents;
@ -104,10 +104,10 @@ export async function DELETE(request: Request) {
return new Response('Unauthorized', { status: 401 });
}
await deleteDocumentsByIdAfterTimestamp({
const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({
id,
timestamp: new Date(timestamp),
});
return new Response('Deleted', { status: 200 });
return Response.json(documentsDeleted, { status: 200 });
}

View file

@ -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';
@ -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
@ -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;
@ -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',

View file

@ -1,6 +1,6 @@
{
"name": "ai-chatbot",
"version": "3.0.2",
"version": "3.0.3",
"private": true,
"scripts": {
"dev": "next dev --turbo",

View file

@ -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',

62
tests/auth-helper.ts Normal file
View file

@ -0,0 +1,62 @@
import fs from 'node:fs';
import path from 'node:path';
import {
type APIRequestContext,
type Browser,
type BrowserContext,
expect,
type Page,
} from '@playwright/test';
import { generateId } from 'ai';
import { getUnixTime } from 'date-fns';
export type UserContext = {
context: BrowserContext;
page: Page;
request: APIRequestContext;
};
export async function createAuthenticatedContext({
browser,
name,
}: {
browser: Browser;
name: string;
}): Promise<UserContext> {
const authDir = path.join(__dirname, '../playwright/.auth');
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true });
}
const storageFile = path.join(authDir, `${name}.json`);
const context = await browser.newContext();
const page = await context.newPage();
const email = `test-${name}-${getUnixTime(new Date())}@playwright.com`;
const password = generateId(16);
await page.goto('http://localhost:3000/register');
await page.getByPlaceholder('user@acme.com').click();
await page.getByPlaceholder('user@acme.com').fill(email);
await page.getByLabel('Password').click();
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign Up' }).click();
await expect(page.getByTestId('toast')).toContainText(
'Account created successfully!',
);
await context.storageState({ path: storageFile });
await page.close();
const newContext = await browser.newContext({ storageState: storageFile });
const newPage = await newContext.newPage();
return {
context: newContext,
page: newPage,
request: newContext.request,
};
}

View file

@ -0,0 +1,213 @@
import type { Document } from '@/lib/db/schema';
import { generateUUID } from '@/lib/utils';
import {
createAuthenticatedContext,
type UserContext,
} from '@/tests/auth-helper';
import { expect, test } from '@playwright/test';
let adaContext: UserContext;
let babbageContext: UserContext;
const documentsCreatedByAda: Array<Document> = [];
test.beforeAll(async ({ browser }) => {
adaContext = await createAuthenticatedContext({
browser,
name: 'ada',
});
babbageContext = await createAuthenticatedContext({
browser,
name: 'babbage',
});
});
test.afterAll(async () => {
await adaContext.context.close();
await babbageContext.context.close();
});
test.describe
.serial('/api/document', () => {
test('Ada cannot retrieve a document without specifying an id', async () => {
const response = await adaContext.request.get('/api/document');
expect(response.status()).toBe(400);
const text = await response.text();
expect(text).toEqual('Missing id');
});
test('Ada cannot retrieve a document that does not exist', async () => {
const documentId = generateUUID();
const response = await adaContext.request.get(
`/api/document?id=${documentId}`,
);
expect(response.status()).toBe(404);
const text = await response.text();
expect(text).toEqual('Not found');
});
test('Ada can create a document', async () => {
const documentId = generateUUID();
const draftDocument = {
title: "Ada's Document",
kind: 'text',
content: 'Created by Ada',
};
const response = await adaContext.request.post(
`/api/document?id=${documentId}`,
{
data: draftDocument,
},
);
expect(response.status()).toBe(200);
const [createdDocument] = await response.json();
expect(createdDocument).toMatchObject(draftDocument);
documentsCreatedByAda.push(createdDocument);
});
test('Ada can retrieve a created document', async () => {
const [document] = documentsCreatedByAda;
const response = await adaContext.request.get(
`/api/document?id=${document.id}`,
);
expect(response.status()).toBe(200);
const retrievedDocuments = await response.json();
expect(retrievedDocuments).toHaveLength(1);
const [retrievedDocument] = retrievedDocuments;
expect(retrievedDocument).toMatchObject(document);
});
test('Ada can save a new version of the document', async () => {
const [firstDocument] = documentsCreatedByAda;
const draftDocument = {
title: "Ada's Document",
kind: 'text',
content: 'Updated by Ada',
};
const response = await adaContext.request.post(
`/api/document?id=${firstDocument.id}`,
{
data: draftDocument,
},
);
expect(response.status()).toBe(200);
const [createdDocument] = await response.json();
expect(createdDocument).toMatchObject(draftDocument);
documentsCreatedByAda.push(createdDocument);
});
test('Ada can retrieve all versions of her documents', async () => {
const [firstDocument, secondDocument] = documentsCreatedByAda;
const response = await adaContext.request.get(
`/api/document?id=${firstDocument.id}`,
);
expect(response.status()).toBe(200);
const retrievedDocuments = await response.json();
expect(retrievedDocuments).toHaveLength(2);
const [firstRetrievedDocument, secondRetrievedDocument] =
retrievedDocuments;
expect(firstRetrievedDocument).toMatchObject(firstDocument);
expect(secondRetrievedDocument).toMatchObject(secondDocument);
});
test('Ada cannot delete a document without specifying an id', async () => {
const response = await adaContext.request.delete(`/api/document`);
expect(response.status()).toBe(400);
const text = await response.text();
expect(text).toEqual('Missing id');
});
test('Ada cannot delete a document without specifying a timestamp', async () => {
const [firstDocument] = documentsCreatedByAda;
const response = await adaContext.request.delete(
`/api/document?id=${firstDocument.id}`,
);
expect(response.status()).toBe(400);
const text = await response.text();
expect(text).toEqual('Missing timestamp');
});
test('Ada can delete a document by specifying id and timestamp', async () => {
const [firstDocument, secondDocument] = documentsCreatedByAda;
const response = await adaContext.request.delete(
`/api/document?id=${firstDocument.id}&timestamp=${firstDocument.createdAt}`,
);
expect(response.status()).toBe(200);
const deletedDocuments = await response.json();
expect(deletedDocuments).toHaveLength(1);
const [deletedDocument] = deletedDocuments;
expect(deletedDocument).toMatchObject(secondDocument);
});
test('Ada can retrieve documents without deleted versions', async () => {
const [firstDocument] = documentsCreatedByAda;
const response = await adaContext.request.get(
`/api/document?id=${firstDocument.id}`,
);
expect(response.status()).toBe(200);
const retrievedDocuments = await response.json();
expect(retrievedDocuments).toHaveLength(1);
const [firstRetrievedDocument] = retrievedDocuments;
expect(firstRetrievedDocument).toMatchObject(firstDocument);
});
test("Babbage cannot update Ada's document", async () => {
const [firstDocument] = documentsCreatedByAda;
const draftDocument = {
title: "Babbage's Document",
kind: 'text',
content: 'Created by Babbage',
};
const response = await babbageContext.request.post(
`/api/document?id=${firstDocument.id}`,
{
data: draftDocument,
},
);
expect(response.status()).toBe(403);
const text = await response.text();
expect(text).toEqual('Forbidden');
});
test("Ada's documents did not get updated", async () => {
const [firstDocument] = documentsCreatedByAda;
const response = await adaContext.request.get(
`/api/document?id=${firstDocument.id}`,
);
expect(response.status()).toBe(200);
const documentsRetrieved = await response.json();
expect(documentsRetrieved).toHaveLength(1);
});
});