diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index d0f3b98..2b3cffd 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1,5 +1,5 @@ import { - UIMessage, + type UIMessage, appendResponseMessages, createDataStreamResponse, smoothStream, @@ -42,7 +42,7 @@ export async function POST(request: Request) { const session = await auth(); - if (!session || !session.user || !session.user.id) { + if (!session?.user?.id) { return new Response('Unauthorized', { status: 401 }); } @@ -62,7 +62,7 @@ export async function POST(request: Request) { await saveChat({ id, userId: session.user.id, title }); } else { if (chat.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response('Forbidden', { status: 403 }); } } @@ -160,7 +160,7 @@ export async function POST(request: Request) { }); } catch (error) { return new Response('An error occurred while processing your request!', { - status: 404, + status: 500, }); } } @@ -175,7 +175,7 @@ export async function DELETE(request: Request) { const session = await auth(); - if (!session || !session.user) { + if (!session?.user?.id) { return new Response('Unauthorized', { status: 401 }); } @@ -183,12 +183,12 @@ export async function DELETE(request: Request) { const chat = await getChatById({ id }); if (chat.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response('Forbidden', { status: 403 }); } - await deleteChatById({ id }); + const deletedChat = await deleteChatById({ id }); - return new Response('Chat deleted', { status: 200 }); + return Response.json(deletedChat, { status: 200 }); } catch (error) { return new Response('An error occurred while processing your request!', { status: 500, diff --git a/lib/db/queries.ts b/lib/db/queries.ts index f83c50b..d79e7a1 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -85,7 +85,11 @@ export async function deleteChatById({ id }: { id: string }) { await db.delete(vote).where(eq(vote.chatId, id)); await db.delete(message).where(eq(message.chatId, id)); - return await db.delete(chat).where(eq(chat.id, id)); + const [chatsDeleted] = await db + .delete(chat) + .where(eq(chat.id, id)) + .returning(); + return chatsDeleted; } catch (error) { console.error('Failed to delete chat by id from database'); throw error; diff --git a/package.json b/package.json index c6165e6..926741d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ai-chatbot", - "version": "3.0.4", + "version": "3.0.5", "private": true, "scripts": { "dev": "next dev --turbo", @@ -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": "export PLAYWRIGHT=True && pnpm exec playwright test" }, "dependencies": { "@ai-sdk/react": "^1.2.8", diff --git a/playwright.config.ts b/playwright.config.ts index ef0772c..cb02180 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -31,7 +31,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 1, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 0000000..07eea52 --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,34 @@ +import { expect as baseExpect, test as baseTest } from '@playwright/test'; +import { createAuthenticatedContext, type UserContext } from './auth-helper'; + +interface Fixtures { + adaContext: UserContext; + babbageContext: UserContext; +} + +export const test = baseTest.extend({ + adaContext: [ + async ({ browser }, use) => { + const ada = await createAuthenticatedContext({ + browser, + name: 'ada', + }); + await use(ada); + await ada.context.close(); + }, + { scope: 'worker' }, + ], + babbageContext: [ + async ({ browser }, use) => { + const babbage = await createAuthenticatedContext({ + browser, + name: 'babbage', + }); + await use(babbage); + await babbage.context.close(); + }, + { scope: 'worker' }, + ], +}); + +export const expect = baseExpect; diff --git a/tests/prompts/basic.ts b/tests/prompts/basic.ts index 4835e33..4dc634d 100644 --- a/tests/prompts/basic.ts +++ b/tests/prompts/basic.ts @@ -1,4 +1,4 @@ -import { CoreMessage } from 'ai'; +import type { CoreMessage } from 'ai'; export const TEST_PROMPTS: Record = { USER_SKY: { diff --git a/tests/prompts/routes.ts b/tests/prompts/routes.ts new file mode 100644 index 0000000..e90a94b --- /dev/null +++ b/tests/prompts/routes.ts @@ -0,0 +1,36 @@ +export const TEST_PROMPTS = { + SKY: { + MESSAGES: [ + { + role: 'user', + content: 'Why is the sky blue?', + parts: [{ type: 'text', text: 'Why is the sky blue?' }], + }, + ], + OUTPUT_STREAM: [ + '0:"It\'s "', + '0:"just "', + '0:"blue "', + '0:"duh! "', + 'e:{"finishReason":"stop","usage":{"promptTokens":3,"completionTokens":10},"isContinued":false}', + 'd:{"finishReason":"stop","usage":{"promptTokens":3,"completionTokens":10}}', + ], + }, + GRASS: { + MESSAGES: [ + { + role: 'user', + content: 'Why is grass green?', + parts: [{ type: 'text', text: 'Why is grass green?' }], + }, + ], + OUTPUT_STREAM: [ + '0:"It\'s "', + '0:"just "', + '0:"green "', + '0:"duh! "', + 'e:{"finishReason":"stop","usage":{"promptTokens":3,"completionTokens":10},"isContinued":false}', + 'd:{"finishReason":"stop","usage":{"promptTokens":3,"completionTokens":10}}', + ], + }, +}; diff --git a/tests/routes/chat.test.ts b/tests/routes/chat.test.ts new file mode 100644 index 0000000..f55c42f --- /dev/null +++ b/tests/routes/chat.test.ts @@ -0,0 +1,81 @@ +import { generateUUID } from '@/lib/utils'; +import { expect, test } from '../fixtures'; +import { TEST_PROMPTS } from '../prompts/routes'; + +const chatIdsCreatedByAda: Array = []; + +test.describe + .serial('/api/chat', () => { + test('Ada cannot invoke a chat generation with empty request body', async ({ + adaContext, + }) => { + const response = await adaContext.request.post('/api/chat', { + data: {}, + }); + expect(response.status()).toBe(500); + + const text = await response.text(); + expect(text).toEqual('An error occurred while processing your request!'); + }); + + test('Ada can invoke chat generation', async ({ adaContext }) => { + const chatId = generateUUID(); + + const response = await adaContext.request.post('api/chat', { + data: { + id: chatId, + messages: TEST_PROMPTS.SKY.MESSAGES, + selectedChatModel: 'chat-model', + }, + }); + expect(response.status()).toBe(200); + + const text = await response.text(); + const lines = text.split('\n'); + + const [_, ...rest] = lines; + expect(rest.filter(Boolean)).toEqual(TEST_PROMPTS.SKY.OUTPUT_STREAM); + + chatIdsCreatedByAda.push(chatId); + }); + + test("Babbage cannot append message to Ada's chat", async ({ + babbageContext, + }) => { + const [chatId] = chatIdsCreatedByAda; + + const response = await babbageContext.request.post('api/chat', { + data: { + id: chatId, + messages: TEST_PROMPTS.GRASS.MESSAGES, + selectedChatModel: 'chat-model', + }, + }); + expect(response.status()).toBe(403); + + const text = await response.text(); + expect(text).toEqual('Forbidden'); + }); + + test("Babbage cannot delete Ada's chat", async ({ babbageContext }) => { + const [chatId] = chatIdsCreatedByAda; + + const response = await babbageContext.request.delete( + `api/chat?id=${chatId}`, + ); + expect(response.status()).toBe(403); + + const text = await response.text(); + expect(text).toEqual('Forbidden'); + }); + + test('Ada can delete her own chat', async ({ adaContext }) => { + const [chatId] = chatIdsCreatedByAda; + + const response = await adaContext.request.delete(`api/chat?id=${chatId}`); + expect(response.status()).toBe(200); + + const deletedChat = await response.json(); + expect(deletedChat).toMatchObject({ id: chatId }); + }); + }); diff --git a/tests/routes/document.test.ts b/tests/routes/document.test.ts index e1887b9..715be14 100644 --- a/tests/routes/document.test.ts +++ b/tests/routes/document.test.ts @@ -1,36 +1,14 @@ 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; +import { expect, test } from '../fixtures'; const documentsCreatedByAda: Array = []; -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 () => { + test('Ada cannot retrieve a document without specifying an id', async ({ + adaContext, + }) => { const response = await adaContext.request.get('/api/document'); expect(response.status()).toBe(400); @@ -38,7 +16,9 @@ test.describe expect(text).toEqual('Missing id'); }); - test('Ada cannot retrieve a document that does not exist', async () => { + test('Ada cannot retrieve a document that does not exist', async ({ + adaContext, + }) => { const documentId = generateUUID(); const response = await adaContext.request.get( @@ -50,7 +30,7 @@ test.describe expect(text).toEqual('Not found'); }); - test('Ada can create a document', async () => { + test('Ada can create a document', async ({ adaContext }) => { const documentId = generateUUID(); const draftDocument = { @@ -73,7 +53,7 @@ test.describe documentsCreatedByAda.push(createdDocument); }); - test('Ada can retrieve a created document', async () => { + test('Ada can retrieve a created document', async ({ adaContext }) => { const [document] = documentsCreatedByAda; const response = await adaContext.request.get( @@ -88,7 +68,9 @@ test.describe expect(retrievedDocument).toMatchObject(document); }); - test('Ada can save a new version of the document', async () => { + test('Ada can save a new version of the document', async ({ + adaContext, + }) => { const [firstDocument] = documentsCreatedByAda; const draftDocument = { @@ -111,7 +93,9 @@ test.describe documentsCreatedByAda.push(createdDocument); }); - test('Ada can retrieve all versions of her documents', async () => { + test('Ada can retrieve all versions of her documents', async ({ + adaContext, + }) => { const [firstDocument, secondDocument] = documentsCreatedByAda; const response = await adaContext.request.get( @@ -128,7 +112,9 @@ test.describe expect(secondRetrievedDocument).toMatchObject(secondDocument); }); - test('Ada cannot delete a document without specifying an id', async () => { + test('Ada cannot delete a document without specifying an id', async ({ + adaContext, + }) => { const response = await adaContext.request.delete(`/api/document`); expect(response.status()).toBe(400); @@ -136,7 +122,9 @@ test.describe expect(text).toEqual('Missing id'); }); - test('Ada cannot delete a document without specifying a timestamp', async () => { + test('Ada cannot delete a document without specifying a timestamp', async ({ + adaContext, + }) => { const [firstDocument] = documentsCreatedByAda; const response = await adaContext.request.delete( @@ -148,7 +136,9 @@ test.describe expect(text).toEqual('Missing timestamp'); }); - test('Ada can delete a document by specifying id and timestamp', async () => { + test('Ada can delete a document by specifying id and timestamp', async ({ + adaContext, + }) => { const [firstDocument, secondDocument] = documentsCreatedByAda; const response = await adaContext.request.delete( @@ -163,7 +153,9 @@ test.describe expect(deletedDocument).toMatchObject(secondDocument); }); - test('Ada can retrieve documents without deleted versions', async () => { + test('Ada can retrieve documents without deleted versions', async ({ + adaContext, + }) => { const [firstDocument] = documentsCreatedByAda; const response = await adaContext.request.get( @@ -178,7 +170,7 @@ test.describe expect(firstRetrievedDocument).toMatchObject(firstDocument); }); - test("Babbage cannot update Ada's document", async () => { + test("Babbage cannot update Ada's document", async ({ babbageContext }) => { const [firstDocument] = documentsCreatedByAda; const draftDocument = { @@ -199,7 +191,7 @@ test.describe expect(text).toEqual('Forbidden'); }); - test("Ada's documents did not get updated", async () => { + test("Ada's documents did not get updated", async ({ adaContext }) => { const [firstDocument] = documentsCreatedByAda; const response = await adaContext.request.get(