feat: add tests for /api/chat (#950)
This commit is contained in:
parent
4ca93aaf89
commit
a159b77fcf
9 changed files with 197 additions and 50 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
UIMessage,
|
type UIMessage,
|
||||||
appendResponseMessages,
|
appendResponseMessages,
|
||||||
createDataStreamResponse,
|
createDataStreamResponse,
|
||||||
smoothStream,
|
smoothStream,
|
||||||
|
|
@ -42,7 +42,7 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session || !session.user || !session.user.id) {
|
if (!session?.user?.id) {
|
||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ export async function POST(request: Request) {
|
||||||
await saveChat({ id, userId: session.user.id, title });
|
await saveChat({ id, userId: session.user.id, title });
|
||||||
} else {
|
} else {
|
||||||
if (chat.userId !== session.user.id) {
|
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) {
|
} catch (error) {
|
||||||
return new Response('An error occurred while processing your request!', {
|
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();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session?.user?.id) {
|
||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,12 +183,12 @@ export async function DELETE(request: Request) {
|
||||||
const chat = await getChatById({ id });
|
const chat = await getChatById({ id });
|
||||||
|
|
||||||
if (chat.userId !== session.user.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) {
|
} catch (error) {
|
||||||
return new Response('An error occurred while processing your request!', {
|
return new Response('An error occurred while processing your request!', {
|
||||||
status: 500,
|
status: 500,
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,11 @@ export async function deleteChatById({ id }: { id: string }) {
|
||||||
await db.delete(vote).where(eq(vote.chatId, id));
|
await db.delete(vote).where(eq(vote.chatId, id));
|
||||||
await db.delete(message).where(eq(message.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) {
|
} catch (error) {
|
||||||
console.error('Failed to delete chat by id from database');
|
console.error('Failed to delete chat by id from database');
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ai-chatbot",
|
"name": "ai-chatbot",
|
||||||
"version": "3.0.4",
|
"version": "3.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"db:pull": "drizzle-kit pull",
|
"db:pull": "drizzle-kit pull",
|
||||||
"db:check": "drizzle-kit check",
|
"db:check": "drizzle-kit check",
|
||||||
"db:up": "drizzle-kit up",
|
"db:up": "drizzle-kit up",
|
||||||
"test": "export PLAYWRIGHT=True && pnpm exec playwright test --workers=4"
|
"test": "export PLAYWRIGHT=True && pnpm exec playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/react": "^1.2.8",
|
"@ai-sdk/react": "^1.2.8",
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default defineConfig({
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 2 : 1,
|
retries: process.env.CI ? 2 : 1,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* 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 to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
|
|
||||||
34
tests/fixtures.ts
Normal file
34
tests/fixtures.ts
Normal file
|
|
@ -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<any, Fixtures>({
|
||||||
|
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;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CoreMessage } from 'ai';
|
import type { CoreMessage } from 'ai';
|
||||||
|
|
||||||
export const TEST_PROMPTS: Record<string, CoreMessage> = {
|
export const TEST_PROMPTS: Record<string, CoreMessage> = {
|
||||||
USER_SKY: {
|
USER_SKY: {
|
||||||
|
|
|
||||||
36
tests/prompts/routes.ts
Normal file
36
tests/prompts/routes.ts
Normal file
|
|
@ -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}}',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
81
tests/routes/chat.test.ts
Normal file
81
tests/routes/chat.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { generateUUID } from '@/lib/utils';
|
||||||
|
import { expect, test } from '../fixtures';
|
||||||
|
import { TEST_PROMPTS } from '../prompts/routes';
|
||||||
|
|
||||||
|
const chatIdsCreatedByAda: Array<string> = [];
|
||||||
|
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,36 +1,14 @@
|
||||||
import type { Document } from '@/lib/db/schema';
|
import type { Document } from '@/lib/db/schema';
|
||||||
import { generateUUID } from '@/lib/utils';
|
import { generateUUID } from '@/lib/utils';
|
||||||
import {
|
import { expect, test } from '../fixtures';
|
||||||
createAuthenticatedContext,
|
|
||||||
type UserContext,
|
|
||||||
} from '@/tests/auth-helper';
|
|
||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
let adaContext: UserContext;
|
|
||||||
let babbageContext: UserContext;
|
|
||||||
|
|
||||||
const documentsCreatedByAda: Array<Document> = [];
|
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
|
test.describe
|
||||||
.serial('/api/document', () => {
|
.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');
|
const response = await adaContext.request.get('/api/document');
|
||||||
expect(response.status()).toBe(400);
|
expect(response.status()).toBe(400);
|
||||||
|
|
||||||
|
|
@ -38,7 +16,9 @@ test.describe
|
||||||
expect(text).toEqual('Missing id');
|
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 documentId = generateUUID();
|
||||||
|
|
||||||
const response = await adaContext.request.get(
|
const response = await adaContext.request.get(
|
||||||
|
|
@ -50,7 +30,7 @@ test.describe
|
||||||
expect(text).toEqual('Not found');
|
expect(text).toEqual('Not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Ada can create a document', async () => {
|
test('Ada can create a document', async ({ adaContext }) => {
|
||||||
const documentId = generateUUID();
|
const documentId = generateUUID();
|
||||||
|
|
||||||
const draftDocument = {
|
const draftDocument = {
|
||||||
|
|
@ -73,7 +53,7 @@ test.describe
|
||||||
documentsCreatedByAda.push(createdDocument);
|
documentsCreatedByAda.push(createdDocument);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Ada can retrieve a created document', async () => {
|
test('Ada can retrieve a created document', async ({ adaContext }) => {
|
||||||
const [document] = documentsCreatedByAda;
|
const [document] = documentsCreatedByAda;
|
||||||
|
|
||||||
const response = await adaContext.request.get(
|
const response = await adaContext.request.get(
|
||||||
|
|
@ -88,7 +68,9 @@ test.describe
|
||||||
expect(retrievedDocument).toMatchObject(document);
|
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 [firstDocument] = documentsCreatedByAda;
|
||||||
|
|
||||||
const draftDocument = {
|
const draftDocument = {
|
||||||
|
|
@ -111,7 +93,9 @@ test.describe
|
||||||
documentsCreatedByAda.push(createdDocument);
|
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 [firstDocument, secondDocument] = documentsCreatedByAda;
|
||||||
|
|
||||||
const response = await adaContext.request.get(
|
const response = await adaContext.request.get(
|
||||||
|
|
@ -128,7 +112,9 @@ test.describe
|
||||||
expect(secondRetrievedDocument).toMatchObject(secondDocument);
|
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`);
|
const response = await adaContext.request.delete(`/api/document`);
|
||||||
expect(response.status()).toBe(400);
|
expect(response.status()).toBe(400);
|
||||||
|
|
||||||
|
|
@ -136,7 +122,9 @@ test.describe
|
||||||
expect(text).toEqual('Missing id');
|
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 [firstDocument] = documentsCreatedByAda;
|
||||||
|
|
||||||
const response = await adaContext.request.delete(
|
const response = await adaContext.request.delete(
|
||||||
|
|
@ -148,7 +136,9 @@ test.describe
|
||||||
expect(text).toEqual('Missing timestamp');
|
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 [firstDocument, secondDocument] = documentsCreatedByAda;
|
||||||
|
|
||||||
const response = await adaContext.request.delete(
|
const response = await adaContext.request.delete(
|
||||||
|
|
@ -163,7 +153,9 @@ test.describe
|
||||||
expect(deletedDocument).toMatchObject(secondDocument);
|
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 [firstDocument] = documentsCreatedByAda;
|
||||||
|
|
||||||
const response = await adaContext.request.get(
|
const response = await adaContext.request.get(
|
||||||
|
|
@ -178,7 +170,7 @@ test.describe
|
||||||
expect(firstRetrievedDocument).toMatchObject(firstDocument);
|
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 [firstDocument] = documentsCreatedByAda;
|
||||||
|
|
||||||
const draftDocument = {
|
const draftDocument = {
|
||||||
|
|
@ -199,7 +191,7 @@ test.describe
|
||||||
expect(text).toEqual('Forbidden');
|
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 [firstDocument] = documentsCreatedByAda;
|
||||||
|
|
||||||
const response = await adaContext.request.get(
|
const response = await adaContext.request.get(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue