598 lines
21 KiB
Go
598 lines
21 KiB
Go
package application_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/application"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/providers"
|
|
)
|
|
|
|
func TestLocalApplicationServesFoundationHealthAndReadiness(t *testing.T) {
|
|
app, err := application.New(application.Options{
|
|
Getenv: func(name string) string {
|
|
if name == "ZHINIAN_DATA_BACKEND" {
|
|
return "local"
|
|
}
|
|
return ""
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
for _, test := range []struct {
|
|
path string
|
|
wantCode int
|
|
}{
|
|
{path: "/api/health", wantCode: http.StatusOK},
|
|
{path: "/api/ready", wantCode: http.StatusOK},
|
|
} {
|
|
t.Run(test.path, func(t *testing.T) {
|
|
response := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodGet, test.path, nil))
|
|
if response.Code != test.wantCode {
|
|
t.Fatalf("status = %d, want %d", response.Code, test.wantCode)
|
|
}
|
|
var payload struct {
|
|
OK bool `json:"ok"`
|
|
Database struct {
|
|
Backend string `json:"backend"`
|
|
Configured bool `json:"configured"`
|
|
} `json:"database"`
|
|
}
|
|
if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if !payload.OK || payload.Database.Backend != "local" || !payload.Database.Configured {
|
|
t.Fatalf("payload = %+v", payload)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplicationRejectsInvalidProductionDatabaseConfiguration(t *testing.T) {
|
|
_, err := application.New(application.Options{
|
|
Getenv: func(name string) string {
|
|
if name == "NODE_ENV" {
|
|
return "production"
|
|
}
|
|
return ""
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("New() error = nil, want fail-closed database configuration error")
|
|
}
|
|
}
|
|
|
|
func TestProductionProviderBootstrapFlagAllowsApplicationComposition(t *testing.T) {
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
|
"NODE_ENV": "production",
|
|
"ZHINIAN_DATA_BACKEND": "postgres",
|
|
"DATABASE_URL": "postgres://user:password@127.0.0.1:5432/zhinian",
|
|
"ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS": "true",
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("New() with provider bootstrap flag = %v", err)
|
|
}
|
|
app.Close()
|
|
}
|
|
|
|
func TestProductionLocalBackendNeverGrantsAnonymousAdministrator(t *testing.T) {
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
|
"NODE_ENV": "production",
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
"ZHINIAN_AUTH_DISABLED": "true",
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
response := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/settings", nil))
|
|
if response.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status=%d body=%s, want 401", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestApplicationDerivesLegacyCompatibleMethodMatrixBeforeAuthentication(t *testing.T) {
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
|
"NODE_ENV": "production",
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
})})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
for _, test := range []struct {
|
|
method, path, allow string
|
|
status int
|
|
}{
|
|
{http.MethodOptions, "/api/admin/accounts", "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT", http.StatusNoContent},
|
|
{http.MethodOptions, "/api/health", "GET, HEAD, OPTIONS", http.StatusNoContent},
|
|
{http.MethodHead, "/api/health", "", http.StatusOK},
|
|
{http.MethodPost, "/api/health", "", http.StatusMethodNotAllowed},
|
|
} {
|
|
response := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(response, httptest.NewRequest(test.method, test.path, nil))
|
|
if response.Code != test.status || response.Body.Len() != 0 || response.Header().Get("Allow") != test.allow {
|
|
t.Fatalf("%s %s status=%d allow=%q body=%q", test.method, test.path, response.Code, response.Header().Get("Allow"), response.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplicationServesAnonymousCurrentSessionWithAuthConfigurationState(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
environment map[string]string
|
|
wantRequired bool
|
|
wantConfigured bool
|
|
}{
|
|
{
|
|
name: "trusted local development",
|
|
environment: map[string]string{"ZHINIAN_DATA_BACKEND": "local"},
|
|
wantRequired: false,
|
|
wantConfigured: false,
|
|
},
|
|
{
|
|
name: "production missing session secret",
|
|
environment: map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
"NODE_ENV": "production",
|
|
},
|
|
wantRequired: true,
|
|
wantConfigured: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(test.environment)})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
response := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/auth/me", nil))
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", response.Code, http.StatusOK)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
want := map[string]any{
|
|
"authenticated": false,
|
|
"authRequired": test.wantRequired,
|
|
"authConfigured": test.wantConfigured,
|
|
"authMode": nil,
|
|
"user": nil,
|
|
}
|
|
if !reflect.DeepEqual(payload, want) {
|
|
t.Fatalf("payload = %#v, want %#v", payload, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplicationComposesSignedCookieResolverWithAuthorizationLoader(t *testing.T) {
|
|
secret := "application-current-session-secret-with-enough-entropy"
|
|
loader := &applicationAuthorizationLoader{snapshot: identity.AuthorizationSnapshot{
|
|
Account: identity.AccountSnapshot{
|
|
ID: "user-1", Phone: "13800138001", DisplayName: "Current User", Role: "user",
|
|
OrganizationID: "org-1", Status: "active", SessionVersion: 7,
|
|
},
|
|
Organization: &identity.OrganizationSnapshot{ID: "org-1", Name: "Primary Organization", Status: "active"},
|
|
}, found: true}
|
|
app, err := application.New(application.Options{
|
|
Getenv: applicationEnv(map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
"ZHINIAN_AUTH_SESSION_SECRET": secret,
|
|
}),
|
|
AuthorizationLoader: loader,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
version := 7
|
|
session := identity.Session{
|
|
Version: 1, AuthMode: identity.AuthModeAdmin,
|
|
IssuedAt: time.Now().Add(-time.Minute).Unix(), ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
|
SessionVersion: &version, AccessToken: "must-not-leak", TokenType: "bearer",
|
|
User: identity.User{
|
|
ID: "user-1", Subject: "forged", DisplayName: "Forged Admin", ClientID: "platform",
|
|
Role: "super_admin", Status: "active", Authorities: []string{"ROLE_SUPER_ADMIN"}, Scope: []string{"forged"},
|
|
},
|
|
}
|
|
raw, err := json.Marshal(session)
|
|
if err != nil {
|
|
t.Fatalf("marshal session: %v", err)
|
|
}
|
|
cookieValue, err := identity.Sign(raw, secret)
|
|
if err != nil {
|
|
t.Fatalf("sign session: %v", err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/auth/me", nil)
|
|
request.AddCookie(&http.Cookie{Name: identity.SessionCookieName, Value: cookieValue})
|
|
response := httptest.NewRecorder()
|
|
|
|
app.Handler().ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", response.Code, http.StatusOK)
|
|
}
|
|
body := response.Body.Bytes()
|
|
var payload struct {
|
|
Authenticated bool `json:"authenticated"`
|
|
User struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"displayName"`
|
|
Role string `json:"role"`
|
|
Authorities []string `json:"authorities"`
|
|
} `json:"user"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if !payload.Authenticated || payload.User.ID != "user-1" || payload.User.DisplayName != "Current User" || payload.User.Role != "user" {
|
|
t.Fatalf("payload = %+v", payload)
|
|
}
|
|
if !reflect.DeepEqual(payload.User.Authorities, []string{"ROLE_USER"}) {
|
|
t.Fatalf("authorities = %#v", payload.User.Authorities)
|
|
}
|
|
if len(loader.ids) != 1 || loader.ids[0] != "user-1" {
|
|
t.Fatalf("loader IDs = %#v", loader.ids)
|
|
}
|
|
for _, forbidden := range []string{"accessToken", "tokenType", "sessionVersion", "expiresAt", "issuedAt", "must-not-leak"} {
|
|
if strings.Contains(string(body), forbidden) {
|
|
t.Fatalf("response leaked %q: %s", forbidden, body)
|
|
}
|
|
}
|
|
|
|
for _, method := range []string{http.MethodHead, http.MethodOptions, http.MethodPost} {
|
|
methodRequest := httptest.NewRequest(method, "/api/auth/me", nil)
|
|
methodResponse := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(methodResponse, methodRequest)
|
|
wantStatus := http.StatusMethodNotAllowed
|
|
if method == http.MethodHead {
|
|
wantStatus = http.StatusOK
|
|
}
|
|
if method == http.MethodOptions {
|
|
wantStatus = http.StatusNoContent
|
|
}
|
|
if methodResponse.Code != wantStatus {
|
|
t.Fatalf("%s status = %d, want %d", method, methodResponse.Code, wantStatus)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalApplicationUsesCoherentAuthorizationAdapterByDefault(t *testing.T) {
|
|
secret := "local-default-adapter-secret-with-enough-entropy"
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
"ZHINIAN_AUTH_SESSION_SECRET": secret,
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
session := identity.Session{
|
|
Version: 1, AuthMode: identity.AuthModeUser,
|
|
IssuedAt: time.Now().Add(-time.Minute).Unix(), ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
|
User: identity.User{
|
|
ID: "demo-merchant", Subject: "demo-merchant", DisplayName: "Forged", ClientID: "platform",
|
|
Authorities: []string{}, Scope: []string{},
|
|
},
|
|
}
|
|
raw, err := json.Marshal(session)
|
|
if err != nil {
|
|
t.Fatalf("marshal session: %v", err)
|
|
}
|
|
cookieValue, err := identity.Sign(raw, secret)
|
|
if err != nil {
|
|
t.Fatalf("sign session: %v", err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/auth/me", nil)
|
|
request.AddCookie(&http.Cookie{Name: identity.SessionCookieName, Value: cookieValue})
|
|
response := httptest.NewRecorder()
|
|
|
|
app.Handler().ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("response = %d %q, want authenticated local session", response.Code, response.Body.String())
|
|
}
|
|
var payload struct {
|
|
Authenticated bool `json:"authenticated"`
|
|
User struct {
|
|
ID, DisplayName, Role, OrganizationID string
|
|
} `json:"user"`
|
|
}
|
|
if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !payload.Authenticated || payload.User.ID != "demo-merchant" || payload.User.DisplayName != "智念用户" || payload.User.Role != "super_admin" || payload.User.OrganizationID != "org-demo" {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestLocalApplicationBusinessModulesDoNotUseUnavailablePostgresShell(t *testing.T) {
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
"ZHINIAN_BILLING_REQUIRED": "0",
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
for _, test := range []struct {
|
|
method, path, body string
|
|
want int
|
|
}{
|
|
{http.MethodGet, "/api/assets", "", http.StatusOK},
|
|
{http.MethodGet, "/api/image-templates", "", http.StatusOK},
|
|
{http.MethodGet, "/api/usage", "", http.StatusOK},
|
|
{http.MethodPost, "/api/generations/image", `{"prompt":"local production path"}`, http.StatusAccepted},
|
|
} {
|
|
req := httptest.NewRequest(test.method, test.path, strings.NewReader(test.body))
|
|
if test.body != "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
res := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(res, req)
|
|
if res.Code != test.want {
|
|
t.Fatalf("%s %s = %d %q, want %d", test.method, test.path, res.Code, res.Body.String(), test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalApplicationRealProviderJobReachesSucceededWithStoredOutput(t *testing.T) {
|
|
app, err := application.New(application.Options{
|
|
Getenv: applicationEnv(map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local", "ZHINIAN_BILLING_REQUIRED": "0", "ZHINIAN_RUNTIME_DIR": t.TempDir(),
|
|
"ZHINIAN_INTERNAL_WORKER_TOKEN": "worker-secret",
|
|
"ZHINIAN_WORKER_POLL_INTERVAL_MS": "1",
|
|
}),
|
|
ProviderRegistry: jobs.ProviderRegistry{
|
|
"volcengine-visual": applicationTestProvider{},
|
|
},
|
|
RemoteFetcher: applicationTestRemoteFetcher{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
create := httptest.NewRequest(http.MethodPost, "/api/generations/image", strings.NewReader(`{"prompt":"local output"}`))
|
|
create.Header.Set("Content-Type", "application/json")
|
|
created := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(created, create)
|
|
if created.Code != http.StatusAccepted {
|
|
t.Fatalf("create = %d %q", created.Code, created.Body.String())
|
|
}
|
|
var creation struct {
|
|
Job jobs.Job `json:"job"`
|
|
}
|
|
if err := json.NewDecoder(created.Body).Decode(&creation); err != nil || creation.Job.ID == "" {
|
|
t.Fatalf("creation = %#v, %v", creation, err)
|
|
}
|
|
|
|
for tick := 0; tick < 2; tick++ {
|
|
if tick > 0 {
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/internal/worker/tick", strings.NewReader(`{}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.Header.Set("Authorization", "Bearer worker-secret")
|
|
response := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("tick %d = %d %q", tick, response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
got := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(got, httptest.NewRequest(http.MethodGet, "/api/generations/image/"+creation.Job.ID, nil))
|
|
if got.Code != http.StatusOK {
|
|
t.Fatalf("get = %d %q", got.Code, got.Body.String())
|
|
}
|
|
var result struct {
|
|
Job jobs.Job `json:"job"`
|
|
}
|
|
if err := json.NewDecoder(got.Body).Decode(&result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Job.Status != jobs.StatusSucceeded || len(result.Job.OutputAssetIDs) == 0 {
|
|
t.Fatalf("job = %#v", result.Job)
|
|
}
|
|
|
|
assetsResponse := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(assetsResponse, httptest.NewRequest(http.MethodGet, "/api/assets", nil))
|
|
if assetsResponse.Code != http.StatusOK || !strings.Contains(assetsResponse.Body.String(), result.Job.OutputAssetIDs[0]) {
|
|
t.Fatalf("assets = %d %q", assetsResponse.Code, assetsResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
type applicationTestProvider struct{}
|
|
|
|
func (applicationTestProvider) Submit(context.Context, providers.Request) (providers.Result, error) {
|
|
return providers.Result{TaskID: "provider-task-1", Status: providers.StatusQueued}, nil
|
|
}
|
|
|
|
func (applicationTestProvider) Query(_ context.Context, taskID string) (providers.Result, error) {
|
|
return providers.Result{TaskID: taskID, Status: providers.StatusSucceeded, OutputURLs: []string{"https://provider.test/generated.png"}}, nil
|
|
}
|
|
|
|
type applicationTestRemoteFetcher struct{}
|
|
|
|
func (applicationTestRemoteFetcher) Fetch(context.Context, string) (assets.Blob, error) {
|
|
content := "real provider output fixture"
|
|
return assets.Blob{Body: io.NopCloser(strings.NewReader(content)), ContentType: "image/png", Size: int64(len(content))}, nil
|
|
}
|
|
|
|
func TestApplicationComposesPasswordLoginAndLogoutHandlers(t *testing.T) {
|
|
secret := "application-password-login-secret-with-enough-entropy"
|
|
authenticator := &applicationCredentialAuthenticator{account: identity.LoginAccount{
|
|
Account: identity.AccountSnapshot{
|
|
ID: "user-1", Phone: "13800138000", DisplayName: "Login User", Role: "user",
|
|
OrganizationID: "org-1", Status: "active", SessionVersion: 9,
|
|
},
|
|
Organization: &identity.OrganizationSnapshot{ID: "org-1", Name: "Primary Organization", Status: "active"},
|
|
}}
|
|
app, err := application.New(application.Options{
|
|
Getenv: applicationEnv(map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
"ZHINIAN_AUTH_SESSION_SECRET": secret,
|
|
"NEXT_PUBLIC_APP_URL": "https://public.example.test",
|
|
}),
|
|
CredentialAuthenticator: authenticator,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
loginRequest := httptest.NewRequest(http.MethodPost, "/api/auth/password", strings.NewReader(`{
|
|
"phone":" (138) 0013-8000 ", "password":" password ", "next":"/create?source=login"
|
|
}`))
|
|
loginRequest.Header.Set("Content-Type", "application/json")
|
|
loginResponse := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(loginResponse, loginRequest)
|
|
|
|
if loginResponse.Code != http.StatusOK {
|
|
t.Fatalf("login response = %d %q", loginResponse.Code, loginResponse.Body.String())
|
|
}
|
|
if len(authenticator.phones) != 1 || authenticator.phones[0] != "13800138000" || authenticator.passwords[0] != "password" {
|
|
t.Fatalf("credential attempts = phones %#v passwords %#v", authenticator.phones, authenticator.passwords)
|
|
}
|
|
cookieValues := make(map[string]string)
|
|
for _, cookie := range loginResponse.Result().Cookies() {
|
|
if !cookie.Secure {
|
|
t.Fatalf("login cookie did not inherit HTTPS public base URL: %#v", cookie)
|
|
}
|
|
if cookie.Value != "" {
|
|
cookieValues[cookie.Name] = cookie.Value
|
|
}
|
|
}
|
|
signed, ok := identity.Reassemble(identity.SessionCookieName, func(name string) (string, bool) {
|
|
value, found := cookieValues[name]
|
|
return value, found
|
|
})
|
|
if !ok {
|
|
t.Fatalf("login response did not contain a session cookie: %#v", loginResponse.Header().Values("Set-Cookie"))
|
|
}
|
|
session, err := identity.Parse(signed, secret, time.Now())
|
|
if err != nil {
|
|
t.Fatalf("parse issued session: %v", err)
|
|
}
|
|
if session.User.ID != "user-1" || session.User.OrganizationName != "Primary Organization" || session.SessionVersion == nil || *session.SessionVersion != 9 {
|
|
t.Fatalf("session = %+v", session)
|
|
}
|
|
|
|
logoutResponse := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(logoutResponse, httptest.NewRequest(http.MethodPost, "http://app.test/api/auth/logout", nil))
|
|
if logoutResponse.Code != http.StatusTemporaryRedirect || logoutResponse.Header().Get("Location") != "http://app.test/auth/login?loggedOut=1" {
|
|
t.Fatalf("logout response = %d location=%q", logoutResponse.Code, logoutResponse.Header().Get("Location"))
|
|
}
|
|
if got := len(logoutResponse.Result().Cookies()); got != identity.CookieMaxChunks {
|
|
t.Fatalf("logout cookies = %d, want %d", got, identity.CookieMaxChunks)
|
|
}
|
|
}
|
|
|
|
func TestApplicationLeavesLogoutAvailableWhenPasswordAuthenticationIsUnconfigured(t *testing.T) {
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
passwordResponse := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(passwordResponse, httptest.NewRequest(http.MethodPost, "/api/auth/password", strings.NewReader(`{"phone":"13800138000","password":"password"}`)))
|
|
if passwordResponse.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("password status = %d body=%q, want 503", passwordResponse.Code, passwordResponse.Body.String())
|
|
}
|
|
|
|
logoutResponse := httptest.NewRecorder()
|
|
app.Handler().ServeHTTP(logoutResponse, httptest.NewRequest(http.MethodGet, "http://app.test/api/auth/logout", nil))
|
|
if logoutResponse.Code != http.StatusTemporaryRedirect {
|
|
t.Fatalf("logout status = %d, want 307", logoutResponse.Code)
|
|
}
|
|
}
|
|
|
|
func TestApplicationOwnsEveryCheckedInHTTPRoute(t *testing.T) {
|
|
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
|
"ZHINIAN_DATA_BACKEND": "local",
|
|
"NODE_ENV": "production",
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
t.Cleanup(app.Close)
|
|
|
|
for _, route := range httpapi.GoRouteSurface() {
|
|
route := route
|
|
t.Run(route.Method+" "+route.Path, func(t *testing.T) {
|
|
path := strings.ReplaceAll(route.Path, "{id}", "contract-id")
|
|
path = strings.ReplaceAll(path, "{path...}", "contract/file.png")
|
|
request := httptest.NewRequest(route.Method, "https://app.example.test"+path, strings.NewReader(`{}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
|
|
app.Handler().ServeHTTP(response, request)
|
|
|
|
if response.Code == http.StatusNotFound {
|
|
t.Fatalf("route %s %s fell through to 404", route.Method, path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type applicationAuthorizationLoader struct {
|
|
snapshot identity.AuthorizationSnapshot
|
|
found bool
|
|
err error
|
|
ids []string
|
|
}
|
|
|
|
type applicationCredentialAuthenticator struct {
|
|
account identity.LoginAccount
|
|
err error
|
|
phones []string
|
|
passwords []string
|
|
}
|
|
|
|
func (authenticator *applicationCredentialAuthenticator) AttemptPasswordLogin(_ context.Context, phone, password string, _ time.Time) (identity.LoginAccount, error) {
|
|
authenticator.phones = append(authenticator.phones, phone)
|
|
authenticator.passwords = append(authenticator.passwords, password)
|
|
return authenticator.account, authenticator.err
|
|
}
|
|
|
|
func (loader *applicationAuthorizationLoader) FindAuthorizationSnapshot(_ context.Context, id string) (identity.AuthorizationSnapshot, bool, error) {
|
|
loader.ids = append(loader.ids, id)
|
|
return loader.snapshot, loader.found, loader.err
|
|
}
|
|
|
|
func applicationEnv(values map[string]string) func(string) string {
|
|
return func(name string) string { return values[name] }
|
|
}
|