ndomo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bun-version +1 -0
- package/.dockerignore +79 -0
- package/.editorconfig +18 -0
- package/.env.example +19 -0
- package/.github/CODEOWNERS +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +2 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/dependabot.yml +36 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/release.yml +30 -0
- package/.github/workflows/gitleaks.yml +28 -0
- package/.github/workflows/release-please.yml +27 -0
- package/.github/workflows/smoke.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/CHANGELOG.md +114 -0
- package/Dockerfile +32 -0
- package/README.es.md +174 -0
- package/README.md +187 -0
- package/agents/chronicler.md +98 -0
- package/agents/ci-smith.md +136 -0
- package/agents/craftsman.md +341 -0
- package/agents/deploy-smith.md +138 -0
- package/agents/foreman.md +377 -0
- package/agents/go-smith.md +164 -0
- package/agents/guild.md +188 -0
- package/agents/inspector.md +83 -0
- package/agents/js-smith.md +127 -0
- package/agents/ops-scout.md +173 -0
- package/agents/painter.md +200 -0
- package/agents/python-smith.md +120 -0
- package/agents/ranger.md +307 -0
- package/agents/release-smith.md +165 -0
- package/agents/rust-smith.md +159 -0
- package/agents/sage.md +178 -0
- package/agents/scout.md +144 -0
- package/agents/scribe.md +156 -0
- package/agents/smith.md +201 -0
- package/agents/vue-smith.md +155 -0
- package/agents/warden.md +216 -0
- package/agents/zig-smith.md +156 -0
- package/bin/ndomo-analyses.ts +4 -0
- package/bin/ndomo-status.ts +4 -0
- package/biome.json +57 -0
- package/bun.lock +514 -0
- package/commitlint.config.js +3 -0
- package/config/ndomo.config.json +258 -0
- package/config/ndomo.schema.json +166 -0
- package/docs/agents.md +375 -0
- package/docs/bugs/plan-create-orphan-fk.md +131 -0
- package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
- package/docs/configuration.md +276 -0
- package/docs/database.md +364 -0
- package/docs/features/feature-flexible-builder-v1.md +724 -0
- package/docs/features/feature-flexible-builder-v2.md +882 -0
- package/docs/features/feature-flexible-builder.md +974 -0
- package/docs/http-server.md +244 -0
- package/docs/installation.md +259 -0
- package/docs/integrations.md +129 -0
- package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
- package/docs/operations/audit-v1.md +417 -0
- package/docs/operations/audit-v2.md +197 -0
- package/docs/operations/audit-v3.md +306 -0
- package/docs/operations/db-optimize-foundations.md +123 -0
- package/docs/operations/verify-gate-architecture.md +82 -0
- package/docs/workflows.md +448 -0
- package/opencode.json +5 -0
- package/package.json +65 -0
- package/release-please-config.json +11 -0
- package/scripts/dev-bust-cache.sh +164 -0
- package/scripts/install.sh +688 -0
- package/scripts/smoke-e2e.ts +704 -0
- package/scripts/smoke-hot.ts +417 -0
- package/scripts/smoke-http.sh +228 -0
- package/scripts/smoke-v4.ts +256 -0
- package/scripts/smoke-v5.ts +397 -0
- package/scripts/smoke.sh +9 -0
- package/scripts/uninstall.sh +224 -0
- package/skills/api-security-best-practices/SKILL.md +915 -0
- package/skills/bash-scripting/SKILL.md +201 -0
- package/skills/bun/SKILL.md +313 -0
- package/skills/cavecrew/SKILL.md +82 -0
- package/skills/caveman/SKILL.md +74 -0
- package/skills/caveman-review/README.md +33 -0
- package/skills/caveman-review/SKILL.md +55 -0
- package/skills/find-skills/SKILL.md +142 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +55 -0
- package/skills/golang-patterns/SKILL.md +674 -0
- package/skills/golang-security/SKILL.md +185 -0
- package/skills/golang-security/evals/evals.json +595 -0
- package/skills/golang-security/references/architecture.md +268 -0
- package/skills/golang-security/references/checklist.md +80 -0
- package/skills/golang-security/references/cookies.md +200 -0
- package/skills/golang-security/references/cryptography.md +424 -0
- package/skills/golang-security/references/filesystem.md +285 -0
- package/skills/golang-security/references/injection.md +315 -0
- package/skills/golang-security/references/logging.md +163 -0
- package/skills/golang-security/references/memory-safety.md +241 -0
- package/skills/golang-security/references/network.md +253 -0
- package/skills/golang-security/references/secrets.md +189 -0
- package/skills/golang-security/references/third-party.md +159 -0
- package/skills/golang-security/references/threat-modeling.md +189 -0
- package/skills/golang-testing/SKILL.md +720 -0
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/javascript-testing-patterns/SKILL.md +537 -0
- package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
- package/skills/modern-javascript-patterns/SKILL.md +43 -0
- package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
- package/skills/modern-javascript-patterns/references/details.md +457 -0
- package/skills/python-anti-patterns/SKILL.md +349 -0
- package/skills/python-design-patterns/SKILL.md +85 -0
- package/skills/python-design-patterns/references/details.md +353 -0
- package/skills/python-error-handling/SKILL.md +193 -0
- package/skills/python-error-handling/references/details.md +171 -0
- package/skills/python-testing-patterns/SKILL.md +278 -0
- package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/skills/python-testing-patterns/references/details.md +349 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +307 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +344 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/zig-0.16/SKILL.md +840 -0
- package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
- package/src/cli/analyses.ts +280 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/serve.ts +192 -0
- package/src/cli/smoke.ts +131 -0
- package/src/cli/status.test.ts +204 -0
- package/src/cli/status.ts +263 -0
- package/src/cli/vacuum.test.ts +82 -0
- package/src/cli/vacuum.ts +96 -0
- package/src/config/schema.test.ts +88 -0
- package/src/config/schema.ts +64 -0
- package/src/db/analyses-migration.test.ts +210 -0
- package/src/db/analyses.test.ts +466 -0
- package/src/db/analyses.ts +375 -0
- package/src/db/auto-checkpoint.ts +131 -0
- package/src/db/client.test.ts +129 -0
- package/src/db/client.ts +55 -0
- package/src/db/fts-escape.ts +20 -0
- package/src/db/incidents.test.ts +201 -0
- package/src/db/incidents.ts +93 -0
- package/src/db/index.ts +86 -0
- package/src/db/migrations-v13.test.ts +141 -0
- package/src/db/migrations-v8.test.ts +301 -0
- package/src/db/migrations.ts +147 -0
- package/src/db/plan-archive.test.ts +180 -0
- package/src/db/plan-archive.ts +274 -0
- package/src/db/plan-create.test.ts +276 -0
- package/src/db/plan-create.ts +78 -0
- package/src/db/plan-files.test.ts +289 -0
- package/src/db/plan-update-status.ts +287 -0
- package/src/db/plans.test.ts +490 -0
- package/src/db/plans.ts +534 -0
- package/src/db/resolve-project-dir.test.ts +143 -0
- package/src/db/resolve-project-dir.ts +75 -0
- package/src/db/rollbacks.test.ts +150 -0
- package/src/db/rollbacks.ts +67 -0
- package/src/db/schema.ts +907 -0
- package/src/db/sessions.test.ts +80 -0
- package/src/db/sessions.ts +135 -0
- package/src/db/shutdown.test.ts +147 -0
- package/src/db/shutdown.ts +45 -0
- package/src/db/tasks.test.ts +921 -0
- package/src/db/tasks.ts +747 -0
- package/src/db/types.ts +619 -0
- package/src/http/__tests__/auth.test.ts +196 -0
- package/src/http/__tests__/routes.test.ts +465 -0
- package/src/http/__tests__/sse.test.ts +317 -0
- package/src/http/auth.ts +72 -0
- package/src/http/middleware/cors.ts +53 -0
- package/src/http/middleware/security-headers.ts +21 -0
- package/src/http/routes/events.ts +112 -0
- package/src/http/routes/health.ts +51 -0
- package/src/http/routes/plans.ts +66 -0
- package/src/http/routes/sessions.ts +50 -0
- package/src/http/routes/tasks.ts +60 -0
- package/src/http/server.ts +95 -0
- package/src/http/sse.ts +116 -0
- package/src/index.ts +37 -0
- package/src/lib.ts +65 -0
- package/src/mem/scoped.ts +65 -0
- package/src/orchestrator/background.test.ts +268 -0
- package/src/orchestrator/background.ts +293 -0
- package/src/orchestrator/memory-hook.ts +182 -0
- package/src/orchestrator/reconciler.ts +123 -0
- package/src/orchestrator/scheduler.test.ts +300 -0
- package/src/orchestrator/scheduler.ts +243 -0
- package/src/plugin.test.ts +2574 -0
- package/src/plugin.ts +1690 -0
- package/src/sdk/client.ts +66 -0
- package/src/worktrees/manager.ts +236 -0
- package/src/worktrees/state.ts +87 -0
- package/tests/integration/ranger-flow.test.ts +257 -0
- package/tools/analysis_archive.ts +28 -0
- package/tools/analysis_create.ts +55 -0
- package/tools/analysis_get.ts +33 -0
- package/tools/analysis_link_plan.ts +44 -0
- package/tools/analysis_list.ts +48 -0
- package/tools/analysis_search.ts +36 -0
- package/tools/analysis_update.ts +44 -0
- package/tools/plan_approve.ts +31 -0
- package/tools/plan_create.ts +58 -0
- package/tools/plan_get.ts +40 -0
- package/tools/plan_list.ts +37 -0
- package/tools/plan_search.ts +34 -0
- package/tools/plan_update_status.ts +71 -0
- package/tools/session_checkpoint.ts +31 -0
- package/tools/session_end.ts +26 -0
- package/tools/session_start.ts +43 -0
- package/tools/task_create_batch.ts +70 -0
- package/tools/task_list.ts +35 -0
- package/tools/task_next_for_agent.ts +30 -0
- package/tools/task_search.ts +34 -0
- package/tools/task_update_status.ts +37 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HTTP Basic Auth middleware (src/http/auth.ts).
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - parseBasicAuthHeader (via integration with authMiddleware)
|
|
6
|
+
* - authMiddleware accepts valid credentials
|
|
7
|
+
* - authMiddleware rejects 401 + WWW-Authenticate on invalid
|
|
8
|
+
* - authMiddleware returns 503 when password env not set + auth required
|
|
9
|
+
* - authMiddleware passes through when auth disabled
|
|
10
|
+
*
|
|
11
|
+
* Uses Elysia app.handle(new Request(...)) pattern for integration.
|
|
12
|
+
* Each test gets a fresh app with isolated config.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
+
import { Elysia } from "elysia";
|
|
17
|
+
import type { HttpConfig } from "../../config/schema.ts";
|
|
18
|
+
import { httpBasicAuth } from "../auth.ts";
|
|
19
|
+
|
|
20
|
+
/** Build a minimal test app with auth middleware mounted. */
|
|
21
|
+
function buildTestApp(httpConfig: HttpConfig) {
|
|
22
|
+
return new Elysia({ name: "test-auth" })
|
|
23
|
+
.use(httpBasicAuth(httpConfig))
|
|
24
|
+
.get("/protected", () => ({ ok: true }))
|
|
25
|
+
.get("/health", () => ({ status: "ok" }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Create a valid Basic auth header from password. */
|
|
29
|
+
function basicAuthHeader(password: string): string {
|
|
30
|
+
const encoded = Buffer.from(`user:${password}`).toString("base64");
|
|
31
|
+
return `Basic ${encoded}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_CONFIG: HttpConfig = {
|
|
35
|
+
enabled: true,
|
|
36
|
+
port: 4097,
|
|
37
|
+
cors: { origins: ["*"] },
|
|
38
|
+
auth: { required: true },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DISABLED_AUTH_CONFIG: HttpConfig = {
|
|
42
|
+
enabled: true,
|
|
43
|
+
port: 4097,
|
|
44
|
+
cors: { origins: ["*"] },
|
|
45
|
+
auth: { required: false },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let savedPassword: string | undefined;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
savedPassword = process.env.OPENCODE_SERVER_PASSWORD;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
if (savedPassword === undefined) {
|
|
56
|
+
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
57
|
+
} else {
|
|
58
|
+
process.env.OPENCODE_SERVER_PASSWORD = savedPassword;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("httpBasicAuth — auth required", () => {
|
|
63
|
+
test("valid credentials → 200 + handler response", async () => {
|
|
64
|
+
process.env.OPENCODE_SERVER_PASSWORD = "test-secret";
|
|
65
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
66
|
+
|
|
67
|
+
const req = new Request("http://localhost/protected", {
|
|
68
|
+
headers: { Authorization: basicAuthHeader("test-secret") },
|
|
69
|
+
});
|
|
70
|
+
const res = await app.handle(req);
|
|
71
|
+
|
|
72
|
+
expect(res.status).toBe(200);
|
|
73
|
+
const body = (await res.json()) as { ok: boolean };
|
|
74
|
+
expect(body.ok).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("wrong password → 401 + WWW-Authenticate header", async () => {
|
|
78
|
+
process.env.OPENCODE_SERVER_PASSWORD = "correct-password";
|
|
79
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
80
|
+
|
|
81
|
+
const req = new Request("http://localhost/protected", {
|
|
82
|
+
headers: { Authorization: basicAuthHeader("wrong-password") },
|
|
83
|
+
});
|
|
84
|
+
const res = await app.handle(req);
|
|
85
|
+
|
|
86
|
+
expect(res.status).toBe(401);
|
|
87
|
+
expect(res.headers.get("WWW-Authenticate")).toContain("Basic");
|
|
88
|
+
const body = (await res.json()) as { error: string };
|
|
89
|
+
expect(body.error).toBe("invalid_credentials");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("missing Authorization header → 401 + WWW-Authenticate", async () => {
|
|
93
|
+
process.env.OPENCODE_SERVER_PASSWORD = "test-secret";
|
|
94
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
95
|
+
|
|
96
|
+
const req = new Request("http://localhost/protected");
|
|
97
|
+
const res = await app.handle(req);
|
|
98
|
+
|
|
99
|
+
expect(res.status).toBe(401);
|
|
100
|
+
expect(res.headers.get("WWW-Authenticate")).toContain("Basic");
|
|
101
|
+
const body = (await res.json()) as { error: string };
|
|
102
|
+
expect(body.error).toBe("invalid_credentials");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("malformed Authorization (no 'Basic ' prefix) → 401", async () => {
|
|
106
|
+
process.env.OPENCODE_SERVER_PASSWORD = "test-secret";
|
|
107
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
108
|
+
|
|
109
|
+
const req = new Request("http://localhost/protected", {
|
|
110
|
+
headers: { Authorization: "Bearer some-token" },
|
|
111
|
+
});
|
|
112
|
+
const res = await app.handle(req);
|
|
113
|
+
|
|
114
|
+
expect(res.status).toBe(401);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("password env not set → 503 auth_not_configured", async () => {
|
|
118
|
+
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
119
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
120
|
+
|
|
121
|
+
const req = new Request("http://localhost/protected", {
|
|
122
|
+
headers: { Authorization: basicAuthHeader("any") },
|
|
123
|
+
});
|
|
124
|
+
const res = await app.handle(req);
|
|
125
|
+
|
|
126
|
+
expect(res.status).toBe(503);
|
|
127
|
+
const body = (await res.json()) as { error: string };
|
|
128
|
+
expect(body.error).toBe("auth_not_configured");
|
|
129
|
+
expect(res.headers.get("WWW-Authenticate")).toContain("Basic");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("/health exempt from auth — no credentials → 200", async () => {
|
|
133
|
+
process.env.OPENCODE_SERVER_PASSWORD = "test-secret";
|
|
134
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
135
|
+
|
|
136
|
+
const req = new Request("http://localhost/health");
|
|
137
|
+
const res = await app.handle(req);
|
|
138
|
+
|
|
139
|
+
expect(res.status).toBe(200);
|
|
140
|
+
const body = (await res.json()) as { status: string };
|
|
141
|
+
expect(body.status).toBe("ok");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("empty password in base64 → 401", async () => {
|
|
145
|
+
process.env.OPENCODE_SERVER_PASSWORD = "real-password";
|
|
146
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
147
|
+
|
|
148
|
+
// "user:" → base64 → colonIdx=4, providedPassword=""
|
|
149
|
+
const encoded = Buffer.from("user:").toString("base64");
|
|
150
|
+
const req = new Request("http://localhost/protected", {
|
|
151
|
+
headers: { Authorization: `Basic ${encoded}` },
|
|
152
|
+
});
|
|
153
|
+
const res = await app.handle(req);
|
|
154
|
+
|
|
155
|
+
expect(res.status).toBe(401);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("no colon in decoded → entire string used as password", async () => {
|
|
159
|
+
process.env.OPENCODE_SERVER_PASSWORD = "nocolon";
|
|
160
|
+
const app = buildTestApp(DEFAULT_CONFIG);
|
|
161
|
+
|
|
162
|
+
// "nocolon" (no user:pass format) → colonIdx=-1, providedPassword="nocolon"
|
|
163
|
+
const encoded = Buffer.from("nocolon").toString("base64");
|
|
164
|
+
const req = new Request("http://localhost/protected", {
|
|
165
|
+
headers: { Authorization: `Basic ${encoded}` },
|
|
166
|
+
});
|
|
167
|
+
const res = await app.handle(req);
|
|
168
|
+
|
|
169
|
+
expect(res.status).toBe(200);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("httpBasicAuth — auth disabled", () => {
|
|
174
|
+
test("no credentials → 200 (auth skipped)", async () => {
|
|
175
|
+
const app = buildTestApp(DISABLED_AUTH_CONFIG);
|
|
176
|
+
|
|
177
|
+
const req = new Request("http://localhost/protected");
|
|
178
|
+
const res = await app.handle(req);
|
|
179
|
+
|
|
180
|
+
expect(res.status).toBe(200);
|
|
181
|
+
const body = (await res.json()) as { ok: boolean };
|
|
182
|
+
expect(body.ok).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("wrong credentials → 200 (auth skipped entirely)", async () => {
|
|
186
|
+
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
187
|
+
const app = buildTestApp(DISABLED_AUTH_CONFIG);
|
|
188
|
+
|
|
189
|
+
const req = new Request("http://localhost/protected", {
|
|
190
|
+
headers: { Authorization: basicAuthHeader("wrong") },
|
|
191
|
+
});
|
|
192
|
+
const res = await app.handle(req);
|
|
193
|
+
|
|
194
|
+
expect(res.status).toBe(200);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for HTTP REST endpoints (src/http/routes/*).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - GET /health → 200, status/version/uptime/dbHealthy
|
|
6
|
+
* - GET /api/plans → 200, returns array (seeds test data)
|
|
7
|
+
* - GET /api/plans/:id → 200 known / 404 unknown
|
|
8
|
+
* - GET /api/tasks?planId=X → 200, filter works
|
|
9
|
+
* - GET /api/tasks/:id → 200 / 404
|
|
10
|
+
* - GET /api/sessions/active → 200, active sessions
|
|
11
|
+
* - GET /api/sessions/:id → 200 / 404
|
|
12
|
+
* - Auth enforcement: no auth → 401, valid auth → 200
|
|
13
|
+
* - CORS preflight OPTIONS → 204 with headers
|
|
14
|
+
* - Security headers present on all responses
|
|
15
|
+
*
|
|
16
|
+
* Uses in-memory SQLite via bun:sqlite. Each test suite gets a fresh DB.
|
|
17
|
+
* Uses Elysia app.handle(new Request(...)) pattern.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Database } from "bun:sqlite";
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
22
|
+
import type { HttpConfig } from "../../config/schema.ts";
|
|
23
|
+
import { runMigrations } from "../../db/migrations.ts";
|
|
24
|
+
import { createPlan } from "../../db/plans.ts";
|
|
25
|
+
import { startSession } from "../../db/sessions.ts";
|
|
26
|
+
import { createTasksBatch } from "../../db/tasks.ts";
|
|
27
|
+
import type { Plan } from "../../db/types.ts";
|
|
28
|
+
import { buildHttpServer } from "../server.ts";
|
|
29
|
+
|
|
30
|
+
let db: Database;
|
|
31
|
+
|
|
32
|
+
const AUTH_CONFIG: HttpConfig = {
|
|
33
|
+
enabled: true,
|
|
34
|
+
port: 4097,
|
|
35
|
+
cors: { origins: ["*"] },
|
|
36
|
+
auth: { required: true },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const NO_AUTH_CONFIG: HttpConfig = {
|
|
40
|
+
enabled: true,
|
|
41
|
+
port: 4097,
|
|
42
|
+
cors: { origins: ["*"] },
|
|
43
|
+
auth: { required: false },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CORS_CONFIG: HttpConfig = {
|
|
47
|
+
enabled: true,
|
|
48
|
+
port: 4097,
|
|
49
|
+
cors: { origins: ["https://example.com"] },
|
|
50
|
+
auth: { required: false },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let savedPassword: string | undefined;
|
|
54
|
+
|
|
55
|
+
function basicAuthHeader(password: string): string {
|
|
56
|
+
const encoded = Buffer.from(`user:${password}`).toString("base64");
|
|
57
|
+
return `Basic ${encoded}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makePlan(overrides: Partial<Parameters<typeof createPlan>[1]> = {}): Plan {
|
|
61
|
+
return createPlan(db, {
|
|
62
|
+
id: crypto.randomUUID(),
|
|
63
|
+
slug: "test-plan",
|
|
64
|
+
title: "Test Plan",
|
|
65
|
+
status: "draft",
|
|
66
|
+
priority: 2,
|
|
67
|
+
approvedAt: null,
|
|
68
|
+
completedAt: null,
|
|
69
|
+
sessionId: null,
|
|
70
|
+
overview: "test overview",
|
|
71
|
+
approach: null,
|
|
72
|
+
complexity: 3,
|
|
73
|
+
createdBy: "test",
|
|
74
|
+
updatedBy: "test",
|
|
75
|
+
sourceSessionId: null,
|
|
76
|
+
sourceMessageId: null,
|
|
77
|
+
category: null,
|
|
78
|
+
metadata: {},
|
|
79
|
+
archivedAt: null,
|
|
80
|
+
...overrides,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
savedPassword = process.env.OPENCODE_SERVER_PASSWORD;
|
|
86
|
+
process.env.OPENCODE_SERVER_PASSWORD = "test-password";
|
|
87
|
+
db = new Database(":memory:");
|
|
88
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
89
|
+
runMigrations(db);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
if (savedPassword === undefined) {
|
|
94
|
+
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
95
|
+
} else {
|
|
96
|
+
process.env.OPENCODE_SERVER_PASSWORD = savedPassword;
|
|
97
|
+
}
|
|
98
|
+
db.close();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("GET /health", () => {
|
|
102
|
+
test("returns 200 with status, version, uptime, dbHealthy", async () => {
|
|
103
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
104
|
+
const res = await app.handle(new Request("http://localhost/health"));
|
|
105
|
+
|
|
106
|
+
expect(res.status).toBe(200);
|
|
107
|
+
const body = (await res.json()) as {
|
|
108
|
+
status: string;
|
|
109
|
+
version: string;
|
|
110
|
+
uptime: number;
|
|
111
|
+
timestamp: number;
|
|
112
|
+
dbHealthy: boolean;
|
|
113
|
+
};
|
|
114
|
+
expect(body.status).toBe("ok");
|
|
115
|
+
expect(typeof body.version).toBe("string");
|
|
116
|
+
expect(typeof body.uptime).toBe("number");
|
|
117
|
+
expect(typeof body.timestamp).toBe("number");
|
|
118
|
+
expect(body.dbHealthy).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("health is exempt from auth", async () => {
|
|
122
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
123
|
+
const res = await app.handle(new Request("http://localhost/health"));
|
|
124
|
+
|
|
125
|
+
expect(res.status).toBe(200);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("GET /api/plans", () => {
|
|
130
|
+
test("returns 200 with array of plans", async () => {
|
|
131
|
+
makePlan({ slug: "plan-1", title: "Plan One" });
|
|
132
|
+
makePlan({ slug: "plan-2", title: "Plan Two" });
|
|
133
|
+
|
|
134
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
135
|
+
const req = new Request("http://localhost/api/plans", {
|
|
136
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
137
|
+
});
|
|
138
|
+
const res = await app.handle(req);
|
|
139
|
+
|
|
140
|
+
expect(res.status).toBe(200);
|
|
141
|
+
const body = (await res.json()) as unknown[];
|
|
142
|
+
expect(Array.isArray(body)).toBe(true);
|
|
143
|
+
expect(body.length).toBe(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("returns empty array when no plans", async () => {
|
|
147
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
148
|
+
const req = new Request("http://localhost/api/plans", {
|
|
149
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
150
|
+
});
|
|
151
|
+
const res = await app.handle(req);
|
|
152
|
+
|
|
153
|
+
expect(res.status).toBe(200);
|
|
154
|
+
const body = (await res.json()) as unknown[];
|
|
155
|
+
expect(body).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("requires auth → 401 without credentials", async () => {
|
|
159
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
160
|
+
const res = await app.handle(new Request("http://localhost/api/plans"));
|
|
161
|
+
|
|
162
|
+
expect(res.status).toBe(401);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("auth disabled → 200 without credentials", async () => {
|
|
166
|
+
makePlan();
|
|
167
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
168
|
+
const res = await app.handle(new Request("http://localhost/api/plans"));
|
|
169
|
+
|
|
170
|
+
expect(res.status).toBe(200);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("GET /api/plans/:id", () => {
|
|
175
|
+
test("returns 200 for known plan id", async () => {
|
|
176
|
+
const plan = makePlan({ slug: "known-plan" });
|
|
177
|
+
|
|
178
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
179
|
+
const req = new Request(`http://localhost/api/plans/${plan.id}`, {
|
|
180
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
181
|
+
});
|
|
182
|
+
const res = await app.handle(req);
|
|
183
|
+
|
|
184
|
+
expect(res.status).toBe(200);
|
|
185
|
+
const body = (await res.json()) as { id: string; slug: string };
|
|
186
|
+
expect(body.id).toBe(plan.id);
|
|
187
|
+
expect(body.slug).toBe("known-plan");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("returns 404 for unknown plan id", async () => {
|
|
191
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
192
|
+
const req = new Request("http://localhost/api/plans/nonexistent-id", {
|
|
193
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
194
|
+
});
|
|
195
|
+
const res = await app.handle(req);
|
|
196
|
+
|
|
197
|
+
expect(res.status).toBe(404);
|
|
198
|
+
const body = (await res.json()) as { error: string };
|
|
199
|
+
expect(body.error).toBe("not_found");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("GET /api/tasks", () => {
|
|
204
|
+
test("returns tasks filtered by planId", async () => {
|
|
205
|
+
const plan = makePlan();
|
|
206
|
+
createTasksBatch(db, plan.id, [
|
|
207
|
+
{
|
|
208
|
+
orderIndex: 0,
|
|
209
|
+
description: "task-1",
|
|
210
|
+
agent: "test",
|
|
211
|
+
files: [],
|
|
212
|
+
complexity: 1,
|
|
213
|
+
dependencies: [],
|
|
214
|
+
createdBy: "test",
|
|
215
|
+
updatedBy: "test",
|
|
216
|
+
sourceSessionId: null,
|
|
217
|
+
sourceMessageId: null,
|
|
218
|
+
reviewedBy: null,
|
|
219
|
+
tokensUsed: null,
|
|
220
|
+
durationMs: null,
|
|
221
|
+
artifacts: [],
|
|
222
|
+
metadata: {},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
orderIndex: 1,
|
|
226
|
+
description: "task-2",
|
|
227
|
+
agent: "test",
|
|
228
|
+
files: [],
|
|
229
|
+
complexity: 1,
|
|
230
|
+
dependencies: [],
|
|
231
|
+
createdBy: "test",
|
|
232
|
+
updatedBy: "test",
|
|
233
|
+
sourceSessionId: null,
|
|
234
|
+
sourceMessageId: null,
|
|
235
|
+
reviewedBy: null,
|
|
236
|
+
tokensUsed: null,
|
|
237
|
+
durationMs: null,
|
|
238
|
+
artifacts: [],
|
|
239
|
+
metadata: {},
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
244
|
+
const req = new Request(`http://localhost/api/tasks?planId=${plan.id}`, {
|
|
245
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
246
|
+
});
|
|
247
|
+
const res = await app.handle(req);
|
|
248
|
+
|
|
249
|
+
expect(res.status).toBe(200);
|
|
250
|
+
const body = (await res.json()) as unknown[];
|
|
251
|
+
expect(body.length).toBe(2);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("returns 422 without planId query param", async () => {
|
|
255
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
256
|
+
const req = new Request("http://localhost/api/tasks", {
|
|
257
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
258
|
+
});
|
|
259
|
+
const res = await app.handle(req);
|
|
260
|
+
|
|
261
|
+
expect(res.status).toBe(422);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("GET /api/tasks/:id", () => {
|
|
266
|
+
test("returns 200 for known task id", async () => {
|
|
267
|
+
const plan = makePlan();
|
|
268
|
+
const tasks = createTasksBatch(db, plan.id, [
|
|
269
|
+
{
|
|
270
|
+
orderIndex: 0,
|
|
271
|
+
description: "known-task",
|
|
272
|
+
agent: "test",
|
|
273
|
+
files: [],
|
|
274
|
+
complexity: 1,
|
|
275
|
+
dependencies: [],
|
|
276
|
+
createdBy: "test",
|
|
277
|
+
updatedBy: "test",
|
|
278
|
+
sourceSessionId: null,
|
|
279
|
+
sourceMessageId: null,
|
|
280
|
+
reviewedBy: null,
|
|
281
|
+
tokensUsed: null,
|
|
282
|
+
durationMs: null,
|
|
283
|
+
artifacts: [],
|
|
284
|
+
metadata: {},
|
|
285
|
+
},
|
|
286
|
+
]);
|
|
287
|
+
const taskId = tasks[0]?.id as string;
|
|
288
|
+
|
|
289
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
290
|
+
const req = new Request(`http://localhost/api/tasks/${taskId}`, {
|
|
291
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
292
|
+
});
|
|
293
|
+
const res = await app.handle(req);
|
|
294
|
+
|
|
295
|
+
expect(res.status).toBe(200);
|
|
296
|
+
const body = (await res.json()) as { id: string; description: string };
|
|
297
|
+
expect(body.id).toBe(taskId);
|
|
298
|
+
expect(body.description).toBe("known-task");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("returns 404 for unknown task id", async () => {
|
|
302
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
303
|
+
const req = new Request("http://localhost/api/tasks/nonexistent", {
|
|
304
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
305
|
+
});
|
|
306
|
+
const res = await app.handle(req);
|
|
307
|
+
|
|
308
|
+
expect(res.status).toBe(404);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe("GET /api/sessions", () => {
|
|
313
|
+
test("returns sessions list", async () => {
|
|
314
|
+
startSession(db, { id: "ses_test_1", goal: "test goal" });
|
|
315
|
+
startSession(db, { id: "ses_test_2", goal: "test goal 2" });
|
|
316
|
+
|
|
317
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
318
|
+
const req = new Request("http://localhost/api/sessions", {
|
|
319
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
320
|
+
});
|
|
321
|
+
const res = await app.handle(req);
|
|
322
|
+
|
|
323
|
+
expect(res.status).toBe(200);
|
|
324
|
+
const body = (await res.json()) as unknown[];
|
|
325
|
+
expect(body.length).toBe(2);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("GET /api/sessions/active", () => {
|
|
330
|
+
test("returns only active sessions (endedAt === null)", async () => {
|
|
331
|
+
startSession(db, { id: "ses_active_1", goal: "active goal" });
|
|
332
|
+
startSession(db, { id: "ses_active_2", goal: "another active" });
|
|
333
|
+
|
|
334
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
335
|
+
const req = new Request("http://localhost/api/sessions/active", {
|
|
336
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
337
|
+
});
|
|
338
|
+
const res = await app.handle(req);
|
|
339
|
+
|
|
340
|
+
expect(res.status).toBe(200);
|
|
341
|
+
const body = (await res.json()) as unknown[];
|
|
342
|
+
expect(body.length).toBe(2);
|
|
343
|
+
for (const session of body as Array<{ endedAt: number | null }>) {
|
|
344
|
+
expect(session.endedAt).toBeNull();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("GET /api/sessions/:id", () => {
|
|
350
|
+
test("returns 200 for known session id", async () => {
|
|
351
|
+
startSession(db, { id: "ses_known", goal: "known goal" });
|
|
352
|
+
|
|
353
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
354
|
+
const req = new Request("http://localhost/api/sessions/ses_known", {
|
|
355
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
356
|
+
});
|
|
357
|
+
const res = await app.handle(req);
|
|
358
|
+
|
|
359
|
+
expect(res.status).toBe(200);
|
|
360
|
+
const body = (await res.json()) as { id: string };
|
|
361
|
+
expect(body.id).toBe("ses_known");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("returns 404 for unknown session id", async () => {
|
|
365
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
366
|
+
const req = new Request("http://localhost/api/sessions/nonexistent", {
|
|
367
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
368
|
+
});
|
|
369
|
+
const res = await app.handle(req);
|
|
370
|
+
|
|
371
|
+
expect(res.status).toBe(404);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("CORS preflight", () => {
|
|
376
|
+
test("OPTIONS → 204 with CORS headers", async () => {
|
|
377
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
378
|
+
const req = new Request("http://localhost/api/plans", {
|
|
379
|
+
method: "OPTIONS",
|
|
380
|
+
headers: {
|
|
381
|
+
Origin: "https://example.com",
|
|
382
|
+
"Access-Control-Request-Method": "GET",
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
const res = await app.handle(req);
|
|
386
|
+
|
|
387
|
+
expect(res.status).toBe(204);
|
|
388
|
+
expect(res.headers.get("Access-Control-Allow-Methods")).toContain("GET");
|
|
389
|
+
expect(res.headers.get("Access-Control-Allow-Headers")).toBeDefined();
|
|
390
|
+
expect(res.headers.get("Access-Control-Max-Age")).toBe("86400");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("wildcard origins → ACAO: *", async () => {
|
|
394
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
395
|
+
const req = new Request("http://localhost/api/plans", {
|
|
396
|
+
headers: { Origin: "https://any-origin.com" },
|
|
397
|
+
});
|
|
398
|
+
const res = await app.handle(req);
|
|
399
|
+
|
|
400
|
+
expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("restricted origins → echoes matching origin", async () => {
|
|
404
|
+
const { app } = await buildHttpServer({ db, httpConfig: CORS_CONFIG });
|
|
405
|
+
const req = new Request("http://localhost/api/plans", {
|
|
406
|
+
headers: { Origin: "https://example.com" },
|
|
407
|
+
});
|
|
408
|
+
const res = await app.handle(req);
|
|
409
|
+
|
|
410
|
+
expect(res.headers.get("Access-Control-Allow-Origin")).toBe("https://example.com");
|
|
411
|
+
expect(res.headers.get("Vary")).toBe("Origin");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("restricted origins → no ACAO for non-matching origin", async () => {
|
|
415
|
+
const { app } = await buildHttpServer({ db, httpConfig: CORS_CONFIG });
|
|
416
|
+
const req = new Request("http://localhost/api/plans", {
|
|
417
|
+
headers: { Origin: "https://evil.com" },
|
|
418
|
+
});
|
|
419
|
+
const res = await app.handle(req);
|
|
420
|
+
|
|
421
|
+
// No Access-Control-Allow-Origin for non-matching origins
|
|
422
|
+
expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull();
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe("Security headers", () => {
|
|
427
|
+
test("responses include X-Content-Type-Options", async () => {
|
|
428
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
429
|
+
const res = await app.handle(new Request("http://localhost/health"));
|
|
430
|
+
|
|
431
|
+
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("responses include X-Frame-Options", async () => {
|
|
435
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
436
|
+
const res = await app.handle(new Request("http://localhost/health"));
|
|
437
|
+
|
|
438
|
+
expect(res.headers.get("X-Frame-Options")).toBe("DENY");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("responses include X-Powered-By: ndomo", async () => {
|
|
442
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
443
|
+
const res = await app.handle(new Request("http://localhost/health"));
|
|
444
|
+
|
|
445
|
+
expect(res.headers.get("X-Powered-By")).toBe("ndomo");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("responses include Referrer-Policy", async () => {
|
|
449
|
+
const { app } = await buildHttpServer({ db, httpConfig: NO_AUTH_CONFIG });
|
|
450
|
+
const res = await app.handle(new Request("http://localhost/health"));
|
|
451
|
+
|
|
452
|
+
expect(res.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("security headers on API routes too", async () => {
|
|
456
|
+
const { app } = await buildHttpServer({ db, httpConfig: AUTH_CONFIG });
|
|
457
|
+
const req = new Request("http://localhost/api/plans", {
|
|
458
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
459
|
+
});
|
|
460
|
+
const res = await app.handle(req);
|
|
461
|
+
|
|
462
|
+
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
463
|
+
expect(res.headers.get("X-Frame-Options")).toBe("DENY");
|
|
464
|
+
});
|
|
465
|
+
});
|