Upload images to your PDS and get instant CDN URLs via images.blue
at main 345 lines 7.9 kB view raw
1package auth 2 3import ( 4 "context" 5 "fmt" 6 "net/http" 7 "testing" 8) 9 10func TestNewOAuthFlow(t *testing.T) { 11 flow, err := NewOAuthFlow("test.bsky.social") 12 if err != nil { 13 t.Fatalf("NewOAuthFlow() error = %v", err) 14 } 15 16 if flow == nil { 17 t.Fatal("NewOAuthFlow() returned nil") 18 } 19 20 if flow.loginIdentifier != "test.bsky.social" { 21 t.Errorf("loginIdentifier = %q, want %q", flow.loginIdentifier, "test.bsky.social") 22 } 23 24 if flow.app == nil { 25 t.Error("app should not be nil") 26 } 27 28 if flow.store == nil { 29 t.Error("store should not be nil") 30 } 31 32 if flow.authSuccess == nil { 33 t.Error("authSuccess channel should not be nil") 34 } 35 36 if flow.authError == nil { 37 t.Error("authError channel should not be nil") 38 } 39 40 if flow.httpClient == nil { 41 t.Error("httpClient should not be nil (default)") 42 } 43 44 if flow.openBrowser == nil { 45 t.Error("openBrowser should not be nil (default)") 46 } 47} 48 49func TestNewOAuthFlowEmptyIdentifier(t *testing.T) { 50 flow, err := NewOAuthFlow("") 51 if err != nil { 52 t.Fatalf("NewOAuthFlow() error = %v", err) 53 } 54 55 if flow == nil { 56 t.Fatal("NewOAuthFlow() returned nil") 57 } 58 59 if flow.loginIdentifier != "" { 60 t.Errorf("loginIdentifier = %q, want empty string", flow.loginIdentifier) 61 } 62} 63 64func TestAuthenticateRequiresIdentifier(t *testing.T) { 65 flow, err := NewOAuthFlow("") 66 if err != nil { 67 t.Fatalf("NewOAuthFlow() error = %v", err) 68 } 69 70 _, err = flow.Authenticate() 71 if err == nil { 72 t.Error("Authenticate() expected error with empty identifier, got nil") 73 } 74 75 expectedErr := "login identifier is required" 76 if err.Error() != expectedErr { 77 t.Errorf("Authenticate() error = %q, want %q", err.Error(), expectedErr) 78 } 79} 80 81// MockHTTPClient implements HTTPDoer for testing 82type MockHTTPClient struct { 83 DoFunc func(req *http.Request) (*http.Response, error) 84} 85 86func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { 87 return m.DoFunc(req) 88} 89 90func TestNewOAuthFlowWithOptions(t *testing.T) { 91 mockStore := NewKeyringAuthStoreWithKeyring(NewMockKeyring()) 92 mockHTTP := &MockHTTPClient{} 93 browserCalled := false 94 mockBrowser := func(url string) error { 95 browserCalled = true 96 return nil 97 } 98 99 flow, err := NewOAuthFlow("test.bsky.social", 100 WithStore(mockStore), 101 WithHTTPClient(mockHTTP), 102 WithBrowserOpener(mockBrowser), 103 ) 104 if err != nil { 105 t.Fatalf("NewOAuthFlow() error = %v", err) 106 } 107 108 if flow.store != mockStore { 109 t.Error("store was not injected correctly") 110 } 111 112 if flow.httpClient != mockHTTP { 113 t.Error("httpClient was not injected correctly") 114 } 115 116 // Test browser opener was injected 117 flow.openBrowser("http://test.com") 118 if !browserCalled { 119 t.Error("openBrowser was not injected correctly") 120 } 121} 122 123func TestNewClientAppWithStore(t *testing.T) { 124 mockKeyring := NewMockKeyring() 125 mockStore := NewKeyringAuthStoreWithKeyring(mockKeyring) 126 127 app, store := NewClientApp(mockStore) 128 129 if app == nil { 130 t.Error("NewClientApp() app should not be nil") 131 } 132 133 if store != mockStore { 134 t.Error("NewClientApp() should return the injected store") 135 } 136} 137 138func TestNewClientAppWithoutStore(t *testing.T) { 139 app, store := NewClientApp() 140 141 if app == nil { 142 t.Error("NewClientApp() app should not be nil") 143 } 144 145 if store == nil { 146 t.Error("NewClientApp() should create a default store") 147 } 148} 149 150func TestParseSSEAuthData(t *testing.T) { 151 tests := []struct { 152 name string 153 input string 154 wantErr bool 155 code string 156 iss string 157 state string 158 }{ 159 { 160 name: "valid auth data", 161 input: `{"code":"auth_code_123","iss":"https://bsky.social","state":"state_abc"}`, 162 wantErr: false, 163 code: "auth_code_123", 164 iss: "https://bsky.social", 165 state: "state_abc", 166 }, 167 { 168 name: "valid with whitespace", 169 input: ` {"code":"code","iss":"iss","state":"state"} `, 170 wantErr: false, 171 code: "code", 172 iss: "iss", 173 state: "state", 174 }, 175 { 176 name: "empty string", 177 input: "", 178 wantErr: true, 179 }, 180 { 181 name: "whitespace only", 182 input: " ", 183 wantErr: true, 184 }, 185 { 186 name: "invalid JSON", 187 input: "not json", 188 wantErr: true, 189 }, 190 { 191 name: "missing code", 192 input: `{"iss":"https://bsky.social","state":"state_abc"}`, 193 wantErr: true, 194 }, 195 { 196 name: "missing iss", 197 input: `{"code":"auth_code_123","state":"state_abc"}`, 198 wantErr: true, 199 }, 200 { 201 name: "missing state", 202 input: `{"code":"auth_code_123","iss":"https://bsky.social"}`, 203 wantErr: true, 204 }, 205 { 206 name: "empty code", 207 input: `{"code":"","iss":"https://bsky.social","state":"state_abc"}`, 208 wantErr: true, 209 }, 210 } 211 212 for _, tt := range tests { 213 t.Run(tt.name, func(t *testing.T) { 214 result, err := parseSSEAuthData(tt.input) 215 216 if tt.wantErr { 217 if err == nil { 218 t.Errorf("parseSSEAuthData() expected error, got nil") 219 } 220 return 221 } 222 223 if err != nil { 224 t.Errorf("parseSSEAuthData() unexpected error: %v", err) 225 return 226 } 227 228 if result.Code != tt.code { 229 t.Errorf("Code = %q, want %q", result.Code, tt.code) 230 } 231 if result.Iss != tt.iss { 232 t.Errorf("Iss = %q, want %q", result.Iss, tt.iss) 233 } 234 if result.State != tt.state { 235 t.Errorf("State = %q, want %q", result.State, tt.state) 236 } 237 }) 238 } 239} 240 241func TestLogoutNoSession(t *testing.T) { 242 // Logout should succeed even when no session exists 243 // Note: This uses the real keyring, but should still work 244 // because it handles the "no session" case gracefully 245 err := Logout() 246 // We can't easily test this without mocking, but we can verify it doesn't panic 247 _ = err 248} 249 250func TestIsTokenRefreshError(t *testing.T) { 251 tests := []struct { 252 name string 253 err error 254 expected bool 255 }{ 256 { 257 name: "nil error", 258 err: nil, 259 expected: false, 260 }, 261 { 262 name: "regular error", 263 err: fmt.Errorf("some random error"), 264 expected: false, 265 }, 266 { 267 name: "network error", 268 err: fmt.Errorf("connection refused"), 269 expected: false, 270 }, 271 { 272 name: "indigo token refresh error", 273 err: fmt.Errorf("failed to refresh OAuth tokens: token refresh failed (HTTP 400): invalid_grant"), 274 expected: true, 275 }, 276 { 277 name: "wrapped token refresh error", 278 err: fmt.Errorf("upload failed: %w", fmt.Errorf("failed to refresh OAuth tokens: something")), 279 expected: true, 280 }, 281 { 282 name: "invalid_grant only", 283 err: fmt.Errorf("invalid_grant"), 284 expected: true, 285 }, 286 { 287 name: "token refresh failed", 288 err: fmt.Errorf("token refresh failed"), 289 expected: true, 290 }, 291 { 292 name: "partial match - failed to refresh", 293 err: fmt.Errorf("failed to refresh OAuth tokens"), 294 expected: true, 295 }, 296 } 297 298 for _, tt := range tests { 299 t.Run(tt.name, func(t *testing.T) { 300 result := IsTokenRefreshError(tt.err) 301 if result != tt.expected { 302 t.Errorf("IsTokenRefreshError(%v) = %v, want %v", tt.err, result, tt.expected) 303 } 304 }) 305 } 306} 307 308func TestExecuteWithReauth_NoError(t *testing.T) { 309 // Test that when the operation succeeds, no re-auth is attempted 310 operationCalls := 0 311 312 // We can't easily mock oauth.ClientSession, so we test with nil 313 // and an operation that doesn't use the session 314 _, err := ExecuteWithReauth(context.TODO(), nil, func(s *ClientSession) error { 315 operationCalls++ 316 return nil 317 }) 318 319 if err != nil { 320 t.Errorf("ExecuteWithReauth() unexpected error: %v", err) 321 } 322 323 if operationCalls != 1 { 324 t.Errorf("operation called %d times, want 1", operationCalls) 325 } 326} 327 328func TestExecuteWithReauth_NonTokenError(t *testing.T) { 329 // Test that non-token errors are returned without retry 330 operationCalls := 0 331 expectedErr := fmt.Errorf("some other error") 332 333 _, err := ExecuteWithReauth(context.TODO(), nil, func(s *ClientSession) error { 334 operationCalls++ 335 return expectedErr 336 }) 337 338 if err != expectedErr { 339 t.Errorf("ExecuteWithReauth() error = %v, want %v", err, expectedErr) 340 } 341 342 if operationCalls != 1 { 343 t.Errorf("operation called %d times, want 1 (no retry for non-token errors)", operationCalls) 344 } 345}