feat: add tests for /api/document (#932)
This commit is contained in:
parent
5b4ad941d3
commit
020494f63b
6 changed files with 316 additions and 19 deletions
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ai-chatbot",
|
||||
"version": "3.0.2",
|
||||
"version": "3.0.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
|
|
|
|||
|
|
@ -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
62
tests/auth-helper.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
213
tests/routes/document.test.ts
Normal file
213
tests/routes/document.test.ts
Normal 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}×tamp=${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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue