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] } }