166 lines
5.5 KiB
Go
166 lines
5.5 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
|
|
)
|
|
|
|
func TestPlatformAuthorizerMatchesSharedContract(t *testing.T) {
|
|
contract := loadPlatformHTTPAuthContract(t)
|
|
for _, contractCase := range contract.Cases {
|
|
t.Run(contractCase.Name, func(t *testing.T) {
|
|
resolver := &platformSessionResolverStub{outcome: contractCase.ResolverOutcome}
|
|
if contractCase.Session != nil {
|
|
resolver.session = fixturePlatformSession(*contractCase.Session)
|
|
}
|
|
authorizer, err := NewPlatformAuthorizer(AuthState(contractCase.State), resolver)
|
|
if err != nil {
|
|
t.Fatalf("NewPlatformAuthorizer: %v", err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
|
if contractCase.ResolverOutcome != "not_called" {
|
|
request.AddCookie(&http.Cookie{Name: contract.CookieName, Value: "signed-cookie"})
|
|
}
|
|
|
|
session, err := authorizer.Authorize(request, PlatformRequirement(contractCase.Requirement))
|
|
assertPlatformAuthOutcome(t, contractCase, session, err)
|
|
wantCalls := 0
|
|
if contractCase.ResolverOutcome != "not_called" {
|
|
wantCalls = 1
|
|
}
|
|
if resolver.calls != wantCalls {
|
|
t.Fatalf("resolver calls = %d, want %d", resolver.calls, wantCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlatformAuthorizerUsesSharedChunkReader(t *testing.T) {
|
|
resolver := &platformSessionResolverStub{outcome: "authenticated", session: fixturePlatformSession(platformHTTPFixtureSession{AuthMode: "user", Role: "user"})}
|
|
authorizer, err := NewPlatformAuthorizer(AuthState{Required: true, Configured: true}, resolver)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
|
request.AddCookie(&http.Cookie{Name: identity.SessionCookieName, Value: "first"})
|
|
request.AddCookie(&http.Cookie{Name: identity.SessionCookieName, Value: "last"})
|
|
request.AddCookie(&http.Cookie{Name: identity.SessionCookieName + ".1", Value: "tail"})
|
|
|
|
if _, err := authorizer.Authorize(request, PlatformApp); err != nil {
|
|
t.Fatalf("Authorize: %v", err)
|
|
}
|
|
if resolver.value != "lasttail" {
|
|
t.Fatalf("resolver value = %q, want lasttail", resolver.value)
|
|
}
|
|
}
|
|
|
|
type platformSessionResolverStub struct {
|
|
outcome string
|
|
session identity.Session
|
|
calls int
|
|
value string
|
|
}
|
|
|
|
func (stub *platformSessionResolverStub) Resolve(_ context.Context, value string) (identity.Session, error) {
|
|
stub.calls++
|
|
stub.value = value
|
|
switch stub.outcome {
|
|
case "authenticated":
|
|
return stub.session, nil
|
|
case "unauthenticated":
|
|
return identity.Session{}, identity.ErrUnauthenticated
|
|
case "infrastructure_error":
|
|
return identity.Session{}, errors.New("database secret must not leak")
|
|
default:
|
|
return identity.Session{}, errors.New("unexpected resolver call")
|
|
}
|
|
}
|
|
|
|
type platformHTTPAuthContract struct {
|
|
Version int `json:"version"`
|
|
CookieName string `json:"cookieName"`
|
|
Cases []platformHTTPAuthCase `json:"cases"`
|
|
}
|
|
|
|
type platformHTTPAuthCase struct {
|
|
Name string `json:"name"`
|
|
State AuthState `json:"state"`
|
|
Requirement string `json:"requirement"`
|
|
ResolverOutcome string `json:"resolverOutcome"`
|
|
Session *platformHTTPFixtureSession `json:"session"`
|
|
Expected struct {
|
|
Outcome string `json:"outcome"`
|
|
Status int `json:"status"`
|
|
Role string `json:"role"`
|
|
AuthMode string `json:"authMode"`
|
|
} `json:"expected"`
|
|
}
|
|
|
|
type platformHTTPFixtureSession struct {
|
|
AuthMode string `json:"authMode"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
func loadPlatformHTTPAuthContract(t *testing.T) platformHTTPAuthContract {
|
|
t.Helper()
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("locate platform_auth_test.go")
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(filepath.Dir(filename), "..", "..", "..", "contracts", "auth", "platform-http-auth-v1.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var contract platformHTTPAuthContract
|
|
if err := json.Unmarshal(raw, &contract); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if contract.Version != 1 || contract.CookieName != identity.SessionCookieName {
|
|
t.Fatalf("invalid contract header: %+v", contract)
|
|
}
|
|
return contract
|
|
}
|
|
|
|
func fixturePlatformSession(input platformHTTPFixtureSession) identity.Session {
|
|
return identity.Session{
|
|
Version: 1,
|
|
AuthMode: identity.AuthMode(input.AuthMode),
|
|
User: identity.User{
|
|
ID: "fixture-user", Subject: "fixture-user", DisplayName: "Fixture User",
|
|
ClientID: "platform", Role: input.Role, Status: "active",
|
|
},
|
|
}
|
|
}
|
|
|
|
func assertPlatformAuthOutcome(t *testing.T, contractCase platformHTTPAuthCase, session identity.Session, err error) {
|
|
t.Helper()
|
|
switch contractCase.Expected.Outcome {
|
|
case "authenticated":
|
|
if err != nil {
|
|
t.Fatalf("Authorize error = %v", err)
|
|
}
|
|
if session.User.Role != contractCase.Expected.Role || string(session.AuthMode) != contractCase.Expected.AuthMode {
|
|
t.Fatalf("session role/mode = %q/%q, want %q/%q", session.User.Role, session.AuthMode, contractCase.Expected.Role, contractCase.Expected.AuthMode)
|
|
}
|
|
case "infrastructure_error":
|
|
var authErr *PlatformAuthError
|
|
if err == nil || errors.As(err, &authErr) {
|
|
t.Fatalf("error = %v, want non-auth infrastructure error", err)
|
|
}
|
|
default:
|
|
var authErr *PlatformAuthError
|
|
if !errors.As(err, &authErr) || authErr.Status != contractCase.Expected.Status || string(authErr.Kind) != contractCase.Expected.Outcome {
|
|
t.Fatalf("error = %#v, want %s/%d", err, contractCase.Expected.Outcome, contractCase.Expected.Status)
|
|
}
|
|
}
|
|
}
|