package handlers import ( "context" "testing" "time" "armature/models" "armature/store" ) // mockGlobalConfig implements store.GlobalConfigStore for tests. type mockGlobalConfig struct { data map[string]models.JSONMap } func (m *mockGlobalConfig) Get(_ context.Context, key string) (models.JSONMap, error) { if v, ok := m.data[key]; ok { return v, nil } return nil, nil } func (m *mockGlobalConfig) Set(_ context.Context, _ string, _ models.JSONMap, _ string) error { return nil } func (m *mockGlobalConfig) GetAll(_ context.Context) (map[string]models.JSONMap, error) { return m.data, nil } func (m *mockGlobalConfig) SaveOIDCState(_ context.Context, _, _, _ string) error { return nil } func (m *mockGlobalConfig) ConsumeOIDCState(_ context.Context, _ string) (string, string, error) { return "", "", nil } func (m *mockGlobalConfig) CleanupOIDCState(_ context.Context) error { return nil } func (m *mockGlobalConfig) GetString(_ context.Context, _ string) (string, error) { return "", nil } func newMockGC(session models.JSONMap) store.GlobalConfigStore { return &mockGlobalConfig{data: map[string]models.JSONMap{"session": session}} } // ── parseDurationString tests ────────────── func TestParseDurationString(t *testing.T) { tests := []struct { input string want time.Duration err bool }{ {"15m", 15 * time.Minute, false}, {"2h", 2 * time.Hour, false}, {"7d", 7 * 24 * time.Hour, false}, {"90d", 90 * 24 * time.Hour, false}, {"30m", 30 * time.Minute, false}, {"1h30m", 90 * time.Minute, false}, {"", 0, true}, {"abc", 0, true}, {"d", 0, true}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got, err := parseDurationString(tt.input) if tt.err { if err == nil { t.Errorf("expected error for %q, got %v", tt.input, got) } return } if err != nil { t.Fatalf("unexpected error for %q: %v", tt.input, err) } if got != tt.want { t.Errorf("parseDurationString(%q) = %v, want %v", tt.input, got, tt.want) } }) } } // ── LoadSessionConfig tests ──────────────── func TestSessionConfigDefaults(t *testing.T) { gc := &mockGlobalConfig{data: map[string]models.JSONMap{}} sc := LoadSessionConfig(context.Background(), gc) if sc.AccessTokenTTL != 15*time.Minute { t.Errorf("AccessTokenTTL = %v, want 15m", sc.AccessTokenTTL) } if sc.RefreshTokenTTL != 7*24*time.Hour { t.Errorf("RefreshTokenTTL = %v, want 7d", sc.RefreshTokenTTL) } if sc.IdleTimeout != 0 { t.Errorf("IdleTimeout = %v, want 0", sc.IdleTimeout) } } func TestSessionConfigCustomValues(t *testing.T) { gc := newMockGC(models.JSONMap{ "access_token_ttl": "30m", "refresh_token_ttl": "30d", "idle_timeout": "1h", }) sc := LoadSessionConfig(context.Background(), gc) if sc.AccessTokenTTL != 30*time.Minute { t.Errorf("AccessTokenTTL = %v, want 30m", sc.AccessTokenTTL) } if sc.RefreshTokenTTL != 30*24*time.Hour { t.Errorf("RefreshTokenTTL = %v, want 30d", sc.RefreshTokenTTL) } if sc.IdleTimeout != 1*time.Hour { t.Errorf("IdleTimeout = %v, want 1h", sc.IdleTimeout) } } func TestSessionConfigClampLow(t *testing.T) { gc := newMockGC(models.JSONMap{ "access_token_ttl": "1m", // below 5m min "refresh_token_ttl": "30m", // below 1h min "idle_timeout": "1m", // below 5m min }) sc := LoadSessionConfig(context.Background(), gc) if sc.AccessTokenTTL != 5*time.Minute { t.Errorf("AccessTokenTTL clamped = %v, want 5m", sc.AccessTokenTTL) } if sc.RefreshTokenTTL != 1*time.Hour { t.Errorf("RefreshTokenTTL clamped = %v, want 1h", sc.RefreshTokenTTL) } if sc.IdleTimeout != 5*time.Minute { t.Errorf("IdleTimeout clamped = %v, want 5m", sc.IdleTimeout) } } func TestSessionConfigClampHigh(t *testing.T) { gc := newMockGC(models.JSONMap{ "access_token_ttl": "2h", // above 60m max "refresh_token_ttl": "365d", // above 90d max "idle_timeout": "48h", // above 24h max }) sc := LoadSessionConfig(context.Background(), gc) if sc.AccessTokenTTL != 60*time.Minute { t.Errorf("AccessTokenTTL clamped = %v, want 60m", sc.AccessTokenTTL) } if sc.RefreshTokenTTL != 90*24*time.Hour { t.Errorf("RefreshTokenTTL clamped = %v, want 90d", sc.RefreshTokenTTL) } if sc.IdleTimeout != 24*time.Hour { t.Errorf("IdleTimeout clamped = %v, want 24h", sc.IdleTimeout) } } func TestSessionConfigEmptyIdleTimeout(t *testing.T) { gc := newMockGC(models.JSONMap{ "access_token_ttl": "15m", "refresh_token_ttl": "7d", "idle_timeout": "", }) sc := LoadSessionConfig(context.Background(), gc) if sc.IdleTimeout != 0 { t.Errorf("IdleTimeout = %v, want 0 (disabled)", sc.IdleTimeout) } } func TestSessionConfigInvalidValues(t *testing.T) { gc := newMockGC(models.JSONMap{ "access_token_ttl": "not-a-duration", "refresh_token_ttl": "xyz", "idle_timeout": "---", }) sc := LoadSessionConfig(context.Background(), gc) // Should fall back to defaults if sc.AccessTokenTTL != 15*time.Minute { t.Errorf("AccessTokenTTL = %v, want 15m (default)", sc.AccessTokenTTL) } if sc.RefreshTokenTTL != 7*24*time.Hour { t.Errorf("RefreshTokenTTL = %v, want 7d (default)", sc.RefreshTokenTTL) } if sc.IdleTimeout != 0 { t.Errorf("IdleTimeout = %v, want 0 (default)", sc.IdleTimeout) } }