A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.

Adds linter

Closes #4

+904 -420
+2 -2
.golangci.yml
··· 1 1 # golangci-lint configuration for ATCR 2 2 # See: https://golangci-lint.run/usage/configuration/ 3 3 version: "2" 4 - linters: 4 + linters: 5 5 settings: 6 6 staticcheck: 7 7 checks: ··· 23 23 formatters: 24 24 enable: 25 25 - gofmt 26 - - goimports 26 + - goimports
+24
.tangled/workflows/lint.yaml
··· 1 + when: 2 + - event: ["push"] 3 + branch: ["*"] 4 + - event: ["pull_request"] 5 + branch: ["main"] 6 + 7 + engine: kubernetes 8 + image: golang:1.25-trixie 9 + architecture: amd64 10 + 11 + steps: 12 + - name: Download and Generate 13 + environment: 14 + CGO_ENABLED: 1 15 + command: | 16 + go mod download 17 + go generate ./... 18 + 19 + - name: Run Linter 20 + environment: 21 + CGO_ENABLED: 1 22 + command: | 23 + curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2 24 + golangci-lint run ./...
+1 -1
cmd/appview/serve.go
··· 397 397 return 398 398 } 399 399 400 - var metadataMap map[string]interface{} 400 + var metadataMap map[string]any 401 401 if err := json.Unmarshal(metadataBytes, &metadataMap); err != nil { 402 402 http.Error(w, "Failed to unmarshal metadata", http.StatusInternalServerError) 403 403 return
+70 -29
cmd/credential-helper/main.go
··· 124 124 fmt.Printf("docker-credential-atcr %s (commit: %s, built: %s)\n", version, commit, date) 125 125 case "update": 126 126 checkOnly := len(os.Args) > 2 && os.Args[2] == "--check" 127 - handleUpdate(checkOnly) 127 + if err := handleUpdate(checkOnly); err != nil { 128 + fmt.Fprintf(os.Stderr, "Could not update: %v", err) 129 + os.Exit(1) 130 + } 128 131 default: 129 132 fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command) 130 133 os.Exit(1) ··· 180 183 181 184 // Wait for user to complete OAuth flow, then retry 182 185 fmt.Fprintf(os.Stderr, "Waiting for authentication") 183 - for i := 0; i < 60; i++ { // Wait up to 2 minutes 186 + for range 60 { // Wait up to 2 minutes 184 187 time.Sleep(2 * time.Second) 185 188 fmt.Fprintf(os.Stderr, ".") 186 189 ··· 246 249 } 247 250 248 251 // Check for updates (non-blocking due to 24h cache) 249 - checkAndNotifyUpdate(appViewURL) 252 + if err := checkAndNotifyUpdate(appViewURL); err != nil { 253 + fmt.Fprintf(os.Stderr, "Error checking for updates: %v", err) 254 + } 250 255 251 256 // Return credentials for Docker 252 257 creds := Credentials{ ··· 385 390 } 386 391 387 392 var tokenResult DeviceTokenResponse 388 - json.NewDecoder(tokenResp.Body).Decode(&tokenResult) 393 + if err := json.NewDecoder(tokenResp.Body).Decode(&tokenResult); err != nil { 394 + return nil, err 395 + } 389 396 tokenResp.Body.Close() 390 397 391 398 if tokenResult.Error == "authorization_pending" { ··· 680 687 } 681 688 682 689 // handleUpdate handles the update command 683 - func handleUpdate(checkOnly bool) { 690 + func handleUpdate(checkOnly bool) error { 684 691 // Default API URL 685 692 apiURL := "https://atcr.io/api/credential-helper/version" 686 693 ··· 704 711 } 705 712 706 713 // Compare versions 707 - if !isNewerVersion(versionInfo.Latest, version) { 714 + shouldUpdate, err := isNewerVersion(versionInfo.Latest, version) 715 + if err != nil { 716 + return err 717 + } 718 + if shouldUpdate { 708 719 fmt.Printf("You're already running the latest version (%s)\n", version) 709 - return 720 + return nil 710 721 } 711 722 712 723 fmt.Printf("New version available: %s (current: %s)\n", versionInfo.Latest, version) 713 724 714 725 if checkOnly { 715 - return 726 + return nil 716 727 } 717 728 718 729 // Perform the update ··· 722 733 } 723 734 724 735 fmt.Println("Update completed successfully!") 736 + return nil 725 737 } 726 738 727 739 // fetchVersionInfo fetches version info from the AppView API ··· 750 762 751 763 // isNewerVersion compares two version strings (simple semver comparison) 752 764 // Returns true if newVersion is newer than currentVersion 753 - func isNewerVersion(newVersion, currentVersion string) bool { 765 + func isNewerVersion(newVersion, currentVersion string) (bool, error) { 754 766 // Handle "dev" version 755 767 if currentVersion == "dev" { 756 - return true 768 + return true, nil 757 769 } 758 770 759 771 // Normalize versions (strip 'v' prefix) ··· 768 780 for i := 0; i < len(newParts) && i < len(curParts); i++ { 769 781 newNum := 0 770 782 curNum := 0 771 - fmt.Sscanf(newParts[i], "%d", &newNum) 772 - fmt.Sscanf(curParts[i], "%d", &curNum) 783 + if _, err := fmt.Sscanf(newParts[i], "%d", &newNum); err != nil { 784 + return false, err 785 + } 786 + if _, err := fmt.Sscanf(curParts[i], "%d", &curNum); err != nil { 787 + return false, err 788 + } 773 789 774 790 if newNum > curNum { 775 - return true 791 + return true, nil 776 792 } 777 793 if newNum < curNum { 778 - return false 794 + return false, nil 779 795 } 780 796 } 781 797 782 798 // If new version has more parts (e.g., 1.0.1 vs 1.0), it's newer 783 - return len(newParts) > len(curParts) 799 + return len(newParts) > len(curParts), nil 784 800 } 785 801 786 802 // getPlatformKey returns the platform key for the current OS/arch ··· 881 897 // Install new binary 882 898 if err := copyFile(binaryPath, currentPath); err != nil { 883 899 // Try to restore backup 884 - os.Rename(backupPath, currentPath) 900 + if err := os.Rename(backupPath, currentPath); err != nil { 901 + return err 902 + } 885 903 return fmt.Errorf("failed to install new binary: %w", err) 886 904 } 887 905 888 906 // Set executable permissions 889 907 if err := os.Chmod(currentPath, 0755); err != nil { 890 908 // Try to restore backup 891 - os.Remove(currentPath) 892 - os.Rename(backupPath, currentPath) 909 + if err := os.Remove(currentPath); err != nil { 910 + return err 911 + } 912 + if err := os.Rename(backupPath, currentPath); err != nil { 913 + return err 914 + } 893 915 return fmt.Errorf("failed to set permissions: %w", err) 894 916 } 895 917 ··· 970 992 } 971 993 972 994 // checkAndNotifyUpdate checks for updates in the background and notifies the user 973 - func checkAndNotifyUpdate(appViewURL string) { 995 + func checkAndNotifyUpdate(appViewURL string) error { 974 996 // Check if we've already checked recently 975 997 cache := loadUpdateCheckCache() 976 998 if cache != nil && time.Since(cache.CheckedAt) < updateCheckCacheTTL && cache.Current == version { 977 999 // Cache is fresh and for current version 978 - if isNewerVersion(cache.Latest, version) { 1000 + shouldUpdate, err := isNewerVersion(cache.Latest, version) 1001 + if err != nil { 1002 + return err 1003 + } 1004 + if shouldUpdate { 979 1005 fmt.Fprintf(os.Stderr, "\nNote: A new version of docker-credential-atcr is available (%s).\n", cache.Latest) 980 1006 fmt.Fprintf(os.Stderr, "Run 'docker-credential-atcr update' to upgrade.\n\n") 981 1007 } 982 - return 1008 + return nil 983 1009 } 984 1010 985 1011 // Fetch version info ··· 987 1013 versionInfo, err := fetchVersionInfo(apiURL) 988 1014 if err != nil { 989 1015 // Silently fail - don't interrupt credential retrieval 990 - return 1016 + return nil 991 1017 } 992 1018 993 1019 // Save to cache 994 - saveUpdateCheckCache(&UpdateCheckCache{ 1020 + err = saveUpdateCheckCache(&UpdateCheckCache{ 995 1021 CheckedAt: time.Now(), 996 1022 Latest: versionInfo.Latest, 997 1023 Current: version, 998 1024 }) 1025 + if err != nil { 1026 + return err 1027 + } 999 1028 1000 1029 // Notify if newer version available 1001 - if isNewerVersion(versionInfo.Latest, version) { 1030 + shouldUpdate, err := isNewerVersion(versionInfo.Latest, version) 1031 + if err != nil { 1032 + return err 1033 + } 1034 + if shouldUpdate { 1002 1035 fmt.Fprintf(os.Stderr, "\nNote: A new version of docker-credential-atcr is available (%s).\n", versionInfo.Latest) 1003 1036 fmt.Fprintf(os.Stderr, "Run 'docker-credential-atcr update' to upgrade.\n\n") 1004 1037 } 1038 + 1039 + return nil 1005 1040 } 1006 1041 1007 1042 // getUpdateCheckCachePath returns the path to the update check cache file ··· 1034 1069 } 1035 1070 1036 1071 // saveUpdateCheckCache saves the update check cache to disk 1037 - func saveUpdateCheckCache(cache *UpdateCheckCache) { 1072 + func saveUpdateCheckCache(cache *UpdateCheckCache) error { 1038 1073 path := getUpdateCheckCachePath() 1039 1074 if path == "" { 1040 - return 1075 + return nil 1041 1076 } 1042 1077 1043 1078 data, err := json.MarshalIndent(cache, "", " ") 1044 1079 if err != nil { 1045 - return 1080 + return nil 1046 1081 } 1047 1082 1048 1083 // Ensure directory exists 1049 1084 dir := filepath.Dir(path) 1050 - os.MkdirAll(dir, 0700) 1085 + if err := os.MkdirAll(dir, 0700); err != nil { 1086 + return err 1087 + } 1051 1088 1052 - os.WriteFile(path, data, 0600) 1089 + if err := os.WriteFile(path, data, 0600); err != nil { 1090 + return err 1091 + } 1092 + 1093 + return nil 1053 1094 }
+2 -2
pkg/appview/config.go
··· 388 388 return checksums 389 389 } 390 390 391 - pairs := strings.Split(checksumsStr, ",") 392 - for _, pair := range pairs { 391 + pairs := strings.SplitSeq(checksumsStr, ",") 392 + for pair := range pairs { 393 393 parts := strings.SplitN(strings.TrimSpace(pair), ":", 2) 394 394 if len(parts) == 2 { 395 395 platform := strings.TrimSpace(parts[0])
+5 -1
pkg/appview/db/device_store.go
··· 288 288 // Check if this device's hash matches the secret 289 289 if err := bcrypt.CompareHashAndPassword([]byte(device.SecretHash), []byte(secret)); err == nil { 290 290 // Update last used asynchronously 291 - go s.UpdateLastUsed(device.SecretHash) 291 + go func() { 292 + if err := s.UpdateLastUsed(device.SecretHash); err != nil { 293 + slog.Error("Failed to update last used", "error", err) 294 + } 295 + }() 292 296 293 297 return &device, nil 294 298 }
+2 -5
pkg/appview/db/device_store_test.go
··· 56 56 func TestGenerateUserCode(t *testing.T) { 57 57 // Generate multiple codes to test 58 58 codes := make(map[string]bool) 59 - for i := 0; i < 100; i++ { 59 + for range 100 { 60 60 code := generateUserCode() 61 61 62 62 // Test format: XXXX-XXXX ··· 372 372 return 373 373 } 374 374 if !tt.wantErr { 375 - if device == nil { 376 - t.Error("Expected device, got nil") 377 - } 378 375 if device.DID != "did:plc:alice123" { 379 376 t.Errorf("DID = %v, want did:plc:alice123", device.DID) 380 377 } ··· 399 396 } 400 397 401 398 // Create 3 devices 402 - for i := 0; i < 3; i++ { 399 + for i := range 3 { 403 400 pending, err := store.CreatePendingAuth("Device "+string(rune('A'+i)), "192.168.1.1", "Agent") 404 401 if err != nil { 405 402 t.Fatalf("CreatePendingAuth() error = %v", err)
+5 -5
pkg/appview/db/oauth_store.go
··· 339 339 340 340 // GetSessionStats returns statistics about stored OAuth sessions 341 341 // Useful for monitoring and debugging session health 342 - func (s *OAuthStore) GetSessionStats(ctx context.Context) (map[string]interface{}, error) { 343 - stats := make(map[string]interface{}) 342 + func (s *OAuthStore) GetSessionStats(ctx context.Context) (map[string]any, error) { 343 + stats := make(map[string]any) 344 344 345 345 // Total sessions 346 346 var totalSessions int ··· 392 392 393 393 // ListSessionsForMonitoring returns a list of all sessions with basic info for monitoring 394 394 // Returns: DID, session age (minutes), last update time 395 - func (s *OAuthStore) ListSessionsForMonitoring(ctx context.Context) ([]map[string]interface{}, error) { 395 + func (s *OAuthStore) ListSessionsForMonitoring(ctx context.Context) ([]map[string]any, error) { 396 396 rows, err := s.db.QueryContext(ctx, ` 397 397 SELECT 398 398 account_did, ··· 408 408 } 409 409 defer rows.Close() 410 410 411 - var sessions []map[string]interface{} 411 + var sessions []map[string]any 412 412 for rows.Next() { 413 413 var did, sessionID, createdAt, updatedAt string 414 414 var idleMinutes int ··· 418 418 continue 419 419 } 420 420 421 - sessions = append(sessions, map[string]interface{}{ 421 + sessions = append(sessions, map[string]any{ 422 422 "did": did, 423 423 "session_id": sessionID, 424 424 "created_at": createdAt,
+21 -7
pkg/appview/db/queries_test.go
··· 1266 1266 1267 1267 // Verify data exists 1268 1268 var count int 1269 - db.QueryRow(`SELECT COUNT(*) FROM manifests WHERE did = ?`, testUser.DID).Scan(&count) 1269 + if err := db.QueryRow(`SELECT COUNT(*) FROM manifests WHERE did = ?`, testUser.DID).Scan(&count); err != nil { 1270 + t.Fatalf("Expected no errors scanning manifest count, got %s", err) 1271 + } 1270 1272 if count != 1 { 1271 1273 t.Fatalf("Expected 1 manifest, got %d", count) 1272 1274 } 1273 - db.QueryRow(`SELECT COUNT(*) FROM tags WHERE did = ?`, testUser.DID).Scan(&count) 1275 + if err := db.QueryRow(`SELECT COUNT(*) FROM tags WHERE did = ?`, testUser.DID).Scan(&count); err != nil { 1276 + t.Fatalf("Expected no errors scanning tag count, got %s", err) 1277 + } 1274 1278 if count != 1 { 1275 1279 t.Fatalf("Expected 1 tag, got %d", count) 1276 1280 } 1277 - db.QueryRow(`SELECT COUNT(*) FROM layers WHERE manifest_id = ?`, manifestID).Scan(&count) 1281 + if err := db.QueryRow(`SELECT COUNT(*) FROM layers WHERE manifest_id = ?`, manifestID).Scan(&count); err != nil { 1282 + t.Fatalf("Expected no errors scanning layer count, got %s", err) 1283 + } 1278 1284 if count != 1 { 1279 1285 t.Fatalf("Expected 1 layer, got %d", count) 1280 1286 } ··· 1285 1291 } 1286 1292 1287 1293 // Verify all data was cascade deleted 1288 - db.QueryRow(`SELECT COUNT(*) FROM users WHERE did = ?`, testUser.DID).Scan(&count) 1294 + if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE did = ?`, testUser.DID).Scan(&count); err != nil { 1295 + t.Errorf("Expected no errors scanning user count, got %s", err) 1296 + } 1289 1297 if count != 0 { 1290 1298 t.Errorf("Expected 0 users, got %d", count) 1291 1299 } 1292 - db.QueryRow(`SELECT COUNT(*) FROM manifests WHERE did = ?`, testUser.DID).Scan(&count) 1300 + if err := db.QueryRow(`SELECT COUNT(*) FROM manifests WHERE did = ?`, testUser.DID).Scan(&count); err != nil { 1301 + t.Errorf("Expected no errors scanning manifest count, got %s", err) 1302 + } 1293 1303 if count != 0 { 1294 1304 t.Errorf("Expected 0 manifests after cascade delete, got %d", count) 1295 1305 } 1296 - db.QueryRow(`SELECT COUNT(*) FROM tags WHERE did = ?`, testUser.DID).Scan(&count) 1306 + if err := db.QueryRow(`SELECT COUNT(*) FROM tags WHERE did = ?`, testUser.DID).Scan(&count); err != nil { 1307 + t.Errorf("Expected no errors scanning tag count, got %s", err) 1308 + } 1297 1309 if count != 0 { 1298 1310 t.Errorf("Expected 0 tags after cascade delete, got %d", count) 1299 1311 } 1300 - db.QueryRow(`SELECT COUNT(*) FROM layers WHERE manifest_id = ?`, manifestID).Scan(&count) 1312 + if err := db.QueryRow(`SELECT COUNT(*) FROM layers WHERE manifest_id = ?`, manifestID).Scan(&count); err != nil { 1313 + t.Errorf("Expected no errors scanning layer count, got %s", err) 1314 + } 1301 1315 if count != 0 { 1302 1316 t.Errorf("Expected 0 layers after cascade delete, got %d", count) 1303 1317 }
+6 -2
pkg/appview/db/readonly.go
··· 102 102 103 103 // Cleanup OAuth sessions (older than 30 days) 104 104 oauthStore := NewOAuthStore(database) 105 - oauthStore.CleanupOldSessions(ctx, 30*24*time.Hour) 106 - oauthStore.CleanupExpiredAuthRequests(ctx) 105 + if err := oauthStore.CleanupOldSessions(ctx, 30*24*time.Hour); err != nil { 106 + slog.Warn("Failed to clean up old sessions", "error", err) 107 + } 108 + if err := oauthStore.CleanupExpiredAuthRequests(ctx); err != nil { 109 + slog.Warn("Failed to clean up expired auth requests", "error", err) 110 + } 107 111 108 112 // Cleanup device pending auths 109 113 deviceStore := NewDeviceStore(database)
+2 -2
pkg/appview/db/schema.go
··· 225 225 var statements []string 226 226 227 227 // Split on semicolons 228 - parts := strings.Split(query, ";") 228 + parts := strings.SplitSeq(query, ";") 229 229 230 - for _, part := range parts { 230 + for part := range parts { 231 231 // Trim whitespace 232 232 stmt := strings.TrimSpace(part) 233 233
+2 -2
pkg/appview/db/session_store_test.go
··· 252 252 253 253 // Create multiple sessions for alice 254 254 sessionIDs := make([]string, 3) 255 - for i := 0; i < 3; i++ { 255 + for i := range 3 { 256 256 id, err := store.Create(did, "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 257 257 if err != nil { 258 258 t.Fatalf("Create() error = %v", err) ··· 516 516 517 517 // Generate multiple session IDs 518 518 ids := make(map[string]bool) 519 - for i := 0; i < 100; i++ { 519 + for range 100 { 520 520 id, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 521 521 if err != nil { 522 522 t.Fatalf("Create() error = %v", err)
+4 -1
pkg/appview/db/tag_delete_test.go
··· 155 155 t.Logf("Remaining tags in database:") 156 156 for rows.Next() { 157 157 var repo, tag string 158 - rows.Scan(&repo, &tag) 158 + if err := rows.Scan(&repo, &tag); err != nil { 159 + t.Logf("Expected no errors scanning row, got %s", err) 160 + continue 161 + } 159 162 t.Logf(" - repository=%s, tag=%s", repo, tag) 160 163 } 161 164 rows.Close()
+32 -8
pkg/appview/handlers/api.go
··· 68 68 // Return success 69 69 w.Header().Set("Content-Type", "application/json") 70 70 w.WriteHeader(http.StatusCreated) 71 - json.NewEncoder(w).Encode(map[string]bool{"starred": true}) 71 + if err := json.NewEncoder(w).Encode(map[string]bool{"starred": true}); err != nil { 72 + slog.Error("failed to encode json to http response body", "error", err) 73 + w.WriteHeader(http.StatusInternalServerError) 74 + } 72 75 } 73 76 74 77 // UnstarRepositoryHandler handles unstarring a repository ··· 123 126 124 127 // Return success 125 128 w.Header().Set("Content-Type", "application/json") 126 - json.NewEncoder(w).Encode(map[string]bool{"starred": false}) 129 + if err := json.NewEncoder(w).Encode(map[string]bool{"starred": false}); err != nil { 130 + slog.Error("failed to encode json to http response body", "error", err) 131 + w.WriteHeader(http.StatusInternalServerError) 132 + } 127 133 } 128 134 129 135 // CheckStarHandler checks if current user has starred a repository ··· 139 145 if user == nil { 140 146 // Not authenticated - return not starred 141 147 w.Header().Set("Content-Type", "application/json") 142 - json.NewEncoder(w).Encode(map[string]bool{"starred": false}) 148 + if err := json.NewEncoder(w).Encode(map[string]bool{"starred": false}); err != nil { 149 + slog.Error("failed to encode json to http response body", "error", err) 150 + w.WriteHeader(http.StatusInternalServerError) 151 + } 143 152 return 144 153 } 145 154 ··· 167 176 if err != nil && handleOAuthError(r.Context(), h.Refresher, user.DID, err) { 168 177 // For a read operation, just return not starred instead of error 169 178 w.Header().Set("Content-Type", "application/json") 170 - json.NewEncoder(w).Encode(map[string]bool{"starred": false}) 179 + if err := json.NewEncoder(w).Encode(map[string]bool{"starred": false}); err != nil { 180 + slog.Error("failed to encode json to http response body", "error", err) 181 + w.WriteHeader(http.StatusInternalServerError) 182 + } 171 183 return 172 184 } 173 185 ··· 175 187 176 188 // Return result 177 189 w.Header().Set("Content-Type", "application/json") 178 - json.NewEncoder(w).Encode(map[string]bool{"starred": starred}) 190 + if err := json.NewEncoder(w).Encode(map[string]bool{"starred": starred}); err != nil { 191 + slog.Error("failed to encode json to http response body", "error", err) 192 + w.WriteHeader(http.StatusInternalServerError) 193 + } 179 194 } 180 195 181 196 // GetStatsHandler returns repository statistics ··· 205 220 206 221 // Return stats as JSON 207 222 w.Header().Set("Content-Type", "application/json") 208 - json.NewEncoder(w).Encode(stats) 223 + if err := json.NewEncoder(w).Encode(stats); err != nil { 224 + slog.Error("failed to encode json to http response body", "error", err) 225 + w.WriteHeader(http.StatusInternalServerError) 226 + } 209 227 } 210 228 211 229 // ManifestDetailHandler returns detailed manifest information including platforms ··· 241 259 242 260 // Return manifest as JSON 243 261 w.Header().Set("Content-Type", "application/json") 244 - json.NewEncoder(w).Encode(manifest) 262 + if err := json.NewEncoder(w).Encode(manifest); err != nil { 263 + slog.Error("failed to encode json to http response body", "error", err) 264 + w.WriteHeader(http.StatusInternalServerError) 265 + } 245 266 } 246 267 247 268 // CredentialHelperVersionResponse is the response for the credential helper version API ··· 299 320 300 321 w.Header().Set("Content-Type", "application/json") 301 322 w.Header().Set("Cache-Control", "public, max-age=300") // Cache for 5 minutes 302 - json.NewEncoder(w).Encode(response) 323 + if err := json.NewEncoder(w).Encode(response); err != nil { 324 + slog.Error("failed to encode json to http response body", "error", err) 325 + w.WriteHeader(http.StatusInternalServerError) 326 + } 303 327 }
+40 -10
pkg/appview/handlers/device.go
··· 74 74 } 75 75 76 76 w.Header().Set("Content-Type", "application/json") 77 - json.NewEncoder(w).Encode(resp) 77 + if err := json.NewEncoder(w).Encode(resp); err != nil { 78 + slog.Error("failed to write to http response", "error", err) 79 + w.WriteHeader(http.StatusInternalServerError) 80 + } 78 81 } 79 82 80 83 // DeviceTokenRequest is the request to poll for device authorization ··· 114 117 Error: "expired_token", 115 118 } 116 119 w.Header().Set("Content-Type", "application/json") 117 - json.NewEncoder(w).Encode(resp) 120 + if err := json.NewEncoder(w).Encode(resp); err != nil { 121 + slog.Error("failed to write to http response", "error", err) 122 + w.WriteHeader(http.StatusInternalServerError) 123 + } 118 124 return 119 125 } 120 126 ··· 125 131 Error: "authorization_pending", 126 132 } 127 133 w.Header().Set("Content-Type", "application/json") 128 - json.NewEncoder(w).Encode(resp) 134 + if err := json.NewEncoder(w).Encode(resp); err != nil { 135 + slog.Error("failed to write to http response", "error", err) 136 + w.WriteHeader(http.StatusInternalServerError) 137 + } 129 138 return 130 139 } 131 140 ··· 148 157 } 149 158 150 159 w.Header().Set("Content-Type", "application/json") 151 - json.NewEncoder(w).Encode(resp) 160 + if err := json.NewEncoder(w).Encode(resp); err != nil { 161 + slog.Error("failed to write to http response", "error", err) 162 + w.WriteHeader(http.StatusInternalServerError) 163 + } 152 164 } 153 165 154 166 // DeviceApprovalPageHandler handles GET /device ··· 265 277 if !req.Approve { 266 278 // User denied 267 279 w.Header().Set("Content-Type", "application/json") 268 - json.NewEncoder(w).Encode(map[string]string{"status": "denied"}) 280 + if err := json.NewEncoder(w).Encode(map[string]string{"status": "denied"}); err != nil { 281 + slog.Error("failed to write to http response", "error", err) 282 + w.WriteHeader(http.StatusInternalServerError) 283 + } 269 284 return 270 285 } 271 286 ··· 277 292 return 278 293 } 279 294 w.Header().Set("Content-Type", "application/json") 280 - json.NewEncoder(w).Encode(map[string]string{"status": "approved"}) 295 + if err := json.NewEncoder(w).Encode(map[string]string{"status": "approved"}); err != nil { 296 + slog.Error("failed to write to http response", "error", err) 297 + w.WriteHeader(http.StatusInternalServerError) 298 + } 281 299 } 282 300 283 301 // ListDevicesHandler handles GET /api/devices ··· 309 327 devices := h.Store.ListDevices(sess.DID) 310 328 311 329 w.Header().Set("Content-Type", "application/json") 312 - json.NewEncoder(w).Encode(devices) 330 + if err := json.NewEncoder(w).Encode(devices); err != nil { 331 + slog.Error("failed to write to http response", "error", err) 332 + w.WriteHeader(http.StatusInternalServerError) 333 + } 313 334 } 314 335 315 336 // RevokeDeviceHandler handles DELETE /api/devices/{id} ··· 370 391 } 371 392 372 393 w.Header().Set("Content-Type", "text/html; charset=utf-8") 373 - tmpl.Execute(w, data) 394 + if err := tmpl.Execute(w, data); err != nil { 395 + slog.Error("failed to write to http response", "error", err) 396 + w.WriteHeader(http.StatusInternalServerError) 397 + } 374 398 } 375 399 376 400 func (h *DeviceApprovalPageHandler) renderSuccess(w http.ResponseWriter, deviceName string) { ··· 382 406 } 383 407 384 408 w.Header().Set("Content-Type", "text/html; charset=utf-8") 385 - tmpl.Execute(w, data) 409 + if err := tmpl.Execute(w, data); err != nil { 410 + slog.Error("failed to write to http response", "error", err) 411 + w.WriteHeader(http.StatusInternalServerError) 412 + } 386 413 } 387 414 388 415 func (h *DeviceApprovalPageHandler) renderError(w http.ResponseWriter, message string) { ··· 395 422 396 423 w.Header().Set("Content-Type", "text/html; charset=utf-8") 397 424 w.WriteHeader(http.StatusBadRequest) 398 - tmpl.Execute(w, data) 425 + if err := tmpl.Execute(w, data); err != nil { 426 + slog.Error("failed to write to http response", "error", err) 427 + w.WriteHeader(http.StatusInternalServerError) 428 + } 399 429 } 400 430 401 431 func getClientIP(r *http.Request) string {
+3 -1
pkg/appview/handlers/device_test.go
··· 603 603 604 604 // Create some devices 605 605 pending, _ := store.CreatePendingAuth("Device 1", "127.0.0.1", "TestAgent/1.0") 606 - store.ApprovePending(pending.UserCode, "did:plc:test123", "test.bsky.social") 606 + if _, err := store.ApprovePending(pending.UserCode, "did:plc:test123", "test.bsky.social"); err != nil { 607 + t.Fatalf("Expected no errors approving pending device, got %s", err) 608 + } 607 609 608 610 handler := &ListDevicesHandler{ 609 611 Store: store,
+13 -4
pkg/appview/handlers/images.go
··· 6 6 "errors" 7 7 "fmt" 8 8 "io" 9 + "log/slog" 9 10 "net/http" 10 11 "strings" 11 12 "time" ··· 94 95 } 95 96 96 97 w.Header().Set("Content-Type", "application/json") 97 - w.WriteHeader(http.StatusConflict) 98 - json.NewEncoder(w).Encode(map[string]interface{}{ 98 + resp := map[string]any{ 99 99 "error": "confirmation_required", 100 100 "message": "This manifest has associated tags that will also be deleted", 101 101 "tags": tags, 102 - }) 102 + } 103 + if err := json.NewEncoder(w).Encode(resp); err != nil { 104 + slog.Error("failed to write response", "error", err) 105 + w.WriteHeader(http.StatusInternalServerError) 106 + } 107 + w.WriteHeader(http.StatusConflict) 103 108 return 104 109 } 105 110 ··· 267 272 // Return new avatar URL 268 273 avatarURL := atproto.BlobCDNURL(user.DID, blobRef.Ref.Link) 269 274 w.Header().Set("Content-Type", "application/json") 270 - json.NewEncoder(w).Encode(map[string]string{"avatarURL": avatarURL}) 275 + resp := map[string]string{"avatarURL": avatarURL} 276 + if err := json.NewEncoder(w).Encode(resp); err != nil { 277 + slog.Error("failed to write response", "error", err) 278 + w.WriteHeader(http.StatusInternalServerError) 279 + } 271 280 }
+9 -6
pkg/appview/handlers/manifest_health.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "log/slog" 5 6 "net/http" 6 7 "net/url" 7 8 "time" ··· 61 62 func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) { 62 63 w.Header().Set("Content-Type", "text/html") 63 64 65 + var resp string 64 66 if pending { 65 67 // Still checking - render badge with HTMX retry after 3 seconds 66 68 retryURL := "/api/manifest-health?endpoint=" + url.QueryEscape(endpoint) 67 - w.Write([]byte(`<span class="checking-badge" 69 + resp = `<span class="checking-badge" 68 70 hx-get="` + retryURL + `" 69 71 hx-trigger="load delay:3s" 70 - hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span>`)) 72 + hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span>` 71 73 } else if !reachable { 72 74 // Unreachable - render offline badge 73 - w.Write([]byte(`<span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span>`)) 74 - } else { 75 - // Reachable - no badge (empty response) 76 - w.Write([]byte(``)) 75 + resp = `<span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span>` 76 + } 77 + if _, err := w.Write([]byte(resp)); err != nil { 78 + slog.Error("failed to write response", "error", err) 79 + w.WriteHeader(http.StatusInternalServerError) 77 80 } 78 81 }
+90 -18
pkg/appview/handlers/opengraph.go
··· 67 67 if avatarURL == "" { 68 68 avatarURL = user.Avatar 69 69 } 70 - card.DrawAvatarOrPlaceholder(avatarURL, layout.IconX, layout.IconY, ogcard.AvatarSize, 70 + err = card.DrawAvatarOrPlaceholder(avatarURL, layout.IconX, layout.IconY, ogcard.AvatarSize, 71 71 strings.ToUpper(string(repository[0]))) 72 + if err != nil { 73 + slog.Error("failed to draw avatar", "error", err) 74 + w.WriteHeader(http.StatusInternalServerError) 75 + return 76 + } 72 77 73 78 // Draw owner handle and repo name - wrap to new line if too long 74 79 ownerText := "@" + user.Handle + " / " ··· 79 84 textY := layout.TextY 80 85 if combinedWidth > layout.MaxWidth { 81 86 // Too long - put repo name on new line 82 - card.DrawText("@"+user.Handle+" /", layout.TextX, textY, ogcard.FontTitle, ogcard.ColorMuted, ogcard.AlignLeft, false) 87 + if err := card.DrawText("@"+user.Handle+" /", layout.TextX, textY, ogcard.FontTitle, ogcard.ColorMuted, ogcard.AlignLeft, false); err != nil { 88 + slog.Error("failed to draw text", "error", err) 89 + w.WriteHeader(http.StatusInternalServerError) 90 + return 91 + } 83 92 textY += ogcard.LineSpacingLarge 84 - card.DrawText(repository, layout.TextX, textY, ogcard.FontTitle, ogcard.ColorText, ogcard.AlignLeft, true) 93 + if err := card.DrawText(repository, layout.TextX, textY, ogcard.FontTitle, ogcard.ColorText, ogcard.AlignLeft, true); err != nil { 94 + slog.Error("failed to draw text", "error", err) 95 + w.WriteHeader(http.StatusInternalServerError) 96 + return 97 + } 85 98 } else { 86 99 // Fits on one line 87 - card.DrawText(ownerText, layout.TextX, textY, ogcard.FontTitle, ogcard.ColorMuted, ogcard.AlignLeft, false) 88 - card.DrawText(repository, layout.TextX+float64(ownerWidth), textY, ogcard.FontTitle, ogcard.ColorText, ogcard.AlignLeft, true) 100 + if err := card.DrawText(ownerText, layout.TextX, textY, ogcard.FontTitle, ogcard.ColorMuted, ogcard.AlignLeft, false); err != nil { 101 + slog.Error("failed to draw text", "error", err) 102 + w.WriteHeader(http.StatusInternalServerError) 103 + return 104 + } 105 + if err := card.DrawText(repository, layout.TextX+float64(ownerWidth), textY, ogcard.FontTitle, ogcard.ColorText, ogcard.AlignLeft, true); err != nil { 106 + slog.Error("failed to draw text", "error", err) 107 + w.WriteHeader(http.StatusInternalServerError) 108 + return 109 + } 89 110 } 90 111 91 112 // Track current Y position for description 92 113 if description != "" { 93 114 textY += ogcard.LineSpacingSmall 94 - card.DrawTextWrapped(description, layout.TextX, textY, ogcard.FontDescription, ogcard.ColorMuted, layout.MaxWidth, false) 115 + if _, err := card.DrawTextWrapped(description, layout.TextX, textY, ogcard.FontDescription, ogcard.ColorMuted, layout.MaxWidth, false); err != nil { 116 + slog.Error("failed to draw text", "error", err) 117 + w.WriteHeader(http.StatusInternalServerError) 118 + return 119 + } 95 120 } 96 121 97 122 // Badges row (version, license) ··· 99 124 badgeX := int(layout.TextX) 100 125 101 126 if version != "" { 102 - width := card.DrawBadge(version, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeAccent, ogcard.ColorText) 127 + width, err := card.DrawBadge(version, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeAccent, ogcard.ColorText) 128 + if err != nil { 129 + slog.Error("failed to draw badge", "error", err) 130 + w.WriteHeader(http.StatusInternalServerError) 131 + return 132 + } 103 133 badgeX += width + ogcard.BadgeGap 104 134 } 105 135 ··· 107 137 // Show first license if multiple 108 138 license := strings.Split(licenses, ",")[0] 109 139 license = strings.TrimSpace(license) 110 - card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText) 140 + if _, err := card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText); err != nil { 141 + slog.Error("failed to draw badge", "error", err) 142 + w.WriteHeader(http.StatusInternalServerError) 143 + return 144 + } 111 145 } 112 146 113 147 // Stats at bottom 114 - statsX := card.DrawStatWithIcon("star", fmt.Sprintf("%d", stats.StarCount), 148 + statsX, err := card.DrawStatWithIcon("star", fmt.Sprintf("%d", stats.StarCount), 115 149 ogcard.Padding, layout.StatsY, ogcard.ColorStar, ogcard.ColorText) 116 - card.DrawStatWithIcon("arrow-down-to-line", fmt.Sprintf("%d pulls", stats.PullCount), 150 + if err != nil { 151 + slog.Error("failed to draw icon", "error", err) 152 + w.WriteHeader(http.StatusInternalServerError) 153 + return 154 + } 155 + _, err = card.DrawStatWithIcon("arrow-down-to-line", fmt.Sprintf("%d pulls", stats.PullCount), 117 156 statsX, layout.StatsY, ogcard.ColorMuted, ogcard.ColorMuted) 157 + if err != nil { 158 + slog.Error("failed to draw icon", "error", err) 159 + w.WriteHeader(http.StatusInternalServerError) 160 + return 161 + } 118 162 119 163 // ATCR branding (bottom right) 120 - card.DrawBranding() 164 + if err := card.DrawBranding(); err != nil { 165 + slog.Error("failed to draw brand", "error", err) 166 + w.WriteHeader(http.StatusInternalServerError) 167 + return 168 + } 121 169 122 170 // Set cache headers and content type 123 171 w.Header().Set("Content-Type", "image/png") ··· 139 187 140 188 // Draw large centered "ATCR" title 141 189 centerY := float64(ogcard.CardHeight) / 2 142 - card.DrawText("ATCR", float64(ogcard.CardWidth)/2, centerY-20, 96.0, ogcard.ColorText, ogcard.AlignCenter, true) 190 + if err := card.DrawText("ATCR", float64(ogcard.CardWidth)/2, centerY-20, 96.0, ogcard.ColorText, ogcard.AlignCenter, true); err != nil { 191 + slog.Error("failed to draw text", "error", err) 192 + w.WriteHeader(http.StatusInternalServerError) 193 + return 194 + } 143 195 144 196 // Draw tagline below 145 - card.DrawText("Distributed Container Registry", float64(ogcard.CardWidth)/2, centerY+60, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignCenter, false) 197 + if err := card.DrawText("Distributed Container Registry", float64(ogcard.CardWidth)/2, centerY+60, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignCenter, false); err != nil { 198 + slog.Error("failed to draw text", "error", err) 199 + w.WriteHeader(http.StatusInternalServerError) 200 + return 201 + } 146 202 147 203 // Draw subtitle 148 - card.DrawText("Push and pull Docker images on the AT Protocol", float64(ogcard.CardWidth)/2, centerY+110, ogcard.FontStats, ogcard.ColorMuted, ogcard.AlignCenter, false) 204 + if err := card.DrawText("Push and pull Docker images on the AT Protocol", float64(ogcard.CardWidth)/2, centerY+110, ogcard.FontStats, ogcard.ColorMuted, ogcard.AlignCenter, false); err != nil { 205 + slog.Error("failed to draw text", "error", err) 206 + w.WriteHeader(http.StatusInternalServerError) 207 + return 208 + } 149 209 150 210 // Set cache headers and content type (cache longer since it's static content) 151 211 w.Header().Set("Content-Type", "image/png") ··· 197 257 if len(user.Handle) > 0 { 198 258 firstChar = strings.ToUpper(string(user.Handle[0])) 199 259 } 200 - card.DrawAvatarOrPlaceholder(user.Avatar, layout.IconX, layout.IconY, ogcard.AvatarSize, firstChar) 260 + if err := card.DrawAvatarOrPlaceholder(user.Avatar, layout.IconX, layout.IconY, ogcard.AvatarSize, firstChar); err != nil { 261 + slog.Warn("Failed to draw avatar", "error", err) 262 + } 201 263 202 264 // Draw handle 203 265 handleText := "@" + user.Handle 204 - card.DrawText(handleText, layout.TextX, layout.TextY, ogcard.FontTitle, ogcard.ColorText, ogcard.AlignLeft, true) 266 + if err := card.DrawText(handleText, layout.TextX, layout.TextY, ogcard.FontTitle, ogcard.ColorText, ogcard.AlignLeft, true); err != nil { 267 + slog.Error("failed to draw text", "error", err) 268 + w.WriteHeader(http.StatusInternalServerError) 269 + return 270 + } 205 271 206 272 // Repository count below (using description font size) 207 273 textY := layout.TextY + ogcard.LineSpacingLarge ··· 214 280 if err := card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted); err != nil { 215 281 slog.Warn("Failed to draw package icon", "error", err) 216 282 } 217 - card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false) 283 + if err := card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false); err != nil { 284 + slog.Error("failed to draw text", "error", err) 285 + w.WriteHeader(http.StatusInternalServerError) 286 + return 287 + } 218 288 219 289 // ATCR branding (bottom right) 220 - card.DrawBranding() 290 + if err := card.DrawBranding(); err != nil { 291 + slog.Warn("Failed to draw brand", "error", err) 292 + } 221 293 222 294 // Set cache headers and content type 223 295 w.Header().Set("Content-Type", "image/png")
+4 -1
pkg/appview/handlers/settings.go
··· 105 105 } 106 106 107 107 w.Header().Set("Content-Type", "text/html") 108 - w.Write([]byte(`<div class="success"><i data-lucide="check"></i> Default hold updated successfully!</div>`)) 108 + if _, err := w.Write([]byte(`<div class="success"><i data-lucide="check"></i> Default hold updated successfully!</div>`)); err != nil { 109 + slog.Error("Failed to write response", "error", err) 110 + w.WriteHeader(http.StatusInternalServerError) 111 + } 109 112 }
+6 -2
pkg/appview/holdhealth/checker_test.go
··· 34 34 t.Errorf("Expected path /xrpc/_health, got %s", r.URL.Path) 35 35 } 36 36 w.WriteHeader(http.StatusOK) 37 - w.Write([]byte(`{"version": "1.0.0"}`)) 37 + if _, err := w.Write([]byte(`{"version": "1.0.0"}`)); err != nil { 38 + t.Errorf("Expected no errors writing response, got %s", err) 39 + } 38 40 })) 39 41 defer server.Close() 40 42 ··· 58 60 t.Errorf("Expected path /xrpc/_health, got %s", r.URL.Path) 59 61 } 60 62 w.WriteHeader(http.StatusOK) 61 - w.Write([]byte(`{"version": "1.0.0"}`)) 63 + if _, err := w.Write([]byte(`{"version": "1.0.0"}`)); err != nil { 64 + t.Errorf("Expected no errors writing response, got %s", err) 65 + } 62 66 })) 63 67 defer server.Close() 64 68
+2 -4
pkg/appview/holdhealth/worker.go
··· 53 53 54 54 // Start begins the background worker 55 55 func (w *Worker) Start(ctx context.Context) { 56 - w.wg.Add(1) 57 - go func() { 58 - defer w.wg.Done() 56 + w.wg.Go(func() { 59 57 60 58 slog.Info("Hold health worker starting background health checks") 61 59 ··· 89 87 w.checker.Cleanup() 90 88 } 91 89 } 92 - }() 90 + }) 93 91 } 94 92 95 93 // Stop gracefully stops the worker
-10
pkg/appview/holdhealth/worker_test.go
··· 1 1 package holdhealth 2 2 3 - import "testing" 4 - 5 - func TestWorker_Struct(t *testing.T) { 6 - // Simple struct test 7 - worker := &Worker{} 8 - if worker == nil { 9 - t.Error("Expected non-nil worker") 10 - } 11 - } 12 - 13 3 // TODO: Add background health check tests
+1 -1
pkg/appview/jetstream/processor_test.go
··· 675 675 } 676 676 677 677 // Test 5: Process multiple deactivation events (idempotent) 678 - for i := 0; i < 3; i++ { 678 + for i := range 3 { 679 679 err = processor.ProcessAccount(context.Background(), testDID, false, "deactivated") 680 680 if err != nil { 681 681 t.Logf("Expected cache invalidation error on iteration %d: %v", i, err)
+7 -4
pkg/appview/jetstream/worker.go
··· 128 128 129 129 // Reset read deadline - we know connection is alive 130 130 // Allow 90 seconds for next pong (3x ping interval) 131 - conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 132 - return nil 131 + return conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 133 132 }) 134 133 135 134 // Set initial read deadline 136 - conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 135 + if err := conn.SetReadDeadline(time.Now().Add(90 * time.Second)); err != nil { 136 + return err 137 + } 137 138 138 139 // Create zstd decoder for decompressing messages 139 140 decoder, err := zstd.NewReader(nil) ··· 179 180 } 180 181 181 182 // Send ping with write deadline 182 - conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) 183 + if err := conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { 184 + slog.Error("Failed to set write deadline", "error", err) 185 + } 183 186 if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { 184 187 slog.Warn("Jetstream failed to send ping", "error", err) 185 188 conn.Close()
+2 -2
pkg/appview/middleware/auth_test.go
··· 318 318 // Pre-create all users and sessions before concurrent access 319 319 // This ensures database is fully initialized before goroutines start 320 320 sessionIDs := make([]string, 10) 321 - for i := 0; i < 10; i++ { 321 + for i := range 10 { 322 322 did := fmt.Sprintf("did:plc:user%d", i) 323 323 handle := fmt.Sprintf("user%d.bsky.social", i) 324 324 ··· 358 358 var wg sync.WaitGroup 359 359 var mu sync.Mutex // Protect results map 360 360 361 - for i := 0; i < 10; i++ { 361 + for i := range 10 { 362 362 wg.Add(1) 363 363 go func(index int, sessionID string) { 364 364 defer wg.Done()
+7 -2
pkg/appview/middleware/registry.go
··· 32 32 // pullerDIDKey is the context key for storing the authenticated user's DID from JWT 33 33 const pullerDIDKey contextKey = "puller.did" 34 34 35 + // httpRequestMethodKey is the context key for the HTTP request method 36 + const httpRequestMethodKey contextKey = "http.request.method" 37 + 35 38 // validationCacheEntry stores a validated service token with expiration 36 39 type validationCacheEntry struct { 37 40 serviceToken string ··· 204 207 205 208 func init() { 206 209 // Register the name resolution middleware 207 - registrymw.Register("atproto-resolver", initATProtoResolver) 210 + if err := registrymw.Register("atproto-resolver", initATProtoResolver); err != nil { 211 + slog.Error("Failed to register middleware", "error", err) 212 + } 208 213 } 209 214 210 215 // NamespaceResolver wraps a namespace and resolves names ··· 555 560 556 561 // Store HTTP method in context for routing decisions 557 562 // This is used by routing_repository.go to distinguish pull (GET/HEAD) from push (PUT/POST) 558 - ctx = context.WithValue(ctx, "http.request.method", r.Method) 563 + ctx = context.WithValue(ctx, httpRequestMethodKey, r.Method) 559 564 560 565 // Extract Authorization header 561 566 authHeader := r.Header.Get("Authorization")
+15 -21
pkg/appview/middleware/registry_test.go
··· 45 45 return nil 46 46 } 47 47 48 - // mockRepository is a minimal mock implementation 49 - type mockRepository struct { 50 - distribution.Repository 51 - name string 52 - } 53 - 54 48 func TestSetGlobalRefresher(t *testing.T) { 55 49 // Test that SetGlobalRefresher doesn't panic 56 50 SetGlobalRefresher(nil) ··· 152 146 if r.URL.Path == "/xrpc/com.atproto.repo.listRecords" { 153 147 // Empty hold records 154 148 w.Header().Set("Content-Type", "application/json") 155 - json.NewEncoder(w).Encode(map[string]any{ 156 - "records": []any{}, 157 - }) 149 + if err := json.NewEncoder(w).Encode(map[string]any{"records": []any{}}); err != nil { 150 + t.Errorf("expected no error encoding json, got %s", err) 151 + } 158 152 return 159 153 } 160 154 w.WriteHeader(http.StatusNotFound) ··· 179 173 // Return sailor profile with defaultHold 180 174 profile := atproto.NewSailorProfileRecord("did:web:user.hold.io") 181 175 w.Header().Set("Content-Type", "application/json") 182 - json.NewEncoder(w).Encode(map[string]any{ 183 - "value": profile, 184 - }) 176 + if err := json.NewEncoder(w).Encode(map[string]any{"value": profile}); err != nil { 177 + t.Errorf("expected no error encoding json, got %s", err) 178 + } 185 179 return 186 180 } 187 181 w.WriteHeader(http.StatusNotFound) ··· 207 201 // Return sailor profile with defaultHold (highest priority) 208 202 profile := atproto.NewSailorProfileRecord("did:web:profile.hold.io") 209 203 w.Header().Set("Content-Type", "application/json") 210 - json.NewEncoder(w).Encode(map[string]any{ 211 - "value": profile, 212 - }) 204 + if err := json.NewEncoder(w).Encode(map[string]any{"value": profile}); err != nil { 205 + t.Errorf("expected no error encoding json, got %s", err) 206 + } 213 207 return 214 208 } 215 209 w.WriteHeader(http.StatusNotFound) ··· 235 229 // Return sailor profile with an unreachable hold 236 230 profile := atproto.NewSailorProfileRecord("did:web:unreachable.hold.io") 237 231 w.Header().Set("Content-Type", "application/json") 238 - json.NewEncoder(w).Encode(map[string]any{ 239 - "value": profile, 240 - }) 232 + if err := json.NewEncoder(w).Encode(map[string]any{"value": profile}); err != nil { 233 + t.Errorf("expected no error encoding json, got %s", err) 234 + } 241 235 return 242 236 } 243 237 w.WriteHeader(http.StatusNotFound) ··· 262 256 mockHold := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 263 257 if r.URL.Path == "/.well-known/did.json" { 264 258 w.Header().Set("Content-Type", "application/json") 265 - json.NewEncoder(w).Encode(map[string]any{ 266 - "id": "did:web:reachable.hold.io", 267 - }) 259 + if err := json.NewEncoder(w).Encode(map[string]any{"id": "did:web:reachable.hold.io"}); err != nil { 260 + t.Errorf("expected no error encoding json, got %s", err) 261 + } 268 262 return 269 263 } 270 264 w.WriteHeader(http.StatusNotFound)
+39 -27
pkg/appview/ogcard/card.go
··· 2 2 package ogcard 3 3 4 4 import ( 5 + "fmt" 5 6 "image" 6 7 "image/color" 7 8 "image/draw" ··· 143 144 defer face.Close() 144 145 145 146 textWidth := font.MeasureString(face, text).Round() 146 - if align == AlignCenter { 147 + switch align { 148 + case AlignCenter: 147 149 x -= float64(textWidth) / 2 148 - } else if align == AlignRight { 150 + case AlignRight: 149 151 x -= float64(textWidth) 150 152 } 151 153 } ··· 174 176 175 177 // DrawTextWrapped draws text with word wrapping within maxWidth 176 178 // Returns the Y position after the last line 177 - func (c *Card) DrawTextWrapped(text string, x, y float64, size float64, col color.Color, maxWidth int, bold bool) float64 { 179 + func (c *Card) DrawTextWrapped(text string, x, y float64, size float64, col color.Color, maxWidth int, bold bool) (float64, error) { 178 180 words := splitWords(text) 179 181 if len(words) == 0 { 180 - return y 182 + return y, nil 181 183 } 182 184 183 185 lineHeight := size * 1.3 ··· 194 196 lineWidth := c.MeasureText(testLine, size, bold) 195 197 if lineWidth > maxWidth && currentLine != "" { 196 198 // Draw current line and start new one 197 - c.DrawText(currentLine, x, currentY, size, col, AlignLeft, bold) 199 + if err := c.DrawText(currentLine, x, currentY, size, col, AlignLeft, bold); err != nil { 200 + return 0, fmt.Errorf("could not write to og card image: %w", err) 201 + } 198 202 currentY += lineHeight 199 203 currentLine = word 200 204 } else { ··· 204 208 205 209 // Draw remaining text 206 210 if currentLine != "" { 207 - c.DrawText(currentLine, x, currentY, size, col, AlignLeft, bold) 211 + if err := c.DrawText(currentLine, x, currentY, size, col, AlignLeft, bold); err != nil { 212 + return 0, fmt.Errorf("could not write to og card image: %w", err) 213 + } 208 214 currentY += lineHeight 209 215 } 210 216 211 - return currentY 217 + return currentY, nil 212 218 } 213 219 214 220 // splitWords splits text into words ··· 270 276 } 271 277 272 278 // DrawPlaceholderCircle draws a colored circle with a letter 273 - func (c *Card) DrawPlaceholderCircle(x, y, diameter int, bgColor, textColor color.Color, letter string) { 279 + func (c *Card) DrawPlaceholderCircle(x, y, diameter int, bgColor, textColor color.Color, letter string) error { 274 280 // Draw filled circle 275 281 radius := diameter / 2 276 282 centerX := x + radius ··· 286 292 287 293 // Draw letter in center 288 294 fontSize := float64(diameter) * 0.5 289 - c.DrawText(letter, float64(centerX), float64(centerY)+fontSize/3, fontSize, textColor, AlignCenter, true) 295 + return c.DrawText(letter, float64(centerX), float64(centerY)+fontSize/3, fontSize, textColor, AlignCenter, true) 290 296 } 291 297 292 298 // DrawRoundedRect draws a filled rounded rectangle 293 299 func (c *Card) DrawRoundedRect(x, y, w, h, radius int, col color.Color) { 294 300 // Draw main rectangle (without corners) 295 301 for dy := radius; dy < h-radius; dy++ { 296 - for dx := 0; dx < w; dx++ { 302 + for dx := range w { 297 303 c.img.Set(x+dx, y+dy, col) 298 304 } 299 305 } 300 306 // Draw top and bottom strips (without corners) 301 - for dy := 0; dy < radius; dy++ { 307 + for dy := range radius { 302 308 for dx := radius; dx < w-radius; dx++ { 303 309 c.img.Set(x+dx, y+dy, col) 304 310 c.img.Set(x+dx, y+h-1-dy, col) 305 311 } 306 312 } 307 313 // Draw rounded corners 308 - for dy := 0; dy < radius; dy++ { 309 - for dx := 0; dx < radius; dx++ { 314 + for dy := range radius { 315 + for dx := range radius { 310 316 // Check if point is within circle 311 317 cx := radius - dx - 1 312 318 cy := radius - dy - 1 ··· 325 331 } 326 332 327 333 // DrawBadge draws a pill-shaped badge with text 328 - func (c *Card) DrawBadge(text string, x, y int, fontSize float64, bgColor, textColor color.Color) int { 334 + func (c *Card) DrawBadge(text string, x, y int, fontSize float64, bgColor, textColor color.Color) (int, error) { 329 335 // Measure text width 330 336 textWidth := c.MeasureText(text, fontSize, false) 331 337 paddingX := 12 ··· 340 346 // Draw text centered in badge 341 347 textX := float64(x + paddingX) 342 348 textY := float64(y + paddingY + int(fontSize) - 2) 343 - c.DrawText(text, textX, textY, fontSize, textColor, AlignLeft, false) 349 + if err := c.DrawText(text, textX, textY, fontSize, textColor, AlignLeft, false); err != nil { 350 + return 0, err 351 + } 344 352 345 - return width 353 + return width, nil 346 354 } 347 355 348 356 // EncodePNG encodes the card as PNG to the writer ··· 351 359 } 352 360 353 361 // DrawAvatarOrPlaceholder draws a circular avatar from URL, falling back to placeholder 354 - func (c *Card) DrawAvatarOrPlaceholder(url string, x, y, size int, letter string) { 362 + func (c *Card) DrawAvatarOrPlaceholder(url string, x, y, size int, letter string) error { 355 363 if url != "" { 356 364 if err := c.FetchAndDrawCircularImage(url, x, y, size); err == nil { 357 - return 365 + return nil 358 366 } 359 367 } 360 - c.DrawPlaceholderCircle(x, y, size, ColorAccent, ColorText, letter) 368 + return c.DrawPlaceholderCircle(x, y, size, ColorAccent, ColorText, letter) 361 369 } 362 370 363 371 // DrawStatWithIcon draws an icon + text stat and returns the next X position 364 - func (c *Card) DrawStatWithIcon(icon string, text string, x, y int, iconColor, textColor color.Color) int { 365 - c.DrawIcon(icon, x, y-int(FontStats), int(FontStats), iconColor) 372 + func (c *Card) DrawStatWithIcon(icon string, text string, x, y int, iconColor, textColor color.Color) (int, error) { 373 + if err := c.DrawIcon(icon, x, y-int(FontStats), int(FontStats), iconColor); err != nil { 374 + return 0, err 375 + } 366 376 x += StatsIconGap 367 - c.DrawText(text, float64(x), float64(y), FontStats, textColor, AlignLeft, false) 368 - return x + c.MeasureText(text, FontStats, false) + StatsItemGap 377 + if err := c.DrawText(text, float64(x), float64(y), FontStats, textColor, AlignLeft, false); err != nil { 378 + return 0, err 379 + } 380 + return x + c.MeasureText(text, FontStats, false) + StatsItemGap, nil 369 381 } 370 382 371 383 // DrawBranding draws "ATCR" in the bottom-right corner 372 - func (c *Card) DrawBranding() { 384 + func (c *Card) DrawBranding() error { 373 385 y := CardHeight - Padding - 10 374 - c.DrawText("ATCR", float64(CardWidth-Padding), float64(y), FontBranding, ColorMuted, AlignRight, true) 386 + return c.DrawText("ATCR", float64(CardWidth-Padding), float64(y), FontBranding, ColorMuted, AlignRight, true) 375 387 } 376 388 377 389 // scaleImage scales an image to the target dimensions ··· 388 400 centerX := radius 389 401 centerY := radius 390 402 391 - for y := 0; y < diameter; y++ { 392 - for x := 0; x < diameter; x++ { 403 + for y := range diameter { 404 + for x := range diameter { 393 405 dx := x - centerX 394 406 dy := y - centerY 395 407 if dx*dx+dy*dy <= radius*radius {
+1
pkg/appview/readme/fetcher.go
··· 1 + // Package readme fetches `README.md` from different hosts for Git repositories 1 2 package readme 2 3 3 4 import (
-6
pkg/appview/storage/context_test.go
··· 17 17 return m.holdDID, nil 18 18 } 19 19 20 - type mockHoldAuthorizer struct{} 21 - 22 - func (m *mockHoldAuthorizer) Authorize(holdDID, userDID, permission string) (bool, error) { 23 - return true, nil 24 - } 25 - 26 20 func TestRegistryContext_Fields(t *testing.T) { 27 21 // Create a sample RegistryContext 28 22 ctx := &RegistryContext{
+54 -18
pkg/appview/storage/manifest_store_test.go
··· 339 339 // Create mock PDS server 340 340 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 341 341 w.WriteHeader(tt.serverStatus) 342 - w.Write([]byte(tt.serverResp)) 342 + if _, err := w.Write([]byte(tt.serverResp)); err != nil { 343 + t.Errorf("Expected no error writing response, got %s", err) 344 + } 343 345 })) 344 346 defer server.Close() 345 347 ··· 453 455 // Handle both getRecord and getBlob requests 454 456 if r.URL.Path == atproto.SyncGetBlob { 455 457 w.WriteHeader(http.StatusOK) 456 - w.Write(tt.blobResp) 458 + if _, err := w.Write([]byte(tt.blobResp)); err != nil { 459 + t.Errorf("Expected no error writing response, got %s", err) 460 + } 457 461 return 458 462 } 459 463 w.WriteHeader(tt.serverStatus) 460 - w.Write([]byte(tt.serverResp)) 464 + if _, err := w.Write([]byte(tt.serverResp)); err != nil { 465 + t.Errorf("Expected no error writing response, got %s", err) 466 + } 461 467 })) 462 468 defer server.Close() 463 469 ··· 556 562 // Handle uploadBlob 557 563 if r.URL.Path == atproto.RepoUploadBlob { 558 564 w.WriteHeader(http.StatusOK) 559 - w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"mimeType":"application/json","size":100}}`)) 565 + if _, err := w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"mimeType":"application/json","size":100}}`)); err != nil { 566 + t.Errorf("Expected no error writing response, got %s", err) 567 + } 560 568 return 561 569 } 562 570 563 571 // Handle putRecord 564 572 if r.URL.Path == atproto.RepoPutRecord { 565 - json.NewDecoder(r.Body).Decode(&lastBody) 573 + if err := json.NewDecoder(r.Body).Decode(&lastBody); err != nil { 574 + t.Errorf("Expected no error writing body, got %s", err) 575 + } 566 576 w.WriteHeader(tt.serverStatus) 577 + var msg string 567 578 if tt.serverStatus == http.StatusOK { 568 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/abc123","cid":"bafytest"}`)) 579 + msg = `{"uri":"at://did:plc:test123/io.atcr.manifest/abc123","cid":"bafytest"}` 569 580 } else { 570 - w.Write([]byte(`{"error":"ServerError"}`)) 581 + msg = `{"error":"ServerError"}` 582 + } 583 + if _, err := w.Write([]byte(msg)); err != nil { 584 + t.Errorf("Expected no error writing response, got %s", err) 571 585 } 572 586 return 573 587 } ··· 617 631 618 632 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 619 633 if r.URL.Path == atproto.RepoUploadBlob { 620 - w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"size":100}}`)) 634 + if _, err := w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"size":100}}`)); err != nil { 635 + t.Errorf("Expected no error writing blob, got %s", err) 636 + } 621 637 return 622 638 } 623 639 if r.URL.Path == atproto.RepoPutRecord { 624 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/config123","cid":"bafytest"}`)) 640 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/config123","cid":"bafytest"}`)); err != nil { 641 + t.Errorf("Expected no error writing response, got %s", err) 642 + } 625 643 return 626 644 } 627 645 w.WriteHeader(http.StatusOK) ··· 700 718 } 701 719 702 720 w.WriteHeader(tt.serverStatus) 703 - w.Write([]byte(tt.serverResp)) 721 + if _, err := w.Write([]byte(tt.serverResp)); err != nil { 722 + t.Errorf("Expected no error writing response, got %s", err) 723 + } 704 724 })) 705 725 defer server.Close() 706 726 ··· 829 849 // Handle uploadBlob 830 850 if r.URL.Path == atproto.RepoUploadBlob { 831 851 w.WriteHeader(http.StatusOK) 832 - w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"mimeType":"application/json","size":100}}`)) 852 + if _, err := w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"mimeType":"application/json","size":100}}`)); err != nil { 853 + t.Errorf("Expected no error writing http body, got %s", err) 854 + } 833 855 return 834 856 } 835 857 ··· 841 863 // If child should exist, return it; otherwise return RecordNotFound 842 864 if tt.childExists || rkey == childDigest.Encoded() { 843 865 w.WriteHeader(http.StatusOK) 844 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/` + rkey + `","cid":"bafytest","value":{}}`)) 866 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/` + rkey + `","cid":"bafytest","value":{}}`)); err != nil { 867 + t.Errorf("Expected no error writing http body, got %s", err) 868 + } 845 869 } else { 846 870 w.WriteHeader(http.StatusBadRequest) 847 - w.Write([]byte(`{"error":"RecordNotFound","message":"Record not found"}`)) 871 + if _, err := w.Write([]byte(`{"error":"RecordNotFound","message":"Record not found"}`)); err != nil { 872 + t.Errorf("Expected no error writing http body, got %s", err) 873 + } 848 874 } 849 875 return 850 876 } ··· 852 878 // Handle putRecord 853 879 if r.URL.Path == atproto.RepoPutRecord { 854 880 w.WriteHeader(http.StatusOK) 855 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/test123","cid":"bafytest"}`)) 881 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/test123","cid":"bafytest"}`)); err != nil { 882 + t.Errorf("Expected no error writing http body, got %s", err) 883 + } 856 884 return 857 885 } 858 886 ··· 913 941 914 942 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 915 943 if r.URL.Path == atproto.RepoUploadBlob { 916 - w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"size":100}}`)) 944 + if _, err := w.Write([]byte(`{"blob":{"$type":"blob","ref":{"$link":"bafytest"},"size":100}}`)); err != nil { 945 + t.Errorf("Expected no error writing http body, got %s", err) 946 + } 917 947 return 918 948 } 919 949 920 950 if r.URL.Path == atproto.RepoGetRecord { 921 951 rkey := r.URL.Query().Get("rkey") 922 952 if existingManifests[rkey] { 923 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/` + rkey + `","cid":"bafytest","value":{}}`)) 953 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/` + rkey + `","cid":"bafytest","value":{}}`)); err != nil { 954 + t.Errorf("Expected no error writing http body, got %s", err) 955 + } 924 956 } else { 925 957 w.WriteHeader(http.StatusBadRequest) 926 - w.Write([]byte(`{"error":"RecordNotFound"}`)) 958 + if _, err := w.Write([]byte(`{"error":"RecordNotFound"}`)); err != nil { 959 + t.Errorf("Expected no error writing http body, got %s", err) 960 + } 927 961 } 928 962 return 929 963 } 930 964 931 965 if r.URL.Path == atproto.RepoPutRecord { 932 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/test123","cid":"bafytest"}`)) 966 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.manifest/test123","cid":"bafytest"}`)); err != nil { 967 + t.Errorf("Expected no error writing http body, got %s", err) 968 + } 933 969 return 934 970 } 935 971
+49 -20
pkg/appview/storage/profile_test.go
··· 51 51 // Second request: PutRecord (create profile) 52 52 if r.Method == "POST" && strings.Contains(r.URL.Path, "putRecord") { 53 53 var body map[string]any 54 - json.NewDecoder(r.Body).Decode(&body) 54 + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 55 + t.Errorf("Expected no errors decoding request body, got %s", err) 56 + } 55 57 56 58 // Verify profile data 57 59 recordData := body["record"].(map[string]any) ··· 72 74 73 75 // Store for later verification 74 76 profileBytes, _ := json.Marshal(recordData) 75 - json.Unmarshal(profileBytes, &createdProfile) 77 + if err := json.Unmarshal(profileBytes, &createdProfile); err != nil { 78 + t.Errorf("Expected no errors unmarshaling profile, got %s", err) 79 + } 80 + 81 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)); err != nil { 82 + t.Errorf("Expected no error writing response, got %s", err) 83 + } 76 84 77 85 w.WriteHeader(http.StatusOK) 78 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)) 79 86 return 80 87 } 81 88 ··· 124 131 } 125 132 }` 126 133 w.WriteHeader(http.StatusOK) 127 - w.Write([]byte(response)) 134 + if _, err := w.Write([]byte(response)); err != nil { 135 + t.Errorf("Expected no error writing response, got %s", err) 136 + } 128 137 return 129 138 } 130 139 ··· 227 236 // GetRecord 228 237 if r.Method == "GET" { 229 238 w.WriteHeader(tt.serverStatus) 230 - w.Write([]byte(tt.serverResponse)) 239 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 240 + t.Errorf("Expected no error writing response, got %s", err) 241 + } 231 242 return 232 243 } 233 244 ··· 235 246 if r.Method == "POST" && strings.Contains(r.URL.Path, "putRecord") { 236 247 mu.Lock() 237 248 putRecordCalled = true 238 - json.NewDecoder(r.Body).Decode(&migrationRequest) 249 + if err := json.NewDecoder(r.Body).Decode(&migrationRequest); err != nil { 250 + t.Errorf("Expected no errors decoding migration request, got %s", err) 251 + } 239 252 mu.Unlock() 240 253 w.WriteHeader(http.StatusOK) 241 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)) 254 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)); err != nil { 255 + t.Errorf("Expected no error writing response, got %s", err) 256 + } 242 257 return 243 258 } 244 259 })) ··· 316 331 } 317 332 }` 318 333 w.WriteHeader(http.StatusOK) 319 - w.Write([]byte(response)) 334 + if _, err := w.Write([]byte(response)); err != nil { 335 + t.Errorf("Expected no error writing response, got %s", err) 336 + } 320 337 return 321 338 } 322 339 ··· 330 347 time.Sleep(10 * time.Millisecond) 331 348 332 349 w.WriteHeader(http.StatusOK) 333 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)) 350 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)); err != nil { 351 + t.Errorf("Expected no error writing response, got %s", err) 352 + } 334 353 return 335 354 } 336 355 })) ··· 340 359 341 360 // Make 5 concurrent GetProfile calls 342 361 var wg sync.WaitGroup 343 - for i := 0; i < 5; i++ { 344 - wg.Add(1) 345 - go func() { 346 - defer wg.Done() 362 + for range 5 { 363 + wg.Go(func() { 347 364 _, err := GetProfile(context.Background(), client) 348 365 if err != nil { 349 366 t.Errorf("GetProfile() error = %v", err) 350 367 } 351 - }() 368 + }) 352 369 } 353 370 354 371 wg.Wait() ··· 416 433 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 417 434 if r.Method == "POST" && strings.Contains(r.URL.Path, "putRecord") { 418 435 var body map[string]any 419 - json.NewDecoder(r.Body).Decode(&body) 436 + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 437 + t.Errorf("Expected no errors decoding request body, got %s", err) 438 + } 420 439 sentProfile = body 421 440 422 441 // Verify rkey is "self" ··· 425 444 } 426 445 427 446 w.WriteHeader(http.StatusOK) 428 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)) 447 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`)); err != nil { 448 + t.Errorf("Expected no error writing response, got %s", err) 449 + } 429 450 return 430 451 } 431 452 w.WriteHeader(http.StatusBadRequest) ··· 481 502 // PutRecord: fail with server error 482 503 if r.Method == "POST" { 483 504 w.WriteHeader(http.StatusInternalServerError) 484 - w.Write([]byte(`{"error":"InternalServerError"}`)) 505 + if _, err := w.Write([]byte(`{"error":"InternalServerError"}`)); err != nil { 506 + t.Errorf("Expected no error writing response, got %s", err) 507 + } 485 508 return 486 509 } 487 510 })) ··· 503 526 "value": "not-valid-json-object" 504 527 }` 505 528 w.WriteHeader(http.StatusOK) 506 - w.Write([]byte(response)) 529 + if _, err := w.Write([]byte(response)); err != nil { 530 + t.Errorf("Expected no error writing response, got %s", err) 531 + } 507 532 })) 508 533 defer server.Close() 509 534 ··· 528 553 } 529 554 }` 530 555 w.WriteHeader(http.StatusOK) 531 - w.Write([]byte(response)) 556 + if _, err := w.Write([]byte(response)); err != nil { 557 + t.Errorf("Expected no error writing response, got %s", err) 558 + } 532 559 })) 533 560 defer server.Close() 534 561 ··· 548 575 func TestUpdateProfile_ServerError(t *testing.T) { 549 576 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 550 577 w.WriteHeader(http.StatusInternalServerError) 551 - w.Write([]byte(`{"error":"InternalServerError"}`)) 578 + if _, err := w.Write([]byte(`{"error":"InternalServerError"}`)); err != nil { 579 + t.Errorf("Expected no error writing response, got %s", err) 580 + } 552 581 })) 553 582 defer server.Close() 554 583
+14 -11
pkg/appview/storage/proxy_blob_store.go
··· 62 62 63 63 // doAuthenticatedRequest performs an HTTP request with service token authentication 64 64 // Uses the service token from middleware to authenticate requests to the hold service 65 - func (p *ProxyBlobStore) doAuthenticatedRequest(ctx context.Context, req *http.Request) (*http.Response, error) { 65 + func (p *ProxyBlobStore) doAuthenticatedRequest(req *http.Request) (*http.Response, error) { 66 66 // Use service token that middleware already validated and cached 67 67 // Middleware fails fast with HTTP 401 if OAuth session is invalid 68 68 if p.ctx.ServiceToken == "" { ··· 170 170 // Return a minimal descriptor with size from Content-Length if available 171 171 size := int64(0) 172 172 if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { 173 - fmt.Sscanf(contentLength, "%d", &size) 173 + if _, err := fmt.Sscanf(contentLength, "%d", &size); err != nil { 174 + return distribution.Descriptor{}, err 175 + } 174 176 } 175 177 176 178 return distribution.Descriptor{ ··· 389 391 return "", fmt.Errorf("failed to create request: %w", err) 390 392 } 391 393 392 - resp, err := p.doAuthenticatedRequest(ctx, req) 394 + resp, err := p.doAuthenticatedRequest(req) 393 395 if err != nil { 394 396 // Don't wrap errcode errors - return them directly 395 397 if _, ok := err.(errcode.Error); ok { ··· 439 441 req.Header.Set("Content-Type", "application/json") 440 442 441 443 // Use authenticated request (OAuth with DPoP) 442 - resp, err := p.doAuthenticatedRequest(ctx, req) 444 + resp, err := p.doAuthenticatedRequest(req) 443 445 if err != nil { 444 446 return "", err 445 447 } ··· 468 470 } 469 471 470 472 // getPartUploadInfo gets structured upload info for uploading a specific part via XRPC 471 - func (p *ProxyBlobStore) getPartUploadInfo(ctx context.Context, digest, uploadID string, partNumber int) (*PartUploadInfo, error) { 473 + func (p *ProxyBlobStore) getPartUploadInfo(ctx context.Context, uploadID string, partNumber int) (*PartUploadInfo, error) { 472 474 reqBody := map[string]any{ 473 475 "uploadId": uploadID, 474 476 "partNumber": partNumber, ··· 487 489 req.Header.Set("Content-Type", "application/json") 488 490 489 491 // Use authenticated request (OAuth with DPoP) 490 - resp, err := p.doAuthenticatedRequest(ctx, req) 492 + resp, err := p.doAuthenticatedRequest(req) 491 493 if err != nil { 492 494 return nil, err 493 495 } ··· 537 539 req.Header.Set("Content-Type", "application/json") 538 540 539 541 // Use authenticated request (OAuth with DPoP) 540 - resp, err := p.doAuthenticatedRequest(ctx, req) 542 + resp, err := p.doAuthenticatedRequest(req) 541 543 if err != nil { 542 544 return err 543 545 } ··· 570 572 req.Header.Set("Content-Type", "application/json") 571 573 572 574 // Use authenticated request (OAuth with DPoP) 573 - resp, err := p.doAuthenticatedRequest(ctx, req) 575 + resp, err := p.doAuthenticatedRequest(req) 574 576 if err != nil { 575 577 return err 576 578 } ··· 644 646 defer cancel() 645 647 646 648 // Get structured upload info for this part 647 - tempDigest := fmt.Sprintf("uploads/temp-%s", w.id) 648 - uploadInfo, err := w.store.getPartUploadInfo(ctx, tempDigest, w.uploadID, w.partNumber) 649 + uploadInfo, err := w.store.getPartUploadInfo(ctx, w.uploadID, w.partNumber) 649 650 if err != nil { 650 651 return fmt.Errorf("failed to get part upload info: %w", err) 651 652 } ··· 761 762 if err := w.flushPart(); err != nil { 762 763 // Try to abort multipart on error 763 764 tempDigest := fmt.Sprintf("uploads/temp-%s", w.id) 764 - w.store.abortMultipartUpload(ctx, tempDigest, w.uploadID) 765 + if err := w.store.abortMultipartUpload(ctx, tempDigest, w.uploadID); err != nil { 766 + return distribution.Descriptor{}, err 767 + } 765 768 return distribution.Descriptor{}, fmt.Errorf("failed to flush final part: %w", err) 766 769 } 767 770 }
+35 -14
pkg/appview/storage/proxy_blob_store_test.go
··· 56 56 // Test 4: Expired token - GetServiceToken automatically removes it 57 57 expiredPayload := fmt.Sprintf(`{"exp":%d}`, time.Now().Add(-1*time.Hour).Unix()) 58 58 expiredToken := "eyJhbGciOiJIUzI1NiJ9." + base64URLEncode(expiredPayload) + ".signature" 59 - auth.SetServiceToken(userDID, holdDID, expiredToken) 59 + if err := auth.SetServiceToken(userDID, holdDID, expiredToken); err != nil { 60 + t.Fatalf("Expected no errors setting expired service token, got %s", err) 61 + } 60 62 61 63 // GetServiceToken should return empty string for expired token 62 64 cachedToken, _ = auth.GetServiceToken(userDID, holdDID) ··· 131 133 } 132 134 133 135 // Do authenticated request 134 - resp, err := store.doAuthenticatedRequest(context.Background(), req) 136 + resp, err := store.doAuthenticatedRequest(req) 135 137 if err != nil { 136 138 t.Fatalf("doAuthenticatedRequest failed: %v", err) 137 139 } ··· 173 175 } 174 176 175 177 // Do authenticated request - should fail when no service token 176 - resp, err := store.doAuthenticatedRequest(context.Background(), req) 178 + resp, err := store.doAuthenticatedRequest(req) 177 179 if err == nil { 178 180 t.Fatal("Expected doAuthenticatedRequest to fail when no service token is available") 179 181 } ··· 234 236 // Insert expired token 235 237 expiredPayload := fmt.Sprintf(`{"exp":%d}`, time.Now().Add(-1*time.Hour).Unix()) 236 238 expiredToken := "eyJhbGciOiJIUzI1NiJ9." + base64URLEncode(expiredPayload) + ".signature" 237 - auth.SetServiceToken(userDID, holdDID, expiredToken) 239 + if err := auth.SetServiceToken(userDID, holdDID, expiredToken); err != nil { 240 + t.Fatalf("Expected no errors setting expired service token, got %s", err) 241 + } 238 242 239 243 // GetServiceToken should automatically remove expired tokens 240 244 cachedToken, expiresAt := auth.GetServiceToken(userDID, holdDID) ··· 310 314 311 315 testPayload := fmt.Sprintf(`{"exp":%d}`, time.Now().Add(50*time.Second).Unix()) 312 316 testTokenStr := "eyJhbGciOiJIUzI1NiJ9." + base64URLEncode(testPayload) + ".signature" 313 - auth.SetServiceToken(userDID, holdDID, testTokenStr) 317 + if err := auth.SetServiceToken(userDID, holdDID, testTokenStr); err != nil { 318 + b.Fatalf("Expected no errors setting service token, got %s", err) 319 + } 314 320 315 321 for b.Loop() { 316 322 cachedToken, expiresAt := auth.GetServiceToken(userDID, holdDID) ··· 341 347 342 348 w.Header().Set("Content-Type", "application/json") 343 349 w.WriteHeader(http.StatusOK) 344 - w.Write([]byte(`{}`)) 350 + if _, err := w.Write([]byte(`{}`)); err != nil { 351 + t.Errorf("Expected no error writing response, got %s", err) 352 + } 345 353 })) 346 354 defer holdServer.Close() 347 355 ··· 420 428 if s3ReceivedAuthHeader != "" { 421 429 t.Errorf("S3 received Authorization header: %s (should be empty for presigned URLs)", s3ReceivedAuthHeader) 422 430 w.WriteHeader(http.StatusForbidden) 423 - w.Write([]byte(`<?xml version="1.0"?><Error><Code>SignatureDoesNotMatch</Code></Error>`)) 431 + if _, err := w.Write([]byte(`<?xml version="1.0"?><Error><Code>SignatureDoesNotMatch</Code></Error>`)); err != nil { 432 + t.Errorf("Expected no error writing response, got %s", err) 433 + } 424 434 return 425 435 } 426 436 427 437 // Return blob data 428 438 w.WriteHeader(http.StatusOK) 429 - w.Write(blobData) 439 + if _, err := w.Write(blobData); err != nil { 440 + t.Errorf("Expected no error writing response, got %s", err) 441 + } 430 442 })) 431 443 defer s3Server.Close() 432 444 ··· 438 450 resp := map[string]string{ 439 451 "url": s3Server.URL + "/blob?X-Amz-Signature=fake-signature", 440 452 } 441 - json.NewEncoder(w).Encode(resp) 453 + if err := json.NewEncoder(w).Encode(resp); err != nil { 454 + t.Errorf("Expected no error unmarshalling to response, got %s", err) 455 + } 442 456 })) 443 457 defer holdServer.Close() 444 458 ··· 489 503 } 490 504 491 505 w.WriteHeader(http.StatusOK) 492 - w.Write(blobData) 506 + if _, err := w.Write(blobData); err != nil { 507 + t.Errorf("Expected no error writing response, got %s", err) 508 + } 493 509 })) 494 510 defer s3Server.Close() 495 511 ··· 497 513 holdServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 498 514 w.Header().Set("Content-Type", "application/json") 499 515 w.WriteHeader(http.StatusOK) 500 - json.NewEncoder(w).Encode(map[string]string{ 516 + resp := map[string]string{ 501 517 "url": s3Server.URL + "/blob?X-Amz-Signature=fake", 502 - }) 518 + } 519 + if err := json.NewEncoder(w).Encode(resp); err != nil { 520 + t.Errorf("Expected no error unmarshalling to response, got %s", err) 521 + } 503 522 })) 504 523 defer holdServer.Close() 505 524 ··· 547 566 { 548 567 name: "getPartUploadInfo", 549 568 testFunc: func(store *ProxyBlobStore) error { 550 - _, err := store.getPartUploadInfo(context.Background(), "sha256:test", "upload-123", 1) 569 + _, err := store.getPartUploadInfo(context.Background(), "upload-123", 1) 551 570 return err 552 571 }, 553 572 expectedPath: atproto.HoldGetPartUploadURL, ··· 584 603 "uploadId": "test-upload-id", 585 604 "url": "https://s3.example.com/presigned", 586 605 } 587 - json.NewEncoder(w).Encode(resp) 606 + if err := json.NewEncoder(w).Encode(resp); err != nil { 607 + t.Errorf("Expected no error unmarshalling to response, got %s", err) 608 + } 588 609 })) 589 610 defer holdServer.Close() 590 611
+11 -7
pkg/appview/storage/routing_repository_test.go
··· 12 12 "atcr.io/pkg/atproto" 13 13 ) 14 14 15 + type contextKey string 16 + 17 + const httpRequestMethodKey contextKey = "http.request.method" 18 + 15 19 // mockDatabase is a simple mock for testing 16 20 type mockDatabase struct { 17 21 holdDID string ··· 126 130 } 127 131 repo := NewRoutingRepository(nil, ctx) 128 132 129 - pullCtx := context.WithValue(context.Background(), "http.request.method", method) 133 + pullCtx := context.WithValue(context.Background(), httpRequestMethodKey, method) 130 134 blobStore := repo.Blobs(pullCtx) 131 135 132 136 assert.NotNil(t, blobStore) ··· 164 168 repo := NewRoutingRepository(nil, ctx) 165 169 166 170 // Create context with push method 167 - pushCtx := context.WithValue(context.Background(), "http.request.method", tc.method) 171 + pushCtx := context.WithValue(context.Background(), httpRequestMethodKey, tc.method) 168 172 blobStore := repo.Blobs(pushCtx) 169 173 170 174 assert.NotNil(t, blobStore) ··· 317 321 blobStores := make([]distribution.BlobStore, numGoroutines) 318 322 319 323 // Concurrent access to Manifests() 320 - for i := 0; i < numGoroutines; i++ { 324 + for i := range numGoroutines { 321 325 wg.Add(1) 322 326 go func(index int) { 323 327 defer wg.Done() ··· 330 334 wg.Wait() 331 335 332 336 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 333 - for i := 0; i < numGoroutines; i++ { 337 + for i := range numGoroutines { 334 338 assert.NotNil(t, manifestStores[i], "manifest store should not be nil") 335 339 } 336 340 ··· 340 344 assert.NotNil(t, cachedStore) 341 345 342 346 // Concurrent access to Blobs() 343 - for i := 0; i < numGoroutines; i++ { 347 + for i := range numGoroutines { 344 348 wg.Add(1) 345 349 go func(index int) { 346 350 defer wg.Done() ··· 351 355 wg.Wait() 352 356 353 357 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 354 - for i := 0; i < numGoroutines; i++ { 358 + for i := range numGoroutines { 355 359 assert.NotNil(t, blobStores[i], "blob store should not be nil") 356 360 } 357 361 ··· 376 380 repo := NewRoutingRepository(nil, ctx) 377 381 378 382 // For pull (GET), database should take priority 379 - pullCtx := context.WithValue(context.Background(), "http.request.method", "GET") 383 + pullCtx := context.WithValue(context.Background(), httpRequestMethodKey, "GET") 380 384 blobStore := repo.Blobs(pullCtx) 381 385 382 386 assert.NotNil(t, blobStore)
+48 -16
pkg/appview/storage/tag_store_test.go
··· 77 77 } 78 78 79 79 w.WriteHeader(tt.serverStatus) 80 - w.Write([]byte(tt.serverResponse)) 80 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 81 + t.Errorf("Expected no error writing response, got %s", err) 82 + } 81 83 })) 82 84 defer server.Close() 83 85 ··· 116 118 } 117 119 }` 118 120 w.WriteHeader(http.StatusOK) 119 - w.Write([]byte(response)) 121 + if _, err := w.Write([]byte(response)); err != nil { 122 + t.Errorf("Expected no error writing response, got %s", err) 123 + } 120 124 })) 121 125 defer server.Close() 122 126 ··· 145 149 } 146 150 }` 147 151 w.WriteHeader(http.StatusOK) 148 - w.Write([]byte(response)) 152 + if _, err := w.Write([]byte(response)); err != nil { 153 + t.Errorf("Expected no error writing response, got %s", err) 154 + } 149 155 })) 150 156 defer server.Close() 151 157 ··· 178 184 } 179 185 }` 180 186 w.WriteHeader(http.StatusOK) 181 - w.Write([]byte(response)) 187 + if _, err := w.Write([]byte(response)); err != nil { 188 + t.Errorf("Expected no error writing response, got %s", err) 189 + } 182 190 })) 183 191 defer server.Close() 184 192 ··· 238 246 239 247 // Parse request body 240 248 var body map[string]any 241 - json.NewDecoder(r.Body).Decode(&body) 249 + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 250 + t.Errorf("Expected no error decoding, got %s", err) 251 + } 242 252 243 253 // Verify rkey 244 254 expectedRKey := atproto.RepositoryTagToRKey("myapp", tt.tag) ··· 255 265 recordData := body["record"].(map[string]any) 256 266 recordBytes, _ := json.Marshal(recordData) 257 267 var tagRecord atproto.TagRecord 258 - json.Unmarshal(recordBytes, &tagRecord) 268 + if err := json.Unmarshal(recordBytes, &tagRecord); err != nil { 269 + t.Errorf("Expected no errors unmarshaling tag record, got %s", err) 270 + } 259 271 sentTagRecord = &tagRecord 260 272 261 273 w.WriteHeader(tt.serverStatus) 262 274 if tt.serverStatus == http.StatusOK { 263 - w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.tag/` + expectedRKey + `","cid":"bafytest"}`)) 275 + if _, err := w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.tag/` + expectedRKey + `","cid":"bafytest"}`)); err != nil { 276 + t.Errorf("Expected no error writing response, got %s", err) 277 + } 264 278 } else { 265 - w.Write([]byte(`{"error":"ServerError"}`)) 279 + if _, err := w.Write([]byte(`{"error":"ServerError"}`)); err != nil { 280 + t.Errorf("Expected no error writing response, got %s", err) 281 + } 266 282 } 267 283 })) 268 284 defer server.Close() ··· 339 355 340 356 // Parse body to verify delete parameters 341 357 var body map[string]any 342 - json.NewDecoder(r.Body).Decode(&body) 358 + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 359 + t.Errorf("Expected no error decoding, got %s", err) 360 + } 343 361 344 362 expectedRKey := atproto.RepositoryTagToRKey("myapp", tt.tag) 345 363 if body["rkey"] != expectedRKey { ··· 348 366 349 367 w.WriteHeader(tt.serverStatus) 350 368 if tt.serverStatus == http.StatusOK { 351 - w.Write([]byte(`{}`)) 369 + if _, err := w.Write([]byte(`{}`)); err != nil { 370 + t.Errorf("Expected no error writing response, got %s", err) 371 + } 352 372 } else { 353 - w.Write([]byte(`{"error":"ServerError"}`)) 373 + if _, err := w.Write([]byte(`{"error":"ServerError"}`)); err != nil { 374 + t.Errorf("Expected no error writing response, got %s", err) 375 + } 354 376 } 355 377 })) 356 378 defer server.Close() ··· 431 453 } 432 454 433 455 w.WriteHeader(http.StatusOK) 434 - w.Write([]byte(tt.serverResponse)) 456 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 457 + t.Errorf("Expected no error writing response, got %s", err) 458 + } 435 459 })) 436 460 defer server.Close() 437 461 ··· 493 517 ] 494 518 }` 495 519 w.WriteHeader(http.StatusOK) 496 - w.Write([]byte(response)) 520 + if _, err := w.Write([]byte(response)); err != nil { 521 + t.Errorf("Expected no error writing response, got %s", err) 522 + } 497 523 })) 498 524 defer server.Close() 499 525 ··· 581 607 t.Run(tt.name, func(t *testing.T) { 582 608 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 583 609 w.WriteHeader(http.StatusOK) 584 - w.Write([]byte(tt.serverResponse)) 610 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 611 + t.Errorf("Expected no error writing response, got %s", err) 612 + } 585 613 })) 586 614 defer server.Close() 587 615 ··· 643 671 ] 644 672 }` 645 673 w.WriteHeader(http.StatusOK) 646 - w.Write([]byte(response)) 674 + if _, err := w.Write([]byte(response)); err != nil { 675 + t.Errorf("Expected no error writing response, got %s", err) 676 + } 647 677 })) 648 678 defer server.Close() 649 679 ··· 673 703 func TestTagStore_ListRecordsError(t *testing.T) { 674 704 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 675 705 w.WriteHeader(http.StatusInternalServerError) 676 - w.Write([]byte(`{"error":"ServerError"}`)) 706 + if _, err := w.Write([]byte(`{"error":"ServerError"}`)); err != nil { 707 + t.Errorf("Expected no error writing response, got %s", err) 708 + } 677 709 })) 678 710 defer server.Close() 679 711
+51 -17
pkg/atproto/client_test.go
··· 110 110 111 111 // Send response 112 112 w.WriteHeader(tt.serverStatus) 113 - w.Write([]byte(tt.serverResponse)) 113 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 114 + t.Errorf("expected no error writing response, got %s", err) 115 + } 114 116 })) 115 117 defer server.Close() 116 118 ··· 217 219 218 220 // Send response 219 221 w.WriteHeader(tt.serverStatus) 220 - w.Write([]byte(tt.serverResponse)) 222 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 223 + t.Errorf("expected no error writing response, got %s", err) 224 + } 221 225 })) 222 226 defer server.Close() 223 227 ··· 307 311 308 312 // Send response 309 313 w.WriteHeader(tt.serverStatus) 310 - w.Write([]byte(tt.serverResponse)) 314 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 315 + t.Errorf("expected no error writing response, got %s", err) 316 + } 311 317 })) 312 318 defer server.Close() 313 319 ··· 348 354 ] 349 355 }` 350 356 w.WriteHeader(http.StatusOK) 351 - w.Write([]byte(response)) 357 + if _, err := w.Write([]byte(response)); err != nil { 358 + t.Errorf("expected no error writing response, got %s", err) 359 + } 352 360 })) 353 361 defer server.Close() 354 362 ··· 396 404 } 397 405 }` 398 406 w.WriteHeader(http.StatusOK) 399 - w.Write([]byte(response)) 407 + if _, err := w.Write([]byte(response)); err != nil { 408 + t.Errorf("expected no error writing response, got %s", err) 409 + } 400 410 })) 401 411 defer server.Close() 402 412 ··· 473 483 } else { 474 484 w.Header().Set("Content-Type", tt.contentType) 475 485 w.WriteHeader(http.StatusOK) 476 - w.Write([]byte(tt.serverResponse)) 486 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 487 + t.Errorf("expected no error writing response, got %s", err) 488 + } 477 489 } 478 490 })) 479 491 defer server.Close() ··· 582 594 t.Run(tt.name, func(t *testing.T) { 583 595 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 584 596 w.WriteHeader(tt.serverStatus) 585 - w.Write([]byte(tt.serverResponse)) 597 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 598 + t.Errorf("expected no error writing response, got %s", err) 599 + } 586 600 })) 587 601 defer server.Close() 588 602 ··· 611 625 612 626 response := `{"uri":"at://did:plc:test123/io.atcr.manifest/abc123","cid":"bafytest","value":{}}` 613 627 w.WriteHeader(http.StatusOK) 614 - w.Write([]byte(response)) 628 + if _, err := w.Write([]byte(response)); err != nil { 629 + t.Errorf("expected no error writing response, got %s", err) 630 + } 615 631 })) 616 632 defer server.Close() 617 633 ··· 649 665 "cursor": "nextcursor456" 650 666 }` 651 667 w.WriteHeader(http.StatusOK) 652 - w.Write([]byte(response)) 668 + if _, err := w.Write([]byte(response)); err != nil { 669 + t.Errorf("expected no error writing response, got %s", err) 670 + } 653 671 })) 654 672 defer server.Close() 655 673 ··· 675 693 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 676 694 time.Sleep(100 * time.Millisecond) 677 695 w.WriteHeader(http.StatusOK) 678 - w.Write([]byte(`{}`)) 696 + if _, err := w.Write([]byte(`{}`)); err != nil { 697 + w.WriteHeader(http.StatusInternalServerError) 698 + } 679 699 })) 680 700 defer server.Close() 681 701 ··· 775 795 776 796 // Send response 777 797 w.WriteHeader(tt.serverStatus) 778 - w.Write([]byte(tt.serverResponse)) 798 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 799 + t.Errorf("expected no error writing response, got %s", err) 800 + } 779 801 })) 780 802 defer server.Close() 781 803 ··· 875 897 876 898 // Send response 877 899 w.WriteHeader(tt.serverStatus) 878 - w.Write([]byte(tt.serverResponse)) 900 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 901 + t.Errorf("expected no error writing response, got %s", err) 902 + } 879 903 })) 880 904 defer server.Close() 881 905 ··· 964 988 965 989 // Send response 966 990 w.WriteHeader(tt.serverStatus) 967 - w.Write([]byte(tt.serverResponse)) 991 + if _, err := w.Write([]byte(tt.serverResponse)); err != nil { 992 + t.Errorf("expected no error writing response, got %s", err) 993 + } 968 994 })) 969 995 defer server.Close() 970 996 ··· 1007 1033 func TestListRecordsError(t *testing.T) { 1008 1034 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1009 1035 w.WriteHeader(http.StatusInternalServerError) 1010 - w.Write([]byte(`{"error":"InternalError"}`)) 1036 + if _, err := w.Write([]byte(`{"error":"InternalError"}`)); err != nil { 1037 + w.WriteHeader(http.StatusInternalServerError) 1038 + } 1011 1039 })) 1012 1040 defer server.Close() 1013 1041 ··· 1023 1051 func TestUploadBlobError(t *testing.T) { 1024 1052 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1025 1053 w.WriteHeader(http.StatusBadRequest) 1026 - w.Write([]byte(`{"error":"InvalidBlob"}`)) 1054 + if _, err := w.Write([]byte(`{"error":"InvalidBlob"}`)); err != nil { 1055 + w.WriteHeader(http.StatusInternalServerError) 1056 + } 1027 1057 })) 1028 1058 defer server.Close() 1029 1059 ··· 1039 1069 func TestGetBlobServerError(t *testing.T) { 1040 1070 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1041 1071 w.WriteHeader(http.StatusInternalServerError) 1042 - w.Write([]byte(`{"error":"InternalError"}`)) 1072 + if _, err := w.Write([]byte(`{"error":"InternalError"}`)); err != nil { 1073 + w.WriteHeader(http.StatusInternalServerError) 1074 + } 1043 1075 })) 1044 1076 defer server.Close() 1045 1077 ··· 1059 1091 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1060 1092 // Return JSON string with invalid base64 1061 1093 w.WriteHeader(http.StatusOK) 1062 - w.Write([]byte(`"not-valid-base64!!!"`)) 1094 + if _, err := w.Write([]byte(`"not-valid-base64!!!"`)); err != nil { 1095 + w.WriteHeader(http.StatusInternalServerError) 1096 + } 1063 1097 })) 1064 1098 defer server.Close() 1065 1099
+7 -7
pkg/atproto/directory_test.go
··· 32 32 wg.Add(numGoroutines) 33 33 34 34 // Channel to collect all directory instances 35 - instances := make(chan interface{}, numGoroutines) 35 + instances := make(chan any, numGoroutines) 36 36 37 37 // Launch many goroutines concurrently accessing GetDirectory 38 - for i := 0; i < numGoroutines; i++ { 38 + for range numGoroutines { 39 39 go func() { 40 40 defer wg.Done() 41 41 dir := GetDirectory() ··· 48 48 close(instances) 49 49 50 50 // Collect all instances 51 - var dirs []interface{} 51 + var dirs []any 52 52 for dir := range instances { 53 53 dirs = append(dirs, dir) 54 54 } ··· 72 72 func TestGetDirectorySequential(t *testing.T) { 73 73 t.Run("multiple calls in sequence", func(t *testing.T) { 74 74 // Get directory multiple times in sequence 75 - dirs := make([]interface{}, 10) 76 - for i := 0; i < 10; i++ { 75 + dirs := make([]any, 10) 76 + for i := range 10 { 77 77 dirs[i] = GetDirectory() 78 78 } 79 79 ··· 122 122 var wg sync.WaitGroup 123 123 wg.Add(numGoroutines) 124 124 125 - instances := make([]interface{}, numGoroutines) 125 + instances := make([]any, numGoroutines) 126 126 var mu sync.Mutex 127 127 128 128 // Simulate many goroutines trying to get the directory simultaneously 129 - for i := 0; i < numGoroutines; i++ { 129 + for i := range numGoroutines { 130 130 go func(idx int) { 131 131 defer wg.Done() 132 132 dir := GetDirectory()
-1
pkg/atproto/generate.go
··· 1 1 //go:build ignore 2 - // +build ignore 3 2 4 3 package main 5 4
+1 -1
pkg/auth/cache.go
··· 1 - // Package token provides service token caching and management for AppView. 1 + // Package auth provides service token caching and management for AppView. 2 2 // Service tokens are JWTs issued by a user's PDS to authorize AppView to 3 3 // act on their behalf when communicating with hold services. Tokens are 4 4 // cached with automatic expiry parsing and 10-second safety margins.
+10 -9
pkg/auth/hold_remote_test.go
··· 20 20 if authorizer == nil { 21 21 t.Fatal("Expected non-nil authorizer") 22 22 } 23 - 24 - // Verify it implements the HoldAuthorizer interface 25 - var _ HoldAuthorizer = authorizer 26 23 } 27 24 28 25 func TestNewRemoteHoldAuthorizer_TestMode(t *testing.T) { ··· 78 75 } 79 76 80 77 // Return mock response 81 - response := map[string]interface{}{ 78 + response := map[string]any{ 82 79 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 83 80 "cid": "bafytest123", 84 - "value": map[string]interface{}{ 81 + "value": map[string]any{ 85 82 "$type": atproto.CaptainCollection, 86 83 "owner": "did:plc:owner123", 87 84 "public": true, ··· 93 90 } 94 91 95 92 w.Header().Set("Content-Type", "application/json") 96 - json.NewEncoder(w).Encode(response) 93 + if err := json.NewEncoder(w).Encode(response); err != nil { 94 + t.Errorf("expected no error encoding json, got %s", err) 95 + } 97 96 })) 98 97 defer server.Close() 99 98 ··· 281 280 func TestCheckReadAccess_PublicHold(t *testing.T) { 282 281 // Create mock server that returns public captain record 283 282 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 284 - response := map[string]interface{}{ 283 + response := map[string]any{ 285 284 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 286 285 "cid": "bafytest123", 287 - "value": map[string]interface{}{ 286 + "value": map[string]any{ 288 287 "$type": atproto.CaptainCollection, 289 288 "owner": "did:plc:owner123", 290 289 "public": true, // Public hold ··· 294 293 } 295 294 296 295 w.Header().Set("Content-Type", "application/json") 297 - json.NewEncoder(w).Encode(response) 296 + if err := json.NewEncoder(w).Encode(response); err != nil { 297 + t.Errorf("expected no error encoding json, got %s", err) 298 + } 298 299 })) 299 300 defer server.Close() 300 301
+2 -1
pkg/auth/oauth/client_test.go
··· 1 1 package oauth 2 2 3 3 import ( 4 + "testing" 5 + 4 6 "github.com/bluesky-social/indigo/atproto/auth/oauth" 5 - "testing" 6 7 ) 7 8 8 9 func TestNewClientApp(t *testing.T) {
+2 -11
pkg/auth/oauth/server_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "github.com/bluesky-social/indigo/atproto/auth/oauth" 6 5 "net/http" 7 6 "net/http/httptest" 8 7 "strings" 9 8 "testing" 10 9 "time" 10 + 11 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 12 ) 12 13 13 14 func TestNewServer(t *testing.T) { ··· 112 113 func (m *mockUISessionStore) DeleteByDID(did string) { 113 114 if m.deleteByDIDFunc != nil { 114 115 m.deleteByDIDFunc(did) 115 - } 116 - } 117 - 118 - type mockRefresher struct { 119 - invalidateSessionFunc func(did string) 120 - } 121 - 122 - func (m *mockRefresher) InvalidateSession(did string) { 123 - if m.invalidateSessionFunc != nil { 124 - m.invalidateSessionFunc(did) 125 116 } 126 117 } 127 118
+1
pkg/auth/token/claims.go
··· 1 + // Package token handles JWT-related operations 1 2 package token 2 3 3 4 import (
+4 -1
pkg/auth/token/handler.go
··· 120 120 Message: "OAuth session expired or invalidated. Please re-authenticate in your browser.", 121 121 LoginURL: loginURL, 122 122 } 123 - json.NewEncoder(w).Encode(resp) 123 + if err := json.NewEncoder(w).Encode(resp); err != nil { 124 + slog.Error("failed to encode json", "error", err, "response", resp) 125 + w.WriteHeader(http.StatusInternalServerError) 126 + } 124 127 } 125 128 126 129 // ServeHTTP handles the token request
+1 -3
pkg/auth/token/handler_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "crypto/rsa" 6 5 "crypto/tls" 7 6 "database/sql" 8 7 "encoding/base64" ··· 22 21 // Shared test key to avoid generating a new RSA key for each test 23 22 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves ~4.5s for 32 tests 24 23 var ( 25 - sharedTestKey *rsa.PrivateKey 26 24 sharedTestKeyPath string 27 25 sharedTestKeyOnce sync.Once 28 26 sharedTestKeyDir string ··· 513 511 } 514 512 515 513 // Verify JSON structure 516 - var decoded map[string]interface{} 514 + var decoded map[string]any 517 515 if err := json.Unmarshal(data, &decoded); err != nil { 518 516 t.Fatalf("Failed to unmarshal JSON: %v", err) 519 517 }
+4 -5
pkg/auth/token/issuer_test.go
··· 19 19 // Shared test key to avoid generating a new RSA key for each test 20 20 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves significant time 21 21 var ( 22 - issuerSharedTestKey *rsa.PrivateKey 23 22 issuerSharedTestKeyPath string 24 23 issuerSharedTestKeyOnce sync.Once 25 24 issuerSharedTestKeyDir string ··· 207 206 } 208 207 209 208 // Parse and validate the token 210 - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 209 + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { 211 210 return issuer.publicKey, nil 212 211 }) 213 212 if err != nil { ··· 289 288 } 290 289 291 290 // x5c should be a slice of base64-encoded certificates 292 - x5cSlice, ok := x5c.([]interface{}) 291 + x5cSlice, ok := x5c.([]any) 293 292 if !ok { 294 293 t.Fatal("Expected x5c to be a slice") 295 294 } ··· 384 383 tokens := make([]string, numGoroutines) 385 384 errors := make([]error, numGoroutines) 386 385 387 - for i := 0; i < numGoroutines; i++ { 386 + for i := range numGoroutines { 388 387 go func(idx int) { 389 388 defer wg.Done() 390 389 subject := "did:plc:user" + string(rune('0'+idx)) ··· 575 574 } 576 575 577 576 // Parse token and verify expiration 578 - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 577 + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { 579 578 return issuer.publicKey, nil 580 579 }) 581 580 if err != nil {
+7 -2
pkg/hold/oci/xrpc_test.go
··· 6 6 "encoding/json" 7 7 "fmt" 8 8 "io" 9 + "log/slog" 9 10 "net/http" 10 11 "net/http/httptest" 11 12 "os" ··· 116 117 // Restore stdout 117 118 w.Close() 118 119 os.Stdout = oldStdout 119 - io.ReadAll(r) // Drain the pipe 120 + if _, err := io.ReadAll(r); err != nil { // Drain the pipe 121 + t.Errorf("Expected no errors draining pipe, got %s", err) 122 + } 120 123 121 124 if err != nil { 122 125 t.Fatalf("Failed to bootstrap PDS: %v", err) ··· 136 139 func makeJSONRequest(method, url string, body any) *http.Request { 137 140 var buf bytes.Buffer 138 141 if body != nil { 139 - json.NewEncoder(&buf).Encode(body) 142 + if err := json.NewEncoder(&buf).Encode(body); err != nil { 143 + slog.Error("failed to encode request body", "error", err) 144 + } 140 145 } 141 146 req := httptest.NewRequest(method, url, &buf) 142 147 req.Header.Set("Content-Type", "application/json")
+6 -2
pkg/hold/pds/auth_test.go
··· 327 327 parts := strings.Split(token, ".") 328 328 claimsJSON, _ := base64.RawURLEncoding.DecodeString(parts[1]) 329 329 var claims map[string]any 330 - json.Unmarshal(claimsJSON, &claims) 330 + if err := json.Unmarshal(claimsJSON, &claims); err != nil { 331 + t.Errorf("Expected no errors unmarshaling claims, got %s", err) 332 + } 331 333 332 334 exp := int64(claims["exp"].(float64)) 333 335 if time.Unix(exp, 0).After(time.Now()) { ··· 360 362 parts := strings.Split(token, ".") 361 363 claimsJSON, _ := base64.RawURLEncoding.DecodeString(parts[1]) 362 364 var claims map[string]any 363 - json.Unmarshal(claimsJSON, &claims) 365 + if err := json.Unmarshal(claimsJSON, &claims); err != nil { 366 + t.Errorf("Expected no error unmarshaling, got %s", err) 367 + } 364 368 365 369 aud := claims["aud"].(string) 366 370 if aud == correctHoldDID {
+3 -1
pkg/hold/pds/captain_test.go
··· 59 59 60 60 w.Close() 61 61 os.Stdout = oldStdout 62 - io.ReadAll(r) // Drain the pipe 62 + if _, err := io.ReadAll(r); err != nil { // Drain the pipe 63 + t.Errorf("Expected no errors draining pipe, got %s", err) 64 + } 63 65 64 66 if err != nil { 65 67 t.Fatalf("Failed to bootstrap PDS: %v", err)
+1 -1
pkg/hold/pds/events_test.go
··· 150 150 testCID, _ := cid.Decode("bafyreib2rxk3rkhh5ylyxj3x3gathxt3s32qvwj2lf3qg4kmzr6b7teqke") 151 151 152 152 // Broadcast 5 events (exceeds maxHistory of 3) 153 - for i := 0; i < 5; i++ { 153 + for range 5 { 154 154 event := &RepoEvent{ 155 155 NewRoot: testCID, 156 156 Rev: "test-rev",
+34 -33
pkg/hold/pds/records_test.go
··· 1 1 package pds 2 2 3 3 import ( 4 - "context" 5 4 "os" 6 5 "path/filepath" 7 6 "testing" 8 7 9 - "github.com/bluesky-social/indigo/repo" 10 8 _ "github.com/mattn/go-sqlite3" 11 9 ) 12 10 ··· 324 322 defer ri.Close() 325 323 326 324 // Add 5 records 327 - for i := 0; i < 5; i++ { 325 + for i := range 5 { 328 326 rkey := string(rune('a' + i)) 329 327 if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey); err != nil { 330 328 t.Fatalf("IndexRecord() error = %v", err) ··· 475 473 defer ri.Close() 476 474 477 475 // Add records to two collections 478 - for i := 0; i < 3; i++ { 479 - ri.IndexRecord("io.atcr.hold.crew", string(rune('a'+i)), "cid1") 476 + for i := range 3 { 477 + if err := ri.IndexRecord("io.atcr.hold.crew", string(rune('a'+i)), "cid1"); err != nil { 478 + t.Errorf("Expected no errors indexing crew record, got %s", err) 479 + } 480 480 } 481 - for i := 0; i < 5; i++ { 482 - ri.IndexRecord("io.atcr.hold.captain", string(rune('a'+i)), "cid2") 481 + for i := range 5 { 482 + if err := ri.IndexRecord("io.atcr.hold.captain", string(rune('a'+i)), "cid2"); err != nil { 483 + t.Errorf("Expected no errors indexing captain record, got %s", err) 484 + } 483 485 } 484 486 485 487 // Count crew ··· 529 531 defer ri.Close() 530 532 531 533 // Add records to multiple collections 532 - ri.IndexRecord("io.atcr.hold.crew", "a", "cid1") 533 - ri.IndexRecord("io.atcr.hold.crew", "b", "cid2") 534 - ri.IndexRecord("io.atcr.hold.captain", "self", "cid3") 535 - ri.IndexRecord("io.atcr.manifest", "abc123", "cid4") 534 + if err := ri.IndexRecord("io.atcr.hold.crew", "a", "cid1"); err != nil { 535 + t.Errorf("Expected no errors indexing crew record, got %s", err) 536 + } 537 + 538 + if err := ri.IndexRecord("io.atcr.hold.crew", "b", "cid2"); err != nil { 539 + t.Errorf("Expected no errors indexing crew record, got %s", err) 540 + } 541 + if err := ri.IndexRecord("io.atcr.hold.captain", "self", "cid3"); err != nil { 542 + t.Errorf("Expected no errors indexing captain record, got %s", err) 543 + } 544 + if err := ri.IndexRecord("io.atcr.manifest", "abc123", "cid4"); err != nil { 545 + t.Errorf("Expected no errors indexing manifest record, got %s", err) 546 + } 536 547 537 548 count, err := ri.TotalCount() 538 549 if err != nil { ··· 583 594 defer ri.Close() 584 595 585 596 // Add records to different collections with same rkeys 586 - ri.IndexRecord("io.atcr.hold.crew", "abc", "cid-crew") 587 - ri.IndexRecord("io.atcr.hold.captain", "abc", "cid-captain") 588 - ri.IndexRecord("io.atcr.manifest", "abc", "cid-manifest") 597 + if err := ri.IndexRecord("io.atcr.hold.crew", "abc", "cid-crew"); err != nil { 598 + t.Errorf("Expected no errors indexing crew record, got %s", err) 599 + } 600 + if err := ri.IndexRecord("io.atcr.hold.captain", "abc", "cid-captain"); err != nil { 601 + t.Errorf("Expected no errors indexing caotain record, got %s", err) 602 + } 603 + if err := ri.IndexRecord("io.atcr.manifest", "abc", "cid-manifest"); err != nil { 604 + t.Errorf("Expected no errors indexing manifest record, got %s", err) 605 + } 589 606 590 607 // Listing should only return records from requested collection 591 608 records, _, err := ri.ListRecords("io.atcr.hold.crew", 10, "", false) ··· 600 617 } 601 618 602 619 // Delete from one collection shouldn't affect others 603 - ri.DeleteRecord("io.atcr.hold.crew", "abc") 620 + if err := ri.DeleteRecord("io.atcr.hold.crew", "abc"); err != nil { 621 + t.Errorf("Expected no errors deleting crew record, got %s", err) 622 + } 604 623 605 624 count, _ := ri.Count("io.atcr.hold.captain") 606 625 if count != 1 { 607 626 t.Errorf("Expected captain count 1 after deleting crew, got %d", count) 608 627 } 609 628 } 610 - 611 - // mockRepo is a minimal mock for testing backfill 612 - // Note: Full backfill testing requires integration tests with real repo 613 - type mockRepo struct { 614 - records map[string]string // key -> cid 615 - } 616 - 617 - func (m *mockRepo) ForEach(ctx context.Context, prefix string, fn func(string, interface{}) error) error { 618 - for k, v := range m.records { 619 - if err := fn(k, v); err != nil { 620 - if err == repo.ErrDoneIterating { 621 - return nil 622 - } 623 - return err 624 - } 625 - } 626 - return nil 627 - }
+2 -2
pkg/hold/pds/server.go
··· 103 103 // Uses same database as carstore for simplicity 104 104 var recordsIndex *RecordsIndex 105 105 if dbPath != ":memory:" { 106 - recordsDbPath := dbPath + "/db.sqlite3" 107 - recordsIndex, err = NewRecordsIndex(recordsDbPath) 106 + recordsDBPath := dbPath + "/db.sqlite3" 107 + recordsIndex, err = NewRecordsIndex(recordsDBPath) 108 108 if err != nil { 109 109 return nil, fmt.Errorf("failed to create records index: %w", err) 110 110 }
+103 -30
pkg/hold/pds/xrpc.go
··· 201 201 } 202 202 203 203 w.Header().Set("Content-Type", "application/json") 204 - json.NewEncoder(w).Encode(response) 204 + if err := json.NewEncoder(w).Encode(response); err != nil { 205 + slog.Error("failed to encode json", "error", err, "response", response) 206 + w.WriteHeader(http.StatusInternalServerError) 207 + } 205 208 } 206 209 207 210 // HandleDescribeServer returns server metadata ··· 221 224 } 222 225 223 226 w.Header().Set("Content-Type", "application/json") 224 - json.NewEncoder(w).Encode(response) 227 + if err := json.NewEncoder(w).Encode(response); err != nil { 228 + slog.Error("failed to encode json", "error", err, "response", response) 229 + w.WriteHeader(http.StatusInternalServerError) 230 + } 225 231 } 226 232 227 233 // HandleResolveHandle resolves a handle to a DID ··· 249 255 } 250 256 251 257 w.Header().Set("Content-Type", "application/json") 252 - json.NewEncoder(w).Encode(response) 258 + if err := json.NewEncoder(w).Encode(response); err != nil { 259 + slog.Error("failed to encode json", "error", err, "response", response) 260 + w.WriteHeader(http.StatusInternalServerError) 261 + } 253 262 } 254 263 255 264 // HandleGetProfile returns aggregated profile information ··· 284 293 response := h.buildProfileResponse(r.Context()) 285 294 286 295 w.Header().Set("Content-Type", "application/json") 287 - json.NewEncoder(w).Encode(response) 296 + if err := json.NewEncoder(w).Encode(response); err != nil { 297 + slog.Error("failed to encode json", "error", err, "response", response) 298 + w.WriteHeader(http.StatusInternalServerError) 299 + } 288 300 } 289 301 290 302 // HandleGetProfiles returns aggregated profile information for multiple actors ··· 335 347 } 336 348 337 349 w.Header().Set("Content-Type", "application/json") 338 - json.NewEncoder(w).Encode(response) 350 + if err := json.NewEncoder(w).Encode(response); err != nil { 351 + slog.Error("failed to encode json", "error", err, "response", response) 352 + w.WriteHeader(http.StatusInternalServerError) 353 + } 339 354 } 340 355 341 356 // buildProfileResponse builds a profile response map (shared by GetProfile and GetProfiles) ··· 429 444 } 430 445 431 446 w.Header().Set("Content-Type", "application/json") 432 - json.NewEncoder(w).Encode(response) 447 + if err := json.NewEncoder(w).Encode(response); err != nil { 448 + slog.Error("failed to encode json", "error", err, "response", response) 449 + w.WriteHeader(http.StatusInternalServerError) 450 + } 433 451 } 434 452 435 453 // HandleGetRecord retrieves a record from the repository ··· 473 491 } 474 492 475 493 w.Header().Set("Content-Type", "application/json") 476 - json.NewEncoder(w).Encode(response) 494 + if err := json.NewEncoder(w).Encode(response); err != nil { 495 + slog.Error("failed to encode json", "error", err, "response", response) 496 + w.WriteHeader(http.StatusInternalServerError) 497 + } 477 498 } 478 499 479 500 // HandleListRecords lists records in a collection ··· 545 566 // Empty repo, return empty list 546 567 response := map[string]any{"records": []any{}} 547 568 w.Header().Set("Content-Type", "application/json") 548 - json.NewEncoder(w).Encode(response) 569 + if err := json.NewEncoder(w).Encode(response); err != nil { 570 + slog.Error("failed to encode json", "error", err, "response", response) 571 + w.WriteHeader(http.StatusInternalServerError) 572 + } 549 573 return 550 574 } 551 575 ··· 592 616 } 593 617 594 618 w.Header().Set("Content-Type", "application/json") 595 - json.NewEncoder(w).Encode(response) 619 + if err := json.NewEncoder(w).Encode(response); err != nil { 620 + slog.Error("failed to encode json", "error", err, "response", response) 621 + w.WriteHeader(http.StatusInternalServerError) 622 + } 596 623 } 597 624 598 625 // handleListRecordsMST uses the legacy MST-based listing (fallback for tests) ··· 614 641 // Empty repo, return empty list 615 642 response := map[string]any{"records": []any{}} 616 643 w.Header().Set("Content-Type", "application/json") 617 - json.NewEncoder(w).Encode(response) 644 + if err := json.NewEncoder(w).Encode(response); err != nil { 645 + slog.Error("failed to encode json", "error", err, "response", response) 646 + w.WriteHeader(http.StatusInternalServerError) 647 + } 618 648 return 619 649 } 620 650 ··· 723 753 } 724 754 725 755 w.Header().Set("Content-Type", "application/json") 726 - json.NewEncoder(w).Encode(response) 756 + if err := json.NewEncoder(w).Encode(response); err != nil { 757 + slog.Error("failed to encode json", "error", err, "response", response) 758 + w.WriteHeader(http.StatusInternalServerError) 759 + } 727 760 } 728 761 729 762 // HandleDeleteRecord deletes a record from the repository ··· 793 826 if !currentCID.Equals(swapRecordCID) { 794 827 // Swap failed - record CID doesn't match 795 828 w.WriteHeader(http.StatusBadRequest) 796 - json.NewEncoder(w).Encode(map[string]any{ 829 + m := map[string]any{ 797 830 "error": "InvalidSwap", 798 831 "message": "record CID does not match swapRecord", 799 - }) 832 + } 833 + if err := json.NewEncoder(w).Encode(m); err != nil { 834 + slog.Error("failed to encode json", "error", err, "response", m) 835 + w.WriteHeader(http.StatusInternalServerError) 836 + } 800 837 return 801 838 } 802 839 } ··· 840 877 } 841 878 842 879 w.Header().Set("Content-Type", "application/json") 843 - json.NewEncoder(w).Encode(response) 880 + if err := json.NewEncoder(w).Encode(response); err != nil { 881 + slog.Error("failed to encode json", "error", err, "response", response) 882 + w.WriteHeader(http.StatusInternalServerError) 883 + } 844 884 } 845 885 846 886 // HandleSyncGetRecord returns a single record as a CAR file for sync ··· 894 934 } 895 935 896 936 // Write the CAR data to the response 897 - w.Write(buf.Bytes()) 937 + if _, err := w.Write(buf.Bytes()); err != nil { 938 + slog.Error("failed to write car", "error", err) 939 + w.WriteHeader(http.StatusInternalServerError) 940 + } 898 941 } 899 942 900 943 // HandleGetRepo returns the full repository as a CAR file ··· 1057 1100 } 1058 1101 1059 1102 w.Header().Set("Content-Type", "application/json") 1060 - json.NewEncoder(w).Encode(response) 1103 + if err := json.NewEncoder(w).Encode(response); err != nil { 1104 + slog.Error("failed to encode json", "error", err, "response", response) 1105 + w.WriteHeader(http.StatusInternalServerError) 1106 + } 1061 1107 } 1062 1108 1063 1109 // HandleGetBlob routes blob requests to appropriate handlers based on blob type ··· 1081 1127 // Route based on blob type 1082 1128 if strings.HasPrefix(cidOrDigest, "sha256:") { 1083 1129 // OCI blob (container image layers) 1084 - h.handleGetOCIBlob(w, r, did, cidOrDigest) 1130 + h.handleGetOCIBlob(w, r, cidOrDigest) 1085 1131 return 1086 1132 } 1087 1133 ··· 1092 1138 // handleGetOCIBlob handles OCI container image blob requests 1093 1139 // Returns JSON with presigned URL for AppView integration 1094 1140 // Authorization: Protected by hold access control (captain.public or crew with blob:read) 1095 - func (h *XRPCHandler) handleGetOCIBlob(w http.ResponseWriter, r *http.Request, did, digest string) { 1141 + func (h *XRPCHandler) handleGetOCIBlob(w http.ResponseWriter, r *http.Request, digest string) { 1096 1142 slog.Debug("Processing OCI blob", "digest", digest) 1097 1143 1098 1144 // Validate blob read access (hold access control) ··· 1133 1179 "url": presignedURL, 1134 1180 } 1135 1181 w.Header().Set("Content-Type", "application/json") 1136 - json.NewEncoder(w).Encode(response) 1182 + if err := json.NewEncoder(w).Encode(response); err != nil { 1183 + slog.Error("failed to encode json", "error", err, "response", response) 1184 + w.WriteHeader(http.StatusInternalServerError) 1185 + } 1137 1186 } 1138 1187 1139 1188 // handleGetATProtoBlob handles standard ATProto blob requests ··· 1187 1236 "repos": []any{}, 1188 1237 } 1189 1238 w.Header().Set("Content-Type", "application/json") 1190 - json.NewEncoder(w).Encode(response) 1239 + if err := json.NewEncoder(w).Encode(response); err != nil { 1240 + slog.Error("failed to encode json", "error", err, "response", response) 1241 + w.WriteHeader(http.StatusInternalServerError) 1242 + } 1191 1243 return 1192 1244 } 1193 1245 ··· 1199 1251 "repos": []any{}, 1200 1252 } 1201 1253 w.Header().Set("Content-Type", "application/json") 1202 - json.NewEncoder(w).Encode(response) 1254 + if err := json.NewEncoder(w).Encode(response); err != nil { 1255 + slog.Error("failed to encode json", "error", err, "response", response) 1256 + w.WriteHeader(http.StatusInternalServerError) 1257 + } 1203 1258 return 1204 1259 } 1205 1260 ··· 1217 1272 } 1218 1273 1219 1274 w.Header().Set("Content-Type", "application/json") 1220 - json.NewEncoder(w).Encode(response) 1275 + if err := json.NewEncoder(w).Encode(response); err != nil { 1276 + slog.Error("failed to encode json", "error", err, "response", response) 1277 + w.WriteHeader(http.StatusInternalServerError) 1278 + } 1221 1279 } 1222 1280 1223 1281 // HandleGetRepoStatus returns the hosting status for a repository ··· 1246 1304 "active": true, 1247 1305 } 1248 1306 w.Header().Set("Content-Type", "application/json") 1249 - json.NewEncoder(w).Encode(response) 1307 + if err := json.NewEncoder(w).Encode(response); err != nil { 1308 + slog.Error("failed to encode json", "error", err, "response", response) 1309 + w.WriteHeader(http.StatusInternalServerError) 1310 + } 1250 1311 return 1251 1312 } 1252 1313 ··· 1258 1319 } 1259 1320 1260 1321 w.Header().Set("Content-Type", "application/json") 1261 - json.NewEncoder(w).Encode(response) 1322 + if err := json.NewEncoder(w).Encode(response); err != nil { 1323 + slog.Error("failed to encode json", "error", err, "response", response) 1324 + w.WriteHeader(http.StatusInternalServerError) 1325 + } 1262 1326 } 1263 1327 1264 1328 // HandleDIDDocument returns the DID document ··· 1270 1334 } 1271 1335 1272 1336 w.Header().Set("Content-Type", "application/json") 1273 - json.NewEncoder(w).Encode(doc) 1337 + if err := json.NewEncoder(w).Encode(doc); err != nil { 1338 + slog.Error("failed to encode json", "error", err, "response", doc) 1339 + w.WriteHeader(http.StatusInternalServerError) 1340 + } 1274 1341 } 1275 1342 1276 1343 // HandleAtprotoDID returns the DID for handle resolution ··· 1369 1436 } 1370 1437 w.Header().Set("Content-Type", "application/json") 1371 1438 w.WriteHeader(http.StatusOK) 1372 - json.NewEncoder(w).Encode(response) 1439 + if err := json.NewEncoder(w).Encode(response); err != nil { 1440 + slog.Error("failed to encode json", "error", err, "response", response) 1441 + w.WriteHeader(http.StatusInternalServerError) 1442 + } 1373 1443 return 1374 1444 } 1375 1445 } ··· 1402 1472 1403 1473 w.Header().Set("Content-Type", "application/json") 1404 1474 w.WriteHeader(http.StatusCreated) 1405 - json.NewEncoder(w).Encode(response) 1475 + if err := json.NewEncoder(w).Encode(response); err != nil { 1476 + slog.Error("failed to encode json", "error", err, "response", response) 1477 + w.WriteHeader(http.StatusInternalServerError) 1478 + } 1406 1479 } 1407 1480 1408 1481 // GetPresignedURL generates a presigned URL for GET, HEAD, or PUT operations ··· 1470 1543 "operation", operation, 1471 1544 "digest", digest) 1472 1545 slog.Debug("Using XRPC proxy fallback") 1473 - proxyURL := getProxyURL(h.pds.PublicURL, digest, did, operation) 1546 + proxyURL := getProxyURL(h.pds.PublicURL, digest, operation) 1474 1547 if proxyURL == "" { 1475 1548 return "", fmt.Errorf("presign failed and XRPC proxy not supported for PUT operations") 1476 1549 } ··· 1481 1554 } 1482 1555 1483 1556 // Fallback: return XRPC endpoint through this service 1484 - proxyURL := getProxyURL(h.pds.PublicURL, digest, did, operation) 1557 + proxyURL := getProxyURL(h.pds.PublicURL, digest, operation) 1485 1558 if proxyURL == "" { 1486 1559 return "", fmt.Errorf("S3 client not available and XRPC proxy not supported for PUT operations") 1487 1560 } ··· 1500 1573 // getProxyURL returns XRPC endpoint for blob operations (fallback when presigned URLs unavailable) 1501 1574 // For GET/HEAD operations, returns the XRPC getBlob endpoint 1502 1575 // For PUT operations, this fallback is no longer supported - use multipart upload instead 1503 - func getProxyURL(publicURL string, digest, did string, operation string) string { 1576 + func getProxyURL(publicURL string, digest, operation string) string { 1504 1577 // For read operations, use XRPC getBlob endpoint 1505 1578 if operation == http.MethodGet || operation == http.MethodHead { 1506 1579 // Generate hold DID from public URL using shared function
+35 -17
pkg/hold/pds/xrpc_test.go
··· 63 63 // Restore stdout 64 64 w.Close() 65 65 os.Stdout = oldStdout 66 - io.ReadAll(r) // Drain the pipe 66 + if _, err := io.ReadAll(r); err != nil { // Drain the pipe 67 + t.Errorf("Expected no error draining pipe, got %s", err) 68 + } 67 69 68 70 if err != nil { 69 71 t.Fatalf("Failed to bootstrap PDS: %v", err) ··· 121 123 // Restore stdout 122 124 w.Close() 123 125 os.Stdout = oldStdout 124 - io.ReadAll(r) // Drain the pipe 126 + if _, err := io.ReadAll(r); err != nil { // Drain the pipe 127 + t.Errorf("Expected no error draining pipe, got %s", err) 128 + } 125 129 126 130 if err != nil { 127 131 t.Fatalf("Failed to bootstrap PDS: %v", err) ··· 609 613 610 614 // Note: Bootstrap already added 1 crew member 611 615 // Add 4 more for a total of 5 612 - for i := 0; i < 4; i++ { 616 + for i := range 4 { 613 617 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 614 618 if err != nil { 615 619 t.Fatalf("Failed to add crew member: %v", err) ··· 673 677 holdDID := "did:web:hold.example.com" 674 678 675 679 // Add crew members 676 - for i := 0; i < 3; i++ { 680 + for i := range 3 { 677 681 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 678 682 if err != nil { 679 683 t.Fatalf("Failed to add crew member: %v", err) ··· 888 892 holdDID := "did:web:hold.example.com" 889 893 890 894 // Add 4 more crew members for total of 5 891 - for i := 0; i < 4; i++ { 895 + for i := range 4 { 892 896 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 893 897 if err != nil { 894 898 t.Fatalf("Failed to add crew member: %v", err) ··· 968 972 holdDID := "did:web:hold.example.com" 969 973 970 974 // Add crew members 971 - for i := 0; i < 3; i++ { 975 + for i := range 3 { 972 976 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 973 977 if err != nil { 974 978 t.Fatalf("Failed to add crew member: %v", err) ··· 1989 1993 err = pds.Bootstrap(ctx, nil, ownerDID, true, false, "") 1990 1994 1991 1995 // Restore stdout 1992 - w.Close() 1996 + if err := w.Close(); err != nil { 1997 + t.Errorf("Expected no error closing writer, got %s", err) 1998 + } 1993 1999 os.Stdout = oldStdout 1994 - io.ReadAll(r) // Drain the pipe 2000 + if _, err := io.ReadAll(r); err != nil { // Drain the pipe 2001 + t.Errorf("Expected no error draining pipe, got %s", err) 2002 + } 1995 2003 1996 2004 if err != nil { 1997 2005 t.Fatalf("Failed to bootstrap PDS: %v", err) ··· 2429 2437 2430 2438 // Clean up - recreate captain record if it was deleted 2431 2439 if w.Code == http.StatusOK { 2432 - handler.pds.Bootstrap(ctx, nil, "did:plc:testowner123", true, false, "") 2440 + if err := handler.pds.Bootstrap(ctx, nil, "did:plc:testowner123", true, false, ""); err != nil { 2441 + t.Errorf("Expected no errors re-bootstrapping PDS, got %s", err) 2442 + } 2433 2443 } 2434 2444 } 2435 2445 ··· 2641 2651 event := &RepoEvent{ 2642 2652 NewRoot: testCID, 2643 2653 Rev: fmt.Sprintf("rev-%d", i), 2644 - RepoSlice: []byte(fmt.Sprintf("CAR data %d", i)), 2654 + RepoSlice: fmt.Appendf(nil, "CAR data %d", i), 2645 2655 Ops: []RepoOp{}, 2646 2656 } 2647 2657 broadcaster.Broadcast(ctx, event) ··· 2662 2672 defer conn.Close() 2663 2673 2664 2674 // Should receive the 3 historical events 2665 - for i := 0; i < 3; i++ { 2675 + for i := range 3 { 2666 2676 messageType, message, err := conn.ReadMessage() 2667 2677 if err != nil { 2668 2678 t.Fatalf("Failed to read message: %v", err) ··· 2720 2730 } 2721 2731 2722 2732 // Verify no more events (use timeout) 2723 - conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 2733 + if err := conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil { 2734 + t.Errorf("Expected no errors setting read deadline, got %s", err) 2735 + } 2724 2736 _, _, err = conn.ReadMessage() 2725 2737 if err == nil { 2726 2738 t.Error("Expected no more events, but received another message") ··· 2751 2763 broadcaster.Broadcast(ctx, newEvent) 2752 2764 2753 2765 // Should receive ONLY the new event (seq 4), not historical events 1-3 2754 - conn.SetReadDeadline(time.Now().Add(1 * time.Second)) 2766 + if err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil { 2767 + t.Errorf("Expected no error reading deadline, got %s", err) 2768 + } 2755 2769 messageType, message, err := conn.ReadMessage() 2756 2770 if err != nil { 2757 2771 t.Fatalf("Failed to read new event: %v", err) ··· 2772 2786 } 2773 2787 2774 2788 // Verify no more messages (no historical backfill) 2775 - conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 2789 + if err := conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil { 2790 + t.Errorf("Expected no errors setting read deadline, got %s", err) 2791 + } 2776 2792 _, _, err = conn.ReadMessage() 2777 2793 if err == nil { 2778 2794 t.Error("Expected no more events, but received another message (possible backfill leak)") ··· 2789 2805 defer conn.Close() 2790 2806 2791 2807 // Read and discard the 4 historical events (seq 1-4) 2792 - for i := 0; i < 4; i++ { 2808 + for i := range 4 { 2793 2809 _, _, err := conn.ReadMessage() 2794 2810 if err != nil { 2795 2811 t.Fatalf("Failed to read historical event %d: %v", i+1, err) ··· 2801 2817 newEvent := &RepoEvent{ 2802 2818 NewRoot: testCID, 2803 2819 Rev: fmt.Sprintf("rev-%d", i), 2804 - RepoSlice: []byte(fmt.Sprintf("CAR data %d", i)), 2820 + RepoSlice: fmt.Appendf(nil, "CAR data %d", i), 2805 2821 Ops: []RepoOp{}, 2806 2822 } 2807 2823 broadcaster.Broadcast(ctx, newEvent) ··· 2809 2825 2810 2826 // Should receive both new events 2811 2827 for expectedSeq := 5; expectedSeq <= 6; expectedSeq++ { 2812 - conn.SetReadDeadline(time.Now().Add(1 * time.Second)) 2828 + if err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil { 2829 + t.Errorf("Expected no errors setting read deadline, got %s", err) 2830 + } 2813 2831 messageType, message, err := conn.ReadMessage() 2814 2832 if err != nil { 2815 2833 t.Fatalf("Failed to read event seq=%d: %v", expectedSeq, err)
+2 -4
pkg/logging/logger_test.go
··· 365 365 originalLogger := slog.Default() 366 366 defer slog.SetDefault(originalLogger) 367 367 368 - b.ResetTimer() 369 - for i := 0; i < b.N; i++ { 368 + for b.Loop() { 370 369 InitLogger("info") 371 370 } 372 371 } ··· 375 374 originalLogger := slog.Default() 376 375 defer slog.SetDefault(originalLogger) 377 376 378 - b.ResetTimer() 379 - for i := 0; i < b.N; i++ { 377 + for b.Loop() { 380 378 cleanup := SetupTestLogger() 381 379 cleanup() 382 380 }