Skip to content

Commit 6ff2948

Browse files
committed
integration tests
1 parent 56cced8 commit 6ff2948

File tree

3 files changed

+186
-67
lines changed

3 files changed

+186
-67
lines changed

apps/interaction-worker/src/test/integration/health.test.ts

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -17,70 +17,3 @@ describe("Health check endpoints", () => {
1717
expect(data.database).toBe("connected");
1818
});
1919
});
20-
21-
describe("Webhook endpoint", () => {
22-
it("POST / should reject invalid webhook payload", async () => {
23-
const response = await SELF.fetch("https://example.com/", {
24-
method: "POST",
25-
headers: {
26-
"Content-Type": "application/json",
27-
},
28-
body: JSON.stringify({ invalid: "payload" }),
29-
});
30-
31-
expect(response.status).toBe(400);
32-
33-
const data = await response.json<{ success: boolean }>();
34-
expect(data.success).toBe(false);
35-
});
36-
37-
it("POST / should accept valid message_sent webhook", async () => {
38-
const validPayload = {
39-
alert_type: "message_sent",
40-
recipient: "+1234567890",
41-
success: true,
42-
message_id: "test-message-id",
43-
webhook_id: "test-webhook-id",
44-
text: "Test message",
45-
};
46-
47-
const response = await SELF.fetch("https://example.com/", {
48-
method: "POST",
49-
headers: {
50-
"Content-Type": "application/json",
51-
},
52-
body: JSON.stringify(validPayload),
53-
});
54-
55-
expect(response.status).toBe(200);
56-
57-
const data = await response.json<{ success: boolean; read: boolean }>();
58-
expect(data.success).toBe(true);
59-
expect(data.read).toBe(true);
60-
});
61-
62-
it("POST / should accept valid message_failed webhook", async () => {
63-
const validPayload = {
64-
alert_type: "message_failed",
65-
recipient: "+1234567890",
66-
error_code: 500,
67-
message_id: "test-message-id",
68-
webhook_id: "test-webhook-id",
69-
text: "Test message",
70-
};
71-
72-
const response = await SELF.fetch("https://example.com/", {
73-
method: "POST",
74-
headers: {
75-
"Content-Type": "application/json",
76-
},
77-
body: JSON.stringify(validPayload),
78-
});
79-
80-
expect(response.status).toBe(200);
81-
82-
const data = await response.json<{ success: boolean; read: boolean }>();
83-
expect(data.success).toBe(true);
84-
expect(data.read).toBe(true);
85-
});
86-
});
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { conversations, getDb, messages, parts, users } from "@poppy/db";
2+
import { env, SELF } from "cloudflare:test";
3+
import { eq } from "drizzle-orm";
4+
import { desc } from "drizzle-orm";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
6+
7+
// Mock the AI client module
8+
vi.mock("../../clients/ai/openrouter", () => ({
9+
createOpenRouterClient: vi.fn(() => ({
10+
gemini25: {},
11+
gpt4o: {},
12+
})),
13+
}));
14+
15+
// Mock the Loop Message client module
16+
vi.mock("../../clients/loop-message", () => ({
17+
createLoopClient: vi.fn(() => ({
18+
sendMessage: vi.fn().mockResolvedValue({
19+
success: true,
20+
message_id: "mock-message-id",
21+
}),
22+
})),
23+
}));
24+
25+
// Mock the AI SDK functions
26+
vi.mock("ai", async (importOriginal) => {
27+
const actual = await importOriginal<typeof import("ai")>();
28+
return {
29+
...actual,
30+
generateObject: vi.fn().mockResolvedValue({
31+
object: { shouldRespond: true },
32+
}),
33+
generateText: vi.fn().mockResolvedValue({
34+
text: "Mock AI response",
35+
usage: { promptTokens: 10, completionTokens: 10 },
36+
}),
37+
};
38+
});
39+
40+
describe("Webhook endpoint", () => {
41+
it("POST / should reject invalid webhook payload", async () => {
42+
const response = await SELF.fetch("https://example.com/", {
43+
method: "POST",
44+
headers: {
45+
"Content-Type": "application/json",
46+
},
47+
body: JSON.stringify({ invalid: "payload" }),
48+
});
49+
50+
expect(response.status).toBe(400);
51+
52+
const data = await response.json<{ success: boolean }>();
53+
expect(data.success).toBe(false);
54+
});
55+
56+
it("POST / should accept valid message_sent webhook", async () => {
57+
const validPayload = {
58+
alert_type: "message_sent",
59+
recipient: "+1234567890",
60+
success: true,
61+
message_id: "test-message-id",
62+
webhook_id: "test-webhook-id",
63+
text: "Test message",
64+
};
65+
66+
const response = await SELF.fetch("https://example.com/", {
67+
method: "POST",
68+
headers: {
69+
"Content-Type": "application/json",
70+
},
71+
body: JSON.stringify(validPayload),
72+
});
73+
74+
expect(response.status).toBe(200);
75+
76+
const data = await response.json<{ success: boolean; read: boolean }>();
77+
expect(data.success).toBe(true);
78+
expect(data.read).toBe(true);
79+
});
80+
81+
it("POST / should accept valid message_failed webhook", async () => {
82+
const validPayload = {
83+
alert_type: "message_failed",
84+
recipient: "+1234567890",
85+
error_code: 500,
86+
message_id: "test-message-id",
87+
webhook_id: "test-webhook-id",
88+
text: "Test message",
89+
};
90+
91+
const response = await SELF.fetch("https://example.com/", {
92+
method: "POST",
93+
headers: {
94+
"Content-Type": "application/json",
95+
},
96+
body: JSON.stringify(validPayload),
97+
});
98+
99+
expect(response.status).toBe(200);
100+
101+
const data = await response.json<{ success: boolean; read: boolean }>();
102+
expect(data.success).toBe(true);
103+
expect(data.read).toBe(true);
104+
});
105+
106+
it("POST / should accept valid message_inbound webhook and store in database", async () => {
107+
const testPhoneNumber = `+1${Date.now()}`; // Unique phone number for each test run
108+
const validPayload = {
109+
alert_type: "message_inbound",
110+
message_id: `test-msg-${Date.now()}`,
111+
webhook_id: "test-webhook-id",
112+
recipient: testPhoneNumber,
113+
text: "Hello, this is a test message!",
114+
sender_name: "+15555551234",
115+
thread_id: "test-thread-123",
116+
delivery_type: "imessage" as const,
117+
};
118+
119+
// Send the webhook
120+
const response = await SELF.fetch("https://example.com/", {
121+
method: "POST",
122+
headers: {
123+
"Content-Type": "application/json",
124+
},
125+
body: JSON.stringify(validPayload),
126+
});
127+
128+
expect(response.status).toBe(200);
129+
130+
const data = await response.json<{ success: boolean }>();
131+
expect(data.success).toBe(true);
132+
133+
// Wait for debounce window + processing (4 seconds + buffer)
134+
await new Promise((resolve) => setTimeout(resolve, 5000));
135+
136+
// Verify data was stored in database
137+
const db = getDb(env.DATABASE_URL as string);
138+
139+
// Check user was created
140+
const createdUsers = await db
141+
.select()
142+
.from(users)
143+
.where(eq(users.phoneNumber, testPhoneNumber));
144+
expect(createdUsers.length).toBeGreaterThan(0);
145+
const user = createdUsers[0];
146+
expect(user.phoneNumber).toBe(testPhoneNumber);
147+
148+
// Check conversation was created - get the most recent one for this sender
149+
const allConversations = await db
150+
.select()
151+
.from(conversations)
152+
.where(eq(conversations.sender, validPayload.sender_name))
153+
.orderBy(desc(conversations.createdAt));
154+
expect(allConversations.length).toBeGreaterThan(0);
155+
const conversation = allConversations[0]; // Get most recent (first due to desc order)
156+
expect(conversation).toBeDefined();
157+
expect(conversation.isGroup).toBe(false);
158+
expect(conversation.channelType).toBe("loop");
159+
160+
// Check message was created - get the inbound message (not the outbound AI response)
161+
const allMessages = await db
162+
.select()
163+
.from(messages)
164+
.where(eq(messages.conversationId, conversation.id))
165+
.orderBy(desc(messages.createdAt));
166+
expect(allMessages.length).toBeGreaterThanOrEqual(1);
167+
const inboundMessage = allMessages.find((m) => m.isOutbound === false);
168+
expect(inboundMessage).toBeDefined();
169+
expect(inboundMessage!.userId).toBe(user.id);
170+
171+
// Check parts were created for the inbound message
172+
const messageParts = await db
173+
.select()
174+
.from(parts)
175+
.where(eq(parts.messageId, inboundMessage!.id));
176+
expect(messageParts.length).toBeGreaterThan(0);
177+
expect(messageParts[0].type).toBe("text");
178+
expect(messageParts[0].content).toMatchObject({
179+
type: "text",
180+
text: validPayload.text,
181+
});
182+
}, 10000); // Increase timeout to 10 seconds for this test
183+
});

apps/interaction-worker/vitest.config.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export default defineWorkersProject({
1919
],
2020
bindings: {
2121
NODE_ENV: "test",
22+
DATABASE_URL:
23+
process.env.DATABASE_URL ||
24+
"postgresql://postgres:postgres@127.0.0.1:54322/postgres",
2225
},
2326
durableObjects: {
2427
MESSAGE_DEBOUNCER: "MessageDebouncer",

0 commit comments

Comments
 (0)