Skip to content

Commit bd9fb65

Browse files
committed
test: move integration tests to colocated locations
Address PR review feedback: - Delete centralized integration test infrastructure (src/tests/integration/) - Keep original simple server.integration.test.ts (from main) - Add analytics.integration.test.ts colocated with analytics plugin - Use existing mockServiceContext pattern for simpler test setup
1 parent 765ce0a commit bd9fb65

File tree

5 files changed

+281
-874
lines changed

5 files changed

+281
-874
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import type { Server } from "node:http";
2+
import {
3+
createConfigurableMockWorkspaceClient,
4+
createFailedSQLResponse,
5+
createSuccessfulSQLResponse,
6+
mockServiceContext,
7+
parseSSEResponse,
8+
setupDatabricksEnv,
9+
} from "@tools/test-helpers";
10+
import { sql } from "shared";
11+
import {
12+
afterAll,
13+
beforeAll,
14+
beforeEach,
15+
describe,
16+
expect,
17+
test,
18+
vi,
19+
} from "vitest";
20+
import { AppManager } from "../../../app";
21+
import { ServiceContext } from "../../../context/service-context";
22+
import { createApp } from "../../../core";
23+
import { server as serverPlugin } from "../../server";
24+
import { analytics } from "../index";
25+
26+
describe("Analytics Plugin Integration", () => {
27+
let server: Server;
28+
let baseUrl: string;
29+
let serviceContextMock: Awaited<ReturnType<typeof mockServiceContext>>;
30+
let mockClient: ReturnType<typeof createConfigurableMockWorkspaceClient>;
31+
// biome-ignore lint/suspicious/noExplicitAny: vitest spy types are complex
32+
let getAppQuerySpy: any;
33+
const TEST_PORT = 9879;
34+
35+
beforeAll(async () => {
36+
setupDatabricksEnv();
37+
ServiceContext.reset();
38+
39+
mockClient = createConfigurableMockWorkspaceClient();
40+
serviceContextMock = await mockServiceContext({
41+
serviceDatabricksClient: mockClient.client,
42+
});
43+
44+
getAppQuerySpy = vi
45+
.spyOn(AppManager.prototype, "getAppQuery")
46+
.mockResolvedValue(null);
47+
48+
const app = await createApp({
49+
plugins: [
50+
serverPlugin({
51+
port: TEST_PORT,
52+
host: "127.0.0.1",
53+
autoStart: false,
54+
}),
55+
analytics({}),
56+
],
57+
});
58+
59+
await app.server.start();
60+
server = app.server.getServer();
61+
baseUrl = `http://127.0.0.1:${TEST_PORT}`;
62+
63+
await new Promise((resolve) => setTimeout(resolve, 100));
64+
});
65+
66+
afterAll(async () => {
67+
getAppQuerySpy?.mockRestore();
68+
serviceContextMock?.restore();
69+
if (server) {
70+
await new Promise<void>((resolve, reject) => {
71+
server.close((err) => {
72+
if (err) reject(err);
73+
else resolve();
74+
});
75+
});
76+
}
77+
});
78+
79+
beforeEach(() => {
80+
mockClient.mocks.executeStatement.mockReset();
81+
mockClient.mocks.getStatement.mockReset();
82+
getAppQuerySpy.mockReset();
83+
});
84+
85+
describe("Query Execution", () => {
86+
test("should execute query and return transformed data", async () => {
87+
const testQuery = "SELECT name, age FROM users";
88+
const mockData = [
89+
["Alice", "30"],
90+
["Bob", "25"],
91+
];
92+
const mockColumns = [
93+
{ name: "name", type_name: "STRING" },
94+
{ name: "age", type_name: "STRING" },
95+
];
96+
97+
getAppQuerySpy.mockResolvedValueOnce({
98+
query: testQuery,
99+
isAsUser: false,
100+
});
101+
102+
mockClient.mocks.executeStatement.mockResolvedValueOnce(
103+
createSuccessfulSQLResponse(mockData, mockColumns),
104+
);
105+
106+
const response = await fetch(
107+
`${baseUrl}/api/analytics/query/test_query`,
108+
{
109+
method: "POST",
110+
headers: { "Content-Type": "application/json" },
111+
body: JSON.stringify({ parameters: {} }),
112+
},
113+
);
114+
115+
expect(response.status).toBe(200);
116+
expect(response.headers.get("Content-Type")).toBe("text/event-stream");
117+
118+
const sseData = await parseSSEResponse(response);
119+
expect(sseData.eventType).toBe("result");
120+
expect(sseData.data).toEqual([
121+
{ name: "Alice", age: "30" },
122+
{ name: "Bob", age: "25" },
123+
]);
124+
125+
expect(mockClient.mocks.executeStatement).toHaveBeenCalledTimes(1);
126+
expect(mockClient.mocks.executeStatement).toHaveBeenCalledWith(
127+
expect.objectContaining({
128+
statement: testQuery,
129+
warehouse_id: "test-warehouse-id",
130+
}),
131+
expect.anything(),
132+
);
133+
});
134+
135+
test("should pass SQL parameters correctly", async () => {
136+
const testQuery = "SELECT * FROM users WHERE id = :user_id";
137+
138+
getAppQuerySpy.mockResolvedValueOnce({
139+
query: testQuery,
140+
isAsUser: false,
141+
});
142+
143+
mockClient.mocks.executeStatement.mockResolvedValueOnce(
144+
createSuccessfulSQLResponse([["Alice"]], [{ name: "name" }]),
145+
);
146+
147+
const response = await fetch(
148+
`${baseUrl}/api/analytics/query/user_query`,
149+
{
150+
method: "POST",
151+
headers: { "Content-Type": "application/json" },
152+
body: JSON.stringify({
153+
parameters: {
154+
user_id: sql.string("123"),
155+
},
156+
}),
157+
},
158+
);
159+
160+
expect(response.status).toBe(200);
161+
162+
const callArgs = mockClient.mocks.executeStatement.mock.calls[0][0];
163+
expect(callArgs.parameters).toEqual(
164+
expect.arrayContaining([
165+
expect.objectContaining({
166+
name: "user_id",
167+
value: "123",
168+
type: "STRING",
169+
}),
170+
]),
171+
);
172+
});
173+
});
174+
175+
describe("Query Not Found", () => {
176+
test("should return 404 when query does not exist", async () => {
177+
getAppQuerySpy.mockResolvedValueOnce(null);
178+
179+
const response = await fetch(
180+
`${baseUrl}/api/analytics/query/nonexistent`,
181+
{
182+
method: "POST",
183+
headers: { "Content-Type": "application/json" },
184+
body: JSON.stringify({ parameters: {} }),
185+
},
186+
);
187+
188+
expect(response.status).toBe(404);
189+
const data = await response.json();
190+
expect(data).toEqual({ error: "Query not found" });
191+
192+
expect(mockClient.mocks.executeStatement).not.toHaveBeenCalled();
193+
});
194+
});
195+
196+
describe("Error Handling", () => {
197+
test("should handle SQL execution failure", async () => {
198+
getAppQuerySpy.mockResolvedValueOnce({
199+
query: "SELECT * FROM broken",
200+
isAsUser: false,
201+
});
202+
203+
mockClient.mocks.executeStatement.mockResolvedValue(
204+
createFailedSQLResponse("Table not found"),
205+
);
206+
207+
const response = await fetch(`${baseUrl}/api/analytics/query/broken`, {
208+
method: "POST",
209+
headers: { "Content-Type": "application/json" },
210+
body: JSON.stringify({ parameters: {} }),
211+
});
212+
213+
expect(response.status).toBe(200);
214+
const text = await response.text();
215+
expect(text).toContain("event: error");
216+
});
217+
218+
test("should handle SDK exceptions", async () => {
219+
getAppQuerySpy.mockResolvedValueOnce({
220+
query: "SELECT 1",
221+
isAsUser: false,
222+
});
223+
224+
mockClient.mocks.executeStatement.mockRejectedValue(
225+
new Error("Network error"),
226+
);
227+
228+
const response = await fetch(`${baseUrl}/api/analytics/query/error`, {
229+
method: "POST",
230+
headers: { "Content-Type": "application/json" },
231+
body: JSON.stringify({ parameters: {} }),
232+
});
233+
234+
expect(response.status).toBe(200);
235+
const text = await response.text();
236+
expect(text).toContain("event: error");
237+
});
238+
});
239+
240+
describe("Caching", () => {
241+
test("should cache results for identical requests", async () => {
242+
const testQuery = "SELECT * FROM cached";
243+
244+
getAppQuerySpy.mockResolvedValue({
245+
query: testQuery,
246+
isAsUser: false,
247+
});
248+
249+
mockClient.mocks.executeStatement.mockResolvedValue(
250+
createSuccessfulSQLResponse([["cached_value"]], [{ name: "value" }]),
251+
);
252+
253+
const response1 = await fetch(
254+
`${baseUrl}/api/analytics/query/cache_test`,
255+
{
256+
method: "POST",
257+
headers: { "Content-Type": "application/json" },
258+
body: JSON.stringify({ parameters: {} }),
259+
},
260+
);
261+
const data1 = await parseSSEResponse(response1);
262+
263+
const response2 = await fetch(
264+
`${baseUrl}/api/analytics/query/cache_test`,
265+
{
266+
method: "POST",
267+
headers: { "Content-Type": "application/json" },
268+
body: JSON.stringify({ parameters: {} }),
269+
},
270+
);
271+
const data2 = await parseSSEResponse(response2);
272+
273+
expect(data1.data).toEqual([{ value: "cached_value" }]);
274+
expect(data2.data).toEqual([{ value: "cached_value" }]);
275+
expect(mockClient.mocks.executeStatement).toHaveBeenCalledTimes(1);
276+
});
277+
});
278+
});

0 commit comments

Comments
 (0)