Fix date parsing for Checkvist API format

Add custom APITime type with UnmarshalJSON to handle the non-standard
date format returned by the Checkvist API ("2026/01/14 16:07:31 +0000")
instead of the expected RFC3339 format.

Changes:
- Add APITime type with custom JSON marshaling/unmarshaling in models.go
- Replace time.Time with APITime for UpdatedAt/CreatedAt fields in
  Checklist, Task, and Note structs
- Add NewAPITime constructor for convenience
- Update test fixtures to use real API date format
- Add comprehensive unit tests for APITime in models_test.go

Fixes: checkvist-api-4qn
This commit is contained in:
Oliver Jakoubek 2026-01-14 18:10:02 +01:00
commit e4862b8e9b
12 changed files with 241 additions and 30 deletions

166
models_test.go Normal file
View file

@ -0,0 +1,166 @@
package checkvist
import (
"encoding/json"
"testing"
"time"
)
func TestAPITime_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
check func(t *testing.T, got APITime)
}{
{
name: "Checkvist API format",
input: `"2026/01/14 16:07:31 +0000"`,
wantErr: false,
check: func(t *testing.T, got APITime) {
if got.Year() != 2026 || got.Month() != 1 || got.Day() != 14 {
t.Errorf("date mismatch: got %v", got.Time)
}
if got.Hour() != 16 || got.Minute() != 7 || got.Second() != 31 {
t.Errorf("time mismatch: got %v", got.Time)
}
},
},
{
name: "RFC3339 format",
input: `"2026-01-14T10:00:00Z"`,
wantErr: false,
check: func(t *testing.T, got APITime) {
if got.Year() != 2026 || got.Month() != 1 || got.Day() != 14 {
t.Errorf("date mismatch: got %v", got.Time)
}
if got.Hour() != 10 || got.Minute() != 0 {
t.Errorf("time mismatch: got %v", got.Time)
}
},
},
{
name: "RFC3339 with timezone offset",
input: `"2026-01-14T10:00:00+02:00"`,
wantErr: false,
check: func(t *testing.T, got APITime) {
if got.Year() != 2026 || got.Month() != 1 || got.Day() != 14 {
t.Errorf("date mismatch: got %v", got.Time)
}
},
},
{
name: "empty string",
input: `""`,
wantErr: false,
check: func(t *testing.T, got APITime) {
if !got.IsZero() {
t.Error("expected zero time for empty string")
}
},
},
{
name: "null",
input: `null`,
wantErr: false,
check: func(t *testing.T, got APITime) {
if !got.IsZero() {
t.Error("expected zero time for null")
}
},
},
{
name: "invalid format",
input: `"not a date"`,
wantErr: true,
check: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got APITime
err := json.Unmarshal([]byte(tt.input), &got)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.check != nil {
tt.check(t, got)
}
})
}
}
func TestAPITime_MarshalJSON(t *testing.T) {
tests := []struct {
name string
input APITime
expected string
}{
{
name: "normal time",
input: NewAPITime(time.Date(2026, 1, 14, 10, 30, 0, 0, time.UTC)),
expected: `"2026-01-14T10:30:00Z"`,
},
{
name: "zero time",
input: APITime{},
expected: `null`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.input)
if err != nil {
t.Fatalf("MarshalJSON() error = %v", err)
}
if string(got) != tt.expected {
t.Errorf("MarshalJSON() = %s, want %s", got, tt.expected)
}
})
}
}
func TestAPITime_InStruct(t *testing.T) {
// Test unmarshaling a struct with APITime fields using real API format
jsonData := `{
"id": 1,
"name": "Test Checklist",
"updated_at": "2026/01/14 16:07:31 +0000"
}`
type testStruct struct {
ID int `json:"id"`
Name string `json:"name"`
UpdatedAt APITime `json:"updated_at"`
}
var result testStruct
err := json.Unmarshal([]byte(jsonData), &result)
if err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if result.UpdatedAt.Year() != 2026 {
t.Errorf("expected year 2026, got %d", result.UpdatedAt.Year())
}
if result.UpdatedAt.Month() != 1 {
t.Errorf("expected month 1, got %d", result.UpdatedAt.Month())
}
if result.UpdatedAt.Day() != 14 {
t.Errorf("expected day 14, got %d", result.UpdatedAt.Day())
}
}
func TestNewAPITime(t *testing.T) {
now := time.Now()
apiTime := NewAPITime(now)
if !apiTime.Time.Equal(now) {
t.Errorf("NewAPITime() did not preserve time: got %v, want %v", apiTime.Time, now)
}
}