mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
// Copyright (c) 2014-present Hanzo AI, Inc.
|
|
// Licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
func TestRequireIdentity(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
require bool
|
|
headers map[string]string
|
|
wantCode int
|
|
wantOrg string
|
|
wantUser string
|
|
wantEmail string
|
|
wantNext bool
|
|
}{
|
|
{
|
|
name: "headers present, require true",
|
|
require: true,
|
|
headers: map[string]string{
|
|
HeaderOrgID: "hanzo",
|
|
HeaderUserID: "user-123",
|
|
HeaderUserEmail: "[email protected]",
|
|
},
|
|
wantCode: http.StatusOK,
|
|
wantOrg: "hanzo",
|
|
wantUser: "user-123",
|
|
wantEmail: "[email protected]",
|
|
wantNext: true,
|
|
},
|
|
{
|
|
name: "missing headers, require true → 401",
|
|
require: true,
|
|
headers: nil,
|
|
wantCode: http.StatusUnauthorized,
|
|
wantNext: false,
|
|
},
|
|
{
|
|
name: "missing headers, require false → next with empty ctx",
|
|
require: false,
|
|
headers: nil,
|
|
wantCode: http.StatusOK,
|
|
wantNext: true,
|
|
},
|
|
{
|
|
name: "partial headers (only org), require true → allowed",
|
|
require: true,
|
|
headers: map[string]string{HeaderOrgID: "lux"},
|
|
wantCode: http.StatusOK,
|
|
wantOrg: "lux",
|
|
wantNext: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var (
|
|
called bool
|
|
gotOrg string
|
|
gotUser string
|
|
gotEmail string
|
|
)
|
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
gotOrg = OrgID(r.Context())
|
|
gotUser = UserID(r.Context())
|
|
gotEmail = UserEmail(r.Context())
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler := RequireIdentity(tc.require)(next)
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/commerce/health", nil)
|
|
for k, v := range tc.headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != tc.wantCode {
|
|
t.Fatalf("status: want %d got %d (body=%q)", tc.wantCode, rec.Code, rec.Body.String())
|
|
}
|
|
if called != tc.wantNext {
|
|
t.Fatalf("next called: want %v got %v", tc.wantNext, called)
|
|
}
|
|
if gotOrg != tc.wantOrg {
|
|
t.Fatalf("org: want %q got %q", tc.wantOrg, gotOrg)
|
|
}
|
|
if gotUser != tc.wantUser {
|
|
t.Fatalf("user: want %q got %q", tc.wantUser, gotUser)
|
|
}
|
|
if gotEmail != tc.wantEmail {
|
|
t.Fatalf("email: want %q got %q", tc.wantEmail, gotEmail)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIdentity(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
app.Use(Identity(true))
|
|
app.Raw(http.MethodGet, "/x", func(c *zip.Ctx) error {
|
|
return c.String(http.StatusOK, OrgID(c.Context())+":"+UserID(c.Context()))
|
|
})
|
|
|
|
t.Run("401 without identity", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("test request: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("want 401 got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("200 with identity", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
|
req.Header.Set(HeaderOrgID, "hanzo")
|
|
req.Header.Set(HeaderUserID, "u1")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("test request: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if string(body) != "hanzo:u1" {
|
|
t.Fatalf("body: want hanzo:u1 got %q", string(body))
|
|
}
|
|
})
|
|
}
|
|
|
|
// An X-Org-Id header names an org; it authenticates nobody. Only IAMTokenRequired,
|
|
// which requires the validated X-User-Id, marks a request iam_authenticated.
|
|
func TestIdentityOrgHeaderIsNotAuthentication(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
app.Use(Identity(false))
|
|
app.Raw(http.MethodGet, "/x", func(c *zip.Ctx) error {
|
|
if b, _ := c.Locals("iam_authenticated").(bool); b {
|
|
return c.String(http.StatusOK, "authenticated")
|
|
}
|
|
return c.String(http.StatusOK, "anonymous")
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
|
req.Header.Set(HeaderOrgID, "victim")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("test request: %v", err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if string(body) != "anonymous" {
|
|
t.Fatalf("X-Org-Id alone: got %q, want anonymous", body)
|
|
}
|
|
}
|
|
|
|
func TestAccessorsEmptyContext(t *testing.T) {
|
|
ctx := context.Background()
|
|
if OrgID(ctx) != "" || UserID(ctx) != "" || UserEmail(ctx) != "" {
|
|
t.Fatalf("expected all-empty accessors on bare context")
|
|
}
|
|
}
|