Files
Ascently/sync/format_test.go

480 lines
13 KiB
Go

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]
}