Skip to content

Commit 004085a

Browse files
committed
refactor(cli): consolidate test files to reduce duplication
- Extract shared mock client to test-utils.ts for reuse - Parameterize schema validation tests (272 → 81 lines, -70%) - Consolidate CHECK_INFO tests into single parameterized test - Remove duplicate structure tests (schema validation covers them) - Net reduction: ~271 lines while maintaining full test coverage
1 parent 8ef5bb8 commit 004085a

2 files changed

Lines changed: 171 additions & 245 deletions

File tree

cli/test/schema-validation.test.ts

Lines changed: 55 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -1,271 +1,81 @@
11
/**
2-
* JSON Schema validation tests for H001, H002, H004 express checkup reports.
3-
* These tests validate that the generated reports match the schemas in reporter/schemas/.
2+
* JSON Schema validation tests for express checkup reports.
3+
* Validates that generated reports match schemas in reporter/schemas/.
44
*/
55
import { describe, test, expect } from "bun:test";
66
import { resolve } from "path";
77
import { readFileSync } from "fs";
88
import Ajv2020 from "ajv/dist/2020";
99

1010
import * as checkup from "../lib/checkup";
11+
import { createMockClient } from "./test-utils";
1112

1213
const ajv = new Ajv2020({ allErrors: true, strict: false });
1314
const schemasDir = resolve(import.meta.dir, "../../reporter/schemas");
1415

15-
function loadSchema(checkId: string): object {
16+
function validateAgainstSchema(report: any, checkId: string): void {
1617
const schemaPath = resolve(schemasDir, `${checkId}.schema.json`);
17-
return JSON.parse(readFileSync(schemaPath, "utf8"));
18-
}
19-
20-
function validateReport(report: any, checkId: string): { valid: boolean; errors: string[] } {
21-
const schema = loadSchema(checkId);
18+
const schema = JSON.parse(readFileSync(schemaPath, "utf8"));
2219
const validate = ajv.compile(schema);
2320
const valid = validate(report);
24-
const errors = validate.errors?.map(e => `${e.instancePath}: ${e.message}`) || [];
25-
return { valid: !!valid, errors };
21+
if (!valid) {
22+
const errors = validate.errors?.map(e => `${e.instancePath}: ${e.message}`).join(", ");
23+
throw new Error(`${checkId} schema validation failed: ${errors}`);
24+
}
2625
}
2726

28-
// Mock client for testing
29-
function createMockClient(options: {
30-
versionRows?: any[];
31-
settingsRows?: any[];
32-
invalidIndexesRows?: any[];
33-
unusedIndexesRows?: any[];
34-
redundantIndexesRows?: any[];
35-
} = {}) {
36-
const {
37-
versionRows = [
38-
{ name: "server_version", setting: "16.3" },
39-
{ name: "server_version_num", setting: "160003" },
40-
],
41-
settingsRows = [
42-
{ tag_setting_name: "shared_buffers", tag_setting_value: "128MB", tag_unit: "", tag_category: "Resource Usage / Memory", tag_vartype: "string", is_default: 1, setting_normalized: null, unit_normalized: null },
43-
{ tag_setting_name: "work_mem", tag_setting_value: "4MB", tag_unit: "", tag_category: "Resource Usage / Memory", tag_vartype: "string", is_default: 1, setting_normalized: null, unit_normalized: null },
44-
{ tag_setting_name: "autovacuum", tag_setting_value: "on", tag_unit: "", tag_category: "Autovacuum", tag_vartype: "bool", is_default: 1, setting_normalized: null, unit_normalized: null },
45-
{ tag_setting_name: "pg_stat_statements.max", tag_setting_value: "5000", tag_unit: "", tag_category: "Custom", tag_vartype: "integer", is_default: 0, setting_normalized: null, unit_normalized: null },
46-
],
47-
invalidIndexesRows = [],
48-
unusedIndexesRows = [],
49-
redundantIndexesRows = [],
50-
} = options;
51-
52-
return {
53-
query: async (sql: string) => {
54-
// Version query (simple inline - used by getPostgresVersion)
55-
if (sql.includes("server_version") && sql.includes("server_version_num") && sql.includes("pg_settings") && !sql.includes("tag_setting_name")) {
56-
return { rows: versionRows };
57-
}
58-
// Settings metric query (from metrics.yml - has tag_setting_name, tag_setting_value)
59-
if (sql.includes("tag_setting_name") && sql.includes("tag_setting_value") && sql.includes("pg_settings")) {
60-
return { rows: settingsRows };
61-
}
62-
// db_size metric (current database size from metrics.yml)
63-
if (sql.includes("pg_database_size(current_database())") && sql.includes("size_b")) {
64-
return { rows: [{ tag_datname: "testdb", size_b: "1073741824" }] };
65-
}
66-
// Stats reset metric (from metrics.yml)
67-
if (sql.includes("stats_reset") && sql.includes("pg_stat_database") && sql.includes("seconds_since_reset")) {
68-
return { rows: [{
69-
tag_database_name: "testdb",
70-
stats_reset_epoch: "1704067200",
71-
seconds_since_reset: "2592000"
72-
}] };
73-
}
74-
// Postmaster startup time (simple inline - used by getStatsReset)
75-
if (sql.includes("pg_postmaster_start_time") && sql.includes("postmaster_startup_epoch")) {
76-
return { rows: [{
77-
postmaster_startup_epoch: "1704067200",
78-
postmaster_startup_time: "2024-01-01 00:00:00+00"
79-
}] };
80-
}
81-
// Invalid indexes (H001) - from metrics.yml
82-
if (sql.includes("indisvalid = false") && sql.includes("fk_indexes")) {
83-
return { rows: invalidIndexesRows };
84-
}
85-
// Unused indexes (H002) - from metrics.yml
86-
if (sql.includes("Never Used Indexes") && sql.includes("idx_scan = 0")) {
87-
return { rows: unusedIndexesRows };
88-
}
89-
// Redundant indexes (H004) - from metrics.yml
90-
if (sql.includes("redundant_indexes_grouped") && sql.includes("columns like")) {
91-
return { rows: redundantIndexesRows };
92-
}
93-
// D004: pg_stat_statements extension check
94-
if (sql.includes("pg_extension") && sql.includes("pg_stat_statements")) {
95-
return { rows: [] }; // Extension not installed
96-
}
97-
// D004: pg_stat_kcache extension check
98-
if (sql.includes("pg_extension") && sql.includes("pg_stat_kcache")) {
99-
return { rows: [] }; // Extension not installed
100-
}
101-
// G001: Memory settings query
102-
if (sql.includes("pg_size_bytes") && sql.includes("shared_buffers") && sql.includes("work_mem")) {
103-
return { rows: [{
104-
shared_buffers_bytes: "134217728",
105-
wal_buffers_bytes: "4194304",
106-
work_mem_bytes: "4194304",
107-
maintenance_work_mem_bytes: "67108864",
108-
effective_cache_size_bytes: "4294967296",
109-
max_connections: 100,
110-
}] };
111-
}
112-
throw new Error(`Unexpected query: ${sql}`);
113-
},
114-
};
115-
}
116-
117-
describe("H001 schema validation", () => {
118-
test("H001 report with empty data validates against schema", async () => {
119-
const mockClient = createMockClient({ invalidIndexesRows: [] });
120-
const report = await checkup.generateH001(mockClient as any, "node-01");
121-
122-
const result = validateReport(report, "H001");
123-
if (!result.valid) {
124-
console.error("H001 validation errors:", result.errors);
125-
}
126-
expect(result.valid).toBe(true);
127-
});
128-
129-
test("H001 report with data validates against schema", async () => {
130-
const mockClient = createMockClient({
27+
// Test data for index reports
28+
const indexTestData = {
29+
H001: {
30+
emptyRows: { invalidIndexesRows: [] },
31+
dataRows: {
13132
invalidIndexesRows: [
132-
{
133-
schema_name: "public",
134-
table_name: "users",
135-
index_name: "users_email_idx",
136-
relation_name: "users",
137-
index_size_bytes: "1048576",
138-
supports_fk: false
139-
},
33+
{ schema_name: "public", table_name: "users", index_name: "users_email_idx", relation_name: "users", index_size_bytes: "1048576", supports_fk: false },
14034
],
141-
});
142-
const report = await checkup.generateH001(mockClient as any, "node-01");
143-
144-
const result = validateReport(report, "H001");
145-
if (!result.valid) {
146-
console.error("H001 validation errors:", result.errors);
147-
}
148-
expect(result.valid).toBe(true);
149-
});
150-
});
151-
152-
describe("H002 schema validation", () => {
153-
test("H002 report with empty data validates against schema", async () => {
154-
const mockClient = createMockClient({ unusedIndexesRows: [] });
155-
const report = await checkup.generateH002(mockClient as any, "node-01");
156-
157-
const result = validateReport(report, "H002");
158-
if (!result.valid) {
159-
console.error("H002 validation errors:", result.errors);
160-
}
161-
expect(result.valid).toBe(true);
162-
});
163-
164-
test("H002 report with data validates against schema", async () => {
165-
const mockClient = createMockClient({
35+
},
36+
},
37+
H002: {
38+
emptyRows: { unusedIndexesRows: [] },
39+
dataRows: {
16640
unusedIndexesRows: [
167-
{
168-
schema_name: "public",
169-
table_name: "logs",
170-
index_name: "logs_created_idx",
171-
index_definition: "CREATE INDEX logs_created_idx ON public.logs USING btree (created_at)",
172-
reason: "Never Used Indexes",
173-
idx_scan: "0",
174-
index_size_bytes: "8388608",
175-
idx_is_btree: true,
176-
supports_fk: false
177-
},
41+
{ schema_name: "public", table_name: "logs", index_name: "logs_created_idx", index_definition: "CREATE INDEX logs_created_idx ON public.logs USING btree (created_at)", reason: "Never Used Indexes", idx_scan: "0", index_size_bytes: "8388608", idx_is_btree: true, supports_fk: false },
17842
],
179-
});
180-
const report = await checkup.generateH002(mockClient as any, "node-01");
181-
182-
const result = validateReport(report, "H002");
183-
if (!result.valid) {
184-
console.error("H002 validation errors:", result.errors);
185-
}
186-
expect(result.valid).toBe(true);
187-
});
188-
});
189-
190-
describe("H004 schema validation", () => {
191-
test("H004 report with empty data validates against schema", async () => {
192-
const mockClient = createMockClient({ redundantIndexesRows: [] });
193-
const report = await checkup.generateH004(mockClient as any, "node-01");
194-
195-
const result = validateReport(report, "H004");
196-
if (!result.valid) {
197-
console.error("H004 validation errors:", result.errors);
198-
}
199-
expect(result.valid).toBe(true);
200-
});
201-
202-
test("H004 report with data validates against schema", async () => {
203-
const mockClient = createMockClient({
43+
},
44+
},
45+
H004: {
46+
emptyRows: { redundantIndexesRows: [] },
47+
dataRows: {
20448
redundantIndexesRows: [
205-
{
206-
schema_name: "public",
207-
table_name: "orders",
208-
index_name: "orders_user_id_idx",
209-
relation_name: "orders",
210-
access_method: "btree",
211-
reason: "public.orders_user_id_created_idx",
212-
index_size_bytes: "2097152",
213-
table_size_bytes: "16777216",
214-
index_usage: "0",
215-
supports_fk: false,
216-
index_definition: "CREATE INDEX orders_user_id_idx ON public.orders USING btree (user_id)",
217-
redundant_to_json: JSON.stringify([
218-
{ index_name: "public.orders_user_id_created_idx", index_definition: "CREATE INDEX orders_user_id_created_idx ON public.orders USING btree (user_id, created_at)", index_size_bytes: 1048576 }
219-
])
220-
},
49+
{ schema_name: "public", table_name: "orders", index_name: "orders_user_id_idx", relation_name: "orders", access_method: "btree", reason: "public.orders_user_id_created_idx", index_size_bytes: "2097152", table_size_bytes: "16777216", index_usage: "0", supports_fk: false, index_definition: "CREATE INDEX orders_user_id_idx ON public.orders USING btree (user_id)", redundant_to_json: JSON.stringify([{ index_name: "public.orders_user_id_created_idx", index_definition: "CREATE INDEX ...", index_size_bytes: 1048576 }]) },
22150
],
51+
},
52+
},
53+
};
54+
55+
describe("Schema validation", () => {
56+
// Index health checks (H001, H002, H004) - test empty and with data
57+
for (const [checkId, testData] of Object.entries(indexTestData)) {
58+
const generator = checkup.REPORT_GENERATORS[checkId];
59+
60+
test(`${checkId} validates with empty data`, async () => {
61+
const mockClient = createMockClient(testData.emptyRows);
62+
const report = await generator(mockClient as any, "node-01");
63+
validateAgainstSchema(report, checkId);
22264
});
223-
const report = await checkup.generateH004(mockClient as any, "node-01");
224-
225-
const result = validateReport(report, "H004");
226-
if (!result.valid) {
227-
console.error("H004 validation errors:", result.errors);
228-
}
229-
expect(result.valid).toBe(true);
230-
});
231-
});
23265

233-
describe("D004 schema validation", () => {
234-
test("D004 report validates against schema (extensions not installed)", async () => {
235-
const mockClient = createMockClient();
236-
const report = await checkup.REPORT_GENERATORS.D004(mockClient as any, "node-01");
237-
238-
const result = validateReport(report, "D004");
239-
if (!result.valid) {
240-
console.error("D004 validation errors:", result.errors);
241-
}
242-
expect(result.valid).toBe(true);
243-
});
244-
});
245-
246-
describe("F001 schema validation", () => {
247-
test("F001 report validates against schema", async () => {
248-
const mockClient = createMockClient();
249-
const report = await checkup.REPORT_GENERATORS.F001(mockClient as any, "node-01");
250-
251-
const result = validateReport(report, "F001");
252-
if (!result.valid) {
253-
console.error("F001 validation errors:", result.errors);
254-
}
255-
expect(result.valid).toBe(true);
256-
});
257-
});
258-
259-
describe("G001 schema validation", () => {
260-
test("G001 report validates against schema", async () => {
261-
const mockClient = createMockClient();
262-
const report = await checkup.REPORT_GENERATORS.G001(mockClient as any, "node-01");
263-
264-
const result = validateReport(report, "G001");
265-
if (!result.valid) {
266-
console.error("G001 validation errors:", result.errors);
267-
}
268-
expect(result.valid).toBe(true);
269-
});
66+
test(`${checkId} validates with sample data`, async () => {
67+
const mockClient = createMockClient(testData.dataRows);
68+
const report = await generator(mockClient as any, "node-01");
69+
validateAgainstSchema(report, checkId);
70+
});
71+
}
72+
73+
// Settings reports (D004, F001, G001) - single test each
74+
for (const checkId of ["D004", "F001", "G001"]) {
75+
test(`${checkId} validates against schema`, async () => {
76+
const mockClient = createMockClient();
77+
const report = await checkup.REPORT_GENERATORS[checkId](mockClient as any, "node-01");
78+
validateAgainstSchema(report, checkId);
79+
});
80+
}
27081
});
271-

0 commit comments

Comments
 (0)