package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "encoding/json" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) // --- Helper --- func newLinkedRpTestApp(h *AuthHandler) *fiber.App { app := fiber.New() app.Get("/api/v1/user/rp/linked", h.ListLinkedRps) return app } // --- Tests --- func TestListLinkedRps_PriorityAndAggregation(t *testing.T) { transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { switch r.URL.Host { case "kratos.test": if r.URL.Path == "/sessions/whoami" { if r.Header.Get("X-Session-Token") == "" && r.Header.Get("Cookie") == "" { return httpResponse(r, http.StatusUnauthorized, "unauthorized"), nil } return httpJSONAny(r, http.StatusOK, map[string]interface{}{ "identity": map[string]interface{}{ "id": "user-123", "traits": map[string]interface{}{ "email": "user@test.com", }, }, }), nil } case "hydra.test": if r.URL.Path == "/oauth2/auth/sessions/consent" { return httpJSONAny(r, http.StatusOK, []map[string]interface{}{ { "client": map[string]interface{}{ "client_id": "devfront", "client_name": "DevFront", "redirect_uris": []string{ "https://active.example.com/callback", }, }, "grant_scope": []string{"openid", "profile"}, "handled_at": time.Now().Format(time.RFC3339), }, { "client": map[string]interface{}{ "client_id": "orgfront", "client_name": "OrgFront", "metadata": map[string]interface{}{ "auto_login_supported": true, "auto_login_url": "http://localhost:5175/login", }, "redirect_uris": []string{ "http://localhost:5175/auth/callback", }, }, "grant_scope": []string{"openid", "profile"}, "handled_at": time.Now().Format(time.RFC3339), }, }), nil } if r.URL.Path == "/admin/clients/client-audit" { return httpJSONAny(r, http.StatusOK, map[string]interface{}{ "client_id": "client-audit", "client_name": "Audit App", }), nil } if r.URL.Path == "/admin/clients/client-consent" { return httpJSONAny(r, http.StatusOK, map[string]interface{}{ "client_id": "client-consent", "client_name": "Consent App", }), nil } } return httpResponse(r, http.StatusNotFound, "not found"), nil }) client := &http.Client{Transport: transport} origDefault := http.DefaultClient http.DefaultClient = client defer func() { http.DefaultClient = origDefault }() auditRepo := &mockAuditRepo{ logs: []domain.AuditLog{ { UserID: "user-123", EventType: "consent.granted", Timestamp: time.Now().Add(-10 * time.Hour), Details: `{"client_id":"client-audit", "scopes":["audit_scope"]}`, }, }, } consentRepo := &mockConsentRepo{ consents: []domain.ClientConsent{ { Subject: "user-123", ClientID: "client-consent", GrantedScopes: []string{"consent_scope"}, UpdatedAt: time.Now().Add(-2 * time.Hour), }, }, } h := &AuthHandler{ Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", HTTPClient: client, }, AuditRepo: auditRepo, ConsentRepo: consentRepo, KratosAdmin: new(MockKratosAdminService), } t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test") t.Setenv("HYDRA_PUBLIC_URL", "https://sso.example.com/oidc") t.Setenv("DEVFRONT_URL", "http://localhost:5174") app := newLinkedRpTestApp(h) req := httptest.NewRequest(http.MethodGet, "/api/v1/user/rp/linked", nil) req.Header.Set("Cookie", "ory_kratos_session=valid") resp, err := app.Test(req) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) var res struct { Items []struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` Scopes []string `json:"scopes"` InitURL string `json:"init_url"` AutoLoginSupported bool `json:"auto_login_supported"` AutoLoginURL string `json:"auto_login_url"` } `json:"items"` } json.NewDecoder(resp.Body).Decode(&res) assert.Equal(t, 4, len(res.Items)) statusMap := make(map[string]string) for _, item := range res.Items { statusMap[item.ID] = item.Status } assert.Equal(t, "active", statusMap["devfront"]) assert.Equal(t, "active", statusMap["orgfront"]) assert.Equal(t, "inactive", statusMap["client-consent"]) assert.Equal(t, "inactive", statusMap["client-audit"]) var activeInitURL string for _, item := range res.Items { if item.ID == "devfront" { activeInitURL = item.InitURL break } } parsedInitURL, err := url.Parse(activeInitURL) assert.NoError(t, err) assert.Equal(t, "http", parsedInitURL.Scheme) assert.Equal(t, "localhost:5174", parsedInitURL.Host) assert.Equal(t, "/login", parsedInitURL.Path) assert.Equal(t, "1", parsedInitURL.Query().Get("auto")) assert.Equal(t, "/clients", parsedInitURL.Query().Get("returnTo")) var orgfrontItem struct { InitURL string AutoLoginSupported bool AutoLoginURL string } for _, item := range res.Items { if item.ID == "orgfront" { orgfrontItem.InitURL = item.InitURL orgfrontItem.AutoLoginSupported = item.AutoLoginSupported orgfrontItem.AutoLoginURL = item.AutoLoginURL break } } assert.True(t, orgfrontItem.AutoLoginSupported) assert.Equal(t, "http://localhost:5175/login?auto=1", orgfrontItem.AutoLoginURL) assert.Equal(t, orgfrontItem.AutoLoginURL, orgfrontItem.InitURL) } func TestListLinkedRps_EnrichesLogoFromHydraClientWhenConsentSessionOmitsMetadata(t *testing.T) { transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { switch r.URL.Host { case "kratos.test": if r.URL.Path == "/sessions/whoami" { return httpJSONAny(r, http.StatusOK, map[string]interface{}{ "identity": map[string]interface{}{ "id": "user-123", }, }), nil } case "hydra.test": if r.URL.Path == "/oauth2/auth/sessions/consent" { return httpJSONAny(r, http.StatusOK, []map[string]interface{}{ { "client": map[string]interface{}{ "client_id": "gitea-client", "client_name": "Gitea", "redirect_uris": []string{ "https://gitea.example.com/callback", }, }, "grant_scope": []string{"openid", "profile"}, "handled_at": time.Now().Format(time.RFC3339), }, }), nil } if r.URL.Path == "/clients/gitea-client" { return httpJSONAny(r, http.StatusOK, map[string]interface{}{ "client_id": "gitea-client", "client_name": "Gitea", "redirect_uris": []string{ "https://gitea.example.com/callback", }, "metadata": map[string]interface{}{ "logo_url": "https://cdn.example.com/gitea.svg", }, }), nil } } return httpResponse(r, http.StatusNotFound, "not found"), nil }) client := &http.Client{Transport: transport} origDefault := http.DefaultClient http.DefaultClient = client defer func() { http.DefaultClient = origDefault }() h := &AuthHandler{ Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", HTTPClient: client, }, KratosAdmin: new(MockKratosAdminService), } t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test") t.Setenv("HYDRA_PUBLIC_URL", "https://sso.example.com/oidc") app := newLinkedRpTestApp(h) req := httptest.NewRequest(http.MethodGet, "/api/v1/user/rp/linked", nil) req.Header.Set("Cookie", "ory_kratos_session=valid") resp, err := app.Test(req) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) var res struct { Items []struct { ID string `json:"id"` Logo string `json:"logo"` } `json:"items"` } json.NewDecoder(resp.Body).Decode(&res) assert.Len(t, res.Items, 1) assert.Equal(t, "gitea-client", res.Items[0].ID) assert.Equal(t, "https://cdn.example.com/gitea.svg", res.Items[0].Logo) }