half broken
build / build-push (push) Failing after 23s

This commit is contained in:
developer committed 2026-09-27 17:17:53 +00:00
1 parent cc7466823b
commit e8c5b414d9
44 files changed
+7649 -344

No files matched your search

+213
View File
@@ -0,0 +1,213 @@
package api
import (
"net/http"
"strings"
"testing"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
)
// corsHarness starts a harness with the given cors_origins list. The origins
// are read once, when Handler() builds the wrapper, so a config edit afterwards
// does not move them — which is the same freezing the settings route has and is
// pinned by TestCORSOriginsAreReadAtHandlerBuild.
func corsHarness(t *testing.T, origins ...string) *harness {
t.Helper()
return newHarness(t, func(c *config.Config) { c.API.CORSOrigins = origins })
}
// TestCORSAllowedOrigin checks a configured origin is echoed back with the
// header set a credentialed fetch needs, and that Vary: Origin is present. Vary
// is the part that matters operationally: without it a shared cache will serve
// one origin's allow-origin header to another.
func TestCORSAllowedOrigin(t *testing.T) {
h := corsHarness(t, "https://console.example", "http://localhost:5173")
cases := []struct {
name string
origin string
}{
{"https origin", "https://console.example"},
{"plain http origin", "http://localhost:5173"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
res := h.do(http.MethodGet, "/api/whoami", "",
"X-API-Key: rw-key",
"Origin: "+tc.origin)
if res.status != http.StatusOK {
t.Fatalf("GET /api/whoami = %d, want 200: %s", res.status, res.body)
}
if got := res.header.Get("Access-Control-Allow-Origin"); got != tc.origin {
t.Errorf("Access-Control-Allow-Origin = %q, want %q", got, tc.origin)
}
if got := res.header.Get("Vary"); got != "Origin" {
t.Errorf("Vary = %q, want \"Origin\"", got)
}
if got := res.header.Get("Access-Control-Allow-Credentials"); got != "true" {
t.Errorf("Access-Control-Allow-Credentials = %q, want \"true\"", got)
}
if got := res.header.Get("Access-Control-Allow-Methods"); got == "" {
t.Error("no Access-Control-Allow-Methods advertised")
}
if got := res.header.Get("Access-Control-Allow-Headers"); got == "" {
t.Error("no Access-Control-Allow-Headers advertised")
}
})
}
// The allowed methods/header lists must actually name what the console
// sends, or a preflight for a PUT fails and the settings editor is
// unusable from a browser.
res := h.do(http.MethodGet, "/api/whoami", "", "X-API-Key: rw-key", "Origin: https://console.example")
if got := res.header.Get("Access-Control-Allow-Methods"); !strings.Contains(got, "PUT") || !strings.Contains(got, "DELETE") {
t.Errorf("Access-Control-Allow-Methods = %q, want PUT and DELETE in it", got)
}
if got := res.header.Get("Access-Control-Allow-Headers"); !strings.Contains(got, "X-API-Key") || !strings.Contains(got, "Authorization") {
t.Errorf("Access-Control-Allow-Headers = %q, want X-API-Key and Authorization in it", got)
}
}
// TestCORSDisallowedOrigin checks an origin that is not configured gets no CORS
// headers at all. The request still runs — CORS is enforced by the browser, not
// by refusing the request — but the browser must not be told the origin is
// allowed.
func TestCORSDisallowedOrigin(t *testing.T) {
h := corsHarness(t, "https://console.example")
res := h.do(http.MethodGet, "/api/whoami", "", "X-API-Key: rw-key", "Origin: https://evil.example")
if res.status != http.StatusOK {
t.Fatalf("GET /api/whoami = %d, want 200: %s", res.status, res.body)
}
for _, hdr := range []string{
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
} {
if got := res.header.Get(hdr); got != "" {
t.Errorf("%s = %q for a disallowed origin, want it absent", hdr, got)
}
}
// Vary is only added for an allowed origin, which is a caching footgun
// this suite pins: a shared cache that stored the unvaried response could
// hand the console's allow-origin to the other origin.
if got := res.header.Get("Vary"); got != "" {
t.Errorf("Vary = %q for a disallowed origin, want it absent", got)
}
}
// TestCORSWildcardEchoesTheOrigin checks "*" is honoured and, as the code
// comment says, echoed per request rather than answered as "*" — a literal
// wildcard is not accepted by a credentialed fetch.
func TestCORSWildcardEchoesTheOrigin(t *testing.T) {
h := corsHarness(t, "*")
for _, origin := range []string{"https://a.example", "https://b.example", "http://localhost:5173"} {
res := h.do(http.MethodGet, "/api/whoami", "", "X-API-Key: rw-key", "Origin: "+origin)
if got := res.header.Get("Access-Control-Allow-Origin"); got != origin {
t.Errorf("Access-Control-Allow-Origin for %s = %q, want the origin echoed", origin, got)
}
}
// The wildcard is the risky setting, so pin that credentials are still
// advertised for it: the code does NOT distinguish wildcard from an exact
// origin, which means "*" plus credentials is only safe because the
// wildcard is discouraged in the config comment. If this ever changes, the
// browser semantics change with it, so it is pinned rather than assumed.
res := h.do(http.MethodGet, "/api/whoami", "", "X-API-Key: rw-key", "Origin: https://a.example")
if got := res.header.Get("Access-Control-Allow-Credentials"); got != "true" {
t.Errorf("wildcard: Access-Control-Allow-Credentials = %q, want \"true\" (pinned current behaviour)", got)
}
}
// TestCORSPreflight checks OPTIONS is answered by the CORS wrapper with 204 and
// the method/header list, before authentication and before the mux — a
// preflight carries no API key, so a route that answered it would answer 401.
func TestCORSPreflight(t *testing.T) {
h := corsHarness(t, "https://console.example")
cases := []struct {
name string
path string
origin string
wantACA bool
}{
{"registered route", "/api/whoami", "https://console.example", true},
// The preflight is answered for any origin, allowed or not: the
// browser compares Access-Control-Allow-Origin itself, and a 403
// answer to a preflight is not a thing the browser can interpret.
{"disallowed origin", "/api/whoami", "https://evil.example", false},
// Not a registered route at all — the wrapper runs before the mux.
{"unknown route", "/api/nope", "https://console.example", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
res := h.do(http.MethodOptions, tc.path, "",
"Origin: "+tc.origin,
"Access-Control-Request-Method: PUT",
"Access-Control-Request-Headers: x-api-key")
if res.status != http.StatusNoContent {
t.Fatalf("OPTIONS %s = %d, want 204: %s", tc.path, res.status, res.body)
}
if len(res.body) != 0 {
t.Errorf("preflight returned a body: %q", res.body)
}
acao := res.header.Get("Access-Control-Allow-Origin")
if tc.wantACA && acao != tc.origin {
t.Errorf("Access-Control-Allow-Origin = %q, want %q", acao, tc.origin)
}
if !tc.wantACA && acao != "" {
t.Errorf("Access-Control-Allow-Origin = %q for a disallowed origin, want it absent", acao)
}
if tc.wantACA {
if got := res.header.Get("Access-Control-Allow-Methods"); !strings.Contains(got, "PUT") {
t.Errorf("Access-Control-Allow-Methods = %q, want PUT in it", got)
}
// HTTP header names are case-insensitive and the server
// advertises a fixed list, so compare case-insensitively.
if got := strings.ToLower(res.header.Get("Access-Control-Allow-Headers")); !strings.Contains(got, "x-api-key") {
t.Errorf("Access-Control-Allow-Headers = %q, want x-api-key in it", got)
}
}
})
}
}
// TestCORSDisabled checks a config without cors_origins is not wrapped at all:
// no headers, and OPTIONS falls through to the mux, which answers a method it
// does not serve rather than pretending a preflight succeeded.
func TestCORSDisabled(t *testing.T) {
h := newHarness(t, nil) // testConfig leaves CORSOrigins nil
res := h.do(http.MethodGet, "/api/whoami", "", "X-API-Key: rw-key", "Origin: https://console.example")
if res.status != http.StatusOK {
t.Fatalf("GET /api/whoami = %d, want 200: %s", res.status, res.body)
}
if got := res.header.Get("Access-Control-Allow-Origin"); got != "" {
t.Errorf("Access-Control-Allow-Origin = %q with no CORS configured, want it absent", got)
}
res = h.do(http.MethodOptions, "/api/whoami", "", "Origin: https://console.example")
if res.status != http.StatusMethodNotAllowed {
t.Errorf("OPTIONS with no CORS configured = %d, want 405 from the mux: %s", res.status, res.body)
}
}
// TestCORSOriginsAreReadAtHandlerBuild pins the same freezing the settings
// route has for the rest of the auth config: the allow-list is captured when
// Handler() is called, so adding an origin through the settings endpoint
// answers ok and changes nothing until a restart. Written down so that fixing
// the freezing later starts from a test that says what it is.
func TestCORSOriginsAreReadAtHandlerBuild(t *testing.T) {
h := newHarness(t, func(c *config.Config) { c.API.CORSOrigins = []string{"https://old.example"} })
body := settingsBody(t, func(c *config.Config) { c.API.CORSOrigins = []string{"https://new.example"} })
if res := h.do(http.MethodPut, "/api/settings", body, "X-API-Key: rw-key"); res.status != http.StatusOK {
t.Fatalf("settings put = %d: %s", res.status, res.body)
}
// The store carries the new list...
if got := h.store.Get().API.CORSOrigins; len(got) != 1 || got[0] != "https://new.example" {
t.Fatalf("store cors_origins = %v, want the written list", got)
}
// ...but the running server still answers for the origin it was built with.
res := h.do(http.MethodGet, "/api/whoami", "", "X-API-Key: rw-key", "Origin: https://new.example")
if got := res.header.Get("Access-Control-Allow-Origin"); got != "" {
t.Errorf("the written origin is already allowed (%q): the allow-list is not frozen as this test pins it", got)
}
if res := h.do(http.MethodGet, "/api/whoami", "", "X-API-Key: rw-key", "Origin: https://old.example"); res.header.Get("Access-Control-Allow-Origin") == "" {
t.Error("the origin the server was built with is no longer allowed")
}
}