Files
zeekay 3dd01043ec Answer refusals as RFC 9457 problem documents; OAuth keeps its own vocabulary
A refusal now leaves by one door and is written in the vocabulary of the
address it happened at.

HTTPError loses its struct tags and becomes a plain value; MarshalJSON is the
single place that says what it looks like. Its three members already mapped
onto both standards exactly, so no field was added: Status is `status`, Msg is
`detail`, Code is an extension member (RFC 9457), and the same value reads as
`error` / `error_description` under RFC 6749 §5.2. With() members still merge
at the top level per §3.2, with the document's own written last.

zip.OAuth(r) returns a Router whose routes answer the OAuth vocabulary. The
fact rides the route entry rather than a table of addresses, so it survives
composition, and it is a property of the address rather than of the returned
value — which is what makes it reach the refusals no handler wrote (Bind,
validation, panics). A verb the endpoint does not serve is the router
refusing, and answers a problem document with Allow.

middleware.Recover wrote its own 500 body with c.JSON, a second error surface
that spoke only the default vocabulary; it now returns the error so the one
handler renders it. group() copies oauth beside wrap, so a nested scope keeps
the vocabulary.

Allow on 405 comes from the fiber fork's router and was already correct;
TestAWrongVerbSaysWhichVerbsWork now pins it in both directions so an error
handler edit cannot drop it.

Consumers reading `body.error` read `body.detail` now. Each repo takes the
change when it moves its own pin.
2026-08-21 12:47:57 -07:00

114 lines
3.6 KiB
Go

package main
import (
"io"
"net/http/httptest"
"strings"
"testing"
)
// TestExpressInZip is the proof point: the legacy TS handler in app.ts,
// transpiled by real esbuild, loaded into real goja, mounted on real
// Fiber via zip, exercised over a real HTTP roundtrip.
func TestExpressInZip(t *testing.T) {
app, err := setup()
if err != nil {
t.Fatal(err)
}
t.Run("GET /legacy/foo", func(t *testing.T) {
resp, err := app.Fiber().Test(httptest.NewRequest("GET", "/legacy/foo", nil))
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
assertContainsAll(t, string(body), `"ok":true`, `"path":"/foo"`, `"body":null`)
})
t.Run("POST /legacy/bar echoes body", func(t *testing.T) {
req := httptest.NewRequest("POST", "/legacy/bar", strings.NewReader(`{"x":1}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
assertContainsAll(t, string(body), `"ok":true`, `"path":"/bar"`, `"x":1`)
})
}
// TestRuntimeRoute drives the unified runner over HTTP: :lang selects the
// backend and the body carries the source. js evaluates in real goja; an
// unregistered language is a 404 with a structured error.
//
// The body is the op's In as JSON, because the route is a typed op — which is
// what puts "run this source" in the document and in the MCP tool list. A raw
// body would have meant no schema, and no schema means no projection.
func TestRuntimeRoute(t *testing.T) {
app, err := setup()
if err != nil {
t.Fatal(err)
}
t.Run("POST /runtime/js evaluates source", func(t *testing.T) {
req := httptest.NewRequest("POST", "/runtime/js", strings.NewReader(`{"source":"40+2"}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
// goja Export() of an integer arithmetic result is int64; JSON
// encodes it as the bare number 42.
assertContainsAll(t, string(body), `"result":42`)
})
t.Run("POST /runtime/cobol is 404 unknown language", func(t *testing.T) {
req := httptest.NewRequest("POST", "/runtime/cobol", strings.NewReader(`{"source":"DISPLAY 'HI'."}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 404 {
t.Fatalf("status = %d, want 404", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); ct != "application/problem+json" {
t.Fatalf("Content-Type = %q, want the RFC 9457 media type", ct)
}
body, _ := io.ReadAll(resp.Body)
assertContainsAll(t, string(body), `"detail":"unknown language"`, `"status":404`)
})
// The op is in the document because it IS an op — the assertion that says
// step 5 of setup() achieved something the fiber-level registration did not.
t.Run("the runtime op is projected", func(t *testing.T) {
spec := app.OpenAPISpec()
paths, _ := spec["paths"].(map[string]map[string]any)
if _, ok := paths["/runtime/{lang}"]; !ok {
t.Fatalf("the runtime op is missing from the document; paths = %v", paths)
}
if len(app.MCPTools()) != 1 {
t.Fatalf("tools = %v, want the one op", app.MCPTools())
}
})
}
func assertContainsAll(t *testing.T, body string, wants ...string) {
t.Helper()
for _, w := range wants {
if !strings.Contains(body, w) {
t.Fatalf("body %q missing %q", body, w)
}
}
}