From f94025a1236d074b43fb67d0231ed50ece28407e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Apr 2026 17:52:42 -0400 Subject: [PATCH] test(httpapi): cover hono bridge middleware --- .../server/routes/instance/httpapi/project.ts | 4 +- .../test/server/httpapi-bridge.test.ts | 131 ++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/server/httpapi-bridge.test.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts index 10cf25118f06..6d3143df869b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/project.ts @@ -1,4 +1,4 @@ -import { Instance } from "@/project/instance" +import * as InstanceState from "@/effect/instance-state" import { Project } from "@/project" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" @@ -54,7 +54,7 @@ export const projectHandlers = Layer.unwrap( }) const current = Effect.fn("ProjectHttpApi.current")(function* () { - return Instance.project + return (yield* InstanceState.context).project }) return HttpApiBuilder.group(ProjectApi, "project", (handlers) => diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts new file mode 100644 index 000000000000..29f85b88100b --- /dev/null +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -0,0 +1,131 @@ +import { afterEach, describe, expect, test } from "bun:test" +import type { UpgradeWebSocket } from "hono/ws" +import { Flag } from "../../src/flag/flag" +import { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { FilePaths } from "../../src/server/routes/instance/httpapi/file" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = { + OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI, + OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD, + OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME, +} + +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app(input?: { password?: string; username?: string }) { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + Flag.OPENCODE_SERVER_PASSWORD = input?.password + Flag.OPENCODE_SERVER_USERNAME = input?.username + return InstanceRoutes(websocket) +} + +function authorization(username: string, password: string) { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` +} + +function fileUrl(input?: { directory?: string; token?: string }) { + const url = new URL(`http://localhost${FilePaths.content}`) + url.searchParams.set("path", "hello.txt") + if (input?.directory) url.searchParams.set("directory", input.directory) + if (input?.token) url.searchParams.set("auth_token", input.token) + return url +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI + Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD + Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME + await Instance.disposeAll() + await resetDatabase() +}) + +describe("HttpApi Hono bridge", () => { + test("allows requests when auth is disabled", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(`${tmp.path}/hello.txt`, "hello") + + const response = await app().request(fileUrl(), { + headers: { + "x-opencode-directory": tmp.path, + }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ content: "hello" }) + }) + + test("provides instance context to bridged handlers", async () => { + await using tmp = await tmpdir({ git: true }) + + const response = await app().request("/project/current", { + headers: { + "x-opencode-directory": tmp.path, + }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ worktree: tmp.path }) + }) + + test("requires credentials when auth is enabled", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(`${tmp.path}/hello.txt`, "hello") + + const [missing, bad, good] = await Promise.all([ + app({ password: "secret" }).request(fileUrl(), { + headers: { "x-opencode-directory": tmp.path }, + }), + app({ password: "secret" }).request(fileUrl(), { + headers: { + authorization: authorization("opencode", "wrong"), + "x-opencode-directory": tmp.path, + }, + }), + app({ password: "secret" }).request(fileUrl(), { + headers: { + authorization: authorization("opencode", "secret"), + "x-opencode-directory": tmp.path, + }, + }), + ]) + + expect(missing.status).toBe(401) + expect(bad.status).toBe(401) + expect(good.status).toBe(200) + }) + + test("accepts auth_token query credentials", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(`${tmp.path}/hello.txt`, "hello") + + const response = await app({ password: "secret" }).request(fileUrl({ token: Buffer.from("opencode:secret").toString("base64") }), { + headers: { + "x-opencode-directory": tmp.path, + }, + }) + + expect(response.status).toBe(200) + }) + + test("selects instance from query before directory header", async () => { + await using header = await tmpdir({ git: true }) + await using query = await tmpdir({ git: true }) + await Bun.write(`${header.path}/hello.txt`, "header") + await Bun.write(`${query.path}/hello.txt`, "query") + + const response = await app().request(fileUrl({ directory: query.path }), { + headers: { + "x-opencode-directory": header.path, + }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ content: "query" }) + }) +})