Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/__tests__/fixtures/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as grpc from "@grpc/grpc-js";
import type {
DataQueryServiceClient,
QueryRequest,
QueryResponse,
} from "../../gen/data/v1/data";

export function queryData(
client: DataQueryServiceClient,
request: QueryRequest,
metadata: grpc.Metadata
): Promise<QueryResponse> {
return new Promise((resolve, reject) => {
client.query(request, metadata, (error, res) => {
if (error) reject(error);
else if (!res) reject(new Error("empty response"));
else resolve(res);
});
});
}
247 changes: 247 additions & 0 deletions src/__tests__/query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { describe, beforeAll, afterAll, it, expect } from "vitest";
import { DataQueryServiceClient, OrderBy } from "../gen/data/v1/data";
import {
GRPC_ADDRESS,
grpcInsecureCredentials,
grpcMetadata,
} from "./fixtures/grpc";
import { createTestApiKey } from "./fixtures/apiKey";
import { clearDatabase } from "./db";
import { queryData } from "./fixtures/query";
import { getPostgresDB } from "../storage/db/postgres/db";
import { tagsTable } from "../storage/db/postgres/schema";
import { FilterCondition, FilterGroup } from "../gen/query/v1/query";
import { Metadata } from "@grpc/grpc-js";

describe("Data Query", () => {
let client: DataQueryServiceClient;
let rawKey: string;

beforeAll(async () => {
client = new DataQueryServiceClient(GRPC_ADDRESS, grpcInsecureCredentials);
const key = await createTestApiKey();
rawKey = key.rawKey;
});

afterAll(async () => {
await clearDatabase();
client.close();
});

it("1. returns data when auth is valid and table exists", async () => {
const db = getPostgresDB();
await db.insert(tagsTable).values({
key: "PREMIUM_CALL",
amount: 100,
});

const QueryRequest = {
table: "tags",
where: FilterGroup.create({
logical: 1,
conditions: [
FilterCondition.create({
field: "key",
operator: 1,
value: "PREMIUM_CALL",
}),
],
groups: [],
}),
orderBy: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Exercise ordering contract Every request in this new coverage sends orderBy, but the server-side schema currently reads orderByList before transforming it to orderBy. Because these tests only send an empty list, they pass even if client-supplied ordering is ignored by the endpoint. Add a non-empty ordering case using the actual generated request field so the test catches this API/schema mismatch.

limit: 100,
offset: 0,
};

const response = await queryData(
client,
QueryRequest,
grpcMetadata(`Bearer ${rawKey}`)
);

expect(response.columns).toContain("key");
expect(response.columns).toContain("amount");
expect(response.total).toBe(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Clear test state first This test asserts the global query total is exactly 1, but the suite only calls clearDatabase() in afterAll. If a previous run failed before cleanup, or this test is run against a dirty test database, an existing matching tags row makes this assertion fail even though the query endpoint is behaving correctly. Clear the database before the suite or use a unique key and assert only on the inserted row.

expect(response.rows.length).toBe(1);
// expect(response.rows).toBeDefined();
expect(response.rows[0]?.values).toBeDefined();
expect(response.rows[0]?.values).toContain("PREMIUM_CALL");
expect(response.rows[0]?.values).toContain("100");
});

it("2. returns error when table does not exist", async () => {
const QueryRequest = {
table: "nonexistent",
where: FilterGroup.create({
logical: 1,
conditions: [],
groups: [],
}),
orderBy: [],
limit: 100,
offset: 0,
};

await expect(
queryData(client, QueryRequest, grpcMetadata(`Bearer ${rawKey}`))
).rejects.toThrow(
'table: Invalid option: expected one of "users"|"sessions"|"tags"|"expressions"|"metadata"'
);
});

it("3. rejects requests without auth", async () => {
const QueryRequest = {
table: "users",
where: FilterGroup.create({
logical: 1,
conditions: [],
groups: [],
}),
orderBy: [],
limit: 100,
offset: 0,
};

await expect(
queryData(client, QueryRequest, new Metadata())
).rejects.toThrow("Missing Authorization header");
});

it("4. returns 0 results when no rows match the filter", async () => {
const db = getPostgresDB();
await db.insert(tagsTable).values({ key: "CHEAP", amount: 200 });

const request = {
table: "tags",
where: FilterGroup.create({
logical: 1,
conditions: [
FilterCondition.create({
field: "amount",
operator: 1,
value: "696",
}),
],
groups: [],
}),
orderBy: [],
limit: 100,
offset: 0,
};

const response = await queryData(
client,
request,
grpcMetadata(`Bearer ${rawKey}`)
);

expect(response.total).toBe(0);
expect(response.rows.length).toBe(0);
});

it("5. returns all rows when no filter is provided", async () => {
const db = getPostgresDB();
await db.insert(tagsTable).values({ key: "A", amount: 1 });
await db.insert(tagsTable).values({ key: "B", amount: 2 });
await db.insert(tagsTable).values({ key: "C", amount: 3 });

const request = {
table: "tags",
where: FilterGroup.create({
logical: 1,
conditions: [],
groups: [],
}),
orderBy: [],
limit: 100,
offset: 0,
};

const response = await queryData(
client,
request,
grpcMetadata(`Bearer ${rawKey}`)
);

expect(response.total).toBeGreaterThanOrEqual(3);
expect(response.rows.length).toBeGreaterThanOrEqual(3);
expect(response.columns).toContain("key");
expect(response.columns).toContain("amount");
});

it("6. supports pagination with limit and offset", async () => {
const db = getPostgresDB();
await db.insert(tagsTable).values({ key: "P1", amount: 10 });
await db.insert(tagsTable).values({ key: "P2", amount: 20 });
await db.insert(tagsTable).values({ key: "P3", amount: 30 });
await db.insert(tagsTable).values({ key: "P4", amount: 40 });
await db.insert(tagsTable).values({ key: "P5", amount: 50 });

const page1 = await queryData(
client,
{
table: "tags",
where: FilterGroup.create({ logical: 1, conditions: [], groups: [] }),
orderBy: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Order pagination deterministically This pagination test uses offset while leaving orderBy empty. Postgres does not guarantee row order without ORDER BY, so the three pages can overlap, skip rows, or change order as table contents and query plans change. Add a stable ordering and assert the page contents so this test verifies pagination instead of only checking row counts.

limit: 2,
offset: 0,
},
grpcMetadata(`Bearer ${rawKey}`)
);

expect(page1.total).toBeGreaterThanOrEqual(5);
expect(page1.rows.length).toBe(2);

const page2 = await queryData(
client,
{
table: "tags",
where: FilterGroup.create({ logical: 1, conditions: [], groups: [] }),
orderBy: [],
limit: 2,
offset: 2,
},
grpcMetadata(`Bearer ${rawKey}`)
);

expect(page2.rows.length).toBe(2);

const page3 = await queryData(
client,
{
table: "tags",
where: FilterGroup.create({ logical: 1, conditions: [], groups: [] }),
orderBy: [],
limit: 2,
offset: 4,
},
grpcMetadata(`Bearer ${rawKey}`)
);

expect(page3.rows.length).toBeGreaterThanOrEqual(1);
});

it("7. rejects invalid integer value in filter", async () => {
const request = {
table: "tags",
where: FilterGroup.create({
logical: 1,
conditions: [
FilterCondition.create({
field: "amount",
operator: 1,
value: "abc",
}),
],
groups: [],
}),
orderBy: [],
limit: 100,
offset: 0,
};

await expect(
queryData(client, request, grpcMetadata(`Bearer ${rawKey}`))
).rejects.toThrow();
});
});
Loading