From f663fc31bd0cf552f6c8e80bdc8e0c410e1051d6 Mon Sep 17 00:00:00 2001 From: shipperizer Date: Mon, 2 Feb 2026 15:43:13 +0000 Subject: [PATCH 1/2] refactor(test): rewrite tests to follow the same pattern in kratos package --- pkg/kratos/cookies_test.go | 263 +- pkg/kratos/encryption_test.go | 1 + pkg/kratos/handlers_test.go | 1823 +++++------ pkg/kratos/service_test.go | 5762 +++++++++++++++++---------------- 4 files changed, 3785 insertions(+), 4064 deletions(-) diff --git a/pkg/kratos/cookies_test.go b/pkg/kratos/cookies_test.go index 84e48788f..3d41660ab 100644 --- a/pkg/kratos/cookies_test.go +++ b/pkg/kratos/cookies_test.go @@ -1,6 +1,7 @@ // Copyright 2024 Canonical Ltd. // SPDX-License-Identifier: AGPL-3.0 +// Package kratos provides unit tests for cookie management functionality. package kratos import ( @@ -29,147 +30,179 @@ func findCookie(name string, cookies []*http.Cookie) (*http.Cookie, bool) { } func TestAuthCookieManager_ClearStateCookie(t *testing.T) { - ctrl := gomock.NewController(t) - - mockLogger := NewMockLoggerInterface(ctrl) - mockEncrypt := NewMockEncryptInterface(ctrl) - - mockRequest := httptest.NewRequest(http.MethodGet, "/", nil) - mockRequest.AddCookie(&http.Cookie{Name: "state"}) - - mockResponse := httptest.NewRecorder() - - manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) - manager.ClearStateCookie(mockResponse) - - c, _ := findCookie("login_ui_state", mockResponse.Result().Cookies()) - - if c.Expires != epoch { - t.Fatal("did not clear state cookie") + tests := []struct { + name string + }{ + { + name: "ClearState", + }, } -} - -func TestAuthCookieManager_GetStateCookie(t *testing.T) { - ctrl := gomock.NewController(t) - mockLogger := NewMockLoggerInterface(ctrl) - mockEncrypt := NewMockEncryptInterface(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) - state := FlowStateCookie{} - sj, _ := json.Marshal(state) + mockLogger := NewMockLoggerInterface(ctrl) + mockEncrypt := NewMockEncryptInterface(ctrl) - mockEncrypt.EXPECT().Decrypt("mock-state").Return(string(sj), nil) + mockRequest := httptest.NewRequest(http.MethodGet, "/", nil) + mockRequest.AddCookie(&http.Cookie{Name: "state"}) - mockRequest := httptest.NewRequest(http.MethodGet, "/", nil) - mockRequest.AddCookie(&http.Cookie{Name: "login_ui_state", Value: "mock-state"}) + mockResponse := httptest.NewRecorder() - manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) - cookie, err := manager.GetStateCookie(mockRequest) + manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) + manager.ClearStateCookie(mockResponse) - if cookie != state { - t.Fatal("state cookie value does not match expected") - } - - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestAuthCookieManager_GetStateCookieNoCookie(t *testing.T) { - ctrl := gomock.NewController(t) - - mockLogger := NewMockLoggerInterface(ctrl) - mockRequest := httptest.NewRequest(http.MethodGet, "/", nil) - - manager := NewAuthCookieManager(5, nil, mockLogger) - cookie, err := manager.GetStateCookie(mockRequest) - - state := FlowStateCookie{} - if cookie != state { - t.Fatal("state cookie value does not match expected") - } + c, _ := findCookie("login_ui_state", mockResponse.Result().Cookies()) - if err != nil { - t.Fatalf("expected error to be nil, not %v", err) + if c.Expires != epoch { + t.Fatal("did not clear state cookie") + } + }) } } -func TestAuthCookieManager_GetStateCookieDecryptFailure(t *testing.T) { - ctrl := gomock.NewController(t) - mockError := errors.New("mock-error") - - mockLogger := NewMockLoggerInterface(ctrl) - mockLogger.EXPECT().Errorf("can't decrypt cookie value, %v", mockError).Times(1) - - mockEncrypt := NewMockEncryptInterface(ctrl) - mockEncrypt.EXPECT().Decrypt("mock-state").Return("", mockError) - - mockRequest := httptest.NewRequest(http.MethodGet, "/", nil) - mockRequest.AddCookie(&http.Cookie{Name: "login_ui_state", Value: "mock-state"}) - - manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) - cookie, err := manager.GetStateCookie(mockRequest) - - state := FlowStateCookie{} - if cookie != state { - t.Fatal("state cookie value does not match expected") +func TestAuthCookieManager_GetStateCookie(t *testing.T) { + tests := []struct { + name string + setupMocks func(*MockEncryptInterface, *MockLoggerInterface) + requestCookie *http.Cookie + expectedCookie FlowStateCookie + expectedErr bool + }{ + { + name: "Success", + setupMocks: func(mockEncrypt *MockEncryptInterface, mockLogger *MockLoggerInterface) { + state := FlowStateCookie{} + sj, _ := json.Marshal(state) + mockEncrypt.EXPECT().Decrypt("mock-state").Return(string(sj), nil) + }, + requestCookie: &http.Cookie{Name: "login_ui_state", Value: "mock-state"}, + expectedCookie: FlowStateCookie{}, + expectedErr: false, + }, + { + name: "NoCookie", + setupMocks: func(mockEncrypt *MockEncryptInterface, mockLogger *MockLoggerInterface) {}, + requestCookie: nil, + expectedCookie: FlowStateCookie{}, + expectedErr: false, + }, + { + name: "DecryptFailure", + setupMocks: func(mockEncrypt *MockEncryptInterface, mockLogger *MockLoggerInterface) { + mockError := errors.New("mock-error") + mockLogger.EXPECT().Errorf("can't decrypt cookie value, %v", mockError).Times(1) + mockEncrypt.EXPECT().Decrypt("mock-state").Return("", mockError) + }, + requestCookie: &http.Cookie{Name: "login_ui_state", Value: "mock-state"}, + expectedCookie: FlowStateCookie{}, + expectedErr: true, + }, } - if err == nil { - t.Fatalf("expected error to be not nil") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + mockLogger := NewMockLoggerInterface(ctrl) + mockEncrypt := NewMockEncryptInterface(ctrl) + + if tt.requestCookie == nil { + mockEncrypt = nil + } + if mockEncrypt != nil { + tt.setupMocks(mockEncrypt, mockLogger) + } else { + tt.setupMocks(nil, mockLogger) + } + + mockRequest := httptest.NewRequest(http.MethodGet, "/", nil) + if tt.requestCookie != nil { + mockRequest.AddCookie(tt.requestCookie) + } + + manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) + cookie, err := manager.GetStateCookie(mockRequest) + + if cookie != tt.expectedCookie { + t.Fatal("state cookie value does not match expected") + } + + if tt.expectedErr { + if err == nil { + t.Fatalf("expected error to be not nil") + } + } else if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + }) } } func TestAuthCookieManager_SetStateCookie(t *testing.T) { - ctrl := gomock.NewController(t) - - mockLogger := NewMockLoggerInterface(ctrl) - mockEncrypt := NewMockEncryptInterface(ctrl) - - state := FlowStateCookie{} - js, _ := json.Marshal(state) - - mockEncrypt.EXPECT().Encrypt(string(js)).Return("mock-state", nil) - - mockResponse := httptest.NewRecorder() - - manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) - err := manager.SetStateCookie(mockResponse, state) - - c, found := findCookie("login_ui_state", mockResponse.Result().Cookies()) - - if !found { - t.Fatal("did not set state cookie") + tests := []struct { + name string + setupMocks func(*MockEncryptInterface, *MockLoggerInterface) + expectedErr bool + }{ + { + name: "Success", + setupMocks: func(mockEncrypt *MockEncryptInterface, mockLogger *MockLoggerInterface) { + state := FlowStateCookie{} + js, _ := json.Marshal(state) + mockEncrypt.EXPECT().Encrypt(string(js)).Return("mock-state", nil) + }, + expectedErr: false, + }, + { + name: "Failure", + setupMocks: func(mockEncrypt *MockEncryptInterface, mockLogger *MockLoggerInterface) { + mockError := errors.New("mock-error") + state := FlowStateCookie{} + js, _ := json.Marshal(state) + mockLogger.EXPECT().Errorf("can't encrypt cookie value, %v", mockError).Times(1) + mockEncrypt.EXPECT().Encrypt(string(js)).Return("", mockError) + }, + expectedErr: true, + }, } - if c.Value != "mock-state" { - t.Fatal("state cookie value does not match expected") - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} + mockLogger := NewMockLoggerInterface(ctrl) + mockEncrypt := NewMockEncryptInterface(ctrl) + + tt.setupMocks(mockEncrypt, mockLogger) -func TestAuthCookieManager_SetStateCookieFailure(t *testing.T) { - ctrl := gomock.NewController(t) + state := FlowStateCookie{} + mockResponse := httptest.NewRecorder() - mockError := errors.New("mock-error") - state := FlowStateCookie{} - js, _ := json.Marshal(state) + manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) + err := manager.SetStateCookie(mockResponse, state) - mockLogger := NewMockLoggerInterface(ctrl) - mockLogger.EXPECT().Errorf("can't encrypt cookie value, %v", mockError).Times(1) + if tt.expectedErr { + if err == nil { + t.Fatalf("expected error to be not nil") + } + return + } - mockEncrypt := NewMockEncryptInterface(ctrl) - mockEncrypt.EXPECT().Encrypt(string(js)).Return("", mockError) + c, found := findCookie("login_ui_state", mockResponse.Result().Cookies()) - mockResponse := httptest.NewRecorder() + if !found { + t.Fatal("did not set state cookie") + } - manager := NewAuthCookieManager(5, mockEncrypt, mockLogger) - err := manager.SetStateCookie(mockResponse, state) + if c.Value != "mock-state" { + t.Fatal("state cookie value does not match expected") + } - if err == nil { - t.Fatalf("expected error to be not nil") + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + }) } } diff --git a/pkg/kratos/encryption_test.go b/pkg/kratos/encryption_test.go index 78ed32c4e..5268df6c1 100644 --- a/pkg/kratos/encryption_test.go +++ b/pkg/kratos/encryption_test.go @@ -1,6 +1,7 @@ // Copyright 2024 Canonical Ltd. // SPDX-License-Identifier: AGPL-3.0 +// Package kratos provides unit tests for encryption functionality. package kratos import ( diff --git a/pkg/kratos/handlers_test.go b/pkg/kratos/handlers_test.go index 5476c71aa..0fac0a797 100644 --- a/pkg/kratos/handlers_test.go +++ b/pkg/kratos/handlers_test.go @@ -1,3 +1,7 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: AGPL-3.0 + +// Package kratos provides unit tests for Kratos handlers. package kratos import ( @@ -66,7 +70,138 @@ func TestHandleCreateFlowWithoutParams(t *testing.T) { } } -func TestHandleCreateFlowWithoutSessionAcceptJSON(t *testing.T) { +func TestHandleCreateFlowWithoutSession(t *testing.T) { + tests := []struct { + name string + acceptHeader string + createFlowErr error + filterErr error + expectStatus int + expectJSON bool + expectLocation bool + expectLog bool + }{ + { + name: "AcceptJSON", + acceptHeader: "application/json, text/plain, */*", + expectStatus: http.StatusOK, + expectJSON: true, + }, + { + name: "NotAcceptJSON", + acceptHeader: "application/x-www-form-urlencoded", + expectStatus: http.StatusSeeOther, + expectLocation: true, + }, + { + name: "FailOnCreateBrowserLoginFlow", + createFlowErr: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + expectLog: true, + }, + { + name: "FailOnFilterProviders", + filterErr: fmt.Errorf("oh no"), + expectStatus: http.StatusInternalServerError, + }, + { + name: "NoProvidersAllowedAcceptJSON", + acceptHeader: "application/json, text/plain, */*", + expectStatus: http.StatusOK, + expectJSON: true, + }, + { + name: "NoProvidersAllowedNotAcceptJSON", + acceptHeader: "application/x-www-form-urlencoded", + expectStatus: http.StatusSeeOther, + expectLocation: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + + flow := kClient.NewLoginFlowWithDefaults() + flow.Id = "test" + flow.State = "passed_challenge" + + loginChallenge := "login_challenge_2341235123231" + returnTo, _ := url.JoinPath(BASE_URL, "ui/login") + returnTo = returnTo + "?login_challenge=" + loginChallenge + + req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) + values := req.URL.Query() + values.Add("login_challenge", loginChallenge) + req.URL.RawQuery = values.Encode() + if tt.acceptHeader != "" { + req.Header.Set("Accept", tt.acceptHeader) + } + + mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) + mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, nil) + mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, nil, FlowStateCookie{}).Return(true, nil) + + if tt.createFlowErr != nil { + if tt.expectLog { + mockLogger.EXPECT().Errorf("failed to create login flow, err: error") + } + mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(nil, nil, tt.createFlowErr) + } else { + mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(flow, req.Cookies(), nil) + if tt.filterErr != nil { + mockService.EXPECT().FilterFlowProviderList(gomock.Any(), flow).Return(nil, tt.filterErr) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().FilterFlowProviderList(gomock.Any(), flow).Return(flow, nil) + } + } + + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + + mux.ServeHTTP(w, req) + + res := w.Result() + defer res.Body.Close() + + if res.StatusCode != tt.expectStatus { + t.Fatalf("expected HTTP status code %d got %v", tt.expectStatus, res.Status) + } + + if tt.expectJSON { + data, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("expected error to be nil got %v", err) + } + loginFlow := kClient.NewLoginFlowWithDefaults() + if err := json.Unmarshal(data, loginFlow); err != nil { + t.Errorf("expected error to be nil got %v", err) + } + if loginFlow.Id != flow.Id { + t.Fatalf("Invalid flow id, expected: %s, got: %s", flow.Id, loginFlow.Id) + } + } + + if tt.expectLocation { + location, _ := url.JoinPath(BASE_URL, "ui/login") + location = fmt.Sprintf("%s?flow=%s", location, flow.Id) + if res.Header.Get("Location") != location { + t.Fatalf("Invalid location, expected: %s, got: %s", location, res.Header.Get("Location")) + } + } + }) + } +} + +func TestHandleCreateFlowRedirectToSetupWebauthn(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -80,23 +215,28 @@ func TestHandleCreateFlowWithoutSessionAcceptJSON(t *testing.T) { flow.State = "passed_challenge" loginChallenge := "login_challenge_2341235123231" - returnTo, _ := url.JoinPath(BASE_URL, "ui/login") - returnTo = returnTo + "?login_challenge=" + loginChallenge + + session := kClient.NewSession("test") + session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + method := "oidc" + aal := kClient.AUTHENTICATORASSURANCELEVEL_AAL1 + session.AuthenticationMethods = []kClient.SessionAuthenticationMethod{{Method: &method}} + session.AuthenticatorAssuranceLevel = &aal req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) values := req.URL.Query() values.Add("login_challenge", loginChallenge) req.URL.RawQuery = values.Encode() - req.Header.Set("Accept", "application/json, text/plain, */*") - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, nil) - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, nil, FlowStateCookie{}).Return(true, nil) - mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(flow, req.Cookies(), nil) - mockService.EXPECT().FilterFlowProviderList(gomock.Any(), flow).Return(flow, nil) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) + + mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceWebAuthnWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockService.EXPECT().HasWebAuthnAvailable(gomock.Any(), session.Id).Return(false, nil) + mockCookieManager.EXPECT().SetStateCookie(gomock.Any(), gomock.Any()).Return(nil) w := httptest.NewRecorder() mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + NewAPI(mockService, false, true, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) mux.ServeHTTP(w, req) @@ -110,203 +250,391 @@ func TestHandleCreateFlowWithoutSessionAcceptJSON(t *testing.T) { if res.StatusCode != http.StatusOK { t.Fatalf("expected HTTP status code 200 got %v", res.StatusCode) } - loginFlow := kClient.NewLoginFlowWithDefaults() - if err := json.Unmarshal(data, loginFlow); err != nil { + loginFlow := BrowserLocationChangeRequired{} + if err := json.Unmarshal(data, &loginFlow); err != nil { t.Errorf("expected error to be nil got %v", err) } - - if loginFlow.Id != flow.Id { - t.Fatalf("Invalid flow id, expected: %s, got: %s", flow.Id, loginFlow.Id) + if !strings.HasPrefix(*loginFlow.RedirectTo, "/ui/setup_passkey") { + t.Errorf("expected redirect_to to start with '/ui/setup_passkey' got %v", *loginFlow.RedirectTo) } } -func TestHandleCreateFlowWithoutSessionNotAcceptJSON(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestHandleCreateFlowWithSession(t *testing.T) { + tests := []struct { + name string + acceptHeader string + acceptErr error + expectStatus int + expectSuccess bool + }{ + { + name: "AcceptJSON", + acceptHeader: "application/json, text/plain, */*", + expectStatus: http.StatusOK, + expectSuccess: true, + }, + { + name: "NotAcceptJSON", + acceptHeader: "application/x-www-form-urlencoded", + expectStatus: http.StatusOK, + expectSuccess: true, + }, + { + name: "FailOnAcceptLoginRequest", + acceptErr: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + expectSuccess: false, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = "test" - flow.State = "passed_challenge" + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) - loginChallenge := "login_challenge_2341235123231" - returnTo, _ := url.JoinPath(BASE_URL, "ui/login") - returnTo = returnTo + "?login_challenge=" + loginChallenge + session := kClient.NewSession("test") + redirect := "https://some/path/to/somewhere" + redirectTo := BrowserLocationChangeRequired{RedirectTo: &redirect} - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() - req.Header.Set("Accept", "application/x-www-form-urlencoded") - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, nil) - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, nil, FlowStateCookie{}).Return(true, nil) - mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(flow, req.Cookies(), nil) - mockService.EXPECT().FilterFlowProviderList(gomock.Any(), flow).Return(flow, nil) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) + loginChallenge := "login_challenge_2341235123231" - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) + values := req.URL.Query() + values.Add("login_challenge", loginChallenge) + req.URL.RawQuery = values.Encode() + if tt.acceptHeader != "" { + req.Header.Set("Accept", tt.acceptHeader) + } - mux.ServeHTTP(w, req) + mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceWebAuthnWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, session, FlowStateCookie{}).Return(false, nil) + mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) - res := w.Result() + if tt.acceptErr != nil { + mockService.EXPECT().AcceptLoginRequest(gomock.Any(), session, loginChallenge).Return(nil, nil, tt.acceptErr) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().AcceptLoginRequest(gomock.Any(), session, loginChallenge).Return(&redirectTo, req.Cookies(), nil) + mockCookieManager.EXPECT().ClearStateCookie(gomock.Any()).Return() + } - if res.StatusCode != http.StatusSeeOther { - t.Fatalf("expected HTTP status code 303 got %v", res.StatusCode) - } + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + + mux.ServeHTTP(w, req) - location, _ := url.JoinPath(BASE_URL, "ui/login") - location = fmt.Sprintf("%s?flow=%s", location, flow.Id) + res := w.Result() + + if res.StatusCode != tt.expectStatus { + t.Fatalf("Expected HTTP status code %d, got: %v", tt.expectStatus, res.Status) + } - if res.Header.Get("Location") != location { - t.Fatalf("Invalid location, expected: %s, got: %s", location, res.Header.Get("Location")) + if tt.expectSuccess { + data, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + redirectResp := hClient.NewOAuth2RedirectToWithDefaults() + if err := json.Unmarshal(data, redirectResp); err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + if redirectResp.RedirectTo != redirect { + t.Fatalf("Expected redirect to %s, got: %s", redirect, res.Header["Location"][0]) + } + } + }) } } -func TestHandleCreateFlowWithoutSessionFailOnCreateBrowserLoginFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestHandleGetLoginFlow(t *testing.T) { + tests := []struct { + name string + serviceError error + expectStatus int + }{ + { + name: "Success", + serviceError: nil, + expectStatus: http.StatusOK, + }, + { + name: "Fail", + serviceError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = "test" + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) - loginChallenge := "login_challenge_2341235123231" - returnTo, _ := url.JoinPath(BASE_URL, "ui/login") - returnTo = returnTo + "?login_challenge=" + loginChallenge + id := "test" + flow := kClient.NewLoginFlowWithDefaults() + flow.SetId(id) + flow.SetState("choose_method") - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() + req := httptest.NewRequest(http.MethodGet, HANDLE_GET_LOGIN_FLOW_URL, nil) + values := req.URL.Query() + values.Add("id", id) + req.URL.RawQuery = values.Encode() - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) - mockLogger.EXPECT().Errorf("failed to create login flow, err: error") - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, nil) - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, nil, FlowStateCookie{}).Return(true, nil) - mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(nil, nil, fmt.Errorf("error")) + if tt.serviceError != nil { + mockService.EXPECT().GetLoginFlow(gomock.Any(), id, req.Cookies()).Return(nil, nil, tt.serviceError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().GetLoginFlow(gomock.Any(), id, req.Cookies()).Return(flow, req.Cookies(), nil) + } - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - mux.ServeHTTP(w, req) + mux.ServeHTTP(w, req) - res := w.Result() - defer res.Body.Close() + res := w.Result() - if res.StatusCode != http.StatusInternalServerError { - t.Fatalf("expected HTTP status code 500 got %v", res.StatusCode) + if res.StatusCode != tt.expectStatus { + t.Fatalf("Expected HTTP status code %d, got: %v", tt.expectStatus, res.Status) + } + + if tt.serviceError == nil { + data, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + flowResponse := kClient.NewLoginFlowWithDefaults() + if err := json.Unmarshal(data, flowResponse); err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + if flowResponse.Id != flow.Id { + t.Fatalf("Expected id to be: %s, got: %s", flow.Id, flowResponse.Id) + } + } + }) } } -func TestHandleCreateFlowWithoutSessionFailOnFilterProviders(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestHandleUpdateIdentifierFirstFlow(t *testing.T) { + tests := []struct { + name string + parseError error + updateError error + expectStatus int + expectSuccess bool + }{ + { + name: "Success", + parseError: nil, + updateError: nil, + expectStatus: http.StatusOK, + expectSuccess: true, + }, + { + name: "FailOnParseLoginFlowMethodBody", + parseError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, + { + name: "FailOnUpdateIdLoginFlow", + updateError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = "test" + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) - loginChallenge := "login_challenge_2341235123231" - returnTo, _ := url.JoinPath(BASE_URL, "ui/login") - returnTo = returnTo + "?login_challenge=" + loginChallenge + flowId := "test" + redirectTo := "https://some/path/to/somewhere" + redirectFlow := new(BrowserLocationChangeRequired) + redirectFlow.RedirectTo = &redirectTo - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() + flowBody := new(kClient.UpdateLoginFlowWithIdentifierFirstMethod) + flowBody.SetIdentifier("test@example.com") - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, nil) - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, nil, FlowStateCookie{}).Return(true, nil) - mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(flow, req.Cookies(), nil) - mockService.EXPECT().FilterFlowProviderList(gomock.Any(), flow).Return(nil, fmt.Errorf("oh no")) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_IDENTIFIER_FIRST_LOGIN_FLOW_URL, nil) + values := req.URL.Query() + values.Add("flow", flowId) + req.URL.RawQuery = values.Encode() - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + if tt.parseError != nil { + mockService.EXPECT().ParseIdentifierFirstLoginFlowMethodBody(gomock.Any()).Return(flowBody, nil, tt.parseError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().ParseIdentifierFirstLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) + if tt.updateError != nil { + mockService.EXPECT().UpdateIdentifierFirstLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(nil, nil, tt.updateError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().UpdateIdentifierFirstLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, req.Cookies(), nil) + } + } - mux.ServeHTTP(w, req) + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + mux.ServeHTTP(w, req) - res := w.Result() - defer res.Body.Close() + res := w.Result() + defer res.Body.Close() - if res.StatusCode != http.StatusInternalServerError { - t.Fatalf("expected HTTP status code 500 got %v", res.StatusCode) + if res.StatusCode != tt.expectStatus { + t.Fatalf("Expected HTTP status code %d, got: %v", tt.expectStatus, res.Status) + } + + if tt.expectSuccess { + data, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + flowResponse := new(BrowserLocationChangeRequired) + if err := json.Unmarshal(data, flowResponse); err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + if *flowResponse.RedirectTo != redirectTo { + t.Fatalf("Expected redirectTo to be %v not %v", redirectTo, flowResponse.RedirectTo) + } + } + }) } } -func TestHandleCreateFlowWithoutSessionWhenNoProvidersAllowedAcceptJSON(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestHandleUpdateFlow(t *testing.T) { + tests := []struct { + name string + parseError error + checkAllowedErr error + providerAllowed bool + updateError error + expectStatus int + expectSuccess bool + }{ + { + name: "Success", + providerAllowed: true, + expectStatus: http.StatusOK, + expectSuccess: true, + }, + { + name: "WhenProviderNotAllowed", + providerAllowed: false, + expectStatus: http.StatusForbidden, + }, + { + name: "FailOnParseLoginFlowMethodBody", + parseError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, + { + name: "FailOnUpdateOIDCLoginFlow", + providerAllowed: true, + updateError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, + { + name: "FailOnCheckAllowedProvider", + providerAllowed: false, + checkAllowedErr: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = "test" - flow.State = "passed_challenge" + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) - loginChallenge := "login_challenge_2341235123231" - returnTo, _ := url.JoinPath(BASE_URL, "ui/login") - returnTo = returnTo + "?login_challenge=" + loginChallenge + flowId := "test" + flow := kClient.NewLoginFlowWithDefaults() + flow.Id = flowId + flow.ExpiresAt = time.Now().UTC() + redirectTo := "https://some/path/to/somewhere" + redirectFlow := new(BrowserLocationChangeRequired) + redirectFlow.RedirectTo = &redirectTo - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() - req.Header.Set("Accept", "application/json, text/plain, */*") + flowBody := new(kClient.UpdateLoginFlowBody) + flowBody.UpdateLoginFlowWithOidcMethod = kClient.NewUpdateLoginFlowWithOidcMethod("oidc", "oidc") - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, nil) - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, nil, FlowStateCookie{}).Return(true, nil) - mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(flow, req.Cookies(), nil) - mockService.EXPECT().FilterFlowProviderList(gomock.Any(), flow).Return(flow, nil) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) + req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) + values := req.URL.Query() + values.Add("flow", flowId) + req.URL.RawQuery = values.Encode() - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + if tt.parseError != nil { + mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, nil, tt.parseError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) + mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil) + mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(tt.providerAllowed, tt.checkAllowedErr) + + if tt.checkAllowedErr != nil { + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else if !tt.providerAllowed { + // forbidden case + } else if tt.updateError != nil { + mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(nil, nil, nil, tt.updateError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFA").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldRegenerateBackupCodes").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockCookieManager.EXPECT().SetStateCookie(gomock.Any(), gomock.Any()).Return(nil) + mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, nil, req.Cookies(), nil) + } + } - mux.ServeHTTP(w, req) + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + mux.ServeHTTP(w, req) - res := w.Result() - defer res.Body.Close() - data, err := io.ReadAll(res.Body) - if err != nil { - t.Errorf("expected error to be nil got %v", err) - } + res := w.Result() - if res.StatusCode != http.StatusOK { - t.Fatalf("expected HTTP status code 200 got %v", res.StatusCode) - } - loginFlow := kClient.NewLoginFlowWithDefaults() - if err := json.Unmarshal(data, loginFlow); err != nil { - t.Errorf("expected error to be nil got %v", err) - } + if res.StatusCode != tt.expectStatus { + t.Fatalf("Expected HTTP status code %d, got: %v", tt.expectStatus, res.Status) + } - if loginFlow.Id != flow.Id { - t.Fatalf("Invalid flow id, expected: %s, got: %s", flow.Id, loginFlow.Id) + if tt.expectSuccess { + data, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + flowResponse := new(BrowserLocationChangeRequired) + if err := json.Unmarshal(data, flowResponse); err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + } + }) } } -func TestHandleCreateFlowWithoutSessionWhenNoProvidersAllowedNotAcceptJSON(t *testing.T) { +func TestHandleUpdateLoginFlowRedirectToRegenerateBackupCodes(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -315,1017 +643,312 @@ func TestHandleCreateFlowWithoutSessionWhenNoProvidersAllowedNotAcceptJSON(t *te mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) mockTracer := NewMockTracingInterface(ctrl) + session := kClient.NewSession("test") + + lookupMethod := kClient.NewSessionAuthenticationMethodWithDefaults() + lookupMethod.SetMethod("lookup_secret") + + pwdMethod := kClient.NewSessionAuthenticationMethodWithDefaults() + pwdMethod.SetMethod("password") + + session.SetAuthenticatorAssuranceLevel("aal2") + session.AuthenticationMethods = []kClient.SessionAuthenticationMethod{*pwdMethod, *lookupMethod} + + flowId := "test" + redirectTo := "https://some/path/to/somewhere" + redirectFlow := new(BrowserLocationChangeRequired) + redirectFlow.RedirectTo = &redirectTo + flow := kClient.NewLoginFlowWithDefaults() - flow.Id = "test" - flow.State = "passed_challenge" + flow.Id = flowId + returnTo := "https://some/return/url" + flow.ReturnTo = &returnTo - loginChallenge := "login_challenge_2341235123231" - returnTo, _ := url.JoinPath(BASE_URL, "ui/login") - returnTo = returnTo + "?login_challenge=" + loginChallenge + flowBody := new(kClient.UpdateLoginFlowBody) + flowBody.UpdateLoginFlowWithLookupSecretMethod = kClient.NewUpdateLoginFlowWithLookupSecretMethod("xt879l1a", "lookup_secret") - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) + req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) values := req.URL.Query() - values.Add("login_challenge", loginChallenge) + values.Add("flow", flowId) req.URL.RawQuery = values.Encode() - req.Header.Set("Accept", "application/x-www-form-urlencoded") - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, nil) - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, nil, FlowStateCookie{}).Return(true, nil) - mockService.EXPECT().CreateBrowserLoginFlow(gomock.Any(), gomock.Any(), returnTo, loginChallenge, gomock.Any(), req.Cookies()).Return(flow, req.Cookies(), nil) - mockService.EXPECT().FilterFlowProviderList(gomock.Any(), flow).Return(flow, nil) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) + mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) + mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil) + mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, nil, req.Cookies(), nil) + + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFA").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockTracer.EXPECT().Start(gomock.Any(), "kratos.Service.HasTOTPAvailable").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockService.EXPECT().HasTOTPAvailable(gomock.Any(), gomock.Any()).Return(true, nil) + + mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldRegenerateBackupCodes").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() + mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) + mockService.EXPECT().HasNotEnoughLookupSecretsLeft(gomock.Any(), session.Identity.GetId()).Return(true, nil) + mockCookieManager.EXPECT().SetStateCookie(gomock.Any(), gomock.Any()).Return(nil) w := httptest.NewRecorder() mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + NewAPI(mockService, true, true, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) mux.ServeHTTP(w, req) res := w.Result() - if res.StatusCode != http.StatusSeeOther { - t.Fatalf("expected HTTP status code 303 got %v", res.StatusCode) - } - - location, _ := url.JoinPath(BASE_URL, "ui/login") - location = fmt.Sprintf("%s?flow=%s", location, flow.Id) - - if res.Header.Get("Location") != location { - t.Fatalf("Invalid location, expected: %s, got: %s", location, res.Header.Get("Location")) - } -} - -func TestHandleCreateFlowRedirectToSetupWebauthn(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = "test" - flow.State = "passed_challenge" - - loginChallenge := "login_challenge_2341235123231" - - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) - method := "oidc" - aal := kClient.AUTHENTICATORASSURANCELEVEL_AAL1 - session.AuthenticationMethods = []kClient.SessionAuthenticationMethod{{Method: &method}} - session.AuthenticatorAssuranceLevel = &aal - - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceWebAuthnWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockService.EXPECT().HasWebAuthnAvailable(gomock.Any(), session.Id).Return(false, nil) - mockCookieManager.EXPECT().SetStateCookie(gomock.Any(), gomock.Any()).Return(nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, true, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - defer res.Body.Close() - data, err := io.ReadAll(res.Body) - if err != nil { - t.Errorf("expected error to be nil got %v", err) - } - - if res.StatusCode != http.StatusOK { - t.Fatalf("expected HTTP status code 200 got %v", res.StatusCode) - } - loginFlow := BrowserLocationChangeRequired{} - if err := json.Unmarshal(data, &loginFlow); err != nil { - t.Errorf("expected error to be nil got %v", err) - } - if !strings.HasPrefix(*loginFlow.RedirectTo, "/ui/setup_passkey") { - t.Errorf("expected redirect_to to start with '/ui/setup_passkey' got %v", *loginFlow.RedirectTo) - } -} - -func TestHandleCreateFlowWithSessionAcceptJSON(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - session := kClient.NewSession("test") - redirect := "https://some/path/to/somewhere" - redirectTo := BrowserLocationChangeRequired{RedirectTo: &redirect} - - loginChallenge := "login_challenge_2341235123231" - - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() - req.Header.Set("Accept", "application/json, text/plain, */*") - - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceWebAuthnWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, session, FlowStateCookie{}).Return(false, nil) - mockService.EXPECT().AcceptLoginRequest(gomock.Any(), session, loginChallenge).Return(&redirectTo, req.Cookies(), nil) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) - mockCookieManager.EXPECT().ClearStateCookie(gomock.Any()).Return() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - data, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - redirectResp := hClient.NewOAuth2RedirectToWithDefaults() - if err := json.Unmarshal(data, redirectResp); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) - } - if redirectResp.RedirectTo != redirect { - t.Fatalf("Expected redirect to %s, got: %s", redirect, res.Header["Location"][0]) - } -} - -func TestHandleCreateFlowWithSessionNotAcceptJSON(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - session := kClient.NewSession("test") - redirect := "https://some/path/to/somewhere" - redirectTo := BrowserLocationChangeRequired{RedirectTo: &redirect} - - loginChallenge := "login_challenge_2341235123231" - - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() - req.Header.Set("Accept", "application/x-www-form-urlencoded") - - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceWebAuthnWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, session, FlowStateCookie{}).Return(false, nil) - mockService.EXPECT().AcceptLoginRequest(gomock.Any(), session, loginChallenge).Return(&redirectTo, req.Cookies(), nil) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) - mockCookieManager.EXPECT().ClearStateCookie(gomock.Any()).Return() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - data, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - redirectResp := hClient.NewOAuth2RedirectToWithDefaults() - if err := json.Unmarshal(data, redirectResp); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) - } - if redirectResp.RedirectTo != redirect { - t.Fatalf("Expected redirect to %s, got: %s", redirect, res.Header["Location"][0]) - } -} - -func TestHandleCreateFlowWithSessionFailOnAcceptLoginRequest(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - session := kClient.NewSession("test") - - loginChallenge := "login_challenge_2341235123231" - - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_FLOW_URL, nil) - values := req.URL.Query() - values.Add("login_challenge", loginChallenge) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceWebAuthnWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockService.EXPECT().MustReAuthenticate(gomock.Any(), loginChallenge, session, FlowStateCookie{}).Return(false, nil) - mockService.EXPECT().AcceptLoginRequest(gomock.Any(), session, loginChallenge).Return(nil, nil, fmt.Errorf("error")) - mockCookieManager.EXPECT().GetStateCookie(gomock.Any()).Return(FlowStateCookie{}, nil) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) - } -} - -func TestHandleGetLoginFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - id := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.SetId(id) - flow.SetState("choose_method") - - req := httptest.NewRequest(http.MethodGet, HANDLE_GET_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("id", id) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().GetLoginFlow(gomock.Any(), id, req.Cookies()).Return(flow, req.Cookies(), nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) - } - - data, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - flowResponse := kClient.NewLoginFlowWithDefaults() - if err := json.Unmarshal(data, flowResponse); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - if flowResponse.Id != flow.Id { - t.Fatalf("Expected id to be: %s, got: %s", flow.Id, flowResponse.Id) - } -} - -func TestHandleGetLoginFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - id := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.SetId(id) - - req := httptest.NewRequest(http.MethodGet, HANDLE_GET_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("id", id) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().GetLoginFlow(gomock.Any(), id, req.Cookies()).Return(nil, nil, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) - } -} - -func TestHandleUpdateIdentifierFirstFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = flowId - redirectTo := "https://some/path/to/somewhere" - redirectFlow := new(BrowserLocationChangeRequired) - redirectFlow.RedirectTo = &redirectTo - - flowBody := new(kClient.UpdateLoginFlowWithIdentifierFirstMethod) - flowBody.SetIdentifier("test@example.com") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_IDENTIFIER_FIRST_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseIdentifierFirstLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) - mockService.EXPECT().UpdateIdentifierFirstLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, req.Cookies(), nil) - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) - } - - data, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - flowResponse := new(BrowserLocationChangeRequired) - if err := json.Unmarshal(data, flowResponse); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - if *flowResponse.RedirectTo != redirectTo { - t.Fatalf("Expected redirectTo to be %v not %v", redirectTo, flowResponse.RedirectTo) - } -} - -func TestHandleUpdateIdentifierFirstFlowFailOnParseLoginFlowMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flowBody := new(kClient.UpdateLoginFlowWithIdentifierFirstMethod) - flowBody.SetIdentifier("test@example.com") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_IDENTIFIER_FIRST_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseIdentifierFirstLoginFlowMethodBody(gomock.Any()).Return(flowBody, nil, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) - } -} - -func TestHandleUpdateIdentifierFirstFlowFailOnUpdateIdLoginFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = flowId - - flowBody := new(kClient.UpdateLoginFlowWithIdentifierFirstMethod) - flowBody.SetIdentifier("test@example.com") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_IDENTIFIER_FIRST_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseIdentifierFirstLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) - mockService.EXPECT().UpdateIdentifierFirstLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(nil, nil, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) - } -} - -func TestHandleUpdateFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = flowId - flow.ExpiresAt = time.Now().UTC() - redirectTo := "https://some/path/to/somewhere" - redirectFlow := new(BrowserLocationChangeRequired) - redirectFlow.RedirectTo = &redirectTo - - flowBody := new(kClient.UpdateLoginFlowBody) - flowBody.UpdateLoginFlowWithOidcMethod = kClient.NewUpdateLoginFlowWithOidcMethod("oidc", "oidc") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFA").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldRegenerateBackupCodes").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockCookieManager.EXPECT().SetStateCookie(gomock.Any(), gomock.Any()).Return(nil) - mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) - mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, nil, req.Cookies(), nil) - mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil) - mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) - } - - data, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - flowResponse := new(BrowserLocationChangeRequired) - if err := json.Unmarshal(data, flowResponse); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } -} - -func TestHandleUpdateFlowWhenProviderNotAllowed(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = flowId - redirectTo := "https://some/path/to/somewhere" - redirectFlow := new(BrowserLocationChangeRequired) - redirectFlow.RedirectTo = &redirectTo - - flowBody := new(kClient.UpdateLoginFlowBody) - flowBody.UpdateLoginFlowWithOidcMethod = kClient.NewUpdateLoginFlowWithOidcMethod("oidc", "oidc") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) - mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil) - mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusForbidden { - t.Fatal("Expected HTTP status code 403, got: ", res.Status) - } -} - -func TestHandleUpdateFlowFailOnParseLoginFlowMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - - flowBody := new(kClient.UpdateLoginFlowBody) - flowBody.UpdateLoginFlowWithOidcMethod = kClient.NewUpdateLoginFlowWithOidcMethod("oidc", "oidc") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, nil, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) - } -} - -func TestHandleUpdateLoginFlowRedirectToRegenerateBackupCodes(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - session := kClient.NewSession("test") - - lookupMethod := kClient.NewSessionAuthenticationMethodWithDefaults() - lookupMethod.SetMethod("lookup_secret") - - pwdMethod := kClient.NewSessionAuthenticationMethodWithDefaults() - pwdMethod.SetMethod("password") - - session.SetAuthenticatorAssuranceLevel("aal2") - session.AuthenticationMethods = []kClient.SessionAuthenticationMethod{*pwdMethod, *lookupMethod} - - flowId := "test" - redirectTo := "https://some/path/to/somewhere" - redirectFlow := new(BrowserLocationChangeRequired) - redirectFlow.RedirectTo = &redirectTo - - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = flowId - returnTo := "https://some/return/url" - flow.ReturnTo = &returnTo - - flowBody := new(kClient.UpdateLoginFlowBody) - flowBody.UpdateLoginFlowWithLookupSecretMethod = kClient.NewUpdateLoginFlowWithLookupSecretMethod("xt879l1a", "lookup_secret") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) - mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil) - mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, nil, req.Cookies(), nil) - - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFA").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldEnforceMFAWithSession").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockTracer.EXPECT().Start(gomock.Any(), "kratos.Service.HasTOTPAvailable").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockService.EXPECT().HasTOTPAvailable(gomock.Any(), gomock.Any()).Return(true, nil) - - mockTracer.EXPECT().Start(gomock.Any(), "kratos.API.shouldRegenerateBackupCodes").Return(context.Background(), trace.SpanFromContext(context.Background())).AnyTimes() - mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil) - mockService.EXPECT().HasNotEnoughLookupSecretsLeft(gomock.Any(), session.Identity.GetId()).Return(true, nil) - mockCookieManager.EXPECT().SetStateCookie(gomock.Any(), gomock.Any()).Return(nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, true, true, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if _, err := json.Marshal(flow); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) - } -} - -func TestHandleUpdateFlowFailOnUpdateOIDCLoginFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = flowId - - flowBody := new(kClient.UpdateLoginFlowBody) - flowBody.UpdateLoginFlowWithOidcMethod = kClient.NewUpdateLoginFlowWithOidcMethod("oidc", "oidc") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) - mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(nil, nil, nil, fmt.Errorf("error")) - mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil) - mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) - } -} - -func TestHandleUpdateFlowFailOnCheckAllowedProvider(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flow := kClient.NewLoginFlowWithDefaults() - flow.Id = flowId - - flowBody := new(kClient.UpdateLoginFlowBody) - flowBody.UpdateLoginFlowWithOidcMethod = kClient.NewUpdateLoginFlowWithOidcMethod("oidc", "oidc") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_LOGIN_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, req.Cookies(), nil) - mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil) - mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) - } -} - -func TestHandleCreateRecoveryFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - redirect := "https://example.com/ui/reset_email" - - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_RECOVERY_FLOW_URL, nil) - values := req.URL.Query() - req.URL.RawQuery = values.Encode() - - flow := kClient.NewRecoveryFlowWithDefaults() - mockService.EXPECT().CreateBrowserRecoveryFlow(gomock.Any(), redirect, req.Cookies()).Return(flow, req.Cookies(), nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if _, err := json.Marshal(flow); err != nil { - t.Fatalf("Expected error to be nil got %v", err) + if _, err := json.Marshal(flow); err != nil { + t.Fatalf("Expected error to be nil got %v", err) } if res.StatusCode != http.StatusOK { t.Fatal("Expected HTTP status code 200, got: ", res.Status) } } -func TestHandleCreateRecoveryFlowWithSession(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - redirect := "https://example.com/ui/reset_email" - - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_RECOVERY_FLOW_URL, nil) - values := req.URL.Query() - req.URL.RawQuery = values.Encode() - - sessionCookie := &http.Cookie{ - Name: KRATOS_SESSION_COOKIE_NAME, - Value: "some_value", - Path: "/", - HttpOnly: true, - Secure: true, +func TestHandleCreateRecoveryFlow(t *testing.T) { + tests := []struct { + name string + withSession bool + serviceError error + expectStatus int + expectDelete bool + }{ + { + name: "WithoutSession", + expectStatus: http.StatusOK, + }, + { + name: "WithSession", + withSession: true, + expectStatus: http.StatusOK, + expectDelete: true, + }, + { + name: "FailOnCreateBrowserRecoveryFlow", + serviceError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, } - req.AddCookie(sessionCookie) - flow := kClient.NewRecoveryFlowWithDefaults() - mockService.EXPECT().CreateBrowserRecoveryFlow(gomock.Any(), redirect, req.Cookies()).Return(flow, req.Cookies(), nil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) - mux.ServeHTTP(w, req) + redirect := "https://example.com/ui/reset_email" - res := w.Result() + req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_RECOVERY_FLOW_URL, nil) + values := req.URL.Query() + req.URL.RawQuery = values.Encode() - if _, err := json.Marshal(flow); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) - } - deleted := false - for _, c := range res.Cookies() { - if c.Name == KRATOS_SESSION_COOKIE_NAME { - if c.Expires.Equal(time.Unix(0, 0)) { - deleted = true - } else { - t.Fatal("Kratos session cookie was set") + if tt.withSession { + sessionCookie := &http.Cookie{ + Name: KRATOS_SESSION_COOKIE_NAME, + Value: "some_value", + Path: "/", + HttpOnly: true, + Secure: true, + } + req.AddCookie(sessionCookie) } - } - } - if !deleted { - t.Fatal("Kratos session cookie was not deleted") - } -} - -func TestHandleCreateRecoveryFlowFailOnCreateBrowserRecoveryFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - redirect := "https://example.com/ui/reset_email" - - req := httptest.NewRequest(http.MethodGet, HANDLE_CREATE_RECOVERY_FLOW_URL, nil) - values := req.URL.Query() - req.URL.RawQuery = values.Encode() - mockService.EXPECT().CreateBrowserRecoveryFlow(gomock.Any(), redirect, req.Cookies()).Return(nil, nil, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + flow := kClient.NewRecoveryFlowWithDefaults() + if tt.serviceError != nil { + mockService.EXPECT().CreateBrowserRecoveryFlow(gomock.Any(), redirect, req.Cookies()).Return(nil, nil, tt.serviceError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().CreateBrowserRecoveryFlow(gomock.Any(), redirect, req.Cookies()).Return(flow, req.Cookies(), nil) + } - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - mux.ServeHTTP(w, req) + mux.ServeHTTP(w, req) - res := w.Result() + res := w.Result() - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) + if res.StatusCode != tt.expectStatus { + t.Fatalf("Expected HTTP status code %d, got: %v", tt.expectStatus, res.Status) + } + if tt.serviceError == nil { + if _, err := json.Marshal(flow); err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + } + if tt.expectDelete { + deleted := false + for _, c := range res.Cookies() { + if c.Name == KRATOS_SESSION_COOKIE_NAME { + if c.Expires.Equal(time.Unix(0, 0)) { + deleted = true + } else { + t.Fatal("Kratos session cookie was set") + } + } + } + if !deleted { + t.Fatal("Kratos session cookie was not deleted") + } + } + }) } } func TestHandleGetRecoveryFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - id := "test" - flow := kClient.NewRecoveryFlowWithDefaults() - flow.SetId(id) - flow.SetState("choose_method") - - req := httptest.NewRequest(http.MethodGet, HANDLE_GET_RECOVERY_FLOW_URL, nil) - values := req.URL.Query() - values.Add("id", id) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().GetRecoveryFlow(gomock.Any(), id, req.Cookies()).Return(flow, req.Cookies(), nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) + tests := []struct { + name string + serviceError error + expectStatus int + }{ + { + name: "Success", + serviceError: nil, + expectStatus: http.StatusOK, + }, + { + name: "Fail", + serviceError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, } - data, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - flowResponse := kClient.NewRecoveryFlowWithDefaults() - if err := json.Unmarshal(data, flowResponse); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - if flowResponse.Id != flow.Id { - t.Fatalf("Expected id to be: %s, got: %s", flow.Id, flowResponse.Id) - } -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() -func TestHandleGetRecoveryFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) + id := "test" + flow := kClient.NewRecoveryFlowWithDefaults() + flow.SetId(id) + flow.SetState("choose_method") - id := "test" - flow := kClient.NewRecoveryFlowWithDefaults() - flow.SetId(id) + req := httptest.NewRequest(http.MethodGet, HANDLE_GET_RECOVERY_FLOW_URL, nil) + values := req.URL.Query() + values.Add("id", id) + req.URL.RawQuery = values.Encode() - req := httptest.NewRequest(http.MethodGet, HANDLE_GET_RECOVERY_FLOW_URL, nil) - values := req.URL.Query() - values.Add("id", id) - req.URL.RawQuery = values.Encode() + if tt.serviceError != nil { + mockService.EXPECT().GetRecoveryFlow(gomock.Any(), id, req.Cookies()).Return(nil, nil, tt.serviceError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().GetRecoveryFlow(gomock.Any(), id, req.Cookies()).Return(flow, req.Cookies(), nil) + } - mockService.EXPECT().GetRecoveryFlow(gomock.Any(), id, req.Cookies()).Return(nil, nil, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + mux.ServeHTTP(w, req) - mux.ServeHTTP(w, req) + res := w.Result() - res := w.Result() + if res.StatusCode != tt.expectStatus { + t.Fatalf("Expected HTTP status code %d, got: %v", tt.expectStatus, res.Status) + } - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) + if tt.serviceError == nil { + data, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + flowResponse := kClient.NewRecoveryFlowWithDefaults() + if err := json.Unmarshal(data, flowResponse); err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + if flowResponse.Id != flow.Id { + t.Fatalf("Expected id to be: %s, got: %s", flow.Id, flowResponse.Id) + } + } + }) } } func TestHandleUpdateRecoveryFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - - flowId := "test" - flow := kClient.NewRecoveryFlowWithDefaults() - flow.Id = flowId - flow.ExpiresAt = time.Now().UTC() - - redirectTo := "https://example.com/ui/reset_email" - redirectFlow := new(BrowserLocationChangeRequired) - redirectFlow.RedirectTo = &redirectTo - - flowBody := new(kClient.UpdateRecoveryFlowBody) - flowBody.UpdateRecoveryFlowWithCodeMethod = kClient.NewUpdateRecoveryFlowWithCodeMethod("code") - - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_RECOVERY_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() - - mockService.EXPECT().ParseRecoveryFlowMethodBody(gomock.Any()).Return(flowBody, nil) - mockService.EXPECT().UpdateRecoveryFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, req.Cookies(), nil) - - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - - mux.ServeHTTP(w, req) - - res := w.Result() - - if res.StatusCode != http.StatusOK { - t.Fatal("Expected HTTP status code 200, got: ", res.Status) + tests := []struct { + name string + parseError error + expectStatus int + }{ + { + name: "Success", + parseError: nil, + expectStatus: http.StatusOK, + }, + { + name: "FailOnParseRecoveryFlowMethodBody", + parseError: fmt.Errorf("error"), + expectStatus: http.StatusInternalServerError, + }, } - data, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } - flowResponse := new(BrowserLocationChangeRequired) - if err := json.Unmarshal(data, flowResponse); err != nil { - t.Fatalf("Expected error to be nil got %v", err) - } -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() -func TestHandleUpdateRecoveryFlowFailOnParseRecoveryFlowMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + mockLogger := NewMockLoggerInterface(ctrl) + mockService := NewMockServiceInterface(ctrl) + mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) - mockLogger := NewMockLoggerInterface(ctrl) - mockService := NewMockServiceInterface(ctrl) - mockCookieManager := NewMockAuthCookieManagerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) + flowId := "test" + flow := kClient.NewRecoveryFlowWithDefaults() + flow.Id = flowId + flow.ExpiresAt = time.Now().UTC() - flowId := "test" + redirectTo := "https://example.com/ui/reset_email" + redirectFlow := new(BrowserLocationChangeRequired) + redirectFlow.RedirectTo = &redirectTo - flowBody := new(kClient.UpdateRecoveryFlowBody) - flowBody.UpdateRecoveryFlowWithCodeMethod = kClient.NewUpdateRecoveryFlowWithCodeMethod("code") + flowBody := new(kClient.UpdateRecoveryFlowBody) + flowBody.UpdateRecoveryFlowWithCodeMethod = kClient.NewUpdateRecoveryFlowWithCodeMethod("code") - req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_RECOVERY_FLOW_URL, nil) - values := req.URL.Query() - values.Add("flow", flowId) - req.URL.RawQuery = values.Encode() + req := httptest.NewRequest(http.MethodPost, HANDLE_UPDATE_RECOVERY_FLOW_URL, nil) + values := req.URL.Query() + values.Add("flow", flowId) + req.URL.RawQuery = values.Encode() - mockService.EXPECT().ParseRecoveryFlowMethodBody(gomock.Any()).Return(flowBody, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + if tt.parseError != nil { + mockService.EXPECT().ParseRecoveryFlowMethodBody(gomock.Any()).Return(flowBody, tt.parseError) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + } else { + mockService.EXPECT().ParseRecoveryFlowMethodBody(gomock.Any()).Return(flowBody, nil) + mockService.EXPECT().UpdateRecoveryFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, req.Cookies(), nil) + } - w := httptest.NewRecorder() - mux := chi.NewMux() - NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) + w := httptest.NewRecorder() + mux := chi.NewMux() + NewAPI(mockService, false, false, BASE_URL, mockCookieManager, mockTracer, mockLogger).RegisterEndpoints(mux) - mux.ServeHTTP(w, req) + mux.ServeHTTP(w, req) - res := w.Result() + res := w.Result() - if res.StatusCode != http.StatusInternalServerError { - t.Fatal("Expected HTTP status code 500, got: ", res.Status) + if res.StatusCode != tt.expectStatus { + t.Fatalf("Expected HTTP status code %d, got: %v", tt.expectStatus, res.Status) + } + + if tt.parseError == nil { + data, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + flowResponse := new(BrowserLocationChangeRequired) + if err := json.Unmarshal(data, flowResponse); err != nil { + t.Fatalf("Expected error to be nil got %v", err) + } + } + }) } } diff --git a/pkg/kratos/service_test.go b/pkg/kratos/service_test.go index 59c5c7eec..faab3d362 100644 --- a/pkg/kratos/service_test.go +++ b/pkg/kratos/service_test.go @@ -1,3 +1,7 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: AGPL-3.0 + +// Package kratos provides unit tests for Kratos service functionality. package kratos import ( @@ -29,206 +33,227 @@ import ( //go:generate mockgen -build_flags=--mod=mod -package kratos -destination ./mock_identity.go github.com/ory/kratos-client-go/v25 IdentityAPI //go:generate mockgen -build_flags=--mod=mod -package kratos -destination ./mock_hydra.go -source=../../internal/hydra/interfaces.go -func TestCheckSessionSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) - sessionRequest := kClient.FrontendAPIToSessionRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, +func TestCheckSession(t *testing.T) { + tests := []struct { + name string + cookies []*http.Cookie + mockError error + mockSession *kClient.Session + expectError bool + expectNilRes bool + validateCookie bool + }{ + { + name: "success with valid cookies", + cookies: []*http.Cookie{{Name: "test", Value: "test"}}, + mockSession: func() *kClient.Session { + s := kClient.NewSession("test") + s.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + return s + }(), + mockError: nil, + expectError: false, + expectNilRes: false, + validateCookie: true, + }, + { + name: "failure on API error", + cookies: []*http.Cookie{{Name: "test", Value: "test"}}, + mockSession: nil, + mockError: fmt.Errorf("error"), + expectError: true, + expectNilRes: true, + validateCookie: true, + }, } - mockTracer.EXPECT().Start(ctx, "kratos.Service.ToSession").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().ToSession(ctx).Times(1).Return(sessionRequest) - mockKratosFrontendApi.EXPECT().ToSessionExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIToSessionRequest) (*kClient.Session, *http.Response, error) { - // use reflect as cookie is a private attribute, also is a string pointer so need to cast it multiple times - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) - } - - return session, &resp, nil - }, - ) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - s, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CheckSession(ctx, cookies) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - if s != session { - t.Fatalf("expected session to be %v not %v", session, s) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} + ctx := context.Background() + sessionRequest := kClient.FrontendAPIToSessionRequest{ + ApiService: mockKratosFrontendApi, + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{tt.cookies[0].Raw}}, + } -func TestCheckSessionFails(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + mockTracer.EXPECT().Start(ctx, "kratos.Service.ToSession").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().ToSession(ctx).Times(1).Return(sessionRequest) + mockKratosFrontendApi.EXPECT().ToSessionExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIToSessionRequest) (*kClient.Session, *http.Response, error) { + if tt.validateCookie { + if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { + t.Fatalf("expected cookie string as test=test, got %s", *cookie) + } + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) + if tt.mockError != nil { + return nil, new(http.Response), tt.mockError + } + return tt.mockSession, &resp, nil + }, + ) - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookies = append(cookies, &http.Cookie{Name: "test", Value: "test"}) - sessionRequest := kClient.FrontendAPIToSessionRequest{ - ApiService: mockKratosFrontendApi, - } + s, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CheckSession(ctx, tt.cookies) - mockTracer.EXPECT().Start(ctx, "kratos.Service.ToSession").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().ToSession(ctx).Times(1).Return(sessionRequest) - mockKratosFrontendApi.EXPECT().ToSessionExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIToSessionRequest) (*kClient.Session, *http.Response, error) { - // use reflect as cookie is a private attribute, also is a string pointer so need to cast it multiple times - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) + if tt.expectError { + if err == nil { + t.Fatal("expected error but got nil") + } + } else { + if err != nil { + t.Fatalf("expected no error but got %v", err) + } } - return nil, new(http.Response), fmt.Errorf("error") - }, - ) - - s, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CheckSession(ctx, cookies) - - if s != nil { - t.Fatalf("expected session to be nil not %v", s) - } - if c != nil { - t.Fatalf("expected cookies to be nil not %v", c) - } - if err == nil { - t.Fatalf("expected error not nil") + if tt.expectNilRes { + if s != nil { + t.Fatalf("expected session to be nil but got %v", s) + } + if c != nil { + t.Fatalf("expected cookies to be nil but got %v", c) + } + } else { + if s != tt.mockSession { + t.Fatalf("expected session to be %v but got %v", tt.mockSession, s) + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v but got %v", resp.Cookies(), c) + } + } + }) } } -func TestAcceptLoginRequestSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) - - ctx := context.Background() - loginChallenge := "123456" - identityID := "id" - redirectTo := hClient.NewOAuth2RedirectTo("http://redirect/to/path") - acceptLoginRequest := hClient.OAuth2APIAcceptOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, +func TestAcceptLoginRequest(t *testing.T) { + tests := []struct { + name string + loginChallenge string + identityID string + sessionExpiry *time.Time + mockError error + expectError bool + validateTimestamp bool + }{ + { + name: "success with session expiry", + loginChallenge: "123456", + identityID: "id", + sessionExpiry: func() *time.Time { + t := time.Now().Add(300 * time.Second) + return &t + }(), + mockError: nil, + expectError: false, + validateTimestamp: true, + }, + { + name: "failure on API error", + loginChallenge: "123456", + identityID: "test", + sessionExpiry: nil, + mockError: fmt.Errorf("error"), + expectError: true, + validateTimestamp: false, + }, } - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity(identityID, "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) - redirectResp := BrowserLocationChangeRequired{RedirectTo: &redirectTo.RedirectTo} - - session.SetExpiresAt(time.Now().Add(300 * time.Second)) - leeway := int64(2) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - resp := new(http.Response) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockHydraOauthApi := NewMockOAuth2API(ctrl) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().AcceptOAuth2LoginRequest(ctx).Times(1).Return(acceptLoginRequest) - mockHydraOauthApi.EXPECT().AcceptOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( - func(r hClient.OAuth2APIAcceptOAuth2LoginRequestRequest) (*hClient.OAuth2RedirectTo, *http.Response, error) { - if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); *lc != loginChallenge { - t.Fatalf("expected loginChallenge to be %s, got %s", loginChallenge, *lc) - } - if id := (*hClient.AcceptOAuth2LoginRequest)(reflect.ValueOf(r).FieldByName("acceptOAuth2LoginRequest").UnsafePointer()); id.Subject != identityID { - t.Fatalf("expected identityID to be %s, got %s", identityID, id.Subject) - } - if id := (*hClient.AcceptOAuth2LoginRequest)(reflect.ValueOf(r).FieldByName("acceptOAuth2LoginRequest").UnsafePointer()); 300-id.GetRememberFor() > leeway { - t.Fatalf("expected RememberFor to be close to 300, got %v", id.GetRememberFor()) + ctx := context.Background() + redirectTo := hClient.NewOAuth2RedirectTo("http://redirect/to/path") + acceptLoginRequest := hClient.OAuth2APIAcceptOAuth2LoginRequestRequest{ + ApiService: mockHydraOauthApi, } - if id := (*hClient.AcceptOAuth2LoginRequest)(reflect.ValueOf(r).FieldByName("acceptOAuth2LoginRequest").UnsafePointer()); id.GetIdentityProviderSessionId() != session.GetId() { - t.Fatalf("expected session ID to be %s, got %s", session.GetId(), id.GetIdentityProviderSessionId()) + session := kClient.NewSession("test") + session.Identity = kClient.NewIdentity(tt.identityID, "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + + if tt.sessionExpiry != nil { + session.SetExpiresAt(*tt.sessionExpiry) } - return redirectTo, resp, nil - }, - ) - rt, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).AcceptLoginRequest(ctx, session, loginChallenge) + resp := new(http.Response) - if *rt != redirectResp { - t.Fatalf("expected redirect to be %v not %v", redirectResp, *rt) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} + mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) + mockHydraOauthApi.EXPECT().AcceptOAuth2LoginRequest(ctx).Times(1).Return(acceptLoginRequest) + mockHydraOauthApi.EXPECT().AcceptOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( + func(r hClient.OAuth2APIAcceptOAuth2LoginRequestRequest) (*hClient.OAuth2RedirectTo, *http.Response, error) { + if tt.mockError != nil { + return nil, nil, tt.mockError + } -func TestAcceptLoginRequestFails(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()) + if *lc != tt.loginChallenge { + t.Fatalf("expected loginChallenge to be %s, got %s", tt.loginChallenge, *lc) + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) + acceptReq := (*hClient.AcceptOAuth2LoginRequest)(reflect.ValueOf(r).FieldByName("acceptOAuth2LoginRequest").UnsafePointer()) + if acceptReq.Subject != tt.identityID { + t.Fatalf("expected identityID to be %s, got %s", tt.identityID, acceptReq.Subject) + } - ctx := context.Background() - loginChallenge := "123456" - acceptLoginRequest := hClient.OAuth2APIAcceptOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, - } - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + if tt.validateTimestamp { + leeway := int64(2) + if 300-acceptReq.GetRememberFor() > leeway { + t.Fatalf("expected RememberFor to be close to 300, got %v", acceptReq.GetRememberFor()) + } + } - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().AcceptOAuth2LoginRequest(ctx).Times(1).Return(acceptLoginRequest) - mockHydraOauthApi.EXPECT().AcceptOAuth2LoginRequestExecute(gomock.Any()).Times(1).Return(nil, nil, fmt.Errorf("error")) + if acceptReq.GetIdentityProviderSessionId() != session.GetId() { + t.Fatalf("expected session ID to be %s, got %s", session.GetId(), acceptReq.GetIdentityProviderSessionId()) + } - rt, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).AcceptLoginRequest(ctx, session, loginChallenge) + return redirectTo, resp, nil + }, + ) - if rt != nil { - t.Fatalf("expected redirect to be %v not %v", nil, rt) - } - if c != nil { - t.Fatalf("expected cookies to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") + rt, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).AcceptLoginRequest(ctx, session, tt.loginChallenge) + + if tt.expectError { + if err == nil { + t.Fatal("expected error but got nil") + } + if rt != nil || c != nil { + t.Fatal("expected nil response on error") + } + } else { + if err != nil { + t.Fatalf("expected no error but got %v", err) + } + if rt == nil { + t.Fatal("expected redirect but got nil") + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to match") + } + } + }) } } @@ -359,279 +384,805 @@ func TestAcceptLoginRequestWithPopForWebAuthn(t *testing.T) { } } -func TestGetLoginRequestSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) - - ctx := context.Background() - loginChallenge := "123456" - getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, +func TestGetLoginRequest(t *testing.T) { + tests := []struct { + name string + loginChallenge string + mockError error + expectError bool + validateRequest bool + }{ + { + name: "success with valid login challenge", + loginChallenge: "123456", + mockError: nil, + expectError: false, + validateRequest: true, + }, + { + name: "failure on API error", + loginChallenge: "123456", + mockError: fmt.Errorf("error"), + expectError: true, + validateRequest: false, + }, } - lr := hClient.NewOAuth2LoginRequestWithDefaults() - resp := new(http.Response) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockHydraOauthApi := NewMockOAuth2API(ctrl) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( - func(r hClient.OAuth2APIGetOAuth2LoginRequestRequest) (*hClient.OAuth2LoginRequest, *http.Response, error) { - if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); *lc != loginChallenge { - t.Fatalf("expected loginChallenge to be %s, got %s", loginChallenge, *lc) + ctx := context.Background() + getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ + ApiService: mockHydraOauthApi, } - return lr, resp, nil - }, - ) + lr := hClient.NewOAuth2LoginRequestWithDefaults() + resp := new(http.Response) + + mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( + func(r hClient.OAuth2APIGetOAuth2LoginRequestRequest) (*hClient.OAuth2LoginRequest, *http.Response, error) { + if tt.mockError != nil { + return nil, nil, tt.mockError + } - ret, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetLoginRequest(ctx, loginChallenge) + if tt.validateRequest { + lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()) + if *lc != tt.loginChallenge { + t.Fatalf("expected loginChallenge to be %s, got %s", tt.loginChallenge, *lc) + } + } + return lr, resp, nil + }, + ) - if ret != lr { - t.Fatalf("expected response to be %v not %v", lr, ret) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + ret, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetLoginRequest(ctx, tt.loginChallenge) + + if tt.expectError { + if err == nil { + t.Fatal("expected error but got nil") + } + if ret != nil || c != nil { + t.Fatal("expected nil response on error") + } + } else { + if err != nil { + t.Fatalf("expected no error but got %v", err) + } + if ret != lr { + t.Fatalf("expected response to be %v but got %v", lr, ret) + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to match") + } + } + }) } } -func TestGetLoginRequestFails(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestMustReAuthenticate(t *testing.T) { + tests := []struct { + name string + loginChallenge string + session *kClient.Session + state FlowStateCookie + hydraSkip bool + mockError error + expectResult bool + expectError bool + needsHydraCall bool + }{ + { + name: "skip with totp setup", + loginChallenge: "123456", + session: func() *kClient.Session { + s := kClient.NewSession("test") + s.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + return s + }(), + state: FlowStateCookie{LoginChallengeHash: "1234", TotpSetup: false}, + hydraSkip: true, + mockError: nil, + expectResult: false, + expectError: false, + needsHydraCall: true, + }, + { + name: "skip with backup code used", + loginChallenge: "123456", + session: func() *kClient.Session { + s := kClient.NewSession("test") + s.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + return s + }(), + state: FlowStateCookie{LoginChallengeHash: "1234", BackupCodeUsed: true}, + hydraSkip: true, + mockError: nil, + expectResult: false, + expectError: false, + needsHydraCall: true, + }, + { + name: "no skip required", + loginChallenge: "123456", + session: func() *kClient.Session { + s := kClient.NewSession("test") + s.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + return s + }(), + state: FlowStateCookie{LoginChallengeHash: "1234", TotpSetup: false}, + hydraSkip: false, + mockError: nil, + expectResult: true, + expectError: false, + needsHydraCall: true, + }, + { + name: "no login challenge", + loginChallenge: "", + session: func() *kClient.Session { + s := kClient.NewSession("test") + s.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + return s + }(), + state: FlowStateCookie{}, + hydraSkip: false, + mockError: nil, + expectResult: true, + expectError: false, + needsHydraCall: false, + }, + { + name: "no session", + loginChallenge: "123456", + session: nil, + state: FlowStateCookie{}, + hydraSkip: false, + mockError: nil, + expectResult: true, + expectError: false, + needsHydraCall: false, + }, + { + name: "API error", + loginChallenge: "123456", + session: func() *kClient.Session { + s := kClient.NewSession("test") + s.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + return s + }(), + state: FlowStateCookie{}, + hydraSkip: false, + mockError: fmt.Errorf("error"), + expectResult: true, + expectError: true, + needsHydraCall: true, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - ctx := context.Background() - loginChallenge := "123456" - getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, - } + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockHydraOauthApi := NewMockOAuth2API(ctrl) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).Return(nil, nil, fmt.Errorf("error")) + ctx := context.Background() - ret, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetLoginRequest(ctx, loginChallenge) + mockTracer.EXPECT().Start(ctx, "kratos.Service.MustReAuthenticate").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + + if tt.needsHydraCall { + sessionId := "1234" + getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ + ApiService: mockHydraOauthApi, + } + lr := hClient.NewOAuth2LoginRequestWithDefaults() + lr.Skip = tt.hydraSkip + lr.SessionId = &sessionId + resp := new(http.Response) + + mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( + func(r hClient.OAuth2APIGetOAuth2LoginRequestRequest) (*hClient.OAuth2LoginRequest, *http.Response, error) { + if tt.mockError != nil { + return nil, nil, tt.mockError + } + lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()) + if *lc != tt.loginChallenge { + t.Fatalf("expected loginChallenge to be %s, got %s", tt.loginChallenge, *lc) + } + return lr, resp, nil + }, + ) + } - if ret != nil { - t.Fatalf("expected redirect to be %v not %v", nil, ret) - } - if c != nil { - t.Fatalf("expected cookies to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") + ret, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger). + MustReAuthenticate(ctx, tt.loginChallenge, tt.session, tt.state) + + if tt.expectError { + if err == nil { + t.Fatal("expected error but got nil") + } + } else { + if err != nil { + t.Fatalf("expected no error but got %v", err) + } + } + + if ret != tt.expectResult { + t.Fatalf("expected result to be %v but got %v", tt.expectResult, ret) + } + }) } } -func TestMustReAuthenticateSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) - - ctx := context.Background() - loginChallenge := "123456" - sessionId := "1234" - getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, +func TestCreateBrowserLoginFlow(t *testing.T) { + tests := []struct { + name string + aal string + returnTo string + loginChallenge string + refresh bool + oidcWebAuthnSequencingEnabled bool + shouldHydrate bool + shouldFail bool + expectNil bool + expectLoginChallengeNil bool + }{ + { + name: "With login challenge success", + aal: "aal", + returnTo: "https://return/to/somewhere", + loginChallenge: "123456", + refresh: false, + oidcWebAuthnSequencingEnabled: false, + shouldHydrate: true, + shouldFail: false, + expectNil: false, + expectLoginChallengeNil: false, + }, + { + name: "With return to success", + aal: "aal", + returnTo: "https://return/to/somewhere", + loginChallenge: "", + refresh: false, + oidcWebAuthnSequencingEnabled: false, + shouldHydrate: true, + shouldFail: false, + expectNil: false, + expectLoginChallengeNil: false, + }, + { + name: "With sequencing and login challenge", + aal: "aal", + returnTo: "https://return/to/somewhere", + loginChallenge: "123456", + refresh: false, + oidcWebAuthnSequencingEnabled: true, + shouldHydrate: true, + shouldFail: false, + expectNil: false, + expectLoginChallengeNil: true, + }, + { + name: "Without return to and login challenge", + aal: "aal", + returnTo: "", + loginChallenge: "", + refresh: false, + oidcWebAuthnSequencingEnabled: false, + shouldHydrate: false, + shouldFail: false, + expectNil: true, + expectLoginChallengeNil: false, + }, + { + name: "Fail case", + aal: "aal", + returnTo: "https://return/to/somewhere", + loginChallenge: "123456", + refresh: false, + oidcWebAuthnSequencingEnabled: false, + shouldHydrate: false, + shouldFail: true, + expectNil: true, + expectLoginChallengeNil: false, + }, } - lr := hClient.NewOAuth2LoginRequestWithDefaults() - lr.Skip = true - lr.SessionId = &sessionId - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) - state := FlowStateCookie{LoginChallengeHash: sessionId, TotpSetup: false} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - resp := new(http.Response) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + flow := kClient.NewLoginFlowWithDefaults() + request := kClient.FrontendAPICreateBrowserLoginFlowRequest{ + ApiService: mockKratosFrontendApi, + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } - mockTracer.EXPECT().Start(ctx, "kratos.Service.MustReAuthenticate").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( - func(r hClient.OAuth2APIGetOAuth2LoginRequestRequest) (*hClient.OAuth2LoginRequest, *http.Response, error) { - if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); *lc != loginChallenge { - t.Fatalf("expected loginChallenge to be %s, got %s", loginChallenge, *lc) + mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + if tt.shouldHydrate { + mockTracer.EXPECT().Start(ctx, "kratos.Service.hydrateKratosLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) } - return lr, resp, nil - }, - ) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlow(ctx).Times(1).Return(request) + + if tt.shouldFail || !tt.expectNil { + mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPICreateBrowserLoginFlowRequest) (*kClient.LoginFlow, *http.Response, error) { + if _aal := (*string)(reflect.ValueOf(r).FieldByName("aal").UnsafePointer()); *_aal != tt.aal { + t.Fatalf("expected aal to be %s, got %s", tt.aal, *_aal) + } + if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != tt.returnTo { + t.Fatalf("expected returnTo to be %s, got %s", tt.returnTo, *rt) + } + if tt.expectLoginChallengeNil { + if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); lc != nil { + t.Fatalf("expected loginChallenge to be nil, got %s", *lc) + } + } else if tt.loginChallenge != "" { + if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); *lc != tt.loginChallenge { + t.Fatalf("expected loginChallenge to be %s, got %s", tt.loginChallenge, *lc) + } + } + if ref := (*bool)(reflect.ValueOf(r).FieldByName("refresh").UnsafePointer()); *ref != tt.refresh { + t.Fatalf("expected refresh to be %v, got %v", tt.refresh, *ref) + } + if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { + t.Fatalf("expected cookie string as test=test, got %s", *cookie) + } - ret, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger). - MustReAuthenticate(ctx, loginChallenge, session, state) + if tt.shouldFail { + return nil, &resp, fmt.Errorf("error") + } + return flow, &resp, nil + }, + ) + } - if ret != false { - t.Fatalf("expected returned value to be `false` not %v", ret) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, tt.oidcWebAuthnSequencingEnabled, mockTracer, mockMonitor, mockLogger).CreateBrowserLoginFlow(ctx, tt.aal, tt.returnTo, tt.loginChallenge, tt.refresh, cookies) + + if tt.expectNil { + if f != nil { + t.Fatalf("expected flow to be %v not %v", nil, f) + } + if c != nil { + t.Fatalf("expected cookies to be %v not %v", nil, c) + } + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if f != flow { + t.Fatalf("expected flow to be %v not %v", flow, f) + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + } + if tt.shouldFail { + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + } + }) } } -func TestMustReAuthenticateBackupCodeUsed(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) - - ctx := context.Background() - loginChallenge := "123456" - sessionId := "1234" - getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, +func TestGetLoginFlow(t *testing.T) { + tests := []struct { + name string + id string + shouldFail bool + expectNil bool + }{ + { + name: "Success", + id: "id", + shouldFail: false, + expectNil: false, + }, + { + name: "Fail", + id: "id", + shouldFail: true, + expectNil: true, + }, } - lr := hClient.NewOAuth2LoginRequestWithDefaults() - lr.Skip = true - lr.SessionId = &sessionId - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) - state := FlowStateCookie{LoginChallengeHash: sessionId, BackupCodeUsed: true} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - resp := new(http.Response) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + flow := kClient.NewLoginFlowWithDefaults() + request := kClient.FrontendAPIGetLoginFlowRequest{ + ApiService: mockKratosFrontendApi, + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } - mockTracer.EXPECT().Start(ctx, "kratos.Service.MustReAuthenticate").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( - func(r hClient.OAuth2APIGetOAuth2LoginRequestRequest) (*hClient.OAuth2LoginRequest, *http.Response, error) { - if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); *lc != loginChallenge { - t.Fatalf("expected loginChallenge to be %s, got %s", loginChallenge, *lc) + mockTracer.EXPECT().Start(ctx, "kratos.Service.GetLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + if !tt.shouldFail { + mockTracer.EXPECT().Start(ctx, "kratos.Service.hydrateKratosLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) } - return lr, resp, nil - }, - ) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().GetLoginFlow(ctx).Times(1).Return(request) + mockKratosFrontendApi.EXPECT().GetLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIGetLoginFlowRequest) (*kClient.LoginFlow, *http.Response, error) { + if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != tt.id { + t.Fatalf("expected id to be %s, got %s", tt.id, *_id) + } + if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { + t.Fatalf("expected cookie string as test=test, got %s", *cookie) + } - ret, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger). - MustReAuthenticate(ctx, loginChallenge, session, state) + if tt.shouldFail { + return nil, &resp, fmt.Errorf("error") + } + return flow, &resp, nil + }, + ) - if ret != false { - t.Fatalf("expected returned value to be `false` not %v", ret) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetLoginFlow(ctx, tt.id, cookies) + + if tt.expectNil { + if f != nil { + t.Fatalf("expected flow to be %v not %v", nil, f) + } + if c != nil { + t.Fatalf("expected header to be %v not %v", nil, c) + } + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if f != flow { + t.Fatalf("expected flow to be %v not %v", flow, f) + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } -func TestMustReAuthenticateNoSkip(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestUpdateIdentifierFirstLoginFlow(t *testing.T) { + tests := []struct { + name string + csrfToken *string + identifier string + responseStatus int + redirectTo string + expectErr bool + expectedErrText string + expectLog bool + skipExecute bool + }{ + { + name: "Success", + csrfToken: stringPtr("csrf_token_1234"), + identifier: "test@example.com", + responseStatus: http.StatusSeeOther, + redirectTo: "https://redirect/to/path", + expectErr: false, + }, + { + name: "Missing CSRF token", + csrfToken: nil, + identifier: "test@example.com", + expectErr: true, + expectedErrText: "missing csrf token", + skipExecute: true, + }, + { + name: "Status bad request", + csrfToken: stringPtr("csrf_token_1234"), + identifier: "test@example.com", + responseStatus: http.StatusBadRequest, + expectErr: true, + expectLog: true, + }, + { + name: "Unexpected status", + csrfToken: stringPtr("csrf_token_1234"), + identifier: "test@example.com", + responseStatus: http.StatusGone, + expectErr: true, + expectedErrText: "unexpected status: 410", + expectLog: true, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - ctx := context.Background() - loginChallenge := "123456" - sessionId := "1234" - getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, - } - lr := hClient.NewOAuth2LoginRequestWithDefaults() - lr.Skip = false - lr.SessionId = &sessionId - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - state := FlowStateCookie{LoginChallengeHash: sessionId, TotpSetup: false} + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + flowId := "flow" + + body := kClient.UpdateLoginFlowWithIdentifierFirstMethod{ + CsrfToken: tt.csrfToken, + Identifier: tt.identifier, + } - resp := new(http.Response) + mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateIdentifierFirstLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + + if !tt.skipExecute { + resp := &http.Response{ + StatusCode: tt.responseStatus, + Body: io.NopCloser(strings.NewReader("")), + } + if tt.responseStatus == http.StatusSeeOther { + resp.Header = http.Header{ + "Location": []string{tt.redirectTo}, + "Set-Cookie": []string{cookie.String()}, + } + } - mockTracer.EXPECT().Start(ctx, "kratos.Service.MustReAuthenticate").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).DoAndReturn( - func(r hClient.OAuth2APIGetOAuth2LoginRequestRequest) (*hClient.OAuth2LoginRequest, *http.Response, error) { - if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); *lc != loginChallenge { - t.Fatalf("expected loginChallenge to be %s, got %s", loginChallenge, *lc) + mockKratos.EXPECT(). + ExecuteIdentifierFirstUpdateLoginRequest(ctx, flowId, *tt.csrfToken, tt.identifier, cookies). + Return(resp, nil). + Times(1) } - return lr, resp, nil - }, - ) - ret, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger). - MustReAuthenticate(ctx, loginChallenge, session, state) + if tt.expectLog { + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + } - if ret != true { - t.Fatalf("expected returned value to be `true` not %v", ret) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + r, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateIdentifierFirstLoginFlow(ctx, flowId, body, cookies) + + if tt.expectErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + if tt.expectedErrText != "" && !strings.Contains(err.Error(), tt.expectedErrText) { + t.Fatalf("expected %s error, got %v", tt.expectedErrText, err) + } + } else { + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + if *r.RedirectTo != tt.redirectTo { + t.Fatalf("expected redirect URL %s, got %s", tt.redirectTo, *r.RedirectTo) + } + if len(c) != len(cookies) { + t.Fatalf("expected %d cookies, got %d", len(cookies), len(c)) + } + } + }) } } -func TestMustReAuthenticateNoLoginChallenge(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func stringPtr(s string) *string { + return &s +} - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) +func TestUpdateLoginFlow(t *testing.T) { + tests := []struct { + name string + errorMessageId int64 + expectedError string + statusCode int + expectSuccess bool + expectLog bool + expectParseTracer bool + }{ + { + name: "Success", + statusCode: http.StatusUnprocessableEntity, + expectSuccess: true, + expectParseTracer: true, + }, + { + name: "Error WebAuthn not set", + errorMessageId: MissingSecurityKeySetup, + expectedError: "choose a different login method", + statusCode: 400, + expectSuccess: false, + }, + { + name: "Error backup codes not set", + errorMessageId: MissingBackupCodesSetup, + expectedError: "login with backup codes unavailable", + statusCode: 400, + expectSuccess: false, + }, + { + name: "Fail generic", + statusCode: 200, + expectSuccess: false, + expectLog: true, + }, + } - ctx := context.Background() - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockTracer.EXPECT().Start(ctx, "kratos.Service.MustReAuthenticate").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - ret, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger). - MustReAuthenticate(ctx, "", session, FlowStateCookie{}) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + flowId := "flow" + body := new(kClient.UpdateLoginFlowBody) + request := kClient.FrontendAPIUpdateLoginFlowRequest{ + ApiService: mockKratosFrontendApi, + } - if ret != true { - t.Fatalf("expected returned value to be `true` not %v", ret) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + if tt.expectParseTracer { + mockTracer.EXPECT().Start(ctx, "kratos.Service.parseKratosRedirectResponse").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + } + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) + + if tt.expectSuccess { + _redirectTo := "https://redirect/to/path" + flow := ErrorBrowserLocationChangeRequired{ + RedirectBrowserTo: &_redirectTo, + } + flowJson, _ := json.Marshal(flow) + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + Body: io.NopCloser(bytes.NewBuffer(flowJson)), + StatusCode: tt.statusCode, + } + mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIUpdateLoginFlowRequest) (*ErrorBrowserLocationChangeRequired, *http.Response, error) { + if _flow := (*string)(reflect.ValueOf(r).FieldByName("flow").UnsafePointer()); *_flow != flowId { + t.Fatalf("expected id to be %s, got %s", flowId, *_flow) + } + if _body := (*kClient.UpdateLoginFlowBody)(reflect.ValueOf(r).FieldByName("updateLoginFlowBody").UnsafePointer()); *_body != *body { + t.Fatalf("expected id to be %v, got %v", *body, *_body) + } + if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { + t.Fatalf("expected cookie string as test=test, got %s", *cookie) + } + return &flow, &resp, nil + }, + ) + } else { + var respBody []byte + if tt.errorMessageId != 0 { + errorBody := &UiErrorMessages{ + Ui: kClient.UiContainer{ + Messages: []kClient.UiText{{Id: tt.errorMessageId}}, + }, + } + respBody, _ = json.Marshal(errorBody) + } else { + _redirectTo := "https://redirect/to/path" + flow := ErrorBrowserLocationChangeRequired{ + RedirectBrowserTo: &_redirectTo, + } + respBody, _ = json.Marshal(flow) + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + Body: io.NopCloser(bytes.NewBuffer(respBody)), + StatusCode: tt.statusCode, + } + mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) + } + + if tt.expectLog { + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + } + + r, _, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, *body, cookies) + + if tt.expectSuccess { + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + if *r.RedirectTo != "https://redirect/to/path" { + t.Fatalf("expected redirectTo to be https://redirect/to/path not %s", *r.RedirectTo) + } + if c == nil { + t.Fatalf("expected cookies not to be nil") + } + } else { + if err == nil { + t.Fatalf("expected error not nil") + } + if tt.expectedError != "" && err.Error() != tt.expectedError { + t.Fatalf("expected error to be %s not %v", tt.expectedError, err) + } + if r != nil { + t.Fatalf("expected flow to be %v not %+v", nil, r) + } + if c != nil { + t.Fatalf("expected header to be %v not %v", nil, c) + } + } + }) } } -func TestMustReAuthenticateNoSession(t *testing.T) { +func TestUpdateLoginFlowSuccessNative(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -642,24 +1193,49 @@ func TestMustReAuthenticateNoSession(t *testing.T) { mockAuthz := NewMockAuthorizerInterface(ctrl) mockTracer := NewMockTracingInterface(ctrl) mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) ctx := context.Background() - loginChallenge := "123456" + flowId := "flow" + body := kClient.UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(kClient.NewUpdateLoginFlowWithPasswordMethodWithDefaults()) + request := kClient.FrontendAPIUpdateLoginFlowRequest{ApiService: mockKratosFrontendApi} + respCookie := &http.Cookie{Name: "test", Value: "test"} + resp := http.Response{Header: http.Header{"Set-Cookie": []string{respCookie.String()}}, StatusCode: http.StatusOK} + login := kClient.NewSuccessfulNativeLogin(*kClient.NewSession("session-id")) + login.ContinueWith = []kClient.ContinueWith{{}} - mockTracer.EXPECT().Start(ctx, "kratos.Service.MustReAuthenticate").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) + mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIUpdateLoginFlowRequest) (*kClient.SuccessfulNativeLogin, *http.Response, error) { + if _flow := (*string)(reflect.ValueOf(r).FieldByName("flow").UnsafePointer()); *_flow != flowId { + t.Fatalf("expected id to be %s, got %s", flowId, *_flow) + } + return login, &resp, nil + }, + ) - ret, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger). - MustReAuthenticate(ctx, loginChallenge, nil, FlowStateCookie{}) + redirect, nativeLogin, cookies, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, body, []*http.Cookie{}) - if ret != true { - t.Fatalf("expected response to be `true` not %v", ret) - } if err != nil { - t.Fatalf("expected error to be nil not %v", err) + t.Fatalf("expected error to be nil not %v", err) + } + if redirect != nil { + t.Fatalf("expected redirect to be nil not %v", redirect) + } + if nativeLogin == nil { + t.Fatalf("expected native login not nil") + } + if nativeLogin.ContinueWith != nil { + t.Fatalf("expected continue_with to be nil") + } + if len(cookies) != 1 { + t.Fatalf("expected 1 cookie, got %d", len(cookies)) } } -func TestMustReAuthenticateFails(t *testing.T) { +func TestUpdateLoginFlowOidcAddsSessionUnsetCookie(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -670,34 +1246,50 @@ func TestMustReAuthenticateFails(t *testing.T) { mockAuthz := NewMockAuthorizerInterface(ctrl) mockTracer := NewMockTracingInterface(ctrl) mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockHydraOauthApi := NewMockOAuth2API(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) ctx := context.Background() - loginChallenge := "123456" - getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ - ApiService: mockHydraOauthApi, - } - session := kClient.NewSession("test") - session.Identity = kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}) + flowId := "flow" + oidcBody := kClient.NewUpdateLoginFlowWithOidcMethod("oidc", "google") + body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(oidcBody) + request := kClient.FrontendAPIUpdateLoginFlowRequest{ApiService: mockKratosFrontendApi} + respCookie := &http.Cookie{Name: "test", Value: "test"} + resp := http.Response{Header: http.Header{"Set-Cookie": []string{respCookie.String()}}, StatusCode: http.StatusOK} + login := kClient.NewSuccessfulNativeLogin(*kClient.NewSession("session-id")) - mockTracer.EXPECT().Start(ctx, "kratos.Service.MustReAuthenticate").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) - mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).Return(nil, nil, fmt.Errorf("error")) + mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) + mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).Return(login, &resp, nil) - ret, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger). - MustReAuthenticate(ctx, loginChallenge, session, FlowStateCookie{}) + redirect, nativeLogin, cookies, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, body, []*http.Cookie{}) - if ret != true { - t.Fatalf("expected returned value to be `true` not %v", ret) + if err != nil { + t.Fatalf("expected error to be nil not %v", err) } - if err == nil { - t.Fatalf("expected error not nil") + if redirect != nil { + t.Fatalf("expected redirect to be nil not %v", redirect) + } + if nativeLogin == nil { + t.Fatalf("expected native login not nil") + } + if len(cookies) != 2 { + t.Fatalf("expected 2 cookies, got %d", len(cookies)) + } + unset := kratosSessionUnsetCookie() + found := false + for _, c := range cookies { + if c.Name == unset.Name { + found = true + break + } + } + if !found { + t.Fatalf("expected session unset cookie to be added") } } -func TestCreateBrowserLoginFlowWithLoginChallengeSuccess(t *testing.T) { +func TestUpdateLoginFlowParseRedirectError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -711,756 +1303,60 @@ func TestCreateBrowserLoginFlowWithLoginChallengeSuccess(t *testing.T) { mockKratosFrontendApi := NewMockFrontendAPI(ctrl) ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - aal := "aal" - returnTo := "https://return/to/somewhere" - loginChallenge := "123456" - refresh := false - flow := kClient.NewLoginFlowWithDefaults() - request := kClient.FrontendAPICreateBrowserLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, "kratos.Service.hydrateKratosLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPICreateBrowserLoginFlowRequest) (*kClient.LoginFlow, *http.Response, error) { - if _aal := (*string)(reflect.ValueOf(r).FieldByName("aal").UnsafePointer()); *_aal != aal { - t.Fatalf("expected aal to be %s, got %s", aal, *_aal) - } - if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != returnTo { - t.Fatalf("expected returnTo to be %s, got %s", returnTo, *rt) - } - if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); *lc != loginChallenge { - t.Fatalf("expected loginChallenge to be %s, got %s", loginChallenge, *lc) - } - if ref := (*bool)(reflect.ValueOf(r).FieldByName("refresh").UnsafePointer()); *ref != refresh { - t.Fatalf("expected refresh to be %v, got %v", refresh, *ref) - } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) - } + flowId := "flow" + body := kClient.UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(kClient.NewUpdateLoginFlowWithPasswordMethodWithDefaults()) + request := kClient.FrontendAPIUpdateLoginFlowRequest{ApiService: mockKratosFrontendApi} + resp := http.Response{Body: io.NopCloser(strings.NewReader("not-json")), StatusCode: http.StatusUnprocessableEntity} - return flow, &resp, nil - }, - ) + mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockTracer.EXPECT().Start(ctx, "kratos.Service.parseKratosRedirectResponse").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) + mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, nil) - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserLoginFlow(ctx, aal, returnTo, loginChallenge, refresh, cookies) + redirect, nativeLogin, cookies, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, body, []*http.Cookie{}) - if f != flow { - t.Fatalf("expected flow to be %v not %v", flow, f) + if err == nil { + t.Fatalf("expected error not nil") } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + if redirect != nil { + t.Fatalf("expected redirect to be nil not %v", redirect) } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + if nativeLogin != nil { + t.Fatalf("expected native login to be nil") + } + if cookies != nil { + t.Fatalf("expected cookies to be nil") } } -func TestCreateBrowserLoginFlowWithReturnToSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - aal := "aal" - returnTo := "https://return/to/somewhere" - refresh := false - flow := kClient.NewLoginFlowWithDefaults() - request := kClient.FrontendAPICreateBrowserLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, "kratos.Service.hydrateKratosLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPICreateBrowserLoginFlowRequest) (*kClient.LoginFlow, *http.Response, error) { - if _aal := (*string)(reflect.ValueOf(r).FieldByName("aal").UnsafePointer()); *_aal != aal { - t.Fatalf("expected aal to be %s, got %s", aal, *_aal) - } - if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != returnTo { - t.Fatalf("expected returnTo to be %s, got %s", returnTo, *rt) - } - if ref := (*bool)(reflect.ValueOf(r).FieldByName("refresh").UnsafePointer()); *ref != refresh { - t.Fatalf("expected refresh to be %v, got %v", refresh, *ref) - } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) - } - - return flow, &resp, nil +func TestGetUiError(t *testing.T) { + tests := []struct { + name string + messages []kClient.UiText + expectErr string + expectLog bool + }{ + { + name: "incorrect credentials", + messages: []kClient.UiText{{Id: IncorrectCredentials}}, + expectErr: "incorrect username or password", }, - ) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserLoginFlow(ctx, aal, returnTo, "", refresh, cookies) - - if f != flow { - t.Fatalf("expected flow to be %v not %v", flow, f) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestCreateBrowserLoginFlowWithSequencingAndLoginChallenge(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - aal := "aal" - returnTo := "https://return/to/somewhere" - loginChallenge := "123456" - refresh := false - flow := kClient.NewLoginFlowWithDefaults() - request := kClient.FrontendAPICreateBrowserLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, "kratos.Service.hydrateKratosLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPICreateBrowserLoginFlowRequest) (*kClient.LoginFlow, *http.Response, error) { - if _aal := (*string)(reflect.ValueOf(r).FieldByName("aal").UnsafePointer()); *_aal != aal { - t.Fatalf("expected aal to be %s, got %s", aal, *_aal) - } - if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != returnTo { - t.Fatalf("expected returnTo to be %s, got %s", returnTo, *rt) - } - if lc := (*string)(reflect.ValueOf(r).FieldByName("loginChallenge").UnsafePointer()); lc != nil { - t.Fatalf("expected loginChallenge to be nil, got %s", *lc) - } - if ref := (*bool)(reflect.ValueOf(r).FieldByName("refresh").UnsafePointer()); *ref != refresh { - t.Fatalf("expected refresh to be %v, got %v", refresh, *ref) - } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) - } - - return flow, &resp, nil + { + name: "incorrect account identifier", + messages: []kClient.UiText{{Id: IncorrectAccountIdentifier}}, + expectErr: "account does not exist or has no login method configured", }, - ) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, true, mockTracer, mockMonitor, mockLogger).CreateBrowserLoginFlow(ctx, aal, returnTo, loginChallenge, refresh, cookies) - - if f != flow { - t.Fatalf("expected flow to be %v not %v", flow, f) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestCreateBrowserLoginFlowWithoutReturnToLoginChallenge(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - aal := "aal" - refresh := false - request := kClient.FrontendAPICreateBrowserLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlow(ctx).Times(1).Return(request) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserLoginFlow(ctx, aal, "", "", refresh, cookies) - - if f != nil { - t.Fatalf("expected flow to be %v not %v", nil, f) - } - if c != nil { - t.Fatalf("expected cookies to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error to be nil") - } -} - -func TestCreateBrowserLoginFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - aal := "aal" - returnTo := "https://return/to/somewhere" - loginChallenge := "123456" - refresh := false - request := kClient.FrontendAPICreateBrowserLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().CreateBrowserLoginFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserLoginFlow(ctx, aal, returnTo, loginChallenge, refresh, cookies) - - if f != nil { - t.Fatalf("expected flow to be %v not %v", nil, f) - } - if c != nil { - t.Fatalf("expected cookies to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") - } -} - -func TestGetLoginFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - id := "id" - flow := kClient.NewLoginFlowWithDefaults() - request := kClient.FrontendAPIGetLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, "kratos.Service.hydrateKratosLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIGetLoginFlowRequest) (*kClient.LoginFlow, *http.Response, error) { - if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { - t.Fatalf("expected id to be %s, got %s", id, *_id) - } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) - } - - return flow, &resp, nil + { + name: "inactive account", + messages: []kClient.UiText{{Id: InactiveAccount}}, + expectErr: "inactive account", }, - ) - - s, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetLoginFlow(ctx, id, cookies) - - if s != flow { - t.Fatalf("expected flow to be %v not %v", flow, s) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestGetLoginFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - id := "id" - request := kClient.FrontendAPIGetLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetLoginFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetLoginFlow(ctx, id, cookies) - - if f != nil { - t.Fatalf("expected flow to be %v not %v", nil, f) - } - if c != nil { - t.Fatalf("expected header to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") - } -} - -func TestUpdateIdentifierFirstLoginFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - redirectTo := "https://redirect/to/path" - - csrfToken := "csrf_token_1234" - identifier := "test@example.com" - body := kClient.UpdateLoginFlowWithIdentifierFirstMethod{ - CsrfToken: &csrfToken, - Identifier: identifier, - } - - resp := &http.Response{ - StatusCode: http.StatusSeeOther, - Header: http.Header{ - "Location": []string{redirectTo}, - "Set-Cookie": []string{cookie.String()}, - }, - Body: io.NopCloser(strings.NewReader("")), - } - - mockKratos.EXPECT(). - ExecuteIdentifierFirstUpdateLoginRequest(ctx, flowId, csrfToken, identifier, cookies). - Return(resp, nil). - Times(1) - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateIdentifierFirstLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - - r, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateIdentifierFirstLoginFlow(ctx, flowId, body, cookies) - - if *r.RedirectTo != redirectTo { - t.Fatalf("expected redirect URL %s, got %s", redirectTo, *r.RedirectTo) - } - if len(c) != len(cookies) { - t.Fatalf("expected %d cookies, got %d", len(cookies), len(c)) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestUpdateIdentifierFirstLoginFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - identifier := "test@example.com" - body := kClient.UpdateLoginFlowWithIdentifierFirstMethod{ - Identifier: identifier, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateIdentifierFirstLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - - _, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateIdentifierFirstLoginFlow(ctx, flowId, body, cookies) - - expectedErr := "missing csrf token" - if err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Fatalf("expected %s error, got %v", expectedErr, err) - } -} - -func TestUpdateIdentifierFirstLoginFlowFailStatusBadRequest(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - csrfToken := "csrf_token_1234" - identifier := "test@example.com" - body := kClient.UpdateLoginFlowWithIdentifierFirstMethod{ - CsrfToken: &csrfToken, - Identifier: identifier, - } - - resp := &http.Response{ - StatusCode: http.StatusBadRequest, - Body: io.NopCloser(strings.NewReader("")), - } - - mockKratos.EXPECT(). - ExecuteIdentifierFirstUpdateLoginRequest(ctx, flowId, csrfToken, identifier, cookies). - Return(resp, nil). - Times(1) - - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateIdentifierFirstLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - _, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateIdentifierFirstLoginFlow(ctx, flowId, body, cookies) - - if err == nil { - t.Fatalf("expected error, got nil") - } -} - -func TestUpdateIdentifierFirstLoginFlowFailUnexpectedStatus(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - csrfToken := "csrf_token_1234" - identifier := "test@example.com" - body := kClient.UpdateLoginFlowWithIdentifierFirstMethod{ - CsrfToken: &csrfToken, - Identifier: identifier, - } - - resp := &http.Response{ - StatusCode: http.StatusGone, - Body: io.NopCloser(strings.NewReader("")), - } - - mockKratos.EXPECT(). - ExecuteIdentifierFirstUpdateLoginRequest(ctx, flowId, csrfToken, identifier, cookies). - Return(resp, nil). - Times(1) - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateIdentifierFirstLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) - - _, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateIdentifierFirstLoginFlow(ctx, flowId, body, cookies) - - expectedErr := "unexpected status: 410" - if err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Fatalf("expected %s error, got %v", expectedErr, err) - } -} - -func TestUpdateLoginFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - _redirectTo := "https://redirect/to/path" - flow := ErrorBrowserLocationChangeRequired{ - RedirectBrowserTo: &_redirectTo, - } - flowJson, _ := json.Marshal(flow) - body := new(kClient.UpdateLoginFlowBody) - request := kClient.FrontendAPIUpdateLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - Body: io.NopCloser(bytes.NewBuffer(flowJson)), - StatusCode: http.StatusUnprocessableEntity, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, "kratos.Service.parseKratosRedirectResponse").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIUpdateLoginFlowRequest) (*ErrorBrowserLocationChangeRequired, *http.Response, error) { - if _flow := (*string)(reflect.ValueOf(r).FieldByName("flow").UnsafePointer()); *_flow != flowId { - t.Fatalf("expected id to be %s, got %s", flowId, *_flow) - } - if _body := (*kClient.UpdateLoginFlowBody)(reflect.ValueOf(r).FieldByName("updateLoginFlowBody").UnsafePointer()); *_body != *body { - t.Fatalf("expected id to be %v, got %v", *body, *_body) - } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) - } - - return &flow, &resp, nil - }, - ) - - r, _, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, *body, cookies) - - if *r.RedirectTo != *flow.RedirectBrowserTo { - t.Fatalf("expected redirectTo to be %s not %s", *flow.RedirectBrowserTo, *r.RedirectTo) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestUpdateLoginFlowErrorWebAuthnNotSet(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - body := new(kClient.UpdateLoginFlowBody) - - request := kClient.FrontendAPIUpdateLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - errorBody := &UiErrorMessages{ - Ui: kClient.UiContainer{ - Messages: []kClient.UiText{ - { - Id: MissingSecurityKeySetup, - }, - }, - }, - } - errorBodyJson, _ := json.Marshal(errorBody) - resp := http.Response{ - Body: io.NopCloser(bytes.NewBuffer(errorBodyJson)), - StatusCode: 400, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - - _, _, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, *body, cookies) - - if err == nil { - t.Fatalf("expected error not nil") - } - expectedError := fmt.Errorf("choose a different login method") - if err.Error() != expectedError.Error() { - t.Fatalf("expected error to be %v not %v", expectedError, err) - } -} - -func TestUpdateLoginFlowErrorWhenBackupCodesNotSet(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - body := new(kClient.UpdateLoginFlowBody) - - request := kClient.FrontendAPIUpdateLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - errorBody := &UiErrorMessages{ - Ui: kClient.UiContainer{ - Messages: []kClient.UiText{ - { - Id: MissingBackupCodesSetup, - }, - }, - }, - } - errorBodyJson, _ := json.Marshal(errorBody) - resp := http.Response{ - Body: io.NopCloser(bytes.NewBuffer(errorBodyJson)), - StatusCode: 400, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - - _, _, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, *body, cookies) - - if err == nil { - t.Fatalf("expected error not nil") - } - expectedError := fmt.Errorf("login with backup codes unavailable") - if err.Error() != expectedError.Error() { - t.Fatalf("expected error to be %v not %v", expectedError, err) - } -} - -func TestGetUiError(t *testing.T) { - tests := []struct { - name string - messages []kClient.UiText - expectErr string - expectLog bool - }{ - { - name: "incorrect credentials", - messages: []kClient.UiText{{Id: IncorrectCredentials}}, - expectErr: "incorrect username or password", - }, - { - name: "incorrect account identifier", - messages: []kClient.UiText{{Id: IncorrectAccountIdentifier}}, - expectErr: "account does not exist or has no login method configured", - }, - { - name: "inactive account", - messages: []kClient.UiText{{Id: InactiveAccount}}, - expectErr: "inactive account", - }, - { - name: "invalid property", - messages: []kClient.UiText{{Id: InvalidProperty, Context: map[string]interface{}{"property": "email"}}}, - expectErr: "invalid email", + { + name: "invalid property", + messages: []kClient.UiText{{Id: InvalidProperty, Context: map[string]interface{}{"property": "email"}}}, + expectErr: "invalid email", }, { name: "password policy violation", @@ -1513,1256 +1409,592 @@ func TestGetUiError(t *testing.T) { expectErr: "password can not be similar to the email", }, { - name: "unknown code logs and returns server error", - messages: []kClient.UiText{{Id: 9999999}}, - expectErr: "server error", - expectLog: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - if tt.expectLog { - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) - } - - errorResp := UiErrorMessages{Ui: kClient.UiContainer{Messages: tt.messages}} - body, _ := json.Marshal(errorResp) - resp := io.NopCloser(bytes.NewBuffer(body)) - - err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).getUiError(resp) - - if err == nil || err.Error() != tt.expectErr { - t.Fatalf("expected error '%s', got %v", tt.expectErr, err) - } - }) - } -} - -func TestUpdateLoginFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - _redirectTo := "https://redirect/to/path" - flow := ErrorBrowserLocationChangeRequired{ - RedirectBrowserTo: &_redirectTo, - } - flowJson, _ := json.Marshal(flow) - body := new(kClient.UpdateLoginFlowBody) - - request := kClient.FrontendAPIUpdateLoginFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - Body: io.NopCloser(bytes.NewBuffer(flowJson)), - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateLoginFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateLoginFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) - - r, _, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateLoginFlow(ctx, flowId, *body, cookies) - - if r != nil { - t.Fatalf("expected flow to be %v not %+v", nil, r) - } - if c != nil { - t.Fatalf("expected header to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") - } -} - -func TestGetFlowErrorSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - id := "id" - flow := kClient.NewFlowError(id) - request := kClient.FrontendAPIGetFlowErrorRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"K": []string{"V"}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetFlowError").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetFlowError(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetFlowErrorExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIGetFlowErrorRequest) (*kClient.FlowError, *http.Response, error) { - if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { - t.Fatalf("expected id to be %s, got %s", id, *_id) - } - - return flow, &resp, nil - }, - ) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetFlowError(ctx, id) - - if !reflect.DeepEqual(f, flow) { - t.Fatalf("expected flow to be %+v not %+v", flow, f) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestGetFlowErrorFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - id := "id" - request := kClient.FrontendAPIGetFlowErrorRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"K": []string{"V"}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetFlowError").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetFlowError(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetFlowErrorExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetFlowError(ctx, id) - - if f != nil { - t.Fatalf("expected flow to be %v not %+v", nil, f) - } - if c != nil { - t.Fatalf("expected header to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") - } -} - -func TestCheckAllowedProviderAllowedSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - - provider := "provider" - oidcBody := kClient.NewUpdateLoginFlowWithOidcMethod("oidc", provider) - body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(oidcBody) - - client_name := "foo" - client := kClient.NewOAuth2ClientWithDefaults() - client.ClientName = &client_name - loginReq := kClient.NewOAuth2LoginRequestWithDefaults() - loginReq.Client = client - flow := kClient.NewLoginFlowWithDefaults() - flow.Oauth2LoginRequest = loginReq - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CheckAllowedProvider").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]string{provider}, nil) - - allowed, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CheckAllowedProvider(ctx, flow, &body) - - if !allowed { - t.Fatalf("expected allowed to be true") - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestCheckAllowedProviderNotAllowedSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - - provider := "provider" - oidcBody := kClient.NewUpdateLoginFlowWithOidcMethod("oidc", provider) - body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(oidcBody) - - client_name := "foo" - client := kClient.NewOAuth2ClientWithDefaults() - client.ClientName = &client_name - loginReq := kClient.NewOAuth2LoginRequestWithDefaults() - loginReq.Client = client - flow := kClient.NewLoginFlowWithDefaults() - flow.Oauth2LoginRequest = loginReq - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CheckAllowedProvider").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]string{"other_provider"}, nil) - - allowed, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CheckAllowedProvider(ctx, flow, &body) - - if allowed { - t.Fatalf("expected allowed to be false") - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestCheckAllowedProviderFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - provider := "provider" - oidcBody := kClient.NewUpdateLoginFlowWithOidcMethod("oidc", provider) - body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(oidcBody) - - client_name := "foo" - client := kClient.NewOAuth2ClientWithDefaults() - client.ClientName = &client_name - loginReq := kClient.NewOAuth2LoginRequestWithDefaults() - loginReq.Client = client - flow := kClient.NewLoginFlowWithDefaults() - flow.Oauth2LoginRequest = loginReq - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CheckAllowedProvider").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(make([]string, 0), fmt.Errorf("oh no")) - - _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CheckAllowedProvider(ctx, flow, &body) - - if err == nil { - t.Fatalf("expected error not nil") - } -} - -func TestGetClientNameOathkeeper(t *testing.T) { - loginFlow := &kClient.LoginFlow{} - service := NewService(nil, nil, nil, nil, false, nil, nil, nil) - - actualClientName := service.getClientName(loginFlow) - - const expectedClientName = "" - if expectedClientName != actualClientName { - t.Fatalf("Expected client name doesn't match") - } -} - -func TestGetClientNameOAuth2Request(t *testing.T) { - expectedClientName := "mockClientName" - loginFlow := &kClient.LoginFlow{Oauth2LoginRequest: &kClient.OAuth2LoginRequest{Client: &kClient.OAuth2Client{ClientName: &expectedClientName}}} - service := NewService(nil, nil, nil, nil, false, nil, nil, nil) - - actualClientName := service.getClientName(loginFlow) - - if expectedClientName != actualClientName { - t.Fatalf("Expected client name doesn't match") - } -} - -func TestFilterFlowProviderListAllowAll(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - - kratosProviders := []string{"1", "2", "3", "4"} - client_name := "foo" - client := kClient.NewOAuth2ClientWithDefaults() - client.ClientName = &client_name - loginReq := kClient.NewOAuth2LoginRequestWithDefaults() - loginReq.Client = client - ui := *kClient.NewUiContainerWithDefaults() - kClient.NewUiNodeWithDefaults() - for _, p := range kratosProviders { - node := kClient.NewUiNodeWithDefaults() - attributes := kClient.NewUiNodeInputAttributesWithDefaults() - attributes.Value = p - node.Attributes = kClient.UiNodeInputAttributesAsUiNodeAttributes(attributes) - node.Group = "oidc" - ui.Nodes = append(ui.Nodes, *node) - } - flow := kClient.NewLoginFlowWithDefaults() - flow.Oauth2LoginRequest = loginReq - flow.Ui = ui - - mockTracer.EXPECT().Start(ctx, "kratos.Service.FilterFlowProviderList").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(kratosProviders, nil) - - f, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).FilterFlowProviderList(ctx, flow) - - if !reflect.DeepEqual(f.Ui, ui) { - t.Fatalf("expected ui to be %v not %v", ui, f.Ui) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestFilterFlowProviderListAllowSome(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - - kratosProviders := []string{"1", "2", "3", "4"} - allowedProviders := []string{"1", "ab", "ba", "4"} - client_name := "foo" - client := kClient.NewOAuth2ClientWithDefaults() - client.ClientName = &client_name - loginReq := kClient.NewOAuth2LoginRequestWithDefaults() - loginReq.Client = client - ui := *kClient.NewUiContainerWithDefaults() - kClient.NewUiNodeWithDefaults() - for _, p := range kratosProviders { - node := kClient.NewUiNodeWithDefaults() - attributes := kClient.NewUiNodeInputAttributesWithDefaults() - attributes.Value = p - node.Attributes = kClient.UiNodeInputAttributesAsUiNodeAttributes(attributes) - node.Group = "oidc" - ui.Nodes = append(ui.Nodes, *node) - } - flow := kClient.NewLoginFlowWithDefaults() - flow.Oauth2LoginRequest = loginReq - flow.Ui = ui - - mockTracer.EXPECT().Start(ctx, "kratos.Service.FilterFlowProviderList").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(allowedProviders, nil) - - f, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).FilterFlowProviderList(ctx, flow) - - expectedUi := *kClient.NewUiContainerWithDefaults() - expectedUi.Nodes = []kClient.UiNode{ui.Nodes[0], ui.Nodes[3]} - if !reflect.DeepEqual(f.Ui, expectedUi) { - t.Fatalf("expected Ui to be %v not %v", expectedUi, f.Ui) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestFilterFlowProviderListAllowNone(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - - kratosProviders := []string{"1", "2", "3", "4"} - allowedProviders := []string{} - client_name := "foo" - client := kClient.NewOAuth2ClientWithDefaults() - client.ClientName = &client_name - loginReq := kClient.NewOAuth2LoginRequestWithDefaults() - loginReq.Client = client - ui := *kClient.NewUiContainerWithDefaults() - kClient.NewUiNodeWithDefaults() - for _, p := range kratosProviders { - node := kClient.NewUiNodeWithDefaults() - attributes := kClient.NewUiNodeInputAttributesWithDefaults() - attributes.Value = p - node.Attributes = kClient.UiNodeInputAttributesAsUiNodeAttributes(attributes) - node.Group = "oidc" - ui.Nodes = append(ui.Nodes, *node) - } - flow := kClient.NewLoginFlowWithDefaults() - flow.Oauth2LoginRequest = loginReq - flow.Ui = ui - - mockTracer.EXPECT().Start(ctx, "kratos.Service.FilterFlowProviderList").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(allowedProviders, nil) - - f, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).FilterFlowProviderList(ctx, flow) - - if !reflect.DeepEqual(f.Ui, ui) { - t.Fatalf("expected Ui to be %v not %v", ui, f.Ui) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestFilterFlowProviderListFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - ctx := context.Background() - - kratosProviders := []string{"1", "2", "3", "4"} - client_name := "foo" - client := kClient.NewOAuth2ClientWithDefaults() - client.ClientName = &client_name - loginReq := kClient.NewOAuth2LoginRequestWithDefaults() - loginReq.Client = client - ui := *kClient.NewUiContainerWithDefaults() - kClient.NewUiNodeWithDefaults() - for _, p := range kratosProviders { - node := kClient.NewUiNodeWithDefaults() - attributes := kClient.NewUiNodeInputAttributesWithDefaults() - attributes.Value = p - node.Attributes = kClient.UiNodeInputAttributesAsUiNodeAttributes(attributes) - node.Group = "oidc" - ui.Nodes = append(ui.Nodes, *node) - } - flow := kClient.NewLoginFlowWithDefaults() - flow.Oauth2LoginRequest = loginReq - flow.Ui = ui - - mockTracer.EXPECT().Start(ctx, "kratos.Service.FilterFlowProviderList").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil, fmt.Errorf("oh no")) - - _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).FilterFlowProviderList(ctx, flow) - - if err == nil { - t.Fatalf("expected error to be not nil") - } -} - -func TestParseLoginFlowOidcMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(kClient.NewUpdateLoginFlowWithOidcMethodWithDefaults()) - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) - - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("expected flow to be %v not %v", expected, actual) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestParseLoginFlowPasswordMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateLoginFlowWithPasswordMethodWithDefaults() - flow.SetMethod("password") - - body := kClient.UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(flow) - - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) - - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() - - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestParseLoginFlowTotpMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateLoginFlowWithTotpMethodWithDefaults() - flow.SetMethod("totp") - - body := kClient.UpdateLoginFlowWithTotpMethodAsUpdateLoginFlowBody(flow) - - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) - - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() - - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestParseLoginFlowLookupSecretMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateLoginFlowWithLookupSecretMethodWithDefaults() - flow.SetMethod("lookup_secret") - - body := kClient.UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(flow) - - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) - - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() - - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestParseLoginFlowWebAuthnMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateLoginFlowWithWebAuthnMethodWithDefaults() - flow.SetMethod("webauthn") - - body := kClient.UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(flow) - - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) - - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() - - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestGetProviderNameWhenNotOidcMethod(t *testing.T) { - loginFlow := &kClient.UpdateLoginFlowBody{} - service := NewService(nil, nil, nil, nil, false, nil, nil, nil) - - actualProviderName := service.getProviderName(loginFlow) - - expectedProviderName := "" - if expectedProviderName != actualProviderName { - t.Fatalf("Expected the provider to be %v, not %v", expectedProviderName, actualProviderName) - } -} - -func TestGetProviderNameOidc(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - expectedProviderName := "someProvider" - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateLoginFlowWithOidcMethod("", expectedProviderName) - - body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(flow) - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, _, _ := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) - - actualProviderName := b.UpdateLoginFlowWithOidcMethod.Provider - if expectedProviderName != actualProviderName { - t.Fatalf("Expected the provider to be %v, not %v", expectedProviderName, actualProviderName) - } -} - -func TestParseRecoveryFlowCodeMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateRecoveryFlowWithCodeMethodWithDefaults() - flow.SetMethod("code") - - body := kClient.UpdateRecoveryFlowWithCodeMethodAsUpdateRecoveryFlowBody(flow) - - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseRecoveryFlowMethodBody(req) - - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() - - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestGetRecoveryFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - id := "id" - flow := kClient.NewRecoveryFlowWithDefaults() - request := kClient.FrontendAPIGetRecoveryFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetRecoveryFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetRecoveryFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIGetRecoveryFlowRequest) (*kClient.RecoveryFlow, *http.Response, error) { - if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { - t.Fatalf("expected id to be %s, got %s", id, *_id) - } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) - } - - return flow, &resp, nil - }, - ) - - s, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetRecoveryFlow(ctx, id, cookies) - - if s != flow { - t.Fatalf("expected flow to be %v not %v", flow, s) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestGetRecoveryFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - id := "id" - request := kClient.FrontendAPIGetRecoveryFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetRecoveryFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetRecoveryFlow(ctx, id, cookies) - - if f != nil { - t.Fatalf("expected flow to be %v not %v", nil, f) - } - if c != nil { - t.Fatalf("expected header to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") - } -} - -func TestCreateBrowserRecoveryFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - returnTo := "https://example.com/ui/reset_email" - flow := kClient.NewRecoveryFlowWithDefaults() - request := kClient.FrontendAPICreateBrowserRecoveryFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserRecoveryFlow(ctx).Times(1).Return(request) - - mockKratosFrontendApi.EXPECT().CreateBrowserRecoveryFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPICreateBrowserRecoveryFlowRequest) (*kClient.RecoveryFlow, *http.Response, error) { - if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != returnTo { - t.Fatalf("expected returnTo to be %s, got %s", returnTo, *rt) - } - - return flow, &resp, nil + name: "unknown code logs and returns server error", + messages: []kClient.UiText{{Id: 9999999}}, + expectErr: "server error", + expectLog: true, }, - ) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserRecoveryFlow(ctx, returnTo, cookies) - - if f != flow { - t.Fatalf("expected flow to be %v not %v", flow, f) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} -func TestCreateBrowserRecoveryFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - returnTo := "https://example.com/ui/reset_email" - request := kClient.FrontendAPICreateBrowserRecoveryFlowRequest{ - ApiService: mockKratosFrontendApi, - } + if tt.expectLog { + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + } - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserRecoveryFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().CreateBrowserRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, nil, fmt.Errorf("error")) + errorResp := UiErrorMessages{Ui: kClient.UiContainer{Messages: tt.messages}} + body, _ := json.Marshal(errorResp) + resp := io.NopCloser(bytes.NewBuffer(body)) - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserRecoveryFlow(ctx, returnTo, cookies) + err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).getUiError(resp) - if f != nil { - t.Fatalf("expected flow to be %v not %v", nil, f) - } - if c != nil { - t.Fatalf("expected cookies to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") + if err == nil || err.Error() != tt.expectErr { + t.Fatalf("expected error '%s', got %v", tt.expectErr, err) + } + }) } } -func TestUpdateRecoveryFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestGetFlowError(t *testing.T) { + tests := []struct { + name string + id string + shouldFail bool + expectNil bool + }{ + { + name: "Success", + id: "id", + shouldFail: false, + expectNil: false, + }, + { + name: "Fail", + id: "id", + shouldFail: true, + expectNil: true, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - _redirectTo := "https://redirect/to/path" - flow := ErrorBrowserLocationChangeRequired{ - RedirectBrowserTo: &_redirectTo, - } - flowJson, _ := json.Marshal(flow) - body := new(kClient.UpdateRecoveryFlowBody) - request := kClient.FrontendAPIUpdateRecoveryFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - Body: io.NopCloser(bytes.NewBuffer(flowJson)), - } + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, "kratos.Service.parseKratosRedirectResponse").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateRecoveryFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIUpdateRecoveryFlowRequest) (*ErrorBrowserLocationChangeRequired, *http.Response, error) { - if _flow := (*string)(reflect.ValueOf(r).FieldByName("flow").UnsafePointer()); *_flow != flowId { - t.Fatalf("expected id to be %s, got %s", flowId, *_flow) - } - if _body := (*kClient.UpdateRecoveryFlowBody)(reflect.ValueOf(r).FieldByName("updateRecoveryFlowBody").UnsafePointer()); *_body != *body { - t.Fatalf("expected id to be %v, got %v", *body, *_body) + ctx := context.Background() + flow := kClient.NewFlowError(tt.id) + request := kClient.FrontendAPIGetFlowErrorRequest{ + ApiService: mockKratosFrontendApi, } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) + resp := http.Response{ + Header: http.Header{"K": []string{"V"}}, } - return &flow, &resp, nil - }, - ) - - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateRecoveryFlow(ctx, flowId, *body, cookies) + mockTracer.EXPECT().Start(ctx, "kratos.Service.GetFlowError").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().GetFlowError(ctx).Times(1).Return(request) + + if tt.shouldFail { + mockKratosFrontendApi.EXPECT().GetFlowErrorExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) + } else { + mockKratosFrontendApi.EXPECT().GetFlowErrorExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIGetFlowErrorRequest) (*kClient.FlowError, *http.Response, error) { + if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != tt.id { + t.Fatalf("expected id to be %s, got %s", tt.id, *_id) + } + return flow, &resp, nil + }, + ) + } - if *f.RedirectTo != *flow.RedirectBrowserTo { - t.Fatalf("expected redirectTo to be %s not %s", *flow.RedirectBrowserTo, *f.RedirectTo) - } - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetFlowError(ctx, tt.id) + + if tt.expectNil { + if f != nil { + t.Fatalf("expected flow to be %v not %+v", nil, f) + } + if c != nil { + t.Fatalf("expected cookies to be %v not %v", nil, c) + } + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if !reflect.DeepEqual(f, flow) { + t.Fatalf("expected flow to be %+v not %+v", flow, f) + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } -func TestUpdateRecoveryFlowFailOnUpdateRecoveryFlowExecute(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - _redirectTo := "https://redirect/to/path" - flow := ErrorBrowserLocationChangeRequired{ - RedirectBrowserTo: &_redirectTo, +func TestCheckAllowedProvider(t *testing.T) { + tests := []struct { + name string + provider string + allowedList []string + shouldFail bool + expectAllowed bool + }{ + { + name: "Allowed success", + provider: "provider", + allowedList: []string{"provider"}, + shouldFail: false, + expectAllowed: true, + }, + { + name: "Not allowed success", + provider: "provider", + allowedList: []string{"other_provider"}, + shouldFail: false, + expectAllowed: false, + }, + { + name: "Fail", + provider: "provider", + allowedList: []string{}, + shouldFail: true, + expectAllowed: false, + }, } - flowJson, _ := json.Marshal(flow) - body := new(kClient.UpdateRecoveryFlowBody) - request := kClient.FrontendAPIUpdateRecoveryFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - Body: io.NopCloser(bytes.NewBuffer(flowJson)), - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateRecoveryFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateRecoveryFlow(ctx, flowId, *body, cookies) + ctx := context.Background() + oidcBody := kClient.NewUpdateLoginFlowWithOidcMethod("oidc", tt.provider) + body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(oidcBody) + + client_name := "foo" + client := kClient.NewOAuth2ClientWithDefaults() + client.ClientName = &client_name + loginReq := kClient.NewOAuth2LoginRequestWithDefaults() + loginReq.Client = client + flow := kClient.NewLoginFlowWithDefaults() + flow.Oauth2LoginRequest = loginReq + + mockTracer.EXPECT().Start(ctx, "kratos.Service.CheckAllowedProvider").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + + if tt.shouldFail { + mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(tt.allowedList, fmt.Errorf("oh no")) + } else { + mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(tt.allowedList, nil) + } - if f != nil { - t.Fatalf("expected flow to be %v not %+v", nil, f) - } - if c != nil { - t.Fatalf("expected header to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") + allowed, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CheckAllowedProvider(ctx, flow, &body) + + if tt.shouldFail { + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + if allowed != tt.expectAllowed { + t.Fatalf("expected allowed to be %v not %v", tt.expectAllowed, allowed) + } + } + }) } } -func TestUpdateRecoveryFlowFailOnInvalidRecoveryCode(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - flow := &kClient.RecoveryFlow{ - Ui: kClient.UiContainer{ - Messages: []kClient.UiText{ - { - Id: InvalidRecoveryCode, - }, - }, +func TestGetClientName(t *testing.T) { + tests := []struct { + name string + loginFlow *kClient.LoginFlow + expectedClientName string + }{ + { + name: "Oathkeeper", + loginFlow: &kClient.LoginFlow{}, + expectedClientName: "", + }, + { + name: "OAuth2Request", + loginFlow: func() *kClient.LoginFlow { + clientName := "mockClientName" + return &kClient.LoginFlow{Oauth2LoginRequest: &kClient.OAuth2LoginRequest{Client: &kClient.OAuth2Client{ClientName: &clientName}}} + }(), + expectedClientName: "mockClientName", }, } - flowJson, _ := json.Marshal(flow) - body := new(kClient.UpdateRecoveryFlowBody) - - request := kClient.FrontendAPIUpdateRecoveryFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - Body: io.NopCloser(bytes.NewBuffer(flowJson)), - StatusCode: 200, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateRecoveryFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).Return(flow, &resp, nil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := NewService(nil, nil, nil, nil, false, nil, nil, nil) - f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateRecoveryFlow(ctx, flowId, *body, cookies) + actualClientName := service.getClientName(tt.loginFlow) - if f != nil { - t.Fatalf("expected flow to be %v not %+v", nil, f) - } - if c != nil { - t.Fatalf("expected header to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") - } - expectedError := fmt.Errorf("the recovery code is invalid or has already been used") - if err.Error() != expectedError.Error() { - t.Fatalf("expected error to be %v not %v", expectedError, err) + if tt.expectedClientName != actualClientName { + t.Fatalf("Expected client name %s, got %s", tt.expectedClientName, actualClientName) + } + }) } } -func TestParseSettingsFlowPasswordMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateSettingsFlowWithPasswordMethodWithDefaults() - flow.SetMethod("password") - - body := kClient.UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(flow) - - jsonBody, _ := body.MarshalJSON() - - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - - b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseSettingsFlowMethodBody(req) - - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() - - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) +func TestFilterFlowProviderList(t *testing.T) { + tests := []struct { + name string + kratosProviders []string + allowedProviders []string + authzError error + expectedUiNodeCount int + expectedError bool + checkUiMatches bool + }{ + { + name: "AllowAll", + kratosProviders: []string{"1", "2", "3", "4"}, + allowedProviders: []string{"1", "2", "3", "4"}, + authzError: nil, + expectedUiNodeCount: 4, + expectedError: false, + checkUiMatches: true, + }, + { + name: "AllowSome", + kratosProviders: []string{"1", "2", "3", "4"}, + allowedProviders: []string{"1", "ab", "ba", "4"}, + authzError: nil, + expectedUiNodeCount: 2, + expectedError: false, + checkUiMatches: false, + }, + { + name: "AllowNone", + kratosProviders: []string{"1", "2", "3", "4"}, + allowedProviders: []string{}, + authzError: nil, + expectedUiNodeCount: 4, + expectedError: false, + checkUiMatches: true, + }, + { + name: "Fail", + kratosProviders: []string{"1", "2", "3", "4"}, + allowedProviders: nil, + authzError: fmt.Errorf("oh no"), + expectedError: true, + }, } -} - -func TestParseSettingsFlowOidcMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - - flow := kClient.NewUpdateSettingsFlowWithOidcMethodWithDefaults() - flow.SetMethod("oidc") - - body := kClient.UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(flow) - - jsonBody, _ := body.MarshalJSON() - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseSettingsFlowMethodBody(req) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() + ctx := context.Background() - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} + client_name := "foo" + client := kClient.NewOAuth2ClientWithDefaults() + client.ClientName = &client_name + loginReq := kClient.NewOAuth2LoginRequestWithDefaults() + loginReq.Client = client + ui := *kClient.NewUiContainerWithDefaults() + for _, p := range tt.kratosProviders { + node := kClient.NewUiNodeWithDefaults() + attributes := kClient.NewUiNodeInputAttributesWithDefaults() + attributes.Value = p + node.Attributes = kClient.UiNodeInputAttributesAsUiNodeAttributes(attributes) + node.Group = "oidc" + ui.Nodes = append(ui.Nodes, *node) + } + flow := kClient.NewLoginFlowWithDefaults() + flow.Oauth2LoginRequest = loginReq + flow.Ui = ui -func TestParseSettingsFlowTotpMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + mockTracer.EXPECT().Start(ctx, "kratos.Service.FilterFlowProviderList").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockAuthz.EXPECT().ListObjects(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(tt.allowedProviders, tt.authzError) - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + f, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).FilterFlowProviderList(ctx, flow) - flow := kClient.NewUpdateSettingsFlowWithTotpMethodWithDefaults() - flow.SetMethod("totp") + if tt.expectedError { + if err == nil { + t.Fatalf("expected error to be not nil") + } + return + } - body := kClient.UpdateSettingsFlowWithTotpMethodAsUpdateSettingsFlowBody(flow) + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } - jsonBody, _ := body.MarshalJSON() + if tt.checkUiMatches { + if !reflect.DeepEqual(f.Ui, ui) { + t.Fatalf("expected ui to be %v not %v", ui, f.Ui) + } + } else { + expectedUi := *kClient.NewUiContainerWithDefaults() + expectedUi.Nodes = []kClient.UiNode{ui.Nodes[0], ui.Nodes[3]} + if !reflect.DeepEqual(f.Ui, expectedUi) { + t.Fatalf("expected Ui to be %v not %v", expectedUi, f.Ui) + } + } + }) + } +} - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) +func TestParseLoginFlowMethodBody(t *testing.T) { + tests := []struct { + name string + body kClient.UpdateLoginFlowBody + method string + }{ + { + name: "Oidc", + body: kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(kClient.NewUpdateLoginFlowWithOidcMethodWithDefaults()), + method: "oidc", + }, + { + name: "Password", + body: func() kClient.UpdateLoginFlowBody { + flow := kClient.NewUpdateLoginFlowWithPasswordMethodWithDefaults() + flow.SetMethod("password") + return kClient.UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(flow) + }(), + method: "password", + }, + { + name: "Totp", + body: func() kClient.UpdateLoginFlowBody { + flow := kClient.NewUpdateLoginFlowWithTotpMethodWithDefaults() + flow.SetMethod("totp") + return kClient.UpdateLoginFlowWithTotpMethodAsUpdateLoginFlowBody(flow) + }(), + method: "totp", + }, + { + name: "LookupSecret", + body: func() kClient.UpdateLoginFlowBody { + flow := kClient.NewUpdateLoginFlowWithLookupSecretMethodWithDefaults() + flow.SetMethod("lookup_secret") + return kClient.UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(flow) + }(), + method: "lookup_secret", + }, + { + name: "WebAuthn", + body: func() kClient.UpdateLoginFlowBody { + flow := kClient.NewUpdateLoginFlowWithWebAuthnMethodWithDefaults() + flow.SetMethod("webauthn") + return kClient.UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(flow) + }(), + method: "webauthn", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseSettingsFlowMethodBody(req) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() + jsonBody, _ := tt.body.MarshalJSON() + req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + b, _, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) + + actual, _ := b.MarshalJSON() + expected, _ := tt.body.MarshalJSON() + if !reflect.DeepEqual(string(actual), string(expected)) { + t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + }) } } -func TestParseSettingsFlowLookupMethodBody(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestParseLoginFlowMethodBody_ErrorsAndForm(t *testing.T) { + tests := []struct { + name string + setupRequest func() *http.Request + expectError bool + expectedErrMsg string + assertBody func(*testing.T, *kClient.UpdateLoginFlowBody, []*http.Cookie) + }{ + { + name: "ReadBodyError", + setupRequest: func() *http.Request { + errReader := io.NopCloser(readerFunc(func([]byte) (int, error) { + return 0, fmt.Errorf("read error") + })) + return httptest.NewRequest(http.MethodPost, "http://some/path", errReader) + }, + expectError: true, + expectedErrMsg: "unable to read body", + }, + { + name: "WebAuthnFormRemovesSessionCookie", + setupRequest: func() *http.Request { + form := "csrf_token=csrf&webauthn_login=login&identifier=user@example.com" + req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(strings.NewReader(form))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.AddCookie(&http.Cookie{Name: KRATOS_SESSION_COOKIE_NAME, Value: "session"}) + req.AddCookie(&http.Cookie{Name: "other", Value: "value"}) + return req + }, + expectError: false, + assertBody: func(t *testing.T, body *kClient.UpdateLoginFlowBody, cookies []*http.Cookie) { + if body == nil || body.UpdateLoginFlowWithWebAuthnMethod == nil { + t.Fatalf("expected webauthn body to be set") + } + if body.UpdateLoginFlowWithWebAuthnMethod.CsrfToken == nil || *body.UpdateLoginFlowWithWebAuthnMethod.CsrfToken != "csrf" { + t.Fatalf("expected csrf token to be %s", "csrf") + } + if body.UpdateLoginFlowWithWebAuthnMethod.WebauthnLogin == nil || *body.UpdateLoginFlowWithWebAuthnMethod.WebauthnLogin != "login" { + t.Fatalf("expected login to be %s", "login") + } + if body.UpdateLoginFlowWithWebAuthnMethod.Identifier != "user@example.com" { + t.Fatalf("expected identifier to be %s", "user@example.com") + } + for _, c := range cookies { + if c.Name == KRATOS_SESSION_COOKIE_NAME { + t.Fatalf("expected session cookie to be removed") + } + } + if len(cookies) != 1 || cookies[0].Name != "other" { + t.Fatalf("expected only non-session cookies to remain") + } + }, + }, + { + name: "InvalidJSONFallbackWebAuthn", + setupRequest: func() *http.Request { + body := []byte("{not-json") + return httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(body))) + }, + expectError: true, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - flow := kClient.NewUpdateSettingsFlowWithLookupMethodWithDefaults() - flow.SetMethod("lookup_secret") + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - body := kClient.UpdateSettingsFlowWithLookupMethodAsUpdateSettingsFlowBody(flow) + req := tt.setupRequest() + body, cookies, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) + + if tt.expectError { + if err == nil { + t.Fatalf("expected error not nil") + } + if tt.expectedErrMsg != "" && err.Error() != tt.expectedErrMsg { + t.Fatalf("expected error %s got %v", tt.expectedErrMsg, err) + } + return + } - jsonBody, _ := body.MarshalJSON() + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + if tt.assertBody != nil { + tt.assertBody(t, body, cookies) + } + }) + } +} - req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) +func TestGetProviderName(t *testing.T) { + tests := []struct { + name string + setupBody func() kClient.UpdateLoginFlowBody + expectedProviderName string + }{ + { + name: "WhenNotOidcMethod", + setupBody: func() kClient.UpdateLoginFlowBody { + body := kClient.UpdateLoginFlowBody{} + return body + }, + expectedProviderName: "", + }, + { + name: "Oidc", + setupBody: func() kClient.UpdateLoginFlowBody { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + + expectedProviderName := "someProvider" + flow := kClient.NewUpdateLoginFlowWithOidcMethod("", expectedProviderName) + body := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(flow) + jsonBody, _ := body.MarshalJSON() + + req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) + b, _, _ := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseLoginFlowMethodBody(req) + + return *b + }, + expectedProviderName: "someProvider", + }, + } - b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseSettingsFlowMethodBody(req) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := NewService(nil, nil, nil, nil, false, nil, nil, nil) + body := tt.setupBody() - actual, _ := b.MarshalJSON() - expected, _ := body.MarshalJSON() + actualProviderName := service.getProviderName(&body) - if !reflect.DeepEqual(string(actual), string(expected)) { - t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + if tt.expectedProviderName != actualProviderName { + t.Fatalf("Expected the provider to be %v, not %v", tt.expectedProviderName, actualProviderName) + } + }) } } -func TestParseSettingsFlowWebAuthnMethodBody(t *testing.T) { +func TestParseRecoveryFlowCodeMethodBody(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -2774,16 +2006,16 @@ func TestParseSettingsFlowWebAuthnMethodBody(t *testing.T) { mockTracer := NewMockTracingInterface(ctrl) mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - flow := kClient.NewUpdateSettingsFlowWithWebAuthnMethodWithDefaults() - flow.SetMethod("webauthn") + flow := kClient.NewUpdateRecoveryFlowWithCodeMethodWithDefaults() + flow.SetMethod("code") - body := kClient.UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(flow) + body := kClient.UpdateRecoveryFlowWithCodeMethodAsUpdateRecoveryFlowBody(flow) jsonBody, _ := body.MarshalJSON() req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseSettingsFlowMethodBody(req) + b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseRecoveryFlowMethodBody(req) actual, _ := b.MarshalJSON() expected, _ := body.MarshalJSON() @@ -2796,601 +2028,1433 @@ func TestParseSettingsFlowWebAuthnMethodBody(t *testing.T) { } } -func TestGetSettingsFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - id := "id" - - flow := kClient.NewSettingsFlowWithDefaults() - request := kClient.FrontendAPIGetSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetSettingsFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIGetSettingsFlowRequest) (*kClient.SettingsFlow, *http.Response, error) { - if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { - t.Fatalf("expected id to be %s, got %s", id, *_id) - } - - return flow, &http.Response{StatusCode: http.StatusOK}, nil +func TestGetRecoveryFlow(t *testing.T) { + tests := []struct { + name string + setupMocks func(*MockFrontendAPI, *kClient.RecoveryFlow, *http.Response) + expectedError bool + }{ + { + name: "Success", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.RecoveryFlow, resp *http.Response) { + mockKratosFrontendApi.EXPECT().GetRecoveryFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIGetRecoveryFlowRequest) (*kClient.RecoveryFlow, *http.Response, error) { + id := "id" + if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { + t.Fatalf("expected id to be %s, got %s", id, *_id) + } + if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { + t.Fatalf("expected cookie string as test=test, got %s", *cookie) + } + return flow, resp, nil + }, + ) + }, + expectedError: false, }, - ) - - s, r, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetSettingsFlow(ctx, id, cookies) - - if s != flow { - t.Fatalf("expected flow to be %v not %v", flow, s) - } - if r != nil { - t.Fatalf("expected response to be nil not %v", r) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} - -func TestGetSettingsFlowDuplicateIdentifier(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - id := "id" - - duplicateIdentifierMsg := kClient.UiText{ - Id: 4000007, - Text: "duplicate identifier", - Type: "error", - } - - flow := kClient.NewSettingsFlowWithDefaults() - flow.Ui = kClient.UiContainer{ - Messages: []kClient.UiText{duplicateIdentifierMsg}, - } - - request := kClient.FrontendAPIGetSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, - } - - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetSettingsFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIGetSettingsFlowRequest) (*kClient.SettingsFlow, *http.Response, error) { - if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { - t.Fatalf("expected id to be %s, got %s", id, *_id) - } - - return flow, &http.Response{StatusCode: http.StatusOK}, nil + { + name: "Fail", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.RecoveryFlow, resp *http.Response) { + mockKratosFrontendApi.EXPECT().GetRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, resp, fmt.Errorf("error")) + }, + expectedError: true, }, - ) - - s, r, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetSettingsFlow(ctx, id, cookies) - - if err == nil { - t.Fatal("expected error but got nil") } - if !strings.Contains(err.Error(), "an account with the same identifier already exists, contact support") { - t.Fatalf("unexpected error: %v", err) - } - if s != nil { - t.Fatalf("expected flow to be %v not %v", nil, s) - } - if r != nil { - t.Fatalf("expected response to be %v not %v", nil, r) - } -} -func TestGetSettingsFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - id := "id" - request := kClient.FrontendAPIGetSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockTracer.EXPECT().Start(ctx, "kratos.Service.GetSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().GetSettingsFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().GetSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - f, r, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetSettingsFlow(ctx, id, cookies) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + id := "id" + flow := kClient.NewRecoveryFlowWithDefaults() + request := kClient.FrontendAPIGetRecoveryFlowRequest{ + ApiService: mockKratosFrontendApi, + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } - if f != nil { - t.Fatalf("expected flow to be %v not %v", nil, f) - } - if r != nil { - t.Fatalf("expected response to be %v not %v", nil, r) - } - if err == nil { - t.Fatalf("expected error not nil") + mockTracer.EXPECT().Start(ctx, "kratos.Service.GetRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().GetRecoveryFlow(ctx).Times(1).Return(request) + tt.setupMocks(mockKratosFrontendApi, flow, &resp) + + s, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetRecoveryFlow(ctx, id, cookies) + + if tt.expectedError { + if err == nil { + t.Fatalf("expected error not nil") + } + if s != nil { + t.Fatalf("expected flow to be %v not %v", nil, s) + } + if c != nil { + t.Fatalf("expected header to be %v not %v", nil, c) + } + } else { + if s != flow { + t.Fatalf("expected flow to be %v not %v", flow, s) + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } -func TestCreateBrowserSettingsFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - returnTo := "https://example.com/ui/reset_complete" - flow := kClient.NewSettingsFlowWithDefaults() - request := kClient.FrontendAPICreateBrowserSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, +func TestCreateBrowserRecoveryFlow(t *testing.T) { + tests := []struct { + name string + setupMocks func(*MockFrontendAPI, *kClient.RecoveryFlow, *http.Response, string) + expectedError bool + }{ + { + name: "Success", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.RecoveryFlow, resp *http.Response, returnTo string) { + mockKratosFrontendApi.EXPECT().CreateBrowserRecoveryFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPICreateBrowserRecoveryFlowRequest) (*kClient.RecoveryFlow, *http.Response, error) { + if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != returnTo { + t.Fatalf("expected returnTo to be %s, got %s", returnTo, *rt) + } + return flow, resp, nil + }, + ) + }, + expectedError: false, + }, + { + name: "Fail", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.RecoveryFlow, resp *http.Response, returnTo string) { + mockKratosFrontendApi.EXPECT().CreateBrowserRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, nil, fmt.Errorf("error")) + }, + expectedError: true, + }, } - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserSettingsFlow(ctx).Times(1).Return(request) - - mockKratosFrontendApi.EXPECT().CreateBrowserSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPICreateBrowserSettingsFlowRequest) (*kClient.SettingsFlow, *BrowserLocationChangeRequired, error) { - if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != returnTo { - t.Fatalf("expected returnTo to be %s, got %s", returnTo, *rt) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - return flow, nil, nil - }, - ) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - f, r, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserSettingsFlow(ctx, returnTo, cookies) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + returnTo := "https://example.com/ui/reset_email" + flow := kClient.NewRecoveryFlowWithDefaults() + request := kClient.FrontendAPICreateBrowserRecoveryFlowRequest{ + ApiService: mockKratosFrontendApi, + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } - if f != flow { - t.Fatalf("expected flow to be %v not %v", flow, f) - } - if r != nil { - t.Fatalf("expected response to be nil not %v", r) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().CreateBrowserRecoveryFlow(ctx).Times(1).Return(request) + tt.setupMocks(mockKratosFrontendApi, flow, &resp, returnTo) + + f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserRecoveryFlow(ctx, returnTo, cookies) + + if tt.expectedError { + if f != nil { + t.Fatalf("expected flow to be %v not %v", nil, f) + } + if c != nil { + t.Fatalf("expected cookies to be %v not %v", nil, c) + } + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if f != flow { + t.Fatalf("expected flow to be %v not %v", flow, f) + } + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } -func TestCreateBrowserSettingsFlowFail(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - returnTo := "https://example.com/ui/reset_complete" - request := kClient.FrontendAPICreateBrowserSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - StatusCode: http.StatusNotFound, +func TestUpdateRecoveryFlow(t *testing.T) { + tests := []struct { + name string + setupMocks func(*MockFrontendAPI, *MockLoggerInterface, *MockTracingInterface, *http.Response) + expectedError bool + expectedErrorMessage string + checkRedirect bool + checkCookies bool + }{ + { + name: "Success", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response) { + mockTracer.EXPECT().Start(gomock.Any(), "kratos.Service.parseKratosRedirectResponse").Times(1).Return(context.Background(), trace.SpanFromContext(context.Background())) + mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIUpdateRecoveryFlowRequest) (*ErrorBrowserLocationChangeRequired, *http.Response, error) { + flowId := "flow" + if _flow := (*string)(reflect.ValueOf(r).FieldByName("flow").UnsafePointer()); *_flow != flowId { + t.Fatalf("expected id to be %s, got %s", flowId, *_flow) + } + _redirectTo := "https://redirect/to/path" + flow := ErrorBrowserLocationChangeRequired{ + RedirectBrowserTo: &_redirectTo, + } + return &flow, resp, nil + }, + ) + }, + expectedError: false, + checkRedirect: true, + checkCookies: true, + }, + { + name: "FailOnExecute", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response) { + mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, resp, fmt.Errorf("error")) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + }, + expectedError: true, + }, + { + name: "FailOnInvalidCode", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response) { + flow := &kClient.RecoveryFlow{ + Ui: kClient.UiContainer{ + Messages: []kClient.UiText{ + { + Id: InvalidRecoveryCode, + }, + }, + }, + } + resp.StatusCode = 200 + flowJson, _ := json.Marshal(flow) + resp.Body = io.NopCloser(bytes.NewBuffer(flowJson)) + mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).Return(flow, resp, nil) + }, + expectedError: true, + expectedErrorMessage: "the recovery code is invalid or has already been used", + }, + { + name: "BadRequestParseSuccess", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response) { + resp.StatusCode = http.StatusBadRequest + _redirectTo := "https://redirect/to/path" + flow := ErrorBrowserLocationChangeRequired{ + RedirectBrowserTo: &_redirectTo, + } + flowJson, _ := json.Marshal(flow) + resp.Body = io.NopCloser(bytes.NewBuffer(flowJson)) + mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, resp, fmt.Errorf("error")) + mockTracer.EXPECT().Start(gomock.Any(), "kratos.Service.parseKratosRedirectResponse").Times(1).Return(context.Background(), trace.SpanFromContext(context.Background())) + }, + expectedError: false, + checkRedirect: true, + checkCookies: false, + }, + { + name: "BadRequestParseError", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response) { + resp.StatusCode = http.StatusBadRequest + resp.Body = io.NopCloser(strings.NewReader("not-json")) + mockKratosFrontendApi.EXPECT().UpdateRecoveryFlowExecute(gomock.Any()).Times(1).Return(nil, resp, fmt.Errorf("error")) + mockTracer.EXPECT().Start(gomock.Any(), "kratos.Service.parseKratosRedirectResponse").Times(1).Return(context.Background(), trace.SpanFromContext(context.Background())) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + }, + expectedError: true, + checkCookies: false, + }, } - mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().CreateBrowserSettingsFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().CreateBrowserSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - f, r, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserSettingsFlow(ctx, returnTo, cookies) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + flowId := "flow" + _redirectTo := "https://redirect/to/path" + flow := ErrorBrowserLocationChangeRequired{ + RedirectBrowserTo: &_redirectTo, + } + flowJson, _ := json.Marshal(flow) + body := new(kClient.UpdateRecoveryFlowBody) + request := kClient.FrontendAPIUpdateRecoveryFlowRequest{ + ApiService: mockKratosFrontendApi, + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + Body: io.NopCloser(bytes.NewBuffer(flowJson)), + } - if f != nil { - t.Fatalf("expected flow to be %v not %v", nil, f) - } - if r != nil { - t.Fatalf("expected response to be %v not %v", nil, r) - } - if err == nil { - t.Fatalf("expected error not nil") + mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateRecoveryFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().UpdateRecoveryFlow(ctx).Times(1).Return(request) + tt.setupMocks(mockKratosFrontendApi, mockLogger, mockTracer, &resp) + + f, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateRecoveryFlow(ctx, flowId, *body, cookies) + + if tt.expectedError { + if f != nil { + t.Fatalf("expected flow to be %v not %+v", nil, f) + } + if c != nil { + t.Fatalf("expected header to be %v not %v", nil, c) + } + if err == nil { + t.Fatalf("expected error not nil") + } + if tt.expectedErrorMessage != "" && err.Error() != tt.expectedErrorMessage { + t.Fatalf("expected error to be %v not %v", tt.expectedErrorMessage, err) + } + } else { + if tt.checkRedirect { + if *f.RedirectTo != _redirectTo { + t.Fatalf("expected redirectTo to be %s not %s", _redirectTo, *f.RedirectTo) + } + } + if tt.checkCookies && !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } -func TestUpdateSettingsFlowSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestParseSettingsFlowMethodBody(t *testing.T) { + tests := []struct { + name string + body kClient.UpdateSettingsFlowBody + method string + }{ + { + name: "Password", + body: func() kClient.UpdateSettingsFlowBody { + flow := kClient.NewUpdateSettingsFlowWithPasswordMethodWithDefaults() + flow.SetMethod("password") + return kClient.UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(flow) + }(), + method: "password", + }, + { + name: "Oidc", + body: func() kClient.UpdateSettingsFlowBody { + flow := kClient.NewUpdateSettingsFlowWithOidcMethodWithDefaults() + flow.SetMethod("oidc") + return kClient.UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(flow) + }(), + method: "oidc", + }, + { + name: "Totp", + body: func() kClient.UpdateSettingsFlowBody { + flow := kClient.NewUpdateSettingsFlowWithTotpMethodWithDefaults() + flow.SetMethod("totp") + return kClient.UpdateSettingsFlowWithTotpMethodAsUpdateSettingsFlowBody(flow) + }(), + method: "totp", + }, + { + name: "Lookup", + body: func() kClient.UpdateSettingsFlowBody { + flow := kClient.NewUpdateSettingsFlowWithLookupMethodWithDefaults() + flow.SetMethod("lookup_secret") + return kClient.UpdateSettingsFlowWithLookupMethodAsUpdateSettingsFlowBody(flow) + }(), + method: "lookup_secret", + }, + { + name: "WebAuthn", + body: func() kClient.UpdateSettingsFlowBody { + flow := kClient.NewUpdateSettingsFlowWithWebAuthnMethodWithDefaults() + flow.SetMethod("webauthn") + return kClient.UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(flow) + }(), + method: "webauthn", + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - flow := kClient.NewSettingsFlowWithDefaults() + jsonBody, _ := tt.body.MarshalJSON() + req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(jsonBody))) - flowJson, _ := json.Marshal(flow) - body := new(kClient.UpdateSettingsFlowBody) - request := kClient.FrontendAPIUpdateSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - Body: io.NopCloser(bytes.NewBuffer(flowJson)), - } + b, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseSettingsFlowMethodBody(req) - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateSettingsFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.FrontendAPIUpdateSettingsFlowRequest) (*kClient.SettingsFlow, *http.Response, error) { - if _flow := (*string)(reflect.ValueOf(r).FieldByName("flow").UnsafePointer()); *_flow != flowId { - t.Fatalf("expected id to be %s, got %s", flowId, *_flow) - } - if _body := (*kClient.UpdateSettingsFlowBody)(reflect.ValueOf(r).FieldByName("updateSettingsFlowBody").UnsafePointer()); *_body != *body { - t.Fatalf("expected id to be %v, got %v", *body, *_body) + actual, _ := b.MarshalJSON() + expected, _ := tt.body.MarshalJSON() + + if !reflect.DeepEqual(string(actual), string(expected)) { + t.Fatalf("expected flow to be %s not %s", string(expected), string(actual)) } - if cookie := (*string)(reflect.ValueOf(r).FieldByName("cookie").UnsafePointer()); *cookie != "test=test" { - t.Fatalf("expected cookie string as test=test, got %s", *cookie) + if err != nil { + t.Fatalf("expected error to be nil not %v", err) } - - return flow, &resp, nil - }, - ) - - _, _, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateSettingsFlow(ctx, flowId, *body, cookies) - - if !reflect.DeepEqual(c, resp.Cookies()) { - t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + }) } } -func TestUpdateSettingsFlowFailOnUpdateSettingsFlowExecute(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestParseSettingsFlowMethodBody_ErrorsAndForm(t *testing.T) { + tests := []struct { + name string + setupRequest func() *http.Request + expectError bool + expectedErrMsg string + assertBody func(*testing.T, *kClient.UpdateSettingsFlowBody) + }{ + { + name: "ReadBodyError", + setupRequest: func() *http.Request { + errReader := io.NopCloser(readerFunc(func([]byte) (int, error) { + return 0, fmt.Errorf("read error") + })) + return httptest.NewRequest(http.MethodPost, "http://some/path", errReader) + }, + expectError: true, + expectedErrMsg: "unable to read body", + }, + { + name: "UnsupportedMethod", + setupRequest: func() *http.Request { + body := []byte(`{"method":"unknown"}`) + return httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(body))) + }, + expectError: true, + expectedErrMsg: "upsupported method: unknown", + }, + { + name: "WebAuthnForm", + setupRequest: func() *http.Request { + form := "csrf_token=csrf&webauthn_register_displayname=John&webauthn_register=reg&webauthn_remove=rem" + req := httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(strings.NewReader(form))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return req + }, + expectError: false, + assertBody: func(t *testing.T, body *kClient.UpdateSettingsFlowBody) { + if body == nil || body.UpdateSettingsFlowWithWebAuthnMethod == nil { + t.Fatalf("expected webauthn body to be set") + } + if body.UpdateSettingsFlowWithWebAuthnMethod.CsrfToken == nil || *body.UpdateSettingsFlowWithWebAuthnMethod.CsrfToken != "csrf" { + t.Fatalf("expected csrf token to be %s", "csrf") + } + if body.UpdateSettingsFlowWithWebAuthnMethod.WebauthnRegisterDisplayname == nil || *body.UpdateSettingsFlowWithWebAuthnMethod.WebauthnRegisterDisplayname != "John" { + t.Fatalf("expected display name to be %s", "John") + } + if body.UpdateSettingsFlowWithWebAuthnMethod.WebauthnRegister == nil || *body.UpdateSettingsFlowWithWebAuthnMethod.WebauthnRegister != "reg" { + t.Fatalf("expected register to be %s", "reg") + } + if body.UpdateSettingsFlowWithWebAuthnMethod.WebauthnRemove == nil || *body.UpdateSettingsFlowWithWebAuthnMethod.WebauthnRemove != "rem" { + t.Fatalf("expected remove to be %s", "rem") + } + }, + }, + { + name: "InvalidJSONFallbackWebAuthn", + setupRequest: func() *http.Request { + body := []byte("{not-json") + return httptest.NewRequest(http.MethodPost, "http://some/path", io.NopCloser(bytes.NewBuffer(body))) + }, + expectError: true, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - flow := kClient.NewSettingsFlowWithDefaults() - flowJson, _ := json.Marshal(flow) - body := new(kClient.UpdateSettingsFlowBody) + req := tt.setupRequest() + body, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).ParseSettingsFlowMethodBody(req) + + if tt.expectError { + if err == nil { + t.Fatalf("expected error not nil") + } + if tt.expectedErrMsg != "" && err.Error() != tt.expectedErrMsg { + t.Fatalf("expected error %s got %v", tt.expectedErrMsg, err) + } + return + } - request := kClient.FrontendAPIUpdateSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, - } - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - Body: io.NopCloser(bytes.NewBuffer(flowJson)), + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + if tt.assertBody != nil { + tt.assertBody(t, body) + } + }) } +} - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateSettingsFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) +type readerFunc func([]byte) (int, error) - f, r, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateSettingsFlow(ctx, flowId, *body, cookies) +func (r readerFunc) Read(p []byte) (int, error) { + return r(p) +} - if f != nil { - t.Fatalf("expected flow to be %v not %+v", nil, f) - } - if r != nil { - t.Fatalf("expected redirect info to be %v not %+v", nil, f) - } - if c != nil { - t.Fatalf("expected header to be %v not %v", nil, c) - } - if err == nil { - t.Fatalf("expected error not nil") +func TestGetSettingsFlow(t *testing.T) { + tests := []struct { + name string + setupFlow func() *kClient.SettingsFlow + setupMocks func(*MockFrontendAPI, *kClient.SettingsFlow) + expectedError bool + expectedErrorMessage string + expectNilResponse bool + }{ + { + name: "Success", + setupFlow: func() *kClient.SettingsFlow { + return kClient.NewSettingsFlowWithDefaults() + }, + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.SettingsFlow) { + mockKratosFrontendApi.EXPECT().GetSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIGetSettingsFlowRequest) (*kClient.SettingsFlow, *http.Response, error) { + id := "id" + if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { + t.Fatalf("expected id to be %s, got %s", id, *_id) + } + return flow, &http.Response{StatusCode: http.StatusOK}, nil + }, + ) + }, + expectedError: false, + expectNilResponse: true, + }, + { + name: "DuplicateIdentifier", + setupFlow: func() *kClient.SettingsFlow { + duplicateIdentifierMsg := kClient.UiText{ + Id: 4000007, + Text: "duplicate identifier", + Type: "error", + } + flow := kClient.NewSettingsFlowWithDefaults() + flow.Ui = kClient.UiContainer{ + Messages: []kClient.UiText{duplicateIdentifierMsg}, + } + return flow + }, + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.SettingsFlow) { + mockKratosFrontendApi.EXPECT().GetSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIGetSettingsFlowRequest) (*kClient.SettingsFlow, *http.Response, error) { + id := "id" + if _id := (*string)(reflect.ValueOf(r).FieldByName("id").UnsafePointer()); *_id != id { + t.Fatalf("expected id to be %s, got %s", id, *_id) + } + return flow, &http.Response{StatusCode: http.StatusOK}, nil + }, + ) + }, + expectedError: true, + expectedErrorMessage: "an account with the same identifier already exists, contact support", + }, + { + name: "Fail", + setupFlow: func() *kClient.SettingsFlow { + return kClient.NewSettingsFlowWithDefaults() + }, + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.SettingsFlow) { + cookie := &http.Cookie{Name: "test", Value: "test"} + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } + mockKratosFrontendApi.EXPECT().GetSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) + }, + expectedError: true, + }, } -} -func TestUpdateSettingsFlowForbiddenStatus(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosFrontendApi := NewMockFrontendAPI(ctrl) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - ctx := context.Background() - cookies := make([]*http.Cookie, 0) - cookie := &http.Cookie{Name: "test", Value: "test"} - cookies = append(cookies, cookie) - flowId := "flow" - body := new(kClient.UpdateSettingsFlowBody) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + id := "id" + + flow := tt.setupFlow() + request := kClient.FrontendAPIGetSettingsFlowRequest{ + ApiService: mockKratosFrontendApi, + } - redirectTo := "http://kratos/self-service/login/browser?refresh=true" - sessionRequiredErrorId := "session_refresh_required" + mockTracer.EXPECT().Start(ctx, "kratos.Service.GetSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().GetSettingsFlow(ctx).Times(1).Return(request) + tt.setupMocks(mockKratosFrontendApi, flow) + + s, r, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).GetSettingsFlow(ctx, id, cookies) + + if tt.expectedError { + if err == nil { + t.Fatal("expected error but got nil") + } + if tt.expectedErrorMessage != "" && !strings.Contains(err.Error(), tt.expectedErrorMessage) { + t.Fatalf("unexpected error: %v", err) + } + if s != nil { + t.Fatalf("expected flow to be %v not %v", nil, s) + } + if r != nil { + t.Fatalf("expected response to be %v not %v", nil, r) + } + } else { + if s != flow { + t.Fatalf("expected flow to be %v not %v", flow, s) + } + if tt.expectNilResponse && r != nil { + t.Fatalf("expected response to be nil not %v", r) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) + } +} - errorPayload := ErrorBrowserLocationChangeRequired{ - Error: &kClient.GenericError{ - Id: &sessionRequiredErrorId, +func TestCreateBrowserSettingsFlow(t *testing.T) { + tests := []struct { + name string + setupMocks func(*MockFrontendAPI, *kClient.SettingsFlow, string) + expectedError bool + }{ + { + name: "Success", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.SettingsFlow, returnTo string) { + mockKratosFrontendApi.EXPECT().CreateBrowserSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPICreateBrowserSettingsFlowRequest) (*kClient.SettingsFlow, *BrowserLocationChangeRequired, error) { + if rt := (*string)(reflect.ValueOf(r).FieldByName("returnTo").UnsafePointer()); *rt != returnTo { + t.Fatalf("expected returnTo to be %s, got %s", returnTo, *rt) + } + return flow, nil, nil + }, + ) + }, + expectedError: false, + }, + { + name: "Fail", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, flow *kClient.SettingsFlow, returnTo string) { + resp := http.Response{ + StatusCode: http.StatusNotFound, + } + mockKratosFrontendApi.EXPECT().CreateBrowserSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("")) + }, + expectedError: true, }, - RedirectBrowserTo: &redirectTo, } - errorBodyJson, _ := json.Marshal(errorPayload) - request := kClient.FrontendAPIUpdateSettingsFlowRequest{ - ApiService: mockKratosFrontendApi, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) + + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + returnTo := "https://example.com/ui/reset_complete" + flow := kClient.NewSettingsFlowWithDefaults() + request := kClient.FrontendAPICreateBrowserSettingsFlowRequest{ + ApiService: mockKratosFrontendApi, + } + + mockTracer.EXPECT().Start(ctx, "kratos.Service.CreateBrowserSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().CreateBrowserSettingsFlow(ctx).Times(1).Return(request) + tt.setupMocks(mockKratosFrontendApi, flow, returnTo) + + f, r, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).CreateBrowserSettingsFlow(ctx, returnTo, cookies) + + if tt.expectedError { + if f != nil { + t.Fatalf("expected flow to be %v not %v", nil, f) + } + if r != nil { + t.Fatalf("expected response to be %v not %v", nil, r) + } + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if f != flow { + t.Fatalf("expected flow to be %v not %v", flow, f) + } + if r != nil { + t.Fatalf("expected response to be nil not %v", r) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } +} - resp := &http.Response{ - StatusCode: http.StatusForbidden, - Header: http.Header{ - "Set-Cookie": []string{cookie.String()}, +func TestUpdateSettingsFlow(t *testing.T) { + tests := []struct { + name string + setupMocks func(*MockFrontendAPI, *MockLoggerInterface, *MockTracingInterface, *http.Response, *kClient.SettingsFlow) + expectedError bool + checkRedirect bool + expectedRedirect string + }{ + { + name: "Success", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response, flow *kClient.SettingsFlow) { + mockKratosFrontendApi.EXPECT().UpdateSettingsFlowExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.FrontendAPIUpdateSettingsFlowRequest) (*kClient.SettingsFlow, *http.Response, error) { + flowId := "flow" + if _flow := (*string)(reflect.ValueOf(r).FieldByName("flow").UnsafePointer()); *_flow != flowId { + t.Fatalf("expected id to be %s, got %s", flowId, *_flow) + } + return flow, resp, nil + }, + ) + }, + expectedError: false, + }, + { + name: "FailOnExecute", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response, flow *kClient.SettingsFlow) { + mockKratosFrontendApi.EXPECT().UpdateSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, resp, fmt.Errorf("error")) + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + }, + expectedError: true, + }, + { + name: "ForbiddenStatus", + setupMocks: func(mockKratosFrontendApi *MockFrontendAPI, mockLogger *MockLoggerInterface, mockTracer *MockTracingInterface, resp *http.Response, flow *kClient.SettingsFlow) { + cookie := &http.Cookie{Name: "test", Value: "test"} + resp.StatusCode = http.StatusForbidden + resp.Header = http.Header{ + "Set-Cookie": []string{cookie.String()}, + } + redirectTo := "http://kratos/self-service/login/browser?refresh=true" + sessionRequiredErrorId := "session_refresh_required" + errorPayload := ErrorBrowserLocationChangeRequired{ + Error: &kClient.GenericError{ + Id: &sessionRequiredErrorId, + }, + RedirectBrowserTo: &redirectTo, + } + errorBodyJson, _ := json.Marshal(errorPayload) + resp.Body = io.NopCloser(bytes.NewBuffer(errorBodyJson)) + + mockTracer.EXPECT().Start(gomock.Any(), gomock.Any()).Times(1).Return(context.Background(), trace.SpanFromContext(context.Background())) + mockKratosFrontendApi.EXPECT().UpdateSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, resp, fmt.Errorf("forbidden")) + }, + expectedError: false, + checkRedirect: true, + expectedRedirect: "http://kratos/self-service/login/browser?refresh=true", }, - Body: io.NopCloser(bytes.NewBuffer(errorBodyJson)), } - mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockTracer.EXPECT().Start(ctx, gomock.Any()).Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) - mockKratosFrontendApi.EXPECT().UpdateSettingsFlow(ctx).Times(1).Return(request) - mockKratosFrontendApi.EXPECT().UpdateSettingsFlowExecute(gomock.Any()).Times(1).Return(nil, resp, fmt.Errorf("forbidden")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - f, r, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateSettingsFlow(ctx, flowId, *body, cookies) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosFrontendApi := NewMockFrontendAPI(ctrl) - if f != nil { - t.Fatalf("expected flow to be %v, not %v", nil, f) - } - if r == nil { - t.Fatalf("expected redirect info to be not nil") - } - if *r.RedirectTo != redirectTo { - t.Errorf("expected redirect url %s, got %s", redirectTo, *r.RedirectTo) - } - if len(c) == 0 { - t.Fatalf("expected cookies, got empty list") - } - if c[0].Name != "test" { - t.Fatalf("expected cookie name to be 'test', got %v", c[0].Name) - } - if err != nil { - t.Fatalf("expected error to be nil, got %v", err) + ctx := context.Background() + cookies := make([]*http.Cookie, 0) + cookie := &http.Cookie{Name: "test", Value: "test"} + cookies = append(cookies, cookie) + flowId := "flow" + + flow := kClient.NewSettingsFlowWithDefaults() + flowJson, _ := json.Marshal(flow) + body := new(kClient.UpdateSettingsFlowBody) + request := kClient.FrontendAPIUpdateSettingsFlowRequest{ + ApiService: mockKratosFrontendApi, + } + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + Body: io.NopCloser(bytes.NewBuffer(flowJson)), + } + + mockTracer.EXPECT().Start(ctx, "kratos.Service.UpdateSettingsFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockKratos.EXPECT().FrontendApi().Times(1).Return(mockKratosFrontendApi) + mockKratosFrontendApi.EXPECT().UpdateSettingsFlow(ctx).Times(1).Return(request) + tt.setupMocks(mockKratosFrontendApi, mockLogger, mockTracer, &resp, flow) + + f, r, c, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).UpdateSettingsFlow(ctx, flowId, *body, cookies) + + if tt.expectedError { + if f != nil { + t.Fatalf("expected flow to be %v not %+v", nil, f) + } + if r != nil { + t.Fatalf("expected redirect info to be %v not %+v", nil, r) + } + if c != nil { + t.Fatalf("expected header to be %v not %v", nil, c) + } + if err == nil { + t.Fatalf("expected error not nil") + } + } else if tt.checkRedirect { + if f != nil { + t.Fatalf("expected flow to be %v, not %v", nil, f) + } + if r == nil { + t.Fatalf("expected redirect info to be not nil") + } + if *r.RedirectTo != tt.expectedRedirect { + t.Errorf("expected redirect url %s, got %s", tt.expectedRedirect, *r.RedirectTo) + } + if len(c) == 0 { + t.Fatalf("expected cookies, got empty list") + } + if err != nil { + t.Fatalf("expected error to be nil, got %v", err) + } + } else { + if !reflect.DeepEqual(c, resp.Cookies()) { + t.Fatalf("expected cookies to be %v not %v", resp.Cookies(), c) + } + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } -func TestHasNotEnoughLookupSecretsLeftSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +func TestHasNotEnoughLookupSecretsLeft(t *testing.T) { + tests := []struct { + name string + identity *kClient.Identity + executeError error + expectedResult bool + expectedError bool + expectDebug bool + expectErrorf bool + }{ + { + name: "EnoughCodes", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{ + "lookup_secret": { + Config: map[string]interface{}{ + "recovery_codes": []map[string]interface{}{ + {"code": "a"}, + {"code": "b"}, + {"code": "c"}, + {"code": "d"}, + }, + }, + }, + } + return &creds + }(), + }, + expectedResult: false, + expectedError: false, + }, + { + name: "NotEnoughCodes", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{ + "lookup_secret": { + Config: map[string]interface{}{ + "recovery_codes": []map[string]interface{}{ + {"code": "a"}, + {"code": "b"}, + }, + }, + }, + } + return &creds + }(), + }, + expectedResult: true, + expectedError: false, + expectDebug: true, + }, + { + name: "MissingLookupSecret", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{} + return &creds + }(), + }, + expectedResult: false, + expectedError: false, + expectDebug: true, + }, + { + name: "MissingRecoveryCodes", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{ + "lookup_secret": { + Config: map[string]interface{}{}, + }, + } + return &creds + }(), + }, + expectedResult: false, + expectedError: false, + expectDebug: true, + }, + { + name: "MarshalError", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{ + "lookup_secret": { + Config: map[string]interface{}{ + "recovery_codes": func() {}, + }, + }, + } + return &creds + }(), + }, + expectedResult: false, + expectedError: true, + expectErrorf: true, + }, + { + name: "UnmarshalError", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{ + "lookup_secret": { + Config: map[string]interface{}{ + "recovery_codes": "invalid", + }, + }, + } + return &creds + }(), + }, + expectedResult: false, + expectedError: true, + expectErrorf: true, + }, + { + name: "FailonGetIdentityExecute", + identity: nil, + executeError: fmt.Errorf("error"), + expectedError: true, + }, + } - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosIdentityApi := NewMockIdentityAPI(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - ctx := context.Background() - cookie := &http.Cookie{Name: "test", Value: "test"} - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - identityRequest := kClient.IdentityAPIGetIdentityRequest{ - ApiService: mockKratosIdentityApi, - } - identity := kClient.Identity{ - Id: "test", - } + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosIdentityApi := NewMockIdentityAPI(ctrl) - mockTracer.EXPECT().Start(ctx, "kratos.Service.HasNotEnoughLookupSecretsLeft").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAdminKratos.EXPECT().IdentityApi().Times(1).Return(mockKratosIdentityApi) + ctx := context.Background() + cookie := &http.Cookie{Name: "test", Value: "test"} + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } + identityRequest := kClient.IdentityAPIGetIdentityRequest{ + ApiService: mockKratosIdentityApi, + } - mockKratosIdentityApi.EXPECT().GetIdentity(ctx, gomock.Any()).Times(1).Return(identityRequest) - mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.IdentityAPIGetIdentityRequest) (*kClient.Identity, *http.Response, error) { - return &identity, &resp, nil - }, - ) - mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).Times(1) + mockTracer.EXPECT().Start(ctx, "kratos.Service.HasNotEnoughLookupSecretsLeft").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockAdminKratos.EXPECT().IdentityApi().Times(1).Return(mockKratosIdentityApi) + mockKratosIdentityApi.EXPECT().GetIdentity(ctx, gomock.Any()).Times(1).Return(identityRequest) + if tt.expectDebug { + mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).Times(1) + } + if tt.expectErrorf { + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1) + } + identity := tt.identity + if identity == nil { + identity = &kClient.Identity{Id: "test"} + } + mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).Return(identity, &resp, tt.executeError) - hasNotEnoughLookupSecretsLeft, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).HasNotEnoughLookupSecretsLeft(ctx, "test") + hasNotEnoughLookupSecretsLeft, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).HasNotEnoughLookupSecretsLeft(ctx, "test") - if hasNotEnoughLookupSecretsLeft != false { - t.Fatalf("expected return value to be false not %v", hasNotEnoughLookupSecretsLeft) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) + if hasNotEnoughLookupSecretsLeft != tt.expectedResult { + t.Fatalf("expected return value to be %v not %v", tt.expectedResult, hasNotEnoughLookupSecretsLeft) + } + if tt.expectedError { + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } -func TestHasNotEnoughLookupSecretsLeftFailonGetIdentityExecute(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosIdentityApi := NewMockIdentityAPI(ctrl) - - ctx := context.Background() - cookie := &http.Cookie{Name: "test", Value: "test"} - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - identityRequest := kClient.IdentityAPIGetIdentityRequest{ - ApiService: mockKratosIdentityApi, +func TestHasTOTPAvailable(t *testing.T) { + tests := []struct { + name string + identity *kClient.Identity + executeError error + expected bool + expectedError bool + }{ + { + name: "HasTotp", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{ + "totp": {}, + } + return &creds + }(), + }, + expected: true, + expectedError: false, + }, + { + name: "NoTotp", + identity: &kClient.Identity{ + Id: "test", + Credentials: func() *map[string]kClient.IdentityCredentials { + creds := map[string]kClient.IdentityCredentials{} + return &creds + }(), + }, + expected: false, + expectedError: false, + }, + { + name: "ExecuteError", + identity: nil, + executeError: fmt.Errorf("error"), + expected: false, + expectedError: true, + }, } - mockTracer.EXPECT().Start(ctx, "kratos.Service.HasNotEnoughLookupSecretsLeft").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAdminKratos.EXPECT().IdentityApi().Times(1).Return(mockKratosIdentityApi) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockKratosIdentityApi.EXPECT().GetIdentity(ctx, gomock.Any()).Times(1).Return(identityRequest) - mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosIdentityApi := NewMockIdentityAPI(ctrl) - hasNotEnoughLookupSecretsLeft, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).HasNotEnoughLookupSecretsLeft(ctx, "test") + ctx := context.Background() + cookie := &http.Cookie{Name: "test", Value: "test"} + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } + identityRequest := kClient.IdentityAPIGetIdentityRequest{ + ApiService: mockKratosIdentityApi, + } - if hasNotEnoughLookupSecretsLeft != false { - t.Fatalf("expected return value to be false not %v", hasNotEnoughLookupSecretsLeft) - } - if err == nil { - t.Fatalf("expected error not nil") - } -} + mockTracer.EXPECT().Start(ctx, "kratos.Service.HasTOTPAvailable").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockAdminKratos.EXPECT().IdentityApi().Times(1).Return(mockKratosIdentityApi) + mockKratosIdentityApi.EXPECT().GetIdentity(ctx, gomock.Any()).Times(1).Return(identityRequest) -func TestHasWebAuthnAvailableSuccess(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + identity := tt.identity + if identity == nil { + identity = &kClient.Identity{Id: "test"} + } + mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).Return(identity, &resp, tt.executeError) - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosIdentityApi := NewMockIdentityAPI(ctrl) + hasTotp, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).HasTOTPAvailable(ctx, "test") - ctx := context.Background() - cookie := &http.Cookie{Name: "test", Value: "test"} - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, - } - identityRequest := kClient.IdentityAPIGetIdentityRequest{ - ApiService: mockKratosIdentityApi, + if hasTotp != tt.expected { + t.Fatalf("expected return value to be %v not %v", tt.expected, hasTotp) + } + if tt.expectedError { + if err == nil { + t.Fatalf("expected error not nil") + } + } else if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + }) } - identity := kClient.Identity{ - Id: "test", +} + +func TestHydrateKratosLoginFlow(t *testing.T) { + newLoginFlow := func(returnTo string) *kClient.LoginFlow { + now := time.Now() + ui := kClient.NewUiContainer("https://example.com/ui", "POST", []kClient.UiNode{}) + flow := kClient.NewLoginFlow(now.Add(time.Hour), "flow-id", now, "https://example.com/login", "state", "browser", *ui) + flow.ReturnTo = &returnTo + return flow } - mockTracer.EXPECT().Start(ctx, "kratos.Service.HasWebAuthnAvailable").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAdminKratos.EXPECT().IdentityApi().Times(1).Return(mockKratosIdentityApi) - mockKratosIdentityApi.EXPECT().GetIdentity(ctx, gomock.Any()).Times(1).Return(identityRequest) - mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).DoAndReturn( - func(r kClient.IdentityAPIGetIdentityRequest) (*kClient.Identity, *http.Response, error) { - return &identity, &resp, nil + tests := []struct { + name string + flow *kClient.LoginFlow + setupMocks func(context.Context, *MockHydraClientInterface, *MockOAuth2API) + expectChallenge string + expectOauth2Req bool + expectError bool + expectGetReq bool + }{ + { + name: "NoLoginChallenge", + flow: func() *kClient.LoginFlow { + return newLoginFlow("https://example.com") + }(), + setupMocks: func(context.Context, *MockHydraClientInterface, *MockOAuth2API) {}, + expectChallenge: "", + expectOauth2Req: false, + expectError: false, + expectGetReq: false, }, - ) - mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).Times(1) + { + name: "AlreadyHydrated", + flow: func() *kClient.LoginFlow { + flow := newLoginFlow("https://example.com?login_challenge=abc") + flow.Oauth2LoginRequest = kClient.NewOAuth2LoginRequest() + return flow + }(), + setupMocks: func(context.Context, *MockHydraClientInterface, *MockOAuth2API) {}, + expectChallenge: "", + expectOauth2Req: true, + expectError: false, + expectGetReq: false, + }, + { + name: "HydrateError", + flow: func() *kClient.LoginFlow { + return newLoginFlow("https://example.com?login_challenge=abc") + }(), + setupMocks: func(ctx context.Context, mockHydra *MockHydraClientInterface, mockHydraOauthApi *MockOAuth2API) { + getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ApiService: mockHydraOauthApi} + mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).Return(nil, &http.Response{}, fmt.Errorf("error")) + }, + expectChallenge: "abc", + expectOauth2Req: false, + expectError: true, + expectGetReq: true, + }, + { + name: "HydrateSuccess", + flow: func() *kClient.LoginFlow { + return newLoginFlow("https://example.com?login_challenge=abc") + }(), + setupMocks: func(ctx context.Context, mockHydra *MockHydraClientInterface, mockHydraOauthApi *MockOAuth2API) { + getLoginRequest := hClient.OAuth2APIGetOAuth2LoginRequestRequest{ApiService: mockHydraOauthApi} + hydraLoginRequest := hClient.NewOAuth2LoginRequestWithDefaults() + mockHydra.EXPECT().OAuth2API().Times(1).Return(mockHydraOauthApi) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequest(ctx).Times(1).Return(getLoginRequest) + mockHydraOauthApi.EXPECT().GetOAuth2LoginRequestExecute(gomock.Any()).Times(1).Return(hydraLoginRequest, &http.Response{}, nil) + }, + expectChallenge: "abc", + expectOauth2Req: true, + expectError: false, + expectGetReq: true, + }, + } - HasWebAuthnAvailable, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).HasWebAuthnAvailable(ctx, "test") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - if HasWebAuthnAvailable != false { - t.Fatalf("expected return value to be false not %v", HasWebAuthnAvailable) - } - if err != nil { - t.Fatalf("expected error to be nil not %v", err) - } -} + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockHydraOauthApi := NewMockOAuth2API(ctrl) -func TestHasWebAuthnAvailableFailOnGetIdentityExecute(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctx := context.Background() + mockTracer.EXPECT().Start(ctx, "kratos.Service.hydrateKratosLoginFlow").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + if tt.expectGetReq { + mockTracer.EXPECT().Start(ctx, "kratos.Service.GetLoginRequest").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + } + tt.setupMocks(ctx, mockHydra, mockHydraOauthApi) - mockLogger := NewMockLoggerInterface(ctrl) - mockHydra := NewMockHydraClientInterface(ctrl) - mockKratos := NewMockKratosClientInterface(ctrl) - mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) - mockAuthz := NewMockAuthorizerInterface(ctrl) - mockTracer := NewMockTracingInterface(ctrl) - mockMonitor := monitoring.NewMockMonitorInterface(ctrl) - mockKratosIdentityApi := NewMockIdentityAPI(ctrl) + flow, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).hydrateKratosLoginFlow(ctx, tt.flow) - ctx := context.Background() - cookie := &http.Cookie{Name: "test", Value: "test"} - resp := http.Response{ - Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + if tt.expectError { + if err == nil { + t.Fatalf("expected error not nil") + } + } else if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + if flow == nil { + t.Fatalf("expected flow not nil") + } + if tt.expectChallenge != "" { + if flow.Oauth2LoginChallenge == nil || *flow.Oauth2LoginChallenge != tt.expectChallenge { + t.Fatalf("expected login challenge %s", tt.expectChallenge) + } + } + if tt.expectOauth2Req { + if flow.Oauth2LoginRequest == nil { + t.Fatalf("expected oauth2 login request to be set") + } + } else if flow.Oauth2LoginRequest != nil && tt.name != "AlreadyHydrated" { + t.Fatalf("expected oauth2 login request to be nil") + } + }) } - identityRequest := kClient.IdentityAPIGetIdentityRequest{ - ApiService: mockKratosIdentityApi, +} + +func TestHasWebAuthnAvailable(t *testing.T) { + tests := []struct { + name string + identity *kClient.Identity + getIdentityErr error + expected bool + expectedError bool + debugCalls int + }{ + { + name: "NoCredentials", + identity: &kClient.Identity{Id: "test"}, + expected: false, + expectedError: false, + debugCalls: 1, + }, + { + name: "NoWebauthnCredentials", + identity: func() *kClient.Identity { + identity := kClient.Identity{Id: "test"} + credentials := map[string]kClient.IdentityCredentials{} + identity.Credentials = &credentials + return &identity + }(), + expected: false, + expectedError: false, + debugCalls: 1, + }, + { + name: "NoCredentialsList", + identity: func() *kClient.Identity { + identity := kClient.Identity{Id: "test"} + credentials := map[string]kClient.IdentityCredentials{ + "webauthn": {Config: map[string]interface{}{"credentials": "invalid"}}, + } + identity.Credentials = &credentials + return &identity + }(), + expected: false, + expectedError: false, + debugCalls: 1, + }, + { + name: "PasswordlessOnly", + identity: func() *kClient.Identity { + identity := kClient.Identity{Id: "test"} + credentials := map[string]kClient.IdentityCredentials{ + "webauthn": {Config: map[string]interface{}{"credentials": []interface{}{map[string]interface{}{"is_passwordless": true}}}}, + } + identity.Credentials = &credentials + return &identity + }(), + expected: false, + expectedError: false, + debugCalls: 0, + }, + { + name: "HasTwoFactorWebauthn", + identity: func() *kClient.Identity { + identity := kClient.Identity{Id: "test"} + credentials := map[string]kClient.IdentityCredentials{ + "webauthn": {Config: map[string]interface{}{"credentials": []interface{}{map[string]interface{}{"is_passwordless": false}}}}, + } + identity.Credentials = &credentials + return &identity + }(), + expected: true, + expectedError: false, + debugCalls: 1, + }, + { + name: "FailOnGetIdentityExecute", + identity: nil, + getIdentityErr: fmt.Errorf("error"), + expected: false, + expectedError: true, + debugCalls: 0, + }, } - mockTracer.EXPECT().Start(ctx, "kratos.Service.HasWebAuthnAvailable").Times(1).Return(ctx, trace.SpanFromContext(ctx)) - mockAdminKratos.EXPECT().IdentityApi().Times(1).Return(mockKratosIdentityApi) - mockKratosIdentityApi.EXPECT().GetIdentity(ctx, gomock.Any()).Times(1).Return(identityRequest) - mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).Return(nil, &resp, fmt.Errorf("error")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLoggerInterface(ctrl) + mockHydra := NewMockHydraClientInterface(ctrl) + mockKratos := NewMockKratosClientInterface(ctrl) + mockAdminKratos := NewMockKratosAdminClientInterface(ctrl) + mockAuthz := NewMockAuthorizerInterface(ctrl) + mockTracer := NewMockTracingInterface(ctrl) + mockMonitor := monitoring.NewMockMonitorInterface(ctrl) + mockKratosIdentityApi := NewMockIdentityAPI(ctrl) + + ctx := context.Background() + cookie := &http.Cookie{Name: "test", Value: "test"} + resp := http.Response{ + Header: http.Header{"Set-Cookie": []string{cookie.Raw}}, + } + identityRequest := kClient.IdentityAPIGetIdentityRequest{ + ApiService: mockKratosIdentityApi, + } + + mockTracer.EXPECT().Start(ctx, "kratos.Service.HasWebAuthnAvailable").Times(1).Return(ctx, trace.SpanFromContext(ctx)) + mockAdminKratos.EXPECT().IdentityApi().Times(1).Return(mockKratosIdentityApi) + mockKratosIdentityApi.EXPECT().GetIdentity(ctx, gomock.Any()).Times(1).Return(identityRequest) + if tt.debugCalls > 0 { + mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).Times(tt.debugCalls) + } + if tt.expectedError { + mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).Return(nil, &resp, tt.getIdentityErr) + } else { + mockKratosIdentityApi.EXPECT().GetIdentityExecute(gomock.Any()).Times(1).DoAndReturn( + func(r kClient.IdentityAPIGetIdentityRequest) (*kClient.Identity, *http.Response, error) { + return tt.identity, &resp, nil + }, + ) + } - HasWebAuthnAvailable, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).HasWebAuthnAvailable(ctx, "test") + hasWebAuthnAvailable, err := NewService(mockKratos, mockAdminKratos, mockHydra, mockAuthz, false, mockTracer, mockMonitor, mockLogger).HasWebAuthnAvailable(ctx, "test") - if HasWebAuthnAvailable != false { - t.Fatalf("expected return value to be false not %v", HasWebAuthnAvailable) - } - if err == nil { - t.Fatalf("expected error not nil") + if hasWebAuthnAvailable != tt.expected { + t.Fatalf("expected return value to be %v not %v", tt.expected, hasWebAuthnAvailable) + } + if tt.expectedError { + if err == nil { + t.Fatalf("expected error not nil") + } + } else { + if err != nil { + t.Fatalf("expected error to be nil not %v", err) + } + } + }) } } From 387139c58e7ccd3948c68b1a1540595d39188af4 Mon Sep 17 00:00:00 2001 From: shipperizer Date: Tue, 3 Feb 2026 12:45:33 +0000 Subject: [PATCH 2/2] docs: adjust docstrings and copyrights --- pkg/kratos/cookies.go | 8 ++++++++ pkg/kratos/encryption.go | 4 ++++ pkg/kratos/handlers.go | 35 +++++++++++++++++++++++++++++++++++ pkg/kratos/interfaces.go | 12 ++++++++++++ pkg/kratos/service.go | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+) diff --git a/pkg/kratos/cookies.go b/pkg/kratos/cookies.go index 2e34d4350..92f290c58 100644 --- a/pkg/kratos/cookies.go +++ b/pkg/kratos/cookies.go @@ -1,6 +1,7 @@ // Copyright 2024 Canonical Ltd. // SPDX-License-Identifier: AGPL-3.0 +// Package kratos provides state cookie management for login flow state tracking. package kratos import ( @@ -34,6 +35,7 @@ type FlowStateCookie struct { BackupCodeUsed bool `json:"bc,omitempty"` } +// SetStateCookie sets a state cookie on the HTTP response. func (a *AuthCookieManager) SetStateCookie(w http.ResponseWriter, state FlowStateCookie) error { rawState, err := json.Marshal(state) if err != nil { @@ -42,6 +44,7 @@ func (a *AuthCookieManager) SetStateCookie(w http.ResponseWriter, state FlowStat return a.setCookie(w, stateCookieName, string(rawState), defaultCookiePath, a.cookieTTL, http.SameSiteLaxMode) } +// GetStateCookie retrieves a state cookie from the HTTP request. func (a *AuthCookieManager) GetStateCookie(r *http.Request) (FlowStateCookie, error) { var ret FlowStateCookie c, err := a.getCookie(r, stateCookieName) @@ -52,10 +55,12 @@ func (a *AuthCookieManager) GetStateCookie(r *http.Request) (FlowStateCookie, er return ret, err } +// ClearStateCookie clears the state cookie from the HTTP response. func (a *AuthCookieManager) ClearStateCookie(w http.ResponseWriter) { a.clearCookie(w, stateCookieName, defaultCookiePath) } +// setCookie sets a cookie on the HTTP response with the specified parameters. func (a *AuthCookieManager) setCookie(w http.ResponseWriter, name, value string, path string, ttl time.Duration, sameSitePolicy http.SameSite) error { if value == "" { return nil @@ -83,6 +88,7 @@ func (a *AuthCookieManager) setCookie(w http.ResponseWriter, name, value string, return nil } +// clearCookie removes a cookie from the HTTP response. func (a *AuthCookieManager) clearCookie(w http.ResponseWriter, name string, path string) { http.SetCookie(w, &http.Cookie{ Name: name, @@ -96,6 +102,7 @@ func (a *AuthCookieManager) clearCookie(w http.ResponseWriter, name string, path }) } +// getCookie retrieves a cookie value from the HTTP request. func (a *AuthCookieManager) getCookie(r *http.Request, name string) (string, error) { cookie, err := r.Cookie(name) if err != nil { @@ -111,6 +118,7 @@ func (a *AuthCookieManager) getCookie(r *http.Request, name string) (string, err return value, nil } +// NewAuthCookieManager creates a new AuthCookieManager instance. func NewAuthCookieManager( cookieTTLSeconds int, encrypt EncryptInterface, diff --git a/pkg/kratos/encryption.go b/pkg/kratos/encryption.go index 606af8d90..40d80e708 100644 --- a/pkg/kratos/encryption.go +++ b/pkg/kratos/encryption.go @@ -1,6 +1,7 @@ // Copyright 2024 Canonical Ltd. // SPDX-License-Identifier: AGPL-3.0 +// Package kratos provides AES encryption and decryption utilities for securing flow state. package kratos import ( @@ -63,6 +64,7 @@ func (e *Encrypt) Decrypt(hexData string) (string, error) { return string(decryptedData), nil } +// splitNonceFromPayload extracts the nonce and payload from an encrypted byte array. func (e *Encrypt) splitNonceFromPayload(encrypted []byte) ([]byte, []byte, error) { nonceSize := e.gcm.NonceSize() if len(encrypted) <= nonceSize { @@ -73,6 +75,7 @@ func (e *Encrypt) splitNonceFromPayload(encrypted []byte) ([]byte, []byte, error return noncePart, payloadPart, nil } +// generateCipherNonce generates a random nonce for encryption. func (e *Encrypt) generateCipherNonce() ([]byte, error) { nonce := make([]byte, e.gcm.NonceSize()) if _, err := ioReadFull(rand.Reader, nonce); err != nil { @@ -82,6 +85,7 @@ func (e *Encrypt) generateCipherNonce() ([]byte, error) { return nonce, nil } +// NewEncrypt creates a new Encrypt instance with the provided secret key and interfaces. func NewEncrypt(secretKey []byte, logger logging.LoggerInterface, tracer tracing.TracingInterface) *Encrypt { e := new(Encrypt) c, err := aes.NewCipher(secretKey) diff --git a/pkg/kratos/handlers.go b/pkg/kratos/handlers.go index 4805160fd..ee01abbf8 100644 --- a/pkg/kratos/handlers.go +++ b/pkg/kratos/handlers.go @@ -1,3 +1,8 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: AGPL-3.0 + +// Package kratos provides HTTP handlers for Ory Kratos authentication flows. +// It handles login, recovery, and settings flow operations through a REST API. package kratos import ( @@ -38,6 +43,7 @@ type API struct { logger logging.LoggerInterface } +// RegisterEndpoints registers all HTTP endpoints for Kratos authentication flows with the chi router. func (a *API) RegisterEndpoints(mux *chi.Mux) { mux.Post("/api/kratos/self-service/login", a.handleUpdateFlow) mux.Post("/api/kratos/self-service/login/id-first", a.handleUpdateIdentifierFirstFlow) @@ -52,6 +58,7 @@ func (a *API) RegisterEndpoints(mux *chi.Mux) { mux.Get("/api/kratos/self-service/settings/flows", a.handleGetSettingsFlow) } +// handleCreateFlow handles HTTP GET requests to create a new login flow. // TODO: Validate response when server error handling is implemented func (a *API) handleCreateFlow(w http.ResponseWriter, r *http.Request) { var ( @@ -164,6 +171,7 @@ func (a *API) handleCreateFlow(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(response) } +// handleCreateFlowNewSession creates a new login flow for unauthenticated users. func (a *API) handleCreateFlowNewSession(r *http.Request, aal, returnTo, loginChallenge string, refresh bool) (*client.LoginFlow, []*http.Cookie, error) { // redirect user to this endpoint with the login_challenge after login // see https://github.com/ory/kratos/issues/3052 @@ -195,6 +203,7 @@ func (a *API) handleCreateFlowNewSession(r *http.Request, aal, returnTo, loginCh return flow, cookies, nil } +// handleCreateFlowWithSession handles flow creation for authenticated users. func (a *API) handleCreateFlowWithSession(r *http.Request, session *client.Session, loginChallenge string) (*BrowserLocationChangeRequired, []*http.Cookie, error) { response, cookies, err := a.service.AcceptLoginRequest(r.Context(), session, loginChallenge) if err != nil { @@ -227,6 +236,7 @@ func (a *API) returnToUrl(loginChallenge string) (string, error) { return returnTo, nil } +// handleGetLoginFlow retrieves an existing login flow by ID. // TODO: Validate response when server error handling is implemented func (a *API) handleGetLoginFlow(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() @@ -251,6 +261,7 @@ func (a *API) handleGetLoginFlow(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(flow) } +// handleUpdateIdentifierFirstFlow handles POST requests to update an identifier-first login flow. func (a *API) handleUpdateIdentifierFirstFlow(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() flowId := q.Get("flow") @@ -279,6 +290,7 @@ func (a *API) handleUpdateIdentifierFirstFlow(w http.ResponseWriter, r *http.Req } } +// handleUpdateFlow handles POST requests to update a login flow. // TODO: Validate response when server error handling is implemented func (a *API) handleUpdateFlow(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() @@ -484,6 +496,7 @@ func (a *API) shouldEnforceMFA(ctx context.Context, cookies []*http.Cookie) (boo return a.shouldEnforceMFAWithSession(ctx, session) } +// shouldEnforceMFAWithSession checks if MFA should be enforced for the given session. func (a *API) shouldEnforceMFAWithSession(ctx context.Context, session *client.Session) (bool, error) { ctx, span := a.tracer.Start(ctx, "kratos.API.shouldEnforceMFAWithSession") defer span.End() @@ -507,6 +520,7 @@ func (a *API) shouldEnforceMFAWithSession(ctx context.Context, session *client.S return !totpAvailable, nil } +// is40xError checks if the error is a 4xx HTTP error from Kratos. func (a *API) is40xError(err error) bool { if openAPIErr, ok := err.(*client.GenericOpenAPIError); ok { if genericKratosErr, ok := openAPIErr.Model().(client.ErrorGeneric); ok { @@ -518,6 +532,7 @@ func (a *API) is40xError(err error) bool { return false } +// shouldEnforceWebAuthnWithSession checks if WebAuthn enforcement is needed for the given session. func (a *API) shouldEnforceWebAuthnWithSession(ctx context.Context, session *client.Session) (bool, error) { ctx, span := a.tracer.Start(ctx, "kratos.API.shouldEnforceWebAuthnWithSession") defer span.End() @@ -539,6 +554,7 @@ func (a *API) shouldEnforceWebAuthnWithSession(ctx context.Context, session *cli return false, nil } +// webAuthnSettingsRedirect redirects the user to the WebAuthn settings page. func (a *API) webAuthnSettingsRedirect(w http.ResponseWriter, r *http.Request, returnTo string, flowStateCookie FlowStateCookie) { redirect, err := url.JoinPath("/", a.contextPath, "/ui/setup_passkey") if err != nil { @@ -569,6 +585,7 @@ func (a *API) webAuthnSettingsRedirect(w http.ResponseWriter, r *http.Request, r }) } +// mfaSettingsRedirect redirects the user to the MFA settings page. func (a *API) mfaSettingsRedirect(w http.ResponseWriter, r *http.Request, returnTo string, flowStateCookie FlowStateCookie) { redirect, err := url.JoinPath("/", a.contextPath, "/ui/setup_secure") @@ -604,6 +621,7 @@ func (a *API) mfaSettingsRedirect(w http.ResponseWriter, r *http.Request, return }) } +// lookupSecretsSettingsRedirect redirects the user to regenerate backup codes. func (a *API) lookupSecretsSettingsRedirect(w http.ResponseWriter, r *http.Request, flowId, returnTo string, flowStateCookie FlowStateCookie) { redirect, err := url.JoinPath("/", a.contextPath, ui.UI, "/backup_codes_regenerate") if err != nil { @@ -636,6 +654,7 @@ func (a *API) lookupSecretsSettingsRedirect(w http.ResponseWriter, r *http.Reque }) } +// handleKratosError handles HTTP GET requests for retrieving flow error details. // TODO: Validate response when server error handling is implemented func (a *API) handleKratosError(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() @@ -659,6 +678,7 @@ func (a *API) handleKratosError(w http.ResponseWriter, r *http.Request) { w.Write(resp) } +// handleGetRecoveryFlow retrieves an existing recovery flow by ID. func (a *API) handleGetRecoveryFlow(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() @@ -680,6 +700,7 @@ func (a *API) handleGetRecoveryFlow(w http.ResponseWriter, r *http.Request) { w.Write(resp) } +// handleUpdateRecoveryFlow handles POST requests to update a recovery flow. func (a *API) handleUpdateRecoveryFlow(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() flowId := q.Get("flow") @@ -711,6 +732,7 @@ func (a *API) handleUpdateRecoveryFlow(w http.ResponseWriter, r *http.Request) { }) } +// handleCreateRecoveryFlow handles HTTP GET requests to create a new recovery flow. func (a *API) handleCreateRecoveryFlow(w http.ResponseWriter, r *http.Request) { returnTo := r.URL.Query().Get("return_to") @@ -747,6 +769,7 @@ func (a *API) handleCreateRecoveryFlow(w http.ResponseWriter, r *http.Request) { w.Write(resp) } +// handleGetSettingsFlow retrieves an existing settings flow by ID. func (a *API) handleGetSettingsFlow(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() @@ -777,6 +800,7 @@ func (a *API) handleGetSettingsFlow(w http.ResponseWriter, r *http.Request) { w.Write(resp) } +// handleUpdateSettingsFlow handles POST requests to update a settings flow. func (a *API) handleUpdateSettingsFlow(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() flowId := q.Get("flow") @@ -864,6 +888,7 @@ func (a *API) handleUpdateSettingsFlow(w http.ResponseWriter, r *http.Request) { w.Write(resp) } +// settingsReturnToURL constructs the return URL for a settings flow. func (a *API) settingsReturnToURL(r *http.Request, flowId string) (string, error) { currentFlow, _, err := a.service.GetSettingsFlow(r.Context(), flowId, r.Cookies()) if err != nil { @@ -887,6 +912,7 @@ func (a *API) settingsReturnToURL(r *http.Request, flowId string) (string, error return returnTo, nil } +// handleCreateSettingsFlow handles HTTP GET requests to create a new settings flow. func (a *API) handleCreateSettingsFlow(w http.ResponseWriter, r *http.Request) { returnTo := r.URL.Query().Get("return_to") @@ -917,11 +943,13 @@ func (a *API) handleCreateSettingsFlow(w http.ResponseWriter, r *http.Request) { w.Write(resp) } +// isHTMLRequest checks if the request accepts HTML content type. func (a *API) isHTMLRequest(r *http.Request) bool { // Treat requests that don't explicitly accept json as form submissions return r.Header.Get("Accept") != "application/json, text/plain, */*" } +// deleteKratosSession deletes the Kratos session cookie. func (a *API) deleteKratosSession(w http.ResponseWriter) { // To delete the session we delete the kratos session cookie. // This is hacky as it does not call the Kratos API and is likely to break on @@ -931,6 +959,7 @@ func (a *API) deleteKratosSession(w http.ResponseWriter) { http.SetCookie(w, c) } +// getReturnToFromContinueWith extracts the return_to URL from a list of ContinueWith items. func getReturnToFromContinueWith(continueWith []client.ContinueWith) *string { for _, c := range continueWith { if r := c.ContinueWithRedirectBrowserTo; r != nil { @@ -940,6 +969,7 @@ func getReturnToFromContinueWith(continueWith []client.ContinueWith) *string { return nil } +// kratosSessionUnsetCookie creates an HTTP cookie to unset the Kratos session. func kratosSessionUnsetCookie() *http.Cookie { return &http.Cookie{ Name: KRATOS_SESSION_COOKIE_NAME, @@ -951,6 +981,7 @@ func kratosSessionUnsetCookie() *http.Cookie { } } +// NewAPI creates a new API instance with the provided service and configuration. func NewAPI( service ServiceInterface, mfaEnabled, @@ -981,12 +1012,14 @@ func NewAPI( return a } +// setCookies sets the provided cookies on the HTTP response. func setCookies(w http.ResponseWriter, cookies []*http.Cookie, exclude ...string) { for _, c := range filterCookies(cookies, exclude...) { http.SetCookie(w, c) } } +// filterCookies returns a filtered list of cookies excluding those matching the provided names. func filterCookies(cookies []*http.Cookie, exclude ...string) []*http.Cookie { ret := []*http.Cookie{} l1: @@ -1001,12 +1034,14 @@ l1: return ret } +// hash computes an MD5-based hash of a plain string and returns the base64-encoded result. func hash(plain string) string { h := md5.New() h.Write([]byte(plain)) return base64.URLEncoding.EncodeToString(h.Sum(nil)) } +// validateHash verifies that a hash matches the expected signature. func validateHash(plain, sig string) bool { h := md5.New() h.Write([]byte(plain)) diff --git a/pkg/kratos/interfaces.go b/pkg/kratos/interfaces.go index 387db96ca..d907859c0 100644 --- a/pkg/kratos/interfaces.go +++ b/pkg/kratos/interfaces.go @@ -1,3 +1,7 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: AGPL-3.0 + +// Package kratos defines interfaces for Ory Kratos client interactions. package kratos import ( @@ -9,23 +13,28 @@ import ( "github.com/canonical/identity-platform-login-ui/internal/hydra" ) +// KratosClientInterface defines methods for interacting with Ory Kratos frontend API. type KratosClientInterface interface { FrontendApi() kClient.FrontendAPI ExecuteIdentifierFirstUpdateLoginRequest(context.Context, string, string, string, []*http.Cookie) (*http.Response, error) } +// KratosAdminClientInterface defines methods for interacting with Ory Kratos admin API. type KratosAdminClientInterface interface { IdentityApi() kClient.IdentityAPI } +// HydraClientInterface defines methods for interacting with Ory Hydra OAuth2 API. type HydraClientInterface interface { OAuth2API() hydra.OAuth2API } +// AuthorizerInterface defines methods for authorization and object listing. type AuthorizerInterface interface { ListObjects(context.Context, string, string, string) ([]string, error) } +// ServiceInterface defines the core service methods for Kratos authentication flows. type ServiceInterface interface { CheckSession(context.Context, []*http.Cookie) (*kClient.Session, []*http.Cookie, error) AcceptLoginRequest(context.Context, *kClient.Session, string) (*BrowserLocationChangeRequired, []*http.Cookie, error) @@ -52,6 +61,7 @@ type ServiceInterface interface { HasNotEnoughLookupSecretsLeft(context.Context, string) (bool, error) } +// AuthCookieManagerInterface defines methods for managing authentication state cookies. type AuthCookieManagerInterface interface { // SetStateCookie sets the nonce cookie on the response with the specified duration as MaxAge SetStateCookie(http.ResponseWriter, FlowStateCookie) error @@ -61,6 +71,7 @@ type AuthCookieManagerInterface interface { ClearStateCookie(http.ResponseWriter) } +// EncryptInterface defines methods for encrypting and decrypting data. type EncryptInterface interface { // Encrypt a plain text string, returns the encrypted string in hex format or an error Encrypt(string) (string, error) @@ -68,6 +79,7 @@ type EncryptInterface interface { Decrypt(string) (string, error) } +// RedirectToInterface defines methods for responses that include redirect information. type RedirectToInterface interface { GetCode() int GetRedirectTo() string diff --git a/pkg/kratos/service.go b/pkg/kratos/service.go index f65f8e2e9..89b66a675 100644 --- a/pkg/kratos/service.go +++ b/pkg/kratos/service.go @@ -1,3 +1,8 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: AGPL-3.0 + +// Package kratos provides integration with Ory Kratos authentication and identity management services. +// It includes login, recovery, settings flows, session management, and cookie-based state handling. package kratos import ( @@ -66,14 +71,17 @@ type ErrorBrowserLocationChangeRequired struct { RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` } +// HasError returns true if the response contains an error. func (e *BrowserLocationChangeRequired) HasError() bool { return e.Error != nil } +// HasRedirectTo returns true if the response contains a redirect URL. func (e *BrowserLocationChangeRequired) HasRedirectTo() bool { return e.RedirectTo != nil } +// GetCode returns the HTTP status code from the error, or OK if no error. func (e *BrowserLocationChangeRequired) GetCode() int { if !e.HasError() || e.Error.Code == nil { return http.StatusOK @@ -81,6 +89,7 @@ func (e *BrowserLocationChangeRequired) GetCode() int { return int(*e.Error.Code) } +// GetErrorId returns the error ID from the error response, or empty string if no error. func (e *BrowserLocationChangeRequired) GetErrorId() string { if !e.HasError() || e.Error.Id == nil { return "" @@ -88,6 +97,7 @@ func (e *BrowserLocationChangeRequired) GetErrorId() string { return *e.Error.Id } +// GetRedirectTo returns the redirect URL from the response, or empty string if not set. func (e *BrowserLocationChangeRequired) GetRedirectTo() string { if e.RedirectTo == nil { return "" @@ -95,6 +105,7 @@ func (e *BrowserLocationChangeRequired) GetRedirectTo() string { return *e.RedirectTo } +// BrowserLocationChangeRequired represents a response indicating browser location change requirements. type BrowserLocationChangeRequired struct { Error *kClient.GenericError `json:"error,omitempty"` // Points to where to redirect the user to next. @@ -114,6 +125,7 @@ type LookupSecrets []struct { UsedAt time.Time `json:"used_at,omitempty"` } +// CheckSession retrieves the current session information for a user from Kratos. func (s *Service) CheckSession(ctx context.Context, cookies []*http.Cookie) (*kClient.Session, []*http.Cookie, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.ToSession") defer span.End() @@ -137,6 +149,7 @@ func (s *Service) CheckSession(ctx context.Context, cookies []*http.Cookie) (*kC return session, resp.Cookies(), nil } +// AcceptLoginRequest accepts a Kratos login request and redirects the user. func (s *Service) AcceptLoginRequest(ctx context.Context, session *kClient.Session, lc string) (*BrowserLocationChangeRequired, []*http.Cookie, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.AcceptLoginRequest") defer span.End() @@ -183,6 +196,7 @@ func (s *Service) AcceptLoginRequest(ctx context.Context, session *kClient.Sessi return &BrowserLocationChangeRequired{RedirectTo: &redirectTo.RedirectTo}, resp.Cookies(), nil } +// GetLoginRequest retrieves the OAuth2 login request from Hydra. func (s *Service) GetLoginRequest(ctx context.Context, loginChallenge string) (*hClient.OAuth2LoginRequest, []*http.Cookie, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.GetLoginRequest") defer span.End() @@ -206,6 +220,7 @@ func (s *Service) GetLoginRequest(ctx context.Context, loginChallenge string) (* return redirectTo, resp.Cookies(), nil } +// MustReAuthenticate determines if a user needs to re-authenticate based on the session and login challenge. func (s *Service) MustReAuthenticate(ctx context.Context, hydraLoginChallenge string, session *kClient.Session, c FlowStateCookie) (bool, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.MustReAuthenticate") defer span.End() @@ -234,6 +249,7 @@ func (s *Service) MustReAuthenticate(ctx context.Context, hydraLoginChallenge st return !hydraLoginRequest.GetSkip(), nil } +// CreateBrowserLoginFlow creates a new browser login flow with the specified parameters. func (s *Service) CreateBrowserLoginFlow( ctx context.Context, aal, returnTo, loginChallenge string, refresh bool, cookies []*http.Cookie, ) (*kClient.LoginFlow, []*http.Cookie, error) { @@ -275,6 +291,7 @@ func (s *Service) CreateBrowserLoginFlow( return flow, resp.Cookies(), nil } +// CreateBrowserRecoveryFlow creates a new browser recovery flow. func (s *Service) CreateBrowserRecoveryFlow(ctx context.Context, returnTo string, cookies []*http.Cookie) (*kClient.RecoveryFlow, []*http.Cookie, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.CreateBrowserRecoveryFlow") defer span.End() @@ -296,6 +313,7 @@ func (s *Service) CreateBrowserRecoveryFlow(ctx context.Context, returnTo string return flow, resp.Cookies(), nil } +// CreateBrowserSettingsFlow creates a new browser settings flow. func (s *Service) CreateBrowserSettingsFlow(ctx context.Context, returnTo string, cookies []*http.Cookie) (*kClient.SettingsFlow, *BrowserLocationChangeRequired, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.CreateBrowserSettingsFlow") defer span.End() @@ -364,6 +382,7 @@ func (s *Service) GetLoginFlow(ctx context.Context, id string, cookies []*http.C return flow, resp.Cookies(), nil } +// GetRecoveryFlow retrieves a recovery flow by its ID from Kratos. func (s *Service) GetRecoveryFlow(ctx context.Context, id string, cookies []*http.Cookie) (*kClient.RecoveryFlow, []*http.Cookie, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.GetRecoveryFlow") defer span.End() @@ -386,6 +405,7 @@ func (s *Service) GetRecoveryFlow(ctx context.Context, id string, cookies []*htt return flow, resp.Cookies(), nil } +// GetSettingsFlow retrieves a settings flow by its ID from Kratos. func (s *Service) GetSettingsFlow(ctx context.Context, id string, cookies []*http.Cookie) (*kClient.SettingsFlow, *BrowserLocationChangeRequired, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.GetSettingsFlow") defer span.End() @@ -506,6 +526,7 @@ func (s *Service) UpdateRecoveryFlow( return returnToResp, resp.Cookies(), nil } +// UpdateIdentifierFirstLoginFlow updates an identifier-first login flow. func (s *Service) UpdateIdentifierFirstLoginFlow( ctx context.Context, flow string, body kClient.UpdateLoginFlowWithIdentifierFirstMethod, cookies []*http.Cookie, ) (*BrowserLocationChangeRequired, []*http.Cookie, error) { @@ -560,6 +581,7 @@ func (s *Service) UpdateIdentifierFirstLoginFlow( } } +// UpdateLoginFlow updates a login flow with the specified parameters. func (s *Service) UpdateLoginFlow( ctx context.Context, flow string, body kClient.UpdateLoginFlowBody, cookies []*http.Cookie, ) (*BrowserLocationChangeRequired, *kClient.SuccessfulNativeLogin, []*http.Cookie, error) { @@ -617,6 +639,7 @@ func (s *Service) UpdateLoginFlow( return nil, f, c, nil } +// UpdateSettingsFlow updates a settings flow with the specified parameters. func (s *Service) UpdateSettingsFlow( ctx context.Context, flow string, body kClient.UpdateSettingsFlowBody, cookies []*http.Cookie, ) (*kClient.SettingsFlow, *BrowserLocationChangeRequired, []*http.Cookie, error) { @@ -664,6 +687,7 @@ func (s *Service) UpdateSettingsFlow( return settingsFlow, nil, resp.Cookies(), nil } +// getUiError extracts UI error information from an HTTP response body. func (s *Service) getUiError(responseBody io.ReadCloser) (err error) { errorMessages := new(UiErrorMessages) body, _ := io.ReadAll(responseBody) @@ -724,6 +748,7 @@ func (s *Service) getUiError(responseBody io.ReadCloser) (err error) { return err } +// GetFlowError retrieves error details for a flow by its ID. func (s *Service) GetFlowError(ctx context.Context, id string) (*kClient.FlowError, []*http.Cookie, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.GetFlowError") defer span.End() @@ -771,6 +796,7 @@ func (s *Service) getProviderName(updateFlowBody *kClient.UpdateLoginFlowBody) s return "" } +// getClientName extracts the client name from a login flow. func (s *Service) getClientName(loginFlow *kClient.LoginFlow) string { oauth2LoginRequest := loginFlow.Oauth2LoginRequest if oauth2LoginRequest != nil { @@ -780,6 +806,7 @@ func (s *Service) getClientName(loginFlow *kClient.LoginFlow) string { return "" } +// FilterFlowProviderList filters the provider list in a login flow based on authorization rules. func (s *Service) FilterFlowProviderList(ctx context.Context, flow *kClient.LoginFlow) (*kClient.LoginFlow, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.FilterFlowProviderList") defer span.End() @@ -815,6 +842,7 @@ func (s *Service) FilterFlowProviderList(ctx context.Context, flow *kClient.Logi return flow, nil } +// ParseIdentifierFirstLoginFlowMethodBody parses the request body for an identifier-first login flow update. func (s *Service) ParseIdentifierFirstLoginFlowMethodBody(r *http.Request) (*kClient.UpdateLoginFlowWithIdentifierFirstMethod, []*http.Cookie, error) { defer r.Body.Close() @@ -913,6 +941,7 @@ func (s *Service) ParseLoginFlowMethodBody(r *http.Request) (*kClient.UpdateLogi return &ret, cookies, nil } +// ParseRecoveryFlowMethodBody parses the request body for a recovery flow update. func (s *Service) ParseRecoveryFlowMethodBody(r *http.Request) (*kClient.UpdateRecoveryFlowBody, error) { body := new(kClient.UpdateRecoveryFlowWithCodeMethod) @@ -930,6 +959,7 @@ func (s *Service) ParseRecoveryFlowMethodBody(r *http.Request) (*kClient.UpdateR return &ret, nil } +// ParseSettingsFlowMethodBody parses the request body for a settings flow update. func (s *Service) ParseSettingsFlowMethodBody(r *http.Request) (*kClient.UpdateSettingsFlowBody, error) { defer r.Body.Close() @@ -1016,6 +1046,7 @@ func (s *Service) contains(str []string, e string) bool { return false } +// HasTOTPAvailable checks if a user has TOTP (Time-based One-Time Password) available as a credential. func (s *Service) HasTOTPAvailable(ctx context.Context, id string) (bool, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.HasTOTPAvailable") defer span.End() @@ -1033,6 +1064,7 @@ func (s *Service) HasTOTPAvailable(ctx context.Context, id string) (bool, error) return ok, nil } +// HasWebAuthnAvailable checks if a user has WebAuthn available as a 2FA credential. func (s *Service) HasWebAuthnAvailable(ctx context.Context, id string) (bool, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.HasWebAuthnAvailable") defer span.End() @@ -1079,6 +1111,7 @@ func (s *Service) HasWebAuthnAvailable(ctx context.Context, id string) (bool, er return false, nil } +// HasNotEnoughLookupSecretsLeft checks if a user has fewer than the minimum required lookup secrets. func (s *Service) HasNotEnoughLookupSecretsLeft(ctx context.Context, id string) (bool, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.HasNotEnoughLookupSecretsLeft") defer span.End() @@ -1132,6 +1165,7 @@ func (s *Service) HasNotEnoughLookupSecretsLeft(ctx context.Context, id string) return true, nil } +// is1FAMethod determines if an authentication method is a first-factor authentication method. func (s *Service) is1FAMethod(method string) bool { switch method { case "password", "oidc": @@ -1191,6 +1225,7 @@ func (s *Service) hydrateKratosLoginFlow(ctx context.Context, flow *kClient.Logi return newFlow, nil } +// parseKratosRedirectResponse parses an HTTP response from Kratos into a BrowserLocationChangeRequired structure. func (s *Service) parseKratosRedirectResponse(ctx context.Context, resp *http.Response) (*BrowserLocationChangeRequired, error) { ctx, span := s.tracer.Start(ctx, "kratos.Service.parseKratosRedirectResponse") defer span.End() @@ -1208,6 +1243,7 @@ func (s *Service) parseKratosRedirectResponse(ctx context.Context, resp *http.Re }, nil } +// NewService creates and returns a new Service instance with the provided clients and interfaces. func NewService(kratos KratosClientInterface, kratosAdmin KratosAdminClientInterface, hydra HydraClientInterface, authzClient AuthorizerInterface, oidcWebAuthnSequencingEnabled bool, tracer tracing.TracingInterface, monitor monitoring.MonitorInterface, logger logging.LoggerInterface) *Service { s := new(Service) @@ -1225,12 +1261,14 @@ func NewService(kratos KratosClientInterface, kratosAdmin KratosAdminClientInter return s } +// parseBody decodes JSON from a ReadCloser into the provided interface. func parseBody(b io.ReadCloser, body interface{}) error { decoder := json.NewDecoder(b) err := decoder.Decode(body) return err } +// unmarshalByteJson reads all data from a Reader and unmarshals it as JSON into the provided interface. func unmarshalByteJson(data io.Reader, v any) error { json_data, err := io.ReadAll(data) if err != nil {