package main import ( "encoding/json" "strings" "testing" ) func TestDataFormatCompatibility(t *testing.T) { t.Run("JSON Marshaling and Unmarshaling", func(t *testing.T) { originalBackup := ClimbDataBackup{ ExportedAt: "2024-01-01T10:00:00Z", Version: "2.0", FormatVersion: "2.0", Gyms: []BackupGym{ { ID: "gym1", Name: "Test Gym", Location: stringPtr("Test Location"), SupportedClimbTypes: []string{"BOULDER", "ROPE"}, DifficultySystems: []string{"V", "YDS"}, CustomDifficultyGrades: []string{"V0+", "V1+"}, Notes: stringPtr("Test notes"), CreatedAt: "2024-01-01T10:00:00Z", UpdatedAt: "2024-01-01T10:00:00Z", }, }, Problems: []BackupProblem{ { ID: "problem1", GymID: "gym1", Name: stringPtr("Test Problem"), Description: stringPtr("A challenging problem"), ClimbType: "BOULDER", Difficulty: DifficultyGrade{ System: "V", Grade: "V5", NumericValue: 5, }, Tags: []string{"overhang", "crimpy"}, Location: stringPtr("Wall A"), ImagePaths: []string{"image1.jpg", "image2.jpg"}, IsActive: true, DateSet: stringPtr("2024-01-01"), Notes: stringPtr("Watch the start"), CreatedAt: "2024-01-01T10:00:00Z", UpdatedAt: "2024-01-01T10:00:00Z", }, }, Sessions: []BackupClimbSession{ { ID: "session1", GymID: "gym1", Date: "2024-01-01", StartTime: stringPtr("2024-01-01T10:00:00Z"), EndTime: stringPtr("2024-01-01T12:00:00Z"), Duration: int64Ptr(7200), Status: "completed", Notes: stringPtr("Great session"), CreatedAt: "2024-01-01T10:00:00Z", UpdatedAt: "2024-01-01T12:00:00Z", }, }, Attempts: []BackupAttempt{ { ID: "attempt1", SessionID: "session1", ProblemID: "problem1", Result: "completed", HighestHold: stringPtr("Top"), Notes: stringPtr("Clean send"), Duration: int64Ptr(300), RestTime: int64Ptr(120), Timestamp: "2024-01-01T10:30:00Z", CreatedAt: "2024-01-01T10:30:00Z", }, }, } jsonData, err := json.Marshal(originalBackup) if err != nil { t.Fatalf("Failed to marshal backup: %v", err) } var unmarshaledBackup ClimbDataBackup if err := json.Unmarshal(jsonData, &unmarshaledBackup); err != nil { t.Fatalf("Failed to unmarshal backup: %v", err) } if originalBackup.Version != unmarshaledBackup.Version { t.Errorf("Version mismatch: expected %s, got %s", originalBackup.Version, unmarshaledBackup.Version) } if len(originalBackup.Gyms) != len(unmarshaledBackup.Gyms) { t.Errorf("Gyms count mismatch: expected %d, got %d", len(originalBackup.Gyms), len(unmarshaledBackup.Gyms)) } if len(originalBackup.Problems) != len(unmarshaledBackup.Problems) { t.Errorf("Problems count mismatch: expected %d, got %d", len(originalBackup.Problems), len(unmarshaledBackup.Problems)) } if len(originalBackup.Sessions) != len(unmarshaledBackup.Sessions) { t.Errorf("Sessions count mismatch: expected %d, got %d", len(originalBackup.Sessions), len(unmarshaledBackup.Sessions)) } if len(originalBackup.Attempts) != len(unmarshaledBackup.Attempts) { t.Errorf("Attempts count mismatch: expected %d, got %d", len(originalBackup.Attempts), len(unmarshaledBackup.Attempts)) } }) t.Run("Required Fields Validation", func(t *testing.T) { testCases := []struct { name string jsonInput string shouldError bool }{ { name: "Valid minimal backup", jsonInput: `{ "exportedAt": "2024-01-01T10:00:00Z", "version": "2.0", "formatVersion": "2.0", "gyms": [], "problems": [], "sessions": [], "attempts": [] }`, shouldError: false, }, { name: "Missing version field", jsonInput: `{ "exportedAt": "2024-01-01T10:00:00Z", "formatVersion": "2.0", "gyms": [], "problems": [], "sessions": [], "attempts": [] }`, shouldError: false, }, { name: "Invalid JSON structure", jsonInput: `{ "exportedAt": "2024-01-01T10:00:00Z", "version": "2.0", "formatVersion": "2.0", "gyms": "not an array" }`, shouldError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var backup ClimbDataBackup err := json.Unmarshal([]byte(tc.jsonInput), &backup) if tc.shouldError && err == nil { t.Error("Expected error but got none") } if !tc.shouldError && err != nil { t.Errorf("Unexpected error: %v", err) } }) } }) t.Run("Difficulty Grade Format", func(t *testing.T) { testGrades := []DifficultyGrade{ {System: "V", Grade: "V0", NumericValue: 0}, {System: "V", Grade: "V5", NumericValue: 5}, {System: "V", Grade: "V10", NumericValue: 10}, {System: "YDS", Grade: "5.10a", NumericValue: 100}, {System: "YDS", Grade: "5.12d", NumericValue: 124}, {System: "Font", Grade: "6A", NumericValue: 60}, {System: "Custom", Grade: "Beginner", NumericValue: 1}, } for _, grade := range testGrades { jsonData, err := json.Marshal(grade) if err != nil { t.Errorf("Failed to marshal grade %+v: %v", grade, err) continue } var unmarshaledGrade DifficultyGrade if err := json.Unmarshal(jsonData, &unmarshaledGrade); err != nil { t.Errorf("Failed to unmarshal grade %s: %v", string(jsonData), err) continue } if grade.System != unmarshaledGrade.System { t.Errorf("System mismatch for grade %+v: expected %s, got %s", grade, grade.System, unmarshaledGrade.System) } if grade.Grade != unmarshaledGrade.Grade { t.Errorf("Grade mismatch for grade %+v: expected %s, got %s", grade, grade.Grade, unmarshaledGrade.Grade) } if grade.NumericValue != unmarshaledGrade.NumericValue { t.Errorf("NumericValue mismatch for grade %+v: expected %d, got %d", grade, grade.NumericValue, unmarshaledGrade.NumericValue) } } }) t.Run("Null and Optional Fields", func(t *testing.T) { jsonWithNulls := `{ "exportedAt": "2024-01-01T10:00:00Z", "version": "2.0", "formatVersion": "2.0", "gyms": [{ "id": "gym1", "name": "Test Gym", "location": null, "supportedClimbTypes": ["boulder"], "difficultySystems": ["V"], "customDifficultyGrades": [], "notes": null, "createdAt": "2024-01-01T10:00:00Z", "updatedAt": "2024-01-01T10:00:00Z" }], "problems": [{ "id": "problem1", "gymId": "gym1", "name": null, "description": null, "climbType": "boulder", "difficulty": { "system": "V", "grade": "V5", "numericValue": 5 }, "tags": [], "location": null, "imagePaths": [], "isActive": true, "dateSet": null, "notes": null, "createdAt": "2024-01-01T10:00:00Z", "updatedAt": "2024-01-01T10:00:00Z" }], "sessions": [], "attempts": [] }` var backup ClimbDataBackup if err := json.Unmarshal([]byte(jsonWithNulls), &backup); err != nil { t.Fatalf("Failed to unmarshal JSON with nulls: %v", err) } if backup.Gyms[0].Location != nil { t.Error("Expected location to be nil") } if backup.Gyms[0].Notes != nil { t.Error("Expected notes to be nil") } if backup.Problems[0].Name != nil { t.Error("Expected problem name to be nil") } }) t.Run("Date Format Validation", func(t *testing.T) { validDates := []string{ "2024-01-01T10:00:00Z", "2024-12-31T23:59:59Z", "2024-06-15T12:30:45Z", "2024-01-01T00:00:00Z", } invalidDates := []string{ "2024-01-01 10:00:00", "2024/01/01T10:00:00Z", "2024-1-1T10:00:00Z", } for _, date := range validDates { if !isValidISODate(date) { t.Errorf("Valid date %s was marked as invalid", date) } } for _, date := range invalidDates { if isValidISODate(date) { t.Errorf("Invalid date %s was marked as valid", date) } } }) t.Run("Field Length Limits", func(t *testing.T) { longString := strings.Repeat("a", 10000) gym := BackupGym{ ID: "gym1", Name: longString, Location: &longString, SupportedClimbTypes: []string{"boulder"}, DifficultySystems: []string{"V"}, CustomDifficultyGrades: []string{}, Notes: &longString, CreatedAt: "2024-01-01T10:00:00Z", UpdatedAt: "2024-01-01T10:00:00Z", } jsonData, err := json.Marshal(gym) if err != nil { t.Errorf("Failed to marshal gym with long strings: %v", err) } var unmarshaledGym BackupGym if err := json.Unmarshal(jsonData, &unmarshaledGym); err != nil { t.Errorf("Failed to unmarshal gym with long strings: %v", err) } if unmarshaledGym.Name != longString { t.Error("Long name was not preserved") } }) t.Run("Array Field Validation", func(t *testing.T) { backup := ClimbDataBackup{ ExportedAt: "2024-01-01T10:00:00Z", Version: "2.0", FormatVersion: "2.0", Gyms: nil, Problems: []BackupProblem{}, Sessions: []BackupClimbSession{}, Attempts: []BackupAttempt{}, } jsonData, err := json.Marshal(backup) if err != nil { t.Fatalf("Failed to marshal backup with nil gyms: %v", err) } var unmarshaledBackup ClimbDataBackup if err := json.Unmarshal(jsonData, &unmarshaledBackup); err != nil { t.Fatalf("Failed to unmarshal backup with nil gyms: %v", err) } if len(unmarshaledBackup.Gyms) != 0 { t.Error("Expected gyms to be empty or nil") } }) } func isValidISODate(date string) bool { // More robust ISO date validation if !strings.Contains(date, "T") || !strings.HasSuffix(date, "Z") { return false } // Check basic format: YYYY-MM-DDTHH:MM:SSZ parts := strings.Split(date, "T") if len(parts) != 2 { return false } datePart := parts[0] timePart := strings.TrimSuffix(parts[1], "Z") // Date part should be YYYY-MM-DD dateComponents := strings.Split(datePart, "-") if len(dateComponents) != 3 || len(dateComponents[0]) != 4 || len(dateComponents[1]) != 2 || len(dateComponents[2]) != 2 { return false } // Time part should be HH:MM:SS timeComponents := strings.Split(timePart, ":") if len(timeComponents) != 3 || len(timeComponents[0]) != 2 || len(timeComponents[1]) != 2 || len(timeComponents[2]) != 2 { return false } return true } func TestVersionCompatibility(t *testing.T) { testCases := []struct { version string formatVersion string shouldSupport bool }{ {"2.0", "2.0", true}, {"1.0", "1.0", true}, {"2.1", "2.0", false}, {"3.0", "2.0", false}, {"1.0", "2.0", false}, } for _, tc := range testCases { t.Run(tc.version+"/"+tc.formatVersion, func(t *testing.T) { backup := ClimbDataBackup{ Version: tc.version, FormatVersion: tc.formatVersion, } // Only exact version matches are supported for now isSupported := backup.Version == "2.0" && backup.FormatVersion == "2.0" if backup.Version == "1.0" && backup.FormatVersion == "1.0" { isSupported = true } if isSupported != tc.shouldSupport { t.Errorf("Version %s support expectation mismatch: expected %v, got %v", tc.version, tc.shouldSupport, isSupported) } }) } } func TestClimbTypeValidation(t *testing.T) { validClimbTypes := []string{"boulder", "sport", "trad", "toprope", "aid", "ice", "mixed"} invalidClimbTypes := []string{"", "invalid", "BOULDER", "Sport", "unknown"} for _, climbType := range validClimbTypes { if !isValidClimbType(climbType) { t.Errorf("Valid climb type %s was marked as invalid", climbType) } } for _, climbType := range invalidClimbTypes { if isValidClimbType(climbType) { t.Errorf("Invalid climb type %s was marked as valid", climbType) } } } func isValidClimbType(climbType string) bool { validTypes := map[string]bool{ "boulder": true, "sport": true, "trad": true, "toprope": true, "aid": true, "ice": true, "mixed": true, } return validTypes[climbType] } func TestAttemptResultValidation(t *testing.T) { validResults := []string{"completed", "failed", "flash", "project", "attempt"} invalidResults := []string{"", "invalid", "COMPLETED", "Failed", "unknown"} for _, result := range validResults { if !isValidAttemptResult(result) { t.Errorf("Valid attempt result %s was marked as invalid", result) } } for _, result := range invalidResults { if isValidAttemptResult(result) { t.Errorf("Invalid attempt result %s was marked as valid", result) } } } // Helper functions for creating pointers func stringPtr(s string) *string { return &s } func int64Ptr(i int64) *int64 { return &i } func isValidAttemptResult(result string) bool { validResults := map[string]bool{ "completed": true, "failed": true, "flash": true, "project": true, "attempt": true, } return validResults[result] }