480 lines
13 KiB
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]
|
|
}
|