mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-30 16:59:55 +00:00
test: add unit tests for authentication and CORS middleware
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"Noooste/garage-ui/internal/auth"
|
||||
"Noooste/garage-ui/internal/config"
|
||||
logpkg "Noooste/garage-ui/pkg/logger"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// newAuthTestApp builds a fiber.App with RequestID + Logging (buffer-backed) +
|
||||
// AuthMiddleware + a trivial /protected handler that echoes username/auth
|
||||
// method into the JSON body so tests can assert on locals.
|
||||
func newAuthTestApp(t *testing.T, buf *bytes.Buffer, authCfg *config.AuthConfig, svc *auth.Service) *fiber.App {
|
||||
t.Helper()
|
||||
base := zerolog.New(buf)
|
||||
app := fiber.New()
|
||||
app.Use(RequestID())
|
||||
app.Use(Logging(base))
|
||||
app.Use(AuthMiddleware(authCfg, svc))
|
||||
app.Get("/protected", func(c fiber.Ctx) error {
|
||||
uname, _ := c.Locals("username").(string)
|
||||
email, _ := c.Locals("email").(string)
|
||||
logpkg.FromCtx(c.Context()).Info().Msg("in_handler")
|
||||
return c.JSON(fiber.Map{
|
||||
"ok": true,
|
||||
"username": uname,
|
||||
"email": email,
|
||||
})
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
// newAuthSvc returns an *auth.Service with the given auth config, JWT
|
||||
// service initialized, OIDC disabled unless the caller wires it.
|
||||
func newAuthSvc(t *testing.T, authCfg *config.AuthConfig) *auth.Service {
|
||||
t.Helper()
|
||||
svc, err := auth.NewAuthService(authCfg, &config.ServerConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("NewAuthService: %v", err)
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// findLine returns the first parsed log line whose "message" field equals msg,
|
||||
// failing the test if no such line is present.
|
||||
func findLine(t *testing.T, buf *bytes.Buffer, msg string) map[string]any {
|
||||
t.Helper()
|
||||
for _, line := range parseLines(t, buf) {
|
||||
if line["message"] == msg {
|
||||
return line
|
||||
}
|
||||
}
|
||||
t.Fatalf("no %q log line: %s", msg, buf.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_BothDisabled_AllowsRequest(t *testing.T) {
|
||||
authCfg := &config.AuthConfig{
|
||||
Admin: config.AdminAuthConfig{Enabled: false},
|
||||
OIDC: config.OIDCConfig{Enabled: false},
|
||||
}
|
||||
svc := newAuthSvc(t, authCfg)
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, authCfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if body["ok"] != true {
|
||||
t.Errorf("ok = %v, want true", body["ok"])
|
||||
}
|
||||
if body["username"] != "" {
|
||||
t.Errorf("username should be empty when auth disabled, got %q", body["username"])
|
||||
}
|
||||
}
|
||||
|
||||
func newAdminCfg() *config.AuthConfig {
|
||||
return &config.AuthConfig{
|
||||
Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "pw"},
|
||||
OIDC: config.OIDCConfig{Enabled: false},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Admin_BearerValid_AllowsAndEnrichesLogger(t *testing.T) {
|
||||
authCfg := newAdminCfg()
|
||||
svc := newAuthSvc(t, authCfg)
|
||||
tok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "admin", Email: "a@b"})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSessionToken: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, authCfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var body map[string]any
|
||||
_ = json.NewDecoder(resp.Body).Decode(&body)
|
||||
if body["username"] != "admin" {
|
||||
t.Errorf("username = %v, want admin", body["username"])
|
||||
}
|
||||
if body["email"] != "a@b" {
|
||||
t.Errorf("email = %v, want a@b", body["email"])
|
||||
}
|
||||
|
||||
// Enriched handler log line should carry user_id and auth_method=admin.
|
||||
access := findLine(t, &buf, "in_handler")
|
||||
if access["user_id"] != "admin" {
|
||||
t.Errorf("user_id = %v, want admin", access["user_id"])
|
||||
}
|
||||
if access["auth_method"] != "admin" {
|
||||
t.Errorf("auth_method = %v, want admin", access["auth_method"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Admin_BearerInvalid_Returns401(t *testing.T) {
|
||||
authCfg := newAdminCfg()
|
||||
svc := newAuthSvc(t, authCfg)
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, authCfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer not-a-real-token")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
var env struct {
|
||||
Success bool `json:"success"`
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&env)
|
||||
if env.Success {
|
||||
t.Error("success should be false")
|
||||
}
|
||||
if env.Error.Code != "UNAUTHORIZED" {
|
||||
t.Errorf("error.code = %q, want UNAUTHORIZED", env.Error.Code)
|
||||
}
|
||||
|
||||
// Warn log should carry reason=no_valid_credentials without leaking the token.
|
||||
warn := findLine(t, &buf, "authentication_failed")
|
||||
if warn["reason"] != "no_valid_credentials" {
|
||||
t.Errorf("reason = %v", warn["reason"])
|
||||
}
|
||||
if warn["level"] != "warn" {
|
||||
t.Errorf("level = %v, want warn", warn["level"])
|
||||
}
|
||||
if strings.Contains(buf.String(), "not-a-real-token") {
|
||||
t.Error("token value must not appear in logs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Admin_NoAuthHeader_Returns401(t *testing.T) {
|
||||
authCfg := newAdminCfg()
|
||||
svc := newAuthSvc(t, authCfg)
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, authCfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Admin_NonBearerScheme_Returns401(t *testing.T) {
|
||||
authCfg := newAdminCfg()
|
||||
svc := newAuthSvc(t, authCfg)
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, authCfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Basic dXNlcjpwdw==")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func newOIDCCfg(cookieName string) *config.AuthConfig {
|
||||
return &config.AuthConfig{
|
||||
Admin: config.AdminAuthConfig{Enabled: false},
|
||||
OIDC: config.OIDCConfig{
|
||||
Enabled: true,
|
||||
CookieName: cookieName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newOIDCSvc returns a Service whose OIDC is *not* initialized (no dialing
|
||||
// needed), which is fine because AuthMiddleware's OIDC branch only calls
|
||||
// ValidateSessionToken — a pure JWT operation.
|
||||
func newOIDCSvc(t *testing.T) *auth.Service {
|
||||
t.Helper()
|
||||
return newAuthSvc(t, &config.AuthConfig{OIDC: config.OIDCConfig{Enabled: false}})
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_OIDC_ValidCookie_Allows(t *testing.T) {
|
||||
cfg := newOIDCCfg("session")
|
||||
svc := newOIDCSvc(t)
|
||||
|
||||
tok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "alice", Email: "a@x"})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSessionToken: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, cfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: tok})
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
access := findLine(t, &buf, "in_handler")
|
||||
if access["auth_method"] != "oidc" {
|
||||
t.Errorf("auth_method = %v, want oidc", access["auth_method"])
|
||||
}
|
||||
if access["user_id"] != "alice" {
|
||||
t.Errorf("user_id = %v, want alice", access["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_OIDC_InvalidCookie_Returns401(t *testing.T) {
|
||||
cfg := newOIDCCfg("session")
|
||||
svc := newOIDCSvc(t)
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, cfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: "garbage"})
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_OIDC_NoCookie_Returns401(t *testing.T) {
|
||||
cfg := newOIDCCfg("session")
|
||||
svc := newOIDCSvc(t)
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, cfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func newBothCfg(cookieName string) *config.AuthConfig {
|
||||
return &config.AuthConfig{
|
||||
Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "pw"},
|
||||
OIDC: config.OIDCConfig{
|
||||
Enabled: true,
|
||||
CookieName: cookieName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Both_BearerValid_AdminPathWins(t *testing.T) {
|
||||
cfg := newBothCfg("session")
|
||||
// OIDC disabled on the Service is fine — see Task 3 rationale.
|
||||
svc := newAuthSvc(t, &config.AuthConfig{Admin: cfg.Admin})
|
||||
|
||||
tok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "admin"})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSessionToken: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, cfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
// Also set a valid OIDC cookie; admin should still win.
|
||||
cookieTok, _ := svc.GenerateSessionToken(&auth.UserInfo{Username: "alice"})
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: cookieTok})
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
access := findLine(t, &buf, "in_handler")
|
||||
if access["auth_method"] != "admin" {
|
||||
t.Errorf("auth_method = %v, want admin", access["auth_method"])
|
||||
}
|
||||
if access["user_id"] != "admin" {
|
||||
t.Errorf("user_id = %v, want admin", access["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Both_BearerInvalid_FallsThroughToOIDCCookie(t *testing.T) {
|
||||
cfg := newBothCfg("session")
|
||||
svc := newAuthSvc(t, &config.AuthConfig{Admin: cfg.Admin})
|
||||
|
||||
cookieTok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSessionToken: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, cfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer bogus")
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: cookieTok})
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200 (OIDC fallback)", resp.StatusCode)
|
||||
}
|
||||
|
||||
access := findLine(t, &buf, "in_handler")
|
||||
if access["auth_method"] != "oidc" {
|
||||
t.Errorf("auth_method = %v, want oidc", access["auth_method"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Both_AllInvalid_Returns401WithCombinedMethodLabel(t *testing.T) {
|
||||
cfg := newBothCfg("session")
|
||||
svc := newAuthSvc(t, &config.AuthConfig{Admin: cfg.Admin})
|
||||
|
||||
var buf bytes.Buffer
|
||||
app := newAuthTestApp(t, &buf, cfg, svc)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer bogus")
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: "also-bogus"})
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
|
||||
warn := findLine(t, &buf, "authentication_failed")
|
||||
if warn["auth_method"] != "admin+oidc" {
|
||||
t.Errorf("auth_method = %v, want admin+oidc", warn["auth_method"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"Noooste/garage-ui/internal/config"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func newCORSApp(t *testing.T, cfg *config.CORSConfig) *fiber.App {
|
||||
t.Helper()
|
||||
app := fiber.New()
|
||||
app.Use(CORSMiddleware(cfg))
|
||||
app.Get("/x", func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
func TestCORS_Disabled_NoHeadersSet(t *testing.T) {
|
||||
cfg := &config.CORSConfig{Enabled: false}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://foo.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("Allow-Origin = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_Enabled_AllowedOriginEchoes(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"https://ok.example"},
|
||||
AllowedMethods: []string{"GET", "POST"},
|
||||
AllowedHeaders: []string{"Authorization", "Content-Type"},
|
||||
MaxAge: 300,
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://ok.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://ok.example" {
|
||||
t.Errorf("Allow-Origin = %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Vary"); got != "Origin" {
|
||||
t.Errorf("Vary = %q, want Origin", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_Enabled_OriginNotInList_NoHeaders(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"https://ok.example"},
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://evil.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("Allow-Origin = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_Enabled_NoOriginHeader_NoCORSHeaders(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"*"},
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("Allow-Origin = %q, want empty (no Origin header)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_Wildcard_NoCredentials_AllowsAnyOrigin(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowCredentials: false,
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://anywhere.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://anywhere.example" {
|
||||
t.Errorf("Allow-Origin = %q, want echo", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Errorf("Allow-Credentials set unexpectedly: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_Wildcard_WithCredentials_RejectsUnlistedOrigin(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowCredentials: true,
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://evil.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("Allow-Origin = %q, want empty (wildcard+creds must not honor *)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_ExactMatch_WithCredentials_SetsAllowCredentials(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"https://ok.example"},
|
||||
AllowCredentials: true,
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://ok.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://ok.example" {
|
||||
t.Errorf("Allow-Origin = %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "true" {
|
||||
t.Errorf("Allow-Credentials = %q, want true", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_Preflight_AllowedOrigin_Returns204WithHeaders(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"https://ok.example"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT"},
|
||||
AllowedHeaders: []string{"Authorization"},
|
||||
MaxAge: 600,
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("OPTIONS", "/x", nil)
|
||||
req.Header.Set("Origin", "https://ok.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("status = %d, want 204", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Methods"); got != "GET, POST, PUT" {
|
||||
t.Errorf("Allow-Methods = %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Headers"); got != "Authorization" {
|
||||
t.Errorf("Allow-Headers = %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Max-Age"); got != "600" {
|
||||
t.Errorf("Max-Age = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_Preflight_DisallowedOrigin_Returns204NoHeaders(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"https://ok.example"},
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("OPTIONS", "/x", nil)
|
||||
req.Header.Set("Origin", "https://evil.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("status = %d, want 204", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("Allow-Origin set for disallowed preflight: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_EmptyAllowedMethods_NoAllowMethodsHeader(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"https://ok.example"},
|
||||
// AllowedMethods intentionally nil
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://ok.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Methods"); got != "" {
|
||||
t.Errorf("Allow-Methods set when list empty: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORS_MaxAgeZero_NoMaxAgeHeader(t *testing.T) {
|
||||
cfg := &config.CORSConfig{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"https://ok.example"},
|
||||
MaxAge: 0,
|
||||
}
|
||||
app := newCORSApp(t, cfg)
|
||||
|
||||
req := httptest.NewRequest("GET", "/x", nil)
|
||||
req.Header.Set("Origin", "https://ok.example")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Max-Age"); got != "" {
|
||||
t.Errorf("Max-Age set when zero: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAllowedOrigin(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
origin string
|
||||
allowed []string
|
||||
allowCredentials bool
|
||||
want bool
|
||||
}{
|
||||
{"exact match", "https://ok.example", []string{"https://ok.example"}, false, true},
|
||||
{"exact match with creds", "https://ok.example", []string{"https://ok.example"}, true, true},
|
||||
{"wildcard without creds matches", "https://any.example", []string{"*"}, false, true},
|
||||
{"wildcard with creds rejected", "https://any.example", []string{"*"}, true, false},
|
||||
{"no match", "https://evil.example", []string{"https://ok.example"}, false, false},
|
||||
{"empty allowed list", "https://ok.example", nil, false, false},
|
||||
{"multiple entries — exact hit", "https://b.example", []string{"https://a.example", "https://b.example"}, false, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := isAllowedOrigin(tc.origin, tc.allowed, tc.allowCredentials); got != tc.want {
|
||||
t.Errorf("isAllowedOrigin(%q, %v, %v) = %v, want %v",
|
||||
tc.origin, tc.allowed, tc.allowCredentials, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user