Files
molecule-cli/internal/cmd/http_test.go
sdk-dev a002b412e9
Release Go binaries / release (pull_request) Blocked by required conditions
CI / Test / test (pull_request) Successful in 1m51s
Release Go binaries / test (pull_request) Successful in 1m11s
feat(cli): fix runHTTP auth bug + add management verbs
Fix the auth bug FIRST: internal/cmd/http.go runHTTP (and the
internal/client Platform HTTP helpers, which had the same gap) sent NO
Authorization header, so management calls (workspace create/delete,
secrets, tokens, …) 401'd a hardened tenant. Now every request attaches
`Authorization: Bearer $MOLECULE_API_KEY` and, when set,
`X-Molecule-Org-Id: $MOLECULE_ORG_ID` (the tenant TenantGuard routing
gate). Headers are omitted when the env vars are unset so fresh
self-host/dev tenants keep working. Regression test
TestRunHTTP_SetsAuthHeader asserts the header is set and is proven
load-bearing (fails with `Authorization header = ""` when the fix is
reverted).

Add the management verbs (PLATFORM-MANAGEMENT-API.md §5(b)), each wired
to the OpenAPI-documented endpoint at the correct auth tier (verified
against the live workspace-server router.go + handlers and controlplane
orgs handler, since the parallel feat/openapi-management-spec branch
does not exist in molecule-core — reconciled to the actual handler
source instead):

  org   list|get|create --slug/--name|create --template|export
        token list|create|revoke | allowlist
  workspace list|get|create|delete|restart|pause|resume
            budget|billing-mode|token mint
  secret  ws  list|set|delete
          org list|set|delete
  template list|import|refresh
  bundle  export|import
  events
  approvals

Org-lifecycle verbs target the control plane (MOLECULE_CP_URL, default
= api-url); tenant verbs target the tenant host with the Org API Key.
All verbs honor --json (and existing -o table|json|yaml). Request/
response shapes match the handler structs (budget USD-cents
budget_limits; billing-mode {mode}; org import {dir,mode}; secrets
{key,value}; template import {name,files}; etc.).

Tests: table-driven request-construction tests (method/path/body/auth)
for all 30 management methods against an httptest mock, plus
cmd-layer branch tests (budget flag→limits, billing-mode validation,
template file mapping, --json resolution, CP-url fallback). Existing
workspace/agent/platform commands switched to the authenticated client.

go build ./..., go vet ./..., go test ./... all green; gofmt clean on
edited files. Binary smoke-tested end-to-end: auth headers reach the
server and --json output renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:53:04 -07:00

88 lines
2.8 KiB
Go

package cmd
import (
"net/http"
"net/http/httptest"
"testing"
)
// TestRunHTTP_SetsAuthHeader is the regression test for the auth bug: before
// the fix, runHTTP sent NO Authorization header and management calls 401'd a
// hardened tenant. It must now send `Authorization: Bearer $MOLECULE_API_KEY`
// and `X-Molecule-Org-Id: $MOLECULE_ORG_ID`.
func TestRunHTTP_SetsAuthHeader(t *testing.T) {
t.Setenv("MOLECULE_API_KEY", "test-key-123")
t.Setenv("MOLECULE_ORG_ID", "org_abc")
var gotAuth, gotOrg, gotCT string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
gotOrg = r.Header.Get("X-Molecule-Org-Id")
gotCT = r.Header.Get("Content-Type")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
if _, err := runHTTP("POST", srv.URL+"/x", []byte(`{"a":1}`)); err != nil {
t.Fatalf("runHTTP: %v", err)
}
if want := "Bearer test-key-123"; gotAuth != want {
t.Errorf("Authorization header = %q, want %q", gotAuth, want)
}
if want := "org_abc"; gotOrg != want {
t.Errorf("X-Molecule-Org-Id header = %q, want %q", gotOrg, want)
}
if want := "application/json"; gotCT != want {
t.Errorf("Content-Type = %q, want %q", gotCT, want)
}
}
// TestRunHTTP_NoOrgIDWhenUnset confirms the org header is omitted when
// MOLECULE_ORG_ID is unset (so single-tenant/dev hosts that don't gate on it
// keep working) — but the bearer is still set.
func TestRunHTTP_NoOrgIDWhenUnset(t *testing.T) {
t.Setenv("MOLECULE_API_KEY", "k")
t.Setenv("MOLECULE_ORG_ID", "")
var hadOrg bool
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, hadOrg = r.Header["X-Molecule-Org-Id"]
gotAuth = r.Header.Get("Authorization")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
if _, err := runHTTP("GET", srv.URL+"/x", nil); err != nil {
t.Fatalf("runHTTP: %v", err)
}
if hadOrg {
t.Errorf("X-Molecule-Org-Id should be absent when MOLECULE_ORG_ID is unset")
}
if gotAuth != "Bearer k" {
t.Errorf("Authorization = %q, want %q", gotAuth, "Bearer k")
}
}
// TestRunHTTP_NoAuthHeaderWhenKeyUnset confirms no empty bearer is sent when
// MOLECULE_API_KEY is unset (preserves the dev/self-host fail-open path).
func TestRunHTTP_NoAuthHeaderWhenKeyUnset(t *testing.T) {
t.Setenv("MOLECULE_API_KEY", "")
t.Setenv("MOLECULE_ORG_ID", "")
var hadAuth bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, hadAuth = r.Header["Authorization"]
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
if _, err := runHTTP("GET", srv.URL+"/x", nil); err != nil {
t.Fatalf("runHTTP: %v", err)
}
if hadAuth {
t.Errorf("Authorization header should be absent when MOLECULE_API_KEY is unset")
}
}