package httpserver import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "git.yuntuo.hk/k2safe/orbit/internal/consoleauth" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" ) type fakeApplications struct{ calls int } func (f *fakeApplications) ListApplications(context.Context, auth.Token, core.ID) (consoleauth.ApplicationPage, error) { f.calls++ return consoleauth.ApplicationPage{}, nil } func (f *fakeApplications) GetApplication(context.Context, auth.Token, core.ID) (consoleauth.ApplicationDetail, error) { f.calls++ return consoleauth.ApplicationDetail{}, nil } func (f *fakeApplications) PrepareApplication(context.Context, auth.Token, consoleauth.ApplicationInput) (consoleauth.ApplicationOperation, error) { f.calls++ return consoleauth.ApplicationOperation{}, nil } func (f *fakeApplications) ApplyApplication(context.Context, auth.Token, consoleauth.ApplicationOperation) (consoleauth.ApplicationReceipt, error) { f.calls++ return consoleauth.ApplicationReceipt{}, nil } func TestConsoleApplicationSessionAndCSRF(t *testing.T) { ns, _ := core.NewID() id, _ := core.NewID() raw, _, _ := auth.Issue(ns, id) cases := []struct { name, method, path, origin, header string cookie bool body string status int }{ {name: "no login", method: "GET", path: "/console/applications", status: 401}, {name: "SDK bearer only", method: "POST", path: "/console/applications/prepare", header: "1", body: `{}`, status: 401}, {name: "missing csrf header", method: "POST", path: "/console/applications/prepare", cookie: true, body: `{}`, status: 403}, {name: "cross origin", method: "POST", path: "/console/applications/apply", cookie: true, header: "1", origin: "https://evil.example", body: `{}`, status: 403}, {name: "null origin", method: "POST", path: "/console/applications/apply", cookie: true, header: "1", origin: "null", body: `{}`, status: 403}, {name: "same origin", method: "POST", path: "/console/applications/prepare", cookie: true, header: "1", origin: "http://example.com", body: `{}`, status: 200}, {name: "unknown field", method: "POST", path: "/console/applications/prepare", cookie: true, header: "1", body: `{"admin":true}`, status: 400}, {name: "read", method: "GET", path: "/console/applications", cookie: true, status: 200}, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { authStore := &fakeConsoleAuth{valid: true} store := &fakeApplications{} server := NewWithConsoleAuth("", nil, authStore) RegisterConsoleApplications(server, authStore, store) req := httptest.NewRequest(tt.method, "http://example.com"+tt.path, strings.NewReader(tt.body)) req.Header.Set("Authorization", "Bearer "+raw) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Orbit-Console", tt.header) if tt.origin != "" { req.Header.Set("Origin", tt.origin) } if tt.cookie { req.AddCookie(&http.Cookie{Name: consoleSessionCookie, Value: raw}) } res := httptest.NewRecorder() server.Handler.ServeHTTP(res, req) if res.Code != tt.status { t.Fatalf("status=%d wanted=%d", res.Code, tt.status) } if tt.status != 200 && store.calls != 0 { t.Fatal("rejected request reached store") } if res.Header().Get("Cache-Control") != "no-store" { t.Fatal("management response cacheable") } }) } } // 准备接口生成的内容必须可被严格解码器原样提交,覆盖可选 ID 和空能力组。 func TestConsolePreparedApplicationRoundTrip(t *testing.T) { ns, _ := core.NewID() cred, _ := core.NewID() app, _ := core.NewID() queue, _ := core.NewID() opID, _ := core.NewID() token, _, _ := auth.Issue(ns, cred) for _, kind := range []string{"CREATE", "ROTATE"} { op := consoleauth.ApplicationOperation{Input: consoleauth.ApplicationInput{Kind: kind, ValidDays: 30, Reason: "test"}, OperationID: opID, ApplicationID: app, NamespaceID: ns, CredentialID: cred, Token: token, ExpiresAt: time.Now().UTC()} if kind == "CREATE" { op.QueueID = queue op.Input.Application = "sample" op.Input.Namespace = "default" op.Input.Environment = "test" op.Input.Queue = "work" } else { op.Input.NamespaceID = ns op.Input.CredentialID = cred op.Input.ExpectedRevision = 1 } raw, err := json.Marshal(op) if err != nil { t.Fatal(err) } req := httptest.NewRequest("POST", "/console/applications/apply", bytes.NewReader(raw)) req.Header.Set("Content-Type", "application/json") var got consoleauth.ApplicationOperation if err := decodeBody(req, &got); err != nil { t.Fatalf("%s prepared output rejected: %v", kind, err) } if got.Token != token || got.Input.Kind != kind { t.Fatal("prepared operation changed") } } }