Skip to content

Commit af4358f

Browse files
committed
test(server): cover basic-auth 401 Content-Type regression
Both code-review-opus and code-review-gpt5 flagged the missing regression test for the AuthMiddleware Content-Type fix. Without an assertion the bug is invisible to future reviewers: hono's basicAuth constructs the 401 Response with no Content-Type, and the symptom (Chromium's download-attachment race on the unauth navigation, plus the multi-prompt loop) does not surface in any existing test. The new test file exercises AuthMiddleware directly through a minimal Hono app, asserting: - Missing credentials -> 401, Content-Type contains text/plain, never octet-stream, WWW-Authenticate present. - Wrong credentials -> same shape. - Correct credentials -> 200 passes through. - No password set -> 200 passes through. - OPTIONS preflight bypasses auth (not 401). - auth_token query parameter accepted as credentials. Verified the missing/wrong-credential tests fail against pre-fix code and pass post-fix (6/6). Refs upstream issue anomalyco#18325
1 parent 59428c4 commit af4358f

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { afterEach, describe, expect, test } from "bun:test"
2+
import { Hono } from "hono"
3+
import { Flag } from "@opencode-ai/core/flag/flag"
4+
import { AuthMiddleware, ErrorMiddleware } from "../../src/server/middleware"
5+
import { Log } from "../../src/util"
6+
7+
void Log.init({ print: false })
8+
9+
const original = {
10+
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
11+
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
12+
}
13+
14+
afterEach(() => {
15+
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
16+
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
17+
})
18+
19+
function app() {
20+
return new Hono()
21+
.onError(ErrorMiddleware)
22+
.use(AuthMiddleware)
23+
.get("/probe", (c) => c.text("ok"))
24+
}
25+
26+
function authorization(username: string, password: string) {
27+
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
28+
}
29+
30+
describe("AuthMiddleware", () => {
31+
// Regression: hono's basicAuth returns its 401 Response without a
32+
// Content-Type. Bun's HTTP serializer then defaults to
33+
// `application/octet-stream`, which Chromium briefly treats as a download
34+
// attachment before settling on the basic-auth dialog. AuthMiddleware
35+
// wraps basicAuth so the 401 carries text/plain and preserves the
36+
// WWW-Authenticate header. Refs upstream issue anomalyco/opencode#18325.
37+
test("401 carries text/plain Content-Type when credentials are missing", async () => {
38+
Flag.OPENCODE_SERVER_PASSWORD = "secret"
39+
40+
const response = await app().request("/probe")
41+
42+
expect(response.status).toBe(401)
43+
const contentType = response.headers.get("content-type") ?? ""
44+
expect(contentType).toContain("text/plain")
45+
expect(contentType).not.toContain("octet-stream")
46+
expect(response.headers.get("www-authenticate")).toContain("Basic")
47+
expect(await response.text()).toBe("Unauthorized")
48+
})
49+
50+
test("401 carries text/plain Content-Type when credentials are wrong", async () => {
51+
Flag.OPENCODE_SERVER_PASSWORD = "secret"
52+
53+
const response = await app().request("/probe", {
54+
headers: { authorization: authorization("opencode", "wrong") },
55+
})
56+
57+
expect(response.status).toBe(401)
58+
const contentType = response.headers.get("content-type") ?? ""
59+
expect(contentType).toContain("text/plain")
60+
expect(contentType).not.toContain("octet-stream")
61+
})
62+
63+
test("authenticated requests pass through unchanged", async () => {
64+
Flag.OPENCODE_SERVER_PASSWORD = "secret"
65+
66+
const response = await app().request("/probe", {
67+
headers: { authorization: authorization("opencode", "secret") },
68+
})
69+
70+
expect(response.status).toBe(200)
71+
expect(await response.text()).toBe("ok")
72+
})
73+
74+
test("requests pass through when no password is set", async () => {
75+
Flag.OPENCODE_SERVER_PASSWORD = undefined
76+
77+
const response = await app().request("/probe")
78+
79+
expect(response.status).toBe(200)
80+
expect(await response.text()).toBe("ok")
81+
})
82+
83+
test("CORS preflight bypasses auth", async () => {
84+
Flag.OPENCODE_SERVER_PASSWORD = "secret"
85+
86+
const response = await app().request("/probe", { method: "OPTIONS" })
87+
88+
// OPTIONS hits next() with no GET handler matching → 404, not 401.
89+
// The point is that auth did not block it.
90+
expect(response.status).not.toBe(401)
91+
})
92+
93+
test("auth_token query parameter is accepted as credentials", async () => {
94+
Flag.OPENCODE_SERVER_PASSWORD = "secret"
95+
96+
const token = Buffer.from("opencode:secret").toString("base64")
97+
const response = await app().request(`/probe?auth_token=${token}`)
98+
99+
expect(response.status).toBe(200)
100+
expect(await response.text()).toBe("ok")
101+
})
102+
})

0 commit comments

Comments
 (0)