Files
0pt/tests/integration/analytics-clickhouse.integration.test.ts
T
Simon AxelssonandClaude Fable 5 c47e3b468e
CI (SIAX Cloud) / integration (push) Failing after 22s
CI (SIAX Cloud) / security (push) Successful in 22s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / quality (push) Successful in 43s
CI (SIAX Cloud) / integration (pull_request) Failing after 3s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / quality (pull_request) Successful in 43s
test(integration)+ci+docs: live OpenSearch/ClickHouse suites, CI service containers, STATUS.md (§56)
- tests/integration/: 10 tests against REAL OpenSearch 2.11 (tenant
  isolation, purpose/rights filters, generation rebuild/rollback,
  projection-disaster rebuild §68) + REAL ClickHouse 24.8 (tenant-isolated
  metric DSL, TTL schema, injection-surface refusals). Honest skip without
  env - and CI fails hard if the suites skip instead of running.
- .gitea/workflows/ci.yml: new blocking 'integration' job with
  self-identifying service containers (st0re ci.yml's measured
  alias-collision discipline: run-unique cluster.name / credentials,
  getent-enumerated IP selection, no-match = hard fail).
- STATUS.md: full 8-level Master-DoD classification per capability incl.
  the honest gaps blocking PRODUCTION_READY.
- dependency-mode: NEXT_PHASE=phase-production-build is not a serving
  runtime (next build page-data collection must compile; guard re-arms at
  boot) - pinned by contract test.
- docs/02-architecture.md: superseded the accurate-but-now-fixed 'every
  client runs against its mock' paragraph; .env.example: client URLs now
  actually read from env + new P0 backend vars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 16:56:33 +02:00

97 lines
4.0 KiB
TypeScript

/**
* REAL ClickHouse integration test (Master-DoD §48/§50) for the Analytics
* capability. Runs against CLICKHOUSE_URL (+ CLICKHOUSE_AUTH "user:pass"
* when the server requires it); SKIPS HONESTLY when unset.
*
* Proves against the real engine: schema creation with TTL retention,
* JSONEachRow ingest, the closed metric-query DSL, and — the §69-relevant
* part — that the FORCED tenant predicate isolates tenants on real data.
*/
import { describe, it, expect, beforeAll } from "vitest";
import { ClickHouseAnalytics } from "@/src/analytics/clickhouse-analytics";
const CLICKHOUSE_URL = process.env.CLICKHOUSE_URL;
const run = CLICKHOUSE_URL ? describe : describe.skip;
const DB = `opt_itest_${Date.now().toString(36)}`;
run("Analytics capability against a LIVE ClickHouse", () => {
// The skip-variant still evaluates this callback body, so the constructor
// must tolerate the env-less case (the tests themselves never run then).
const analytics = new ClickHouseAnalytics({
baseUrl: CLICKHOUSE_URL ?? "http://skipped.invalid",
basicAuth: process.env.CLICKHOUSE_AUTH,
database: DB,
});
beforeAll(async () => {
expect(await analytics.health()).toBe(true);
await analytics.ensureSchema(30);
// ensureSchema is idempotent (CREATE IF NOT EXISTS).
await analytics.ensureSchema(30);
}, 60_000);
it("ingests events and answers aggregate metric queries", async () => {
const res = await analytics.ingest([
{ tenant_id: "t_alpha", dataset_id: "ds_companies", metric: "rows_ingested", value: 100 },
{ tenant_id: "t_alpha", dataset_id: "ds_companies", metric: "rows_ingested", value: 50 },
{ tenant_id: "t_alpha", dataset_id: "ds_vehicles", metric: "rows_ingested", value: 7 },
{ tenant_id: "t_beta", dataset_id: "ds_companies", metric: "rows_ingested", value: 999 },
]);
expect(res.ingested).toBe(4);
const total = await analytics.queryMetric({
tenant_id: "t_alpha",
metric: "rows_ingested",
aggregate: "sum",
});
expect(total).toEqual([{ value: 157 }]);
const perDataset = await analytics.queryMetric({
tenant_id: "t_alpha",
dataset_id: "ds_companies",
metric: "rows_ingested",
aggregate: "sum",
});
expect(perDataset).toEqual([{ value: 150 }]);
}, 30_000);
it("REAL tenant isolation: t_beta's numbers never leak into t_alpha aggregates", async () => {
const alpha = await analytics.queryMetric({ tenant_id: "t_alpha", metric: "rows_ingested", aggregate: "max" });
expect(alpha[0].value).toBe(100); // NOT 999
const beta = await analytics.queryMetric({ tenant_id: "t_beta", metric: "rows_ingested", aggregate: "sum" });
expect(beta[0].value).toBe(999);
const ghost = await analytics.queryMetric({ tenant_id: "t_gamma", metric: "rows_ingested", aggregate: "count" });
expect(ghost[0].value).toBe(0);
});
it("group-by dimension queries work through the closed DSL", async () => {
await analytics.ingest([
{ tenant_id: "t_alpha", dataset_id: "ds_x", metric: "dq_score", value: 90, dimensions: { severity: "warning" } },
{ tenant_id: "t_alpha", dataset_id: "ds_x", metric: "dq_score", value: 40, dimensions: { severity: "critical" } },
]);
const rows = await analytics.queryMetric({
tenant_id: "t_alpha",
metric: "dq_score",
aggregate: "avg",
group_by: "severity",
});
expect(rows).toHaveLength(2);
expect(rows.find((r) => r.group === "warning")?.value).toBe(90);
});
it("rejects tenant-less queries and invalid group_by identifiers (injection surface closed)", async () => {
await expect(
analytics.queryMetric({ tenant_id: " ", metric: "x", aggregate: "sum" })
).rejects.toMatchObject({ code: "INVALID_REQUEST" });
await expect(
analytics.queryMetric({
tenant_id: "t_alpha",
metric: "x",
aggregate: "sum",
group_by: "sev'; DROP TABLE opt_metric_events; --",
})
).rejects.toMatchObject({ code: "INVALID_REQUEST" });
});
});