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

View file

@ -5,7 +5,7 @@
{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} {"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"}
{"id":"checkvist-api-4qn","title":"Fix date parsing for Checkvist API format","description":"## Problem\n\nThe Checkvist API returns timestamps in a non-standard format that Go's `time.Time` cannot parse with default JSON unmarshaling.\n\n**API returns:** `\"2026/01/14 16:07:31 +0000\"`\n**Go expects:** `\"2006-01-02T15:04:05Z07:00\"` (RFC3339/ISO8601)\n\nThis causes errors like:\n```\nparsing time \"2026/01/14 16:07:31 +0000\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"/01/14 16:07:31 +0000\" as \"-\"\n```\n\n## Affected Fields\n\n- `Checklist.UpdatedAt`\n- `Task.UpdatedAt`\n- `Task.CreatedAt`\n- `Note.UpdatedAt`\n- `Note.CreatedAt`\n\n## Solution\n\nCreate a custom `APITime` type that implements `json.Unmarshaler` to handle the Checkvist date format:\n\n```go\ntype APITime struct {\n time.Time\n}\n\nfunc (t *APITime) UnmarshalJSON(data []byte) error {\n // Try multiple formats:\n // 1. Checkvist format: \"2006/01/02 15:04:05 -0700\"\n // 2. ISO8601/RFC3339: \"2006-01-02T15:04:05Z07:00\"\n}\n```\n\nReplace `time.Time` with `APITime` in all model structs.\n\n## Test\n\nA test has been added that documents this bug:\n`TestChecklists_List_RealAPIDateFormat` in `checklists_test.go`\n\nCurrently skips with message showing the parsing error. Should pass after fix.","status":"open","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T17:55:53.54028308+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T17:55:53.54028308+01:00"} {"id":"checkvist-api-4qn","title":"Fix date parsing for Checkvist API format","description":"## Problem\n\nThe Checkvist API returns timestamps in a non-standard format that Go's `time.Time` cannot parse with default JSON unmarshaling.\n\n**API returns:** `\"2026/01/14 16:07:31 +0000\"`\n**Go expects:** `\"2006-01-02T15:04:05Z07:00\"` (RFC3339/ISO8601)\n\nThis causes errors like:\n```\nparsing time \"2026/01/14 16:07:31 +0000\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"/01/14 16:07:31 +0000\" as \"-\"\n```\n\n## Affected Fields\n\n- `Checklist.UpdatedAt`\n- `Task.UpdatedAt`\n- `Task.CreatedAt`\n- `Note.UpdatedAt`\n- `Note.CreatedAt`\n\n## Solution\n\nCreate a custom `APITime` type that implements `json.Unmarshaler` to handle the Checkvist date format:\n\n```go\ntype APITime struct {\n time.Time\n}\n\nfunc (t *APITime) UnmarshalJSON(data []byte) error {\n // Try multiple formats:\n // 1. Checkvist format: \"2006/01/02 15:04:05 -0700\"\n // 2. ISO8601/RFC3339: \"2006-01-02T15:04:05Z07:00\"\n}\n```\n\nReplace `time.Time` with `APITime` in all model structs.\n\n## Test\n\nA test has been added that documents this bug:\n`TestChecklists_List_RealAPIDateFormat` in `checklists_test.go`\n\nCurrently skips with message showing the parsing error. Should pass after fix.","status":"closed","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T17:55:53.54028308+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T18:09:52.876804767+01:00","closed_at":"2026-01-14T18:09:52.876804767+01:00","close_reason":"APITime type with custom UnmarshalJSON implemented, all tests passing"}
{"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"} {"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"}
{"id":"checkvist-api-8bn","title":"Write unit tests for Client and Auth","description":"Create client_test.go with tests using httptest.Server:\n- TestNewClient_Defaults\n- TestNewClient_WithOptions\n- TestAuthenticate_Success\n- TestAuthenticate_InvalidCredentials\n- TestAuthenticate_2FA\n- TestTokenRefresh_Auto\n- TestTokenRefresh_Manual\n- TestCurrentUser\n- TestRetryLogic_429\n- TestRetryLogic_5xx\n- TestRetryLogic_NetworkError\nUse table-driven tests. Create testdata/auth/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.964610587+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:35:19.981723023+01:00","closed_at":"2026-01-14T13:35:19.981723023+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:33:12.783142853+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:33:13.232028837+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-8bn","title":"Write unit tests for Client and Auth","description":"Create client_test.go with tests using httptest.Server:\n- TestNewClient_Defaults\n- TestNewClient_WithOptions\n- TestAuthenticate_Success\n- TestAuthenticate_InvalidCredentials\n- TestAuthenticate_2FA\n- TestTokenRefresh_Auto\n- TestTokenRefresh_Manual\n- TestCurrentUser\n- TestRetryLogic_429\n- TestRetryLogic_5xx\n- TestRetryLogic_NetworkError\nUse table-driven tests. Create testdata/auth/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.964610587+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:35:19.981723023+01:00","closed_at":"2026-01-14T13:35:19.981723023+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:33:12.783142853+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:33:13.232028837+01:00","created_by":"Oliver Jakoubek"}]}

View file

@ -162,7 +162,7 @@ func TestChecklists_Create(t *testing.T) {
Name: req.Name, Name: req.Name,
Public: false, Public: false,
Archived: false, Archived: false,
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:
@ -208,7 +208,7 @@ func TestChecklists_Update(t *testing.T) {
response := Checklist{ response := Checklist{
ID: 1, ID: 1,
Name: req.Name, Name: req.Name,
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:
@ -284,7 +284,7 @@ func TestChecklists_Archive(t *testing.T) {
ID: 1, ID: 1,
Name: "Archived Checklist", Name: "Archived Checklist",
Archived: true, Archived: true,
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:
@ -328,7 +328,7 @@ func TestChecklists_Unarchive(t *testing.T) {
ID: 1, ID: 1,
Name: "Unarchived Checklist", Name: "Unarchived Checklist",
Archived: false, Archived: false,
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:

View file

@ -2,6 +2,7 @@ package checkvist
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
) )
@ -37,6 +38,50 @@ func (s TaskStatus) String() string {
// Tags represents a set of tags as a map for efficient lookup. // Tags represents a set of tags as a map for efficient lookup.
type Tags map[string]bool type Tags map[string]bool
// APITime wraps time.Time with custom JSON unmarshaling for Checkvist API format.
// The Checkvist API returns timestamps in format "2006/01/02 15:04:05 +0000"
// instead of the standard RFC3339 format that Go expects.
type APITime struct {
time.Time
}
// UnmarshalJSON handles multiple date formats from the Checkvist API.
func (t *APITime) UnmarshalJSON(data []byte) error {
s := strings.Trim(string(data), `"`)
if s == "" || s == "null" {
return nil
}
// Try formats in order of likelihood
formats := []string{
"2006/01/02 15:04:05 -0700", // Checkvist API format
time.RFC3339, // ISO8601 with timezone
"2006-01-02T15:04:05Z", // RFC3339 without offset
}
for _, format := range formats {
if parsed, err := time.Parse(format, s); err == nil {
t.Time = parsed
return nil
}
}
return fmt.Errorf("cannot parse %q as time", s)
}
// MarshalJSON outputs time in RFC3339 format.
func (t APITime) MarshalJSON() ([]byte, error) {
if t.IsZero() {
return []byte("null"), nil
}
return []byte(`"` + t.Format(time.RFC3339) + `"`), nil
}
// NewAPITime creates an APITime from a time.Time value.
func NewAPITime(t time.Time) APITime {
return APITime{Time: t}
}
// Checklist represents a Checkvist checklist. // Checklist represents a Checkvist checklist.
type Checklist struct { type Checklist struct {
// ID is the unique identifier of the checklist. // ID is the unique identifier of the checklist.
@ -58,7 +103,7 @@ type Checklist struct {
// TagsAsText is the raw tags string from the API. // TagsAsText is the raw tags string from the API.
TagsAsText string `json:"tags_as_text"` TagsAsText string `json:"tags_as_text"`
// UpdatedAt is the timestamp of the last update. // UpdatedAt is the timestamp of the last update.
UpdatedAt time.Time `json:"updated_at"` UpdatedAt APITime `json:"updated_at"`
} }
// Task represents a task within a Checkvist checklist. // Task represents a task within a Checkvist checklist.
@ -92,9 +137,9 @@ type Task struct {
// UpdateLine contains brief update information. // UpdateLine contains brief update information.
UpdateLine string `json:"update_line"` UpdateLine string `json:"update_line"`
// UpdatedAt is the timestamp of the last update. // UpdatedAt is the timestamp of the last update.
UpdatedAt time.Time `json:"updated_at"` UpdatedAt APITime `json:"updated_at"`
// CreatedAt is the timestamp when the task was created. // CreatedAt is the timestamp when the task was created.
CreatedAt time.Time `json:"created_at"` CreatedAt APITime `json:"created_at"`
// Children contains nested child tasks (when fetched with tree structure). // Children contains nested child tasks (when fetched with tree structure).
Children []*Task `json:"tasks,omitempty"` Children []*Task `json:"tasks,omitempty"`
// Notes contains the comments/notes attached to this task. // Notes contains the comments/notes attached to this task.
@ -110,9 +155,9 @@ type Note struct {
// Comment is the text content of the note. // Comment is the text content of the note.
Comment string `json:"comment"` Comment string `json:"comment"`
// UpdatedAt is the timestamp of the last update. // UpdatedAt is the timestamp of the last update.
UpdatedAt time.Time `json:"updated_at"` UpdatedAt APITime `json:"updated_at"`
// CreatedAt is the timestamp when the note was created. // CreatedAt is the timestamp when the note was created.
CreatedAt time.Time `json:"created_at"` CreatedAt APITime `json:"created_at"`
} }
// User represents a Checkvist user. // User represents a Checkvist user.

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

View file

@ -71,8 +71,8 @@ func TestNotes_Create(t *testing.T) {
ID: 600, ID: 600,
TaskID: 101, TaskID: 101,
Comment: req.Comment, Comment: req.Comment,
CreatedAt: time.Now(), CreatedAt: NewAPITime(time.Now()),
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:
@ -119,7 +119,7 @@ func TestNotes_Update(t *testing.T) {
ID: 501, ID: 501,
TaskID: 101, TaskID: 101,
Comment: req.Comment, Comment: req.Comment,
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:

View file

@ -98,8 +98,8 @@ func TestTasks_Create(t *testing.T) {
ChecklistID: 1, ChecklistID: 1,
Content: req.Content, Content: req.Content,
Status: StatusOpen, Status: StatusOpen,
CreatedAt: time.Now(), CreatedAt: NewAPITime(time.Now()),
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:
@ -203,7 +203,7 @@ func TestTasks_Update(t *testing.T) {
ID: 101, ID: 101,
ChecklistID: 1, ChecklistID: 1,
Content: "Updated content", Content: "Updated content",
UpdatedAt: time.Now(), UpdatedAt: NewAPITime(time.Now()),
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
default: default:

View file

@ -8,7 +8,7 @@
"task_count": 10, "task_count": 10,
"task_completed": 3, "task_completed": 3,
"tags_as_text": "", "tags_as_text": "",
"updated_at": "2026-01-14T10:00:00Z" "updated_at": "2026/01/14 10:00:00 +0000"
}, },
{ {
"id": 2, "id": 2,
@ -19,6 +19,6 @@
"task_count": 25, "task_count": 25,
"task_completed": 12, "task_completed": 12,
"tags_as_text": "work, important", "tags_as_text": "work, important",
"updated_at": "2026-01-13T15:30:00Z" "updated_at": "2026/01/13 15:30:00 +0000"
} }
] ]

View file

@ -8,6 +8,6 @@
"task_count": 50, "task_count": 50,
"task_completed": 50, "task_completed": 50,
"tags_as_text": "", "tags_as_text": "",
"updated_at": "2025-06-01T09:00:00Z" "updated_at": "2025/06/01 09:00:00 +0000"
} }
] ]

View file

@ -7,5 +7,5 @@
"task_count": 10, "task_count": 10,
"task_completed": 3, "task_completed": 3,
"tags_as_text": "", "tags_as_text": "",
"updated_at": "2026-01-14T10:00:00Z" "updated_at": "2026/01/14 10:00:00 +0000"
} }

View file

@ -3,14 +3,14 @@
"id": 501, "id": 501,
"task_id": 101, "task_id": 101,
"comment": "First comment on task", "comment": "First comment on task",
"updated_at": "2026-01-14T10:00:00Z", "updated_at": "2026/01/14 10:00:00 +0000",
"created_at": "2026-01-13T09:00:00Z" "created_at": "2026/01/13 09:00:00 +0000"
}, },
{ {
"id": 502, "id": 502,
"task_id": 101, "task_id": 101,
"comment": "Second comment with more details", "comment": "Second comment with more details",
"updated_at": "2026-01-14T11:30:00Z", "updated_at": "2026/01/14 11:30:00 +0000",
"created_at": "2026-01-14T11:00:00Z" "created_at": "2026/01/14 11:00:00 +0000"
} }
] ]

View file

@ -12,8 +12,8 @@
"assignee_ids": [], "assignee_ids": [],
"comments_count": 0, "comments_count": 0,
"update_line": "", "update_line": "",
"updated_at": "2026-01-14T10:00:00Z", "updated_at": "2026/01/14 10:00:00 +0000",
"created_at": "2026-01-10T09:00:00Z" "created_at": "2026/01/10 09:00:00 +0000"
}, },
{ {
"id": 102, "id": 102,
@ -28,7 +28,7 @@
"assignee_ids": [1, 2], "assignee_ids": [1, 2],
"comments_count": 3, "comments_count": 3,
"update_line": "", "update_line": "",
"updated_at": "2026-01-14T11:00:00Z", "updated_at": "2026/01/14 11:00:00 +0000",
"created_at": "2026-01-11T10:00:00Z" "created_at": "2026/01/11 10:00:00 +0000"
} }
] ]

View file

@ -11,6 +11,6 @@
"assignee_ids": [], "assignee_ids": [],
"comments_count": 0, "comments_count": 0,
"update_line": "", "update_line": "",
"updated_at": "2026-01-14T10:00:00Z", "updated_at": "2026/01/14 10:00:00 +0000",
"created_at": "2026-01-10T09:00:00Z" "created_at": "2026/01/10 09:00:00 +0000"
} }