Added a proper set of Unit Tests for each sub-project
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m28s
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m28s
This commit is contained in:
479
sync/format_test.go
Normal file
479
sync/format_test.go
Normal file
@@ -0,0 +1,479 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user