diff --git a/internal/serviceconfig/auth/config.go b/internal/serviceconfig/auth/config.go index e8f1c85d..7d9206b9 100644 --- a/internal/serviceconfig/auth/config.go +++ b/internal/serviceconfig/auth/config.go @@ -173,6 +173,7 @@ func InitializeConfig(baseURL string, logger *slog.Logger) *CognitoConfig { AuthURL: fmt.Sprintf("https://%s/oauth2/authorize", domain), AuthJwksURL: fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", region, userPoolID), AuthUserPoolID: userPoolID, + AuthDomain: domain, AuthRegion: region, AuthLoginPath: loginPath, AuthCallbackPath: callbackPath, diff --git a/internal/serviceconfig/auth/config_test.go b/internal/serviceconfig/auth/config_test.go new file mode 100644 index 00000000..ee3ad232 --- /dev/null +++ b/internal/serviceconfig/auth/config_test.go @@ -0,0 +1,373 @@ +package auth + +import ( + "bytes" + "io" + "log/slog" + "os" + "reflect" + "strings" + "testing" +) + +func TestInitializeAuthConfig(t *testing.T) { + // Setup test environment variables + testEnv := map[string]string{ + "AWS_REGION": "us-west-2", + "COGNITO_USER_POOL_ID": "us-west-2_testpool", + "COGNITO_CLIENT_ID": "test-client-id", + "COGNITO_CLIENT_SECRET": "test-client-secret", + "COGNITO_DOMAIN": "test-domain.auth.us-west-2.amazoncognito.com", + "COGNITO_LOGIN_PATH": "/custom-login", + "COGNITO_CALLBACK_PATH": "/custom-callback", + "COGNITO_HOME_PATH": "/custom-home", + "COGNITO_LOGOUT_PATH": "/custom-logout", + "DEBUG": "true", + } + + // Set environment variables + for k, v := range testEnv { + os.Setenv(k, v) + } + defer func() { + // Cleanup environment variables after test + for k := range testEnv { + os.Unsetenv(k) + } + }() + + // Create test logger + logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + testLogger := slog.New(logHandler) + + // Test base URL + baseURL := "https://example.com" + + // Create config instance + cfg := &CognitoConfig{} + err := cfg.InitializeAuthConfig(baseURL, testLogger) + + // Test initialization success + if err != nil { + t.Errorf("InitializeAuthConfig failed: %v", err) + } + + // Test all fields + tests := []struct { + name string + got string + expected string + }{ + {"AuthRegion", cfg.AuthRegion, "us-west-2"}, + {"AuthUserPoolID", cfg.AuthUserPoolID, "us-west-2_testpool"}, + {"AuthClientID", cfg.AuthClientID, "test-client-id"}, + {"AuthClientSecret", cfg.AuthClientSecret, "test-client-secret"}, + {"AuthDomain", cfg.AuthDomain, "test-domain.auth.us-west-2.amazoncognito.com"}, + {"AuthLoginPath", cfg.AuthLoginPath, "/custom-login"}, + {"AuthCallbackPath", cfg.AuthCallbackPath, "/custom-callback"}, + {"AuthHomePath", cfg.AuthHomePath, "/custom-home"}, + {"AuthLogoutPath", cfg.AuthLogoutPath, "/custom-logout"}, + {"AuthRedirectURI", cfg.AuthRedirectURI, "https://example.com/custom-callback"}, + {"AuthTokenURL", cfg.AuthTokenURL, "https://test-domain.auth.us-west-2.amazoncognito.com/oauth2/token"}, + {"AuthURL", cfg.AuthURL, "https://test-domain.auth.us-west-2.amazoncognito.com/oauth2/authorize"}, + {"AuthJwksURL", cfg.AuthJwksURL, "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_testpool/.well-known/jwks.json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.got != tt.expected { + t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected) + } + }) + } + + // Test logger + if cfg.AuthLogger == nil { + t.Error("AuthLogger is nil") + } + + // Test route permissions map initialization + if cfg.AuthRoutePermissions == nil { + t.Error("AuthRoutePermissions map was not initialized") + } +} + +func TestInitializeAuthConfigWithDefaults(t *testing.T) { + // Clear any existing environment variables + envVars := []string{ + "AWS_REGION", + "COGNITO_LOGIN_PATH", + "COGNITO_CALLBACK_PATH", + "COGNITO_HOME_PATH", + "COGNITO_LOGOUT_PATH", + } + for _, env := range envVars { + os.Unsetenv(env) + } + + // Set required variables + os.Setenv("COGNITO_USER_POOL_ID", "us-east-2_testpool") + os.Setenv("COGNITO_CLIENT_ID", "test-client-id") + os.Setenv("COGNITO_CLIENT_SECRET", "test-client-secret") + os.Setenv("COGNITO_DOMAIN", "test-domain.auth.us-east-2.amazoncognito.com") + + defer func() { + for _, env := range append(envVars, "COGNITO_USER_POOL_ID", "COGNITO_CLIENT_ID", "COGNITO_CLIENT_SECRET", "COGNITO_DOMAIN") { + os.Unsetenv(env) + } + }() + + cfg := &CognitoConfig{} + err := cfg.InitializeAuthConfig("https://example.com", nil) + + if err != nil { + t.Errorf("InitializeAuthConfig failed: %v", err) + } + + // Test default values + defaults := []struct { + name string + got string + expected string + }{ + {"AuthRegion", cfg.AuthRegion, "us-east-2"}, + {"AuthLoginPath", cfg.AuthLoginPath, "/login"}, + {"AuthCallbackPath", cfg.AuthCallbackPath, "/login-callback"}, + {"AuthHomePath", cfg.AuthHomePath, "/home"}, + {"AuthLogoutPath", cfg.AuthLogoutPath, "/logout"}, + } + + for _, tt := range defaults { + t.Run(tt.name, func(t *testing.T) { + if tt.got != tt.expected { + t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected) + } + }) + } +} + +func TestGettersAndSetters(t *testing.T) { + // Create a test instance + cfg := &CognitoConfig{} + + // Test logger + logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + testLogger := slog.New(logHandler) + + // Test route permissions + testPermissions := map[string][]string{ + "/api/test": {"read", "write"}, + "/api/admin": {"admin"}, + } + + // Test cases for all getter/setter pairs + testCases := []struct { + name string + setter func() + getter func() interface{} + expected interface{} + }{ + { + name: "AuthRegion", + setter: func() { cfg.SetAuthRegion("us-west-2") }, + getter: func() interface{} { return cfg.GetAuthRegion() }, + expected: "us-west-2", + }, + { + name: "AuthUserPoolID", + setter: func() { cfg.SetAuthUserPoolID("us-west-2_testpool") }, + getter: func() interface{} { return cfg.GetAuthUserPoolID() }, + expected: "us-west-2_testpool", + }, + { + name: "AuthClientSecret", + setter: func() { cfg.SetAuthClientSecret("test-secret") }, + getter: func() interface{} { return cfg.GetAuthClientSecret() }, + expected: "test-secret", + }, + { + name: "AuthDomain", + setter: func() { cfg.SetAuthDomain("test-domain.com") }, + getter: func() interface{} { return cfg.GetAuthDomain() }, + expected: "test-domain.com", + }, + { + name: "AuthClientID", + setter: func() { cfg.SetAuthClientID("test-client-id") }, + getter: func() interface{} { return cfg.GetAuthClientID() }, + expected: "test-client-id", + }, + { + name: "AuthRedirectURI", + setter: func() { cfg.SetAuthRedirectURI("https://example.com/callback") }, + getter: func() interface{} { return cfg.GetAuthRedirectURI() }, + expected: "https://example.com/callback", + }, + { + name: "AuthTokenURL", + setter: func() { cfg.SetAuthTokenURL("https://example.com/token") }, + getter: func() interface{} { return cfg.GetAuthTokenURL() }, + expected: "https://example.com/token", + }, + { + name: "AuthJwksURL", + setter: func() { cfg.SetAuthJwksURL("https://example.com/jwks.json") }, + getter: func() interface{} { return cfg.GetAuthJwksURL() }, + expected: "https://example.com/jwks.json", + }, + { + name: "AuthURL", + setter: func() { cfg.SetAuthURL("https://example.com/auth") }, + getter: func() interface{} { return cfg.GetAuthURL() }, + expected: "https://example.com/auth", + }, + { + name: "AuthLoginPath", + setter: func() { cfg.SetAuthLoginPath("/custom-login") }, + getter: func() interface{} { return cfg.GetAuthLoginPath() }, + expected: "/custom-login", + }, + { + name: "AuthCallbackPath", + setter: func() { cfg.SetAuthCallbackPath("/custom-callback") }, + getter: func() interface{} { return cfg.GetAuthCallbackPath() }, + expected: "/custom-callback", + }, + { + name: "AuthHomePath", + setter: func() { cfg.SetAuthHomePath("/custom-home") }, + getter: func() interface{} { return cfg.GetAuthHomePath() }, + expected: "/custom-home", + }, + { + name: "AuthLogoutPath", + setter: func() { cfg.SetAuthLogoutPath("/custom-logout") }, + getter: func() interface{} { return cfg.GetAuthLogoutPath() }, + expected: "/custom-logout", + }, + { + name: "AuthLogger", + setter: func() { cfg.SetAuthLogger(testLogger) }, + getter: func() interface{} { return cfg.GetAuthLogger() }, + expected: testLogger, + }, + { + name: "AuthRoutePermissions", + setter: func() { cfg.SetAuthRoutePermissions(testPermissions) }, + getter: func() interface{} { return cfg.GetAuthRoutePermissions() }, + expected: testPermissions, + }, + } + + // Run all test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Execute setter + tc.setter() + + // Get value and compare with expected + got := tc.getter() + + // Handle different types of comparisons + switch expected := tc.expected.(type) { + case map[string][]string: + gotMap, ok := got.(map[string][]string) + if !ok { + t.Errorf("failed to convert to map[string][]string: got type %T", got) + return + } + if !reflect.DeepEqual(gotMap, expected) { + t.Errorf("got %v, want %v", gotMap, expected) + } + case *slog.Logger: + gotLogger, ok := got.(*slog.Logger) + if !ok { + t.Errorf("failed to convert to *slog.Logger: got type %T", got) + return + } + if gotLogger != expected { + t.Errorf("got %v, want %v", gotLogger, expected) + } + default: + if got != tc.expected { + t.Errorf("got %v, want %v", got, tc.expected) + } + } + }) + } +} + +func TestPrettyPrintDebugOnly(t *testing.T) { + // Test with DEBUG=true + os.Setenv("DEBUG", "true") + defer os.Unsetenv("DEBUG") + + cfg := &CognitoConfig{ + AuthClientID: "test-client-id", + AuthClientSecret: "test-secret", + AuthRedirectURI: "https://example.com/callback", + AuthRoutePermissions: map[string][]string{ + "/test": {"read"}, + }, + } + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + cfg.PrettyPrintDebugOnly() + + w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("failed to copy output: %v", err) + } + + // And later in the same function: + buf.Reset() + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("failed to copy output: %v", err) + } + output := buf.String() + + // Verify output contains expected values + expectedStrings := []string{ + "CognitoConfig (debug on):", + "AuthClientID: test-client-id", + "AuthClientSecret: test-secret", + "AuthRedirectURI: https://example.com/callback", + "Route: /test, Permissions: [read]", + } + + for _, expected := range expectedStrings { + if !strings.Contains(output, expected) { + t.Errorf("Expected output to contain %q", expected) + } + } + + // Test with DEBUG=false + os.Unsetenv("DEBUG") + r, w, _ = os.Pipe() + os.Stdout = w + + cfg.PrettyPrintDebugOnly() + + w.Close() + os.Stdout = oldStdout + + buf.Reset() + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("failed to copy output: %v", err) + } + + if output != "" { + t.Error("Expected no output when DEBUG=false") + } +} diff --git a/scripts/tests.yml b/scripts/tests.yml index 469fe35a..b5c5da6c 100644 --- a/scripts/tests.yml +++ b/scripts/tests.yml @@ -10,7 +10,7 @@ vars: PKG: "./pkg/..." CMD: "./cmd/..." # yamllint disable-line rule:line-length - EXCLUDED_FILES: ".gen.go|internal/serviceconfig/observability/prometheus/generator/main.go|internal/rbac/cognito.go" + EXCLUDED_FILES: ".gen.go|internal/serviceconfig/observability/prometheus/generator/main.go|internal/cognitoauth/auth.go|internal/cognitoauth/handler.go|internal/cognitoauth/jwks.go|internal/cognitoauth/middleware.go|internal/cognitoauth/models.go|internal/cognitoauth/token.go|internal/cognitoauth/utils.go|api/queryAPI/authHandlers.go" includes: docker: