Skip to content

Commit 38adc13

Browse files
authored
test: cover HttpApi authorization middleware (#25033)
1 parent 4fe14ab commit 38adc13

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { NodeHttpServer } from "@effect/platform-node"
2+
import { Flag } from "@opencode-ai/core/flag/flag"
3+
import { describe, expect } from "bun:test"
4+
import { Effect, Layer, Schema } from "effect"
5+
import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http"
6+
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"
7+
import { Authorization, authorizationLayer } from "../../src/server/routes/instance/httpapi/middleware/authorization"
8+
import { testEffect } from "../lib/effect"
9+
10+
const Api = HttpApi.make("test-authorization").add(
11+
HttpApiGroup.make("test")
12+
.add(
13+
HttpApiEndpoint.get("probe", "/probe", {
14+
success: Schema.String,
15+
}),
16+
)
17+
.middleware(Authorization),
18+
)
19+
20+
const handlers = HttpApiBuilder.group(Api, "test", (handlers) => handlers.handle("probe", () => Effect.succeed("ok")))
21+
22+
const apiLayer = HttpRouter.serve(
23+
HttpApiBuilder.layer(Api).pipe(Layer.provide(handlers), Layer.provide(authorizationLayer)),
24+
{ disableListenLog: true, disableLogger: true },
25+
).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
26+
27+
const testStateLayer = Layer.effectDiscard(
28+
Effect.gen(function* () {
29+
const original = {
30+
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
31+
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
32+
}
33+
Flag.OPENCODE_SERVER_PASSWORD = undefined
34+
Flag.OPENCODE_SERVER_USERNAME = undefined
35+
yield* Effect.addFinalizer(() =>
36+
Effect.sync(() => {
37+
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
38+
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
39+
}),
40+
)
41+
}),
42+
)
43+
44+
const it = testEffect(apiLayer.pipe(Layer.provideMerge(testStateLayer)))
45+
46+
const basic = (username: string, password: string) =>
47+
`Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
48+
49+
const token = (username: string, password: string) => Buffer.from(`${username}:${password}`).toString("base64")
50+
51+
const useAuth = (input: { password: string; username?: string }) =>
52+
Effect.acquireRelease(
53+
Effect.sync(() => {
54+
const original = {
55+
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
56+
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
57+
}
58+
Flag.OPENCODE_SERVER_PASSWORD = input.password
59+
Flag.OPENCODE_SERVER_USERNAME = input.username
60+
return original
61+
}),
62+
(original) =>
63+
Effect.sync(() => {
64+
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
65+
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
66+
}),
67+
)
68+
69+
const getProbe = (headers?: Record<string, string>) =>
70+
HttpClientRequest.get("/probe").pipe(
71+
headers ? HttpClientRequest.setHeaders(headers) : (request) => request,
72+
HttpClient.execute,
73+
)
74+
75+
describe("HttpApi authorization middleware", () => {
76+
it.live("allows requests when server password is not configured", () =>
77+
Effect.gen(function* () {
78+
const response = yield* getProbe()
79+
80+
expect(response.status).toBe(200)
81+
expect(yield* response.json).toBe("ok")
82+
}),
83+
)
84+
85+
it.live("requires configured password for basic auth", () =>
86+
Effect.gen(function* () {
87+
yield* useAuth({ password: "secret" })
88+
89+
const [missing, badPassword, good] = yield* Effect.all(
90+
[
91+
getProbe(),
92+
getProbe({ authorization: basic("opencode", "wrong") }),
93+
getProbe({ authorization: basic("opencode", "secret") }),
94+
],
95+
{ concurrency: "unbounded" },
96+
)
97+
98+
expect(missing.status).toBe(401)
99+
expect(badPassword.status).toBe(401)
100+
expect(good.status).toBe(200)
101+
}),
102+
)
103+
104+
it.live("respects configured basic auth username", () =>
105+
Effect.gen(function* () {
106+
yield* useAuth({ username: "kit", password: "secret" })
107+
108+
const [defaultUser, configuredUser] = yield* Effect.all(
109+
[getProbe({ authorization: basic("opencode", "secret") }), getProbe({ authorization: basic("kit", "secret") })],
110+
{ concurrency: "unbounded" },
111+
)
112+
113+
expect(defaultUser.status).toBe(401)
114+
expect(configuredUser.status).toBe(200)
115+
}),
116+
)
117+
118+
it.live("accepts auth token query credentials", () =>
119+
Effect.gen(function* () {
120+
yield* useAuth({ password: "secret" })
121+
122+
const response = yield* HttpClient.get(`/probe?auth_token=${encodeURIComponent(token("opencode", "secret"))}`)
123+
124+
expect(response.status).toBe(200)
125+
}),
126+
)
127+
128+
it.live("rejects malformed auth token query credentials", () =>
129+
Effect.gen(function* () {
130+
yield* useAuth({ password: "secret" })
131+
132+
const response = yield* HttpClient.get("/probe?auth_token=not-base64")
133+
134+
expect(response.status).toBe(401)
135+
}),
136+
)
137+
})

0 commit comments

Comments
 (0)