Implement entity structs for Kanboard API responses
Add all entity structs matching Kanboard's JSON-RPC API format: Entity structs: - Project, Task, Column, Category, Comment - TaskLink, TaskFile, Tag - TaskStatus enum (StatusActive, StatusInactive) Request structs: - CreateTaskRequest with omitempty for optional fields - UpdateTaskRequest with pointer fields for zero-value distinction Custom JSON types for Kanboard's string-encoded values: - StringBool - handles "0"/"1" string booleans - StringInt - handles string-encoded integers - StringInt64 - handles string-encoded int64 values All structs properly handle Kanboard's quirky JSON format where numeric fields are often returned as quoted strings. Closes: kanboard-api-cyc
This commit is contained in:
parent
dbac08ac1e
commit
a38e62e77a
3 changed files with 779 additions and 1 deletions
|
|
@ -10,7 +10,7 @@
|
|||
{"id":"kanboard-api-9k8","title":"Implement TaskParams builder (Options Pattern)","description":"Implement TaskParams for fluent task creation configuration.\n\n## Requirements\n- TaskParams struct with private fields\n- NewTask(title string) *TaskParams constructor\n- Fluent setter methods:\n - WithDescription(desc string) *TaskParams\n - InColumn(columnID int) *TaskParams\n - WithCategory(categoryID int) *TaskParams\n - WithOwner(ownerID int) *TaskParams\n - WithColor(colorID string) *TaskParams\n - WithPriority(priority int) *TaskParams\n - WithDueDate(date time.Time) *TaskParams\n - WithTags(tags ...string) *TaskParams\n - WithReference(ref string) *TaskParams\n- Internal method to convert to CreateTaskRequest\n\n## Files to create\n- task_params.go\n\n## Example usage\n```go\nparams := kanboard.NewTask(\"My Task\").\n WithDescription(\"Details\").\n InColumn(2).\n WithTags(\"urgent\", \"backend\")\n```\n\n## Acceptance criteria\n- Pure configuration object (no I/O)\n- All setters return *TaskParams for chaining\n- Unset optional fields remain nil","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.439513879+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:35:40.439513879+01:00","dependencies":[{"issue_id":"kanboard-api-9k8","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:31.070100884+01:00","created_by":"Oliver Jakoubek"}]}
|
||||
{"id":"kanboard-api-apl","title":"Implement Project API methods","description":"Implement direct API methods for project/board operations.\n\n## Methods to implement\n- GetAllProjects(ctx) ([]Project, error) - getAllProjects\n- GetProjectByID(ctx, projectID int) (*Project, error) - getProjectById\n- GetProjectByName(ctx, name string) (*Project, error) - getProjectByName (Nice-to-have)\n\n## Files to create\n- projects.go\n\n## Acceptance criteria\n- All methods use context for cancellation\n- Proper error handling and wrapping\n- Returns ErrProjectNotFound when appropriate","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:15.864764497+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:35:15.864764497+01:00","dependencies":[{"issue_id":"kanboard-api-apl","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:52.850751716+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-apl","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:52.946556814+01:00","created_by":"Oliver Jakoubek"}]}
|
||||
{"id":"kanboard-api-c3i","title":"Create README.md with documentation","description":"Create comprehensive README.md documentation in English.\n\n## Sections to include\n- Project overview and features\n- Installation instructions\n- Quick start example\n- API documentation overview\n- Authentication methods (API token, user/password)\n- Fluent API examples\n- Direct API examples\n- Error handling\n- Thread-safety notes\n- Tag operations warning (non-atomic)\n- License (MIT)\n\n## Example code to include\n```go\n// Client creation\nclient := kanboard.NewClient(\"https://kanboard.example.com\").\n WithAPIToken(\"my-token\").\n WithTimeout(30 * time.Second)\n\n// Fluent API\ntask, _ := client.Board(1).CreateTask(ctx,\n kanboard.NewTask(\"My Task\").WithDescription(\"Details\"))\n\n// Direct API\ntasks, _ := client.GetAllTasks(ctx, 1, kanboard.StatusActive)\n```\n\n## Files to create\n- README.md\n\n## Acceptance criteria\n- Clear installation instructions\n- Working code examples\n- Documents all major features","status":"open","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:53.228407343+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:53.228407343+01:00","dependencies":[{"issue_id":"kanboard-api-c3i","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:46:55.445847253+01:00","created_by":"Oliver Jakoubek"}]}
|
||||
{"id":"kanboard-api-cyc","title":"Implement entity structs (types.go)","description":"Implement all entity structs for Kanboard API responses.\n\n## Structs to implement\n- Project (ID, Name, Description, IsActive, Token, LastModified, etc.)\n- Task (ID, Title, Description, dates, ColorID, ProjectID, ColumnID, etc.)\n- Column (ID, Title, Position, ProjectID, TaskLimit, Description)\n- Category (ID, Name, ProjectID, ColorID)\n- Comment (ID, TaskID, UserID, DateCreation, Content, Username, etc.)\n- TaskLink (ID, LinkID, TaskID, OppositeTaskID, Label, Title)\n- TaskFile (ID, Name, Path, IsImage, TaskID, DateCreation, UserID, Size)\n- Tag (ID, Name, ProjectID, ColorID)\n- TaskStatus enum (StatusActive, StatusInactive)\n\n## Request structs\n- CreateTaskRequest\n- UpdateTaskRequest (with pointer fields for optional values)\n\n## Files to create\n- types.go\n\n## Acceptance criteria\n- All JSON tags match Kanboard API\n- Optional fields use pointers with omitempty\n- Timestamp fields use custom Timestamp type","status":"open","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.484472208+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:34:55.484472208+01:00","dependencies":[{"issue_id":"kanboard-api-cyc","depends_on_id":"kanboard-api-25y","type":"blocks","created_at":"2026-01-15T17:42:48.385166815+01:00","created_by":"Oliver Jakoubek"}]}
|
||||
{"id":"kanboard-api-cyc","title":"Implement entity structs (types.go)","description":"Implement all entity structs for Kanboard API responses.\n\n## Structs to implement\n- Project (ID, Name, Description, IsActive, Token, LastModified, etc.)\n- Task (ID, Title, Description, dates, ColorID, ProjectID, ColumnID, etc.)\n- Column (ID, Title, Position, ProjectID, TaskLimit, Description)\n- Category (ID, Name, ProjectID, ColorID)\n- Comment (ID, TaskID, UserID, DateCreation, Content, Username, etc.)\n- TaskLink (ID, LinkID, TaskID, OppositeTaskID, Label, Title)\n- TaskFile (ID, Name, Path, IsImage, TaskID, DateCreation, UserID, Size)\n- Tag (ID, Name, ProjectID, ColorID)\n- TaskStatus enum (StatusActive, StatusInactive)\n\n## Request structs\n- CreateTaskRequest\n- UpdateTaskRequest (with pointer fields for optional values)\n\n## Files to create\n- types.go\n\n## Acceptance criteria\n- All JSON tags match Kanboard API\n- Optional fields use pointers with omitempty\n- Timestamp fields use custom Timestamp type","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.484472208+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:18:40.163015715+01:00","closed_at":"2026-01-15T18:18:40.163015715+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-cyc","depends_on_id":"kanboard-api-25y","type":"blocks","created_at":"2026-01-15T17:42:48.385166815+01:00","created_by":"Oliver Jakoubek"}]}
|
||||
{"id":"kanboard-api-fue","title":"Implement Task search and move methods","description":"Implement task search and movement API methods.\n\n## Methods to implement\n- SearchTasks(ctx, projectID int, query string) ([]Task, error) - searchTasks\n- MoveTaskPosition(ctx, projectID, taskID, columnID, position, swimlaneID int) error - moveTaskPosition\n- MoveTaskToProject(ctx, taskID, projectID int) error - moveTaskToProject\n\n## Files to create\n- tasks.go (extend)\n\n## Acceptance criteria\n- Search supports Kanboard query syntax\n- MoveTaskPosition handles column and position\n- Proper error handling for invalid moves","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:17.380817511+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:35:17.380817511+01:00","dependencies":[{"issue_id":"kanboard-api-fue","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:42:53.59290181+01:00","created_by":"Oliver Jakoubek"}]}
|
||||
{"id":"kanboard-api-h4u","title":"Implement Comment API methods","description":"Implement direct API methods for comment operations.\n\n## Methods to implement\n- GetAllComments(ctx, taskID int) ([]Comment, error) - getAllComments\n- CreateComment(ctx, taskID, userID int, content string) (*Comment, error) - createComment\n- UpdateComment(ctx, commentID int, content string) error - updateComment\n- RemoveComment(ctx, commentID int) error - removeComment\n\n## TaskScope methods to add\n- AddComment(ctx, content string) (*Comment, error)\n- GetComments(ctx) ([]Comment, error)\n\n## Files to create\n- comments.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- CreateComment returns the created comment\n- Returns ErrCommentNotFound when appropriate","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:08.156950163+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:08.156950163+01:00","dependencies":[{"issue_id":"kanboard-api-h4u","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:48.898295911+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-h4u","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.058259216+01:00","created_by":"Oliver Jakoubek"}]}
|
||||
{"id":"kanboard-api-k33","title":"Implement authentication system","description":"Implement HTTP Basic Auth support for both API token and user/password authentication.\n\n## Requirements\n- Authenticator interface for auth strategies\n- API Token auth: HTTP Basic with username `jsonrpc` and API token as password\n- User/Password auth: HTTP Basic with username and password\n- WithAPIToken(token string) fluent method\n- WithBasicAuth(username, password string) fluent method\n- Secure handling - no credential storage/logging\n\n## Files to create\n- auth.go\n\n## Acceptance criteria\n- Both auth methods work correctly\n- Credentials properly encoded in Authorization header\n- No sensitive data logged","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:53.631074781+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:11:42.58778642+01:00","closed_at":"2026-01-15T18:11:42.58778642+01:00","close_reason":"Closed"}
|
||||
|
|
|
|||
257
types.go
Normal file
257
types.go
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
package kanboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// TaskStatus represents the status of a task.
|
||||
type TaskStatus int
|
||||
|
||||
const (
|
||||
// StatusActive represents an open/active task.
|
||||
StatusActive TaskStatus = 1
|
||||
// StatusInactive represents a closed/inactive task.
|
||||
StatusInactive TaskStatus = 0
|
||||
)
|
||||
|
||||
// StringBool is a bool that can be unmarshaled from a string "0" or "1".
|
||||
type StringBool bool
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (b *StringBool) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
// Try as raw bool
|
||||
var boolVal bool
|
||||
if err := json.Unmarshal(data, &boolVal); err != nil {
|
||||
return err
|
||||
}
|
||||
*b = StringBool(boolVal)
|
||||
return nil
|
||||
}
|
||||
*b = s == "1" || s == "true"
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (b StringBool) MarshalJSON() ([]byte, error) {
|
||||
if b {
|
||||
return []byte(`"1"`), nil
|
||||
}
|
||||
return []byte(`"0"`), nil
|
||||
}
|
||||
|
||||
// StringInt is an int that can be unmarshaled from a JSON string.
|
||||
type StringInt int
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (i *StringInt) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
// Try as raw int
|
||||
var intVal int
|
||||
if err := json.Unmarshal(data, &intVal); err != nil {
|
||||
return err
|
||||
}
|
||||
*i = StringInt(intVal)
|
||||
return nil
|
||||
}
|
||||
if s == "" {
|
||||
*i = 0
|
||||
return nil
|
||||
}
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*i = StringInt(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StringInt64 is an int64 that can be unmarshaled from a JSON string.
|
||||
type StringInt64 int64
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (i *StringInt64) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
// Try as raw int64
|
||||
var intVal int64
|
||||
if err := json.Unmarshal(data, &intVal); err != nil {
|
||||
return err
|
||||
}
|
||||
*i = StringInt64(intVal)
|
||||
return nil
|
||||
}
|
||||
if s == "" {
|
||||
*i = 0
|
||||
return nil
|
||||
}
|
||||
val, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*i = StringInt64(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Project represents a Kanboard project (board).
|
||||
type Project struct {
|
||||
ID StringInt `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsActive StringBool `json:"is_active"`
|
||||
Token string `json:"token"`
|
||||
LastModified Timestamp `json:"last_modified"`
|
||||
IsPublic StringBool `json:"is_public"`
|
||||
IsPrivate StringBool `json:"is_private"`
|
||||
DefaultSwimlane string `json:"default_swimlane"`
|
||||
ShowDefaultSwimlane StringBool `json:"show_default_swimlane"`
|
||||
Identifier string `json:"identifier"`
|
||||
StartDate Timestamp `json:"start_date"`
|
||||
EndDate Timestamp `json:"end_date"`
|
||||
OwnerID StringInt `json:"owner_id"`
|
||||
PriorityDefault StringInt `json:"priority_default"`
|
||||
PriorityStart StringInt `json:"priority_start"`
|
||||
PriorityEnd StringInt `json:"priority_end"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// Task represents a Kanboard task.
|
||||
type Task struct {
|
||||
ID StringInt `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DateCreation Timestamp `json:"date_creation"`
|
||||
DateModification Timestamp `json:"date_modification"`
|
||||
DateCompleted Timestamp `json:"date_completed"`
|
||||
DateStarted Timestamp `json:"date_started"`
|
||||
DateDue Timestamp `json:"date_due"`
|
||||
ColorID string `json:"color_id"`
|
||||
ProjectID StringInt `json:"project_id"`
|
||||
ColumnID StringInt `json:"column_id"`
|
||||
OwnerID StringInt `json:"owner_id"`
|
||||
CreatorID StringInt `json:"creator_id"`
|
||||
Position StringInt `json:"position"`
|
||||
IsActive StringBool `json:"is_active"`
|
||||
Score StringInt `json:"score"`
|
||||
CategoryID StringInt `json:"category_id"`
|
||||
SwimlaneID StringInt `json:"swimlane_id"`
|
||||
Priority StringInt `json:"priority"`
|
||||
Reference string `json:"reference"`
|
||||
RecurrenceStatus StringInt `json:"recurrence_status"`
|
||||
RecurrenceTrigger StringInt `json:"recurrence_trigger"`
|
||||
RecurrenceFactor StringInt `json:"recurrence_factor"`
|
||||
RecurrenceTimeframe StringInt `json:"recurrence_timeframe"`
|
||||
RecurrenceBasedate StringInt `json:"recurrence_basedate"`
|
||||
RecurrenceParent StringInt `json:"recurrence_parent"`
|
||||
RecurrenceChild StringInt `json:"recurrence_child"`
|
||||
}
|
||||
|
||||
// Column represents a Kanboard column.
|
||||
type Column struct {
|
||||
ID StringInt `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Position StringInt `json:"position"`
|
||||
ProjectID StringInt `json:"project_id"`
|
||||
TaskLimit StringInt `json:"task_limit"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Category represents a Kanboard category.
|
||||
type Category struct {
|
||||
ID StringInt `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProjectID StringInt `json:"project_id"`
|
||||
ColorID string `json:"color_id"`
|
||||
}
|
||||
|
||||
// Comment represents a Kanboard comment.
|
||||
type Comment struct {
|
||||
ID StringInt `json:"id"`
|
||||
TaskID StringInt `json:"task_id"`
|
||||
UserID StringInt `json:"user_id"`
|
||||
DateCreation Timestamp `json:"date_creation"`
|
||||
Content string `json:"comment"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarPath string `json:"avatar_path"`
|
||||
}
|
||||
|
||||
// TaskLink represents a link between two tasks.
|
||||
type TaskLink struct {
|
||||
ID StringInt `json:"id"`
|
||||
LinkID StringInt `json:"link_id"`
|
||||
TaskID StringInt `json:"task_id"`
|
||||
OppositeTaskID StringInt `json:"opposite_task_id"`
|
||||
Label string `json:"label"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// TaskFile represents a file attached to a task.
|
||||
type TaskFile struct {
|
||||
ID StringInt `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsImage StringBool `json:"is_image"`
|
||||
TaskID StringInt `json:"task_id"`
|
||||
DateCreation Timestamp `json:"date_creation"`
|
||||
UserID StringInt `json:"user_id"`
|
||||
Size StringInt64 `json:"size"`
|
||||
}
|
||||
|
||||
// Tag represents a Kanboard tag.
|
||||
type Tag struct {
|
||||
ID StringInt `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProjectID StringInt `json:"project_id"`
|
||||
ColorID string `json:"color_id"`
|
||||
}
|
||||
|
||||
// CreateTaskRequest is the request payload for creating a task.
|
||||
type CreateTaskRequest struct {
|
||||
Title string `json:"title"`
|
||||
ProjectID int `json:"project_id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ColumnID int `json:"column_id,omitempty"`
|
||||
OwnerID int `json:"owner_id,omitempty"`
|
||||
CreatorID int `json:"creator_id,omitempty"`
|
||||
ColorID string `json:"color_id,omitempty"`
|
||||
CategoryID int `json:"category_id,omitempty"`
|
||||
DateDue int64 `json:"date_due,omitempty"`
|
||||
Score int `json:"score,omitempty"`
|
||||
SwimlaneID int `json:"swimlane_id,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Reference string `json:"reference,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
DateStarted int64 `json:"date_started,omitempty"`
|
||||
RecurrenceStatus int `json:"recurrence_status,omitempty"`
|
||||
RecurrenceTrigger int `json:"recurrence_trigger,omitempty"`
|
||||
RecurrenceFactor int `json:"recurrence_factor,omitempty"`
|
||||
RecurrenceTimeframe int `json:"recurrence_timeframe,omitempty"`
|
||||
RecurrenceBasedate int `json:"recurrence_basedate,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateTaskRequest is the request payload for updating a task.
|
||||
// Pointer fields allow distinguishing between "not set" and "set to zero value".
|
||||
type UpdateTaskRequest struct {
|
||||
ID int `json:"id"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
ColorID *string `json:"color_id,omitempty"`
|
||||
OwnerID *int `json:"owner_id,omitempty"`
|
||||
CategoryID *int `json:"category_id,omitempty"`
|
||||
DateDue *int64 `json:"date_due,omitempty"`
|
||||
Score *int `json:"score,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Reference *string `json:"reference,omitempty"`
|
||||
DateStarted *int64 `json:"date_started,omitempty"`
|
||||
RecurrenceStatus *int `json:"recurrence_status,omitempty"`
|
||||
RecurrenceTrigger *int `json:"recurrence_trigger,omitempty"`
|
||||
RecurrenceFactor *int `json:"recurrence_factor,omitempty"`
|
||||
RecurrenceTimeframe *int `json:"recurrence_timeframe,omitempty"`
|
||||
RecurrenceBasedate *int `json:"recurrence_basedate,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
521
types_test.go
Normal file
521
types_test.go
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
package kanboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTaskStatus_Constants(t *testing.T) {
|
||||
if StatusActive != 1 {
|
||||
t.Errorf("expected StatusActive=1, got %d", StatusActive)
|
||||
}
|
||||
if StatusInactive != 0 {
|
||||
t.Errorf("expected StatusInactive=0, got %d", StatusInactive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringBool_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"string 1", `"1"`, true},
|
||||
{"string 0", `"0"`, false},
|
||||
{"string true", `"true"`, true},
|
||||
{"string false", `"false"`, false},
|
||||
{"bool true", `true`, true},
|
||||
{"bool false", `false`, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var b StringBool
|
||||
if err := json.Unmarshal([]byte(tt.input), &b); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if bool(b) != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, b)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringInt_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{"string number", `"42"`, 42},
|
||||
{"string zero", `"0"`, 0},
|
||||
{"int number", `42`, 42},
|
||||
{"int zero", `0`, 0},
|
||||
{"empty string", `""`, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var i StringInt
|
||||
if err := json.Unmarshal([]byte(tt.input), &i); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if int(i) != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringInt64_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int64
|
||||
}{
|
||||
{"string number", `"1048576"`, 1048576},
|
||||
{"string zero", `"0"`, 0},
|
||||
{"int64 number", `1048576`, 1048576},
|
||||
{"int64 zero", `0`, 0},
|
||||
{"empty string", `""`, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var i StringInt64
|
||||
if err := json.Unmarshal([]byte(tt.input), &i); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if int64(i) != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProject_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "1",
|
||||
"name": "Test Project",
|
||||
"description": "A test project",
|
||||
"is_active": "1",
|
||||
"token": "abc123",
|
||||
"last_modified": 1609459200,
|
||||
"is_public": "0",
|
||||
"is_private": "1",
|
||||
"owner_id": "42",
|
||||
"priority_default": "2"
|
||||
}`
|
||||
|
||||
var project Project
|
||||
if err := json.Unmarshal([]byte(jsonData), &project); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(project.ID) != 1 {
|
||||
t.Errorf("expected ID=1, got %d", project.ID)
|
||||
}
|
||||
if project.Name != "Test Project" {
|
||||
t.Errorf("expected Name='Test Project', got %s", project.Name)
|
||||
}
|
||||
if !bool(project.IsActive) {
|
||||
t.Error("expected IsActive=true")
|
||||
}
|
||||
if bool(project.IsPublic) {
|
||||
t.Error("expected IsPublic=false")
|
||||
}
|
||||
if !bool(project.IsPrivate) {
|
||||
t.Error("expected IsPrivate=true")
|
||||
}
|
||||
if int(project.OwnerID) != 42 {
|
||||
t.Errorf("expected OwnerID=42, got %d", project.OwnerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTask_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "42",
|
||||
"title": "Test Task",
|
||||
"description": "Task description",
|
||||
"date_creation": 1609459200,
|
||||
"date_due": 0,
|
||||
"color_id": "yellow",
|
||||
"project_id": "1",
|
||||
"column_id": "2",
|
||||
"owner_id": "5",
|
||||
"is_active": "1",
|
||||
"priority": "2",
|
||||
"category_id": "3"
|
||||
}`
|
||||
|
||||
var task Task
|
||||
if err := json.Unmarshal([]byte(jsonData), &task); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(task.ID) != 42 {
|
||||
t.Errorf("expected ID=42, got %d", task.ID)
|
||||
}
|
||||
if task.Title != "Test Task" {
|
||||
t.Errorf("expected Title='Test Task', got %s", task.Title)
|
||||
}
|
||||
if int(task.ProjectID) != 1 {
|
||||
t.Errorf("expected ProjectID=1, got %d", task.ProjectID)
|
||||
}
|
||||
if int(task.ColumnID) != 2 {
|
||||
t.Errorf("expected ColumnID=2, got %d", task.ColumnID)
|
||||
}
|
||||
if !bool(task.IsActive) {
|
||||
t.Error("expected IsActive=true")
|
||||
}
|
||||
if task.DateCreation.IsZero() {
|
||||
t.Error("expected DateCreation to be set")
|
||||
}
|
||||
if !task.DateDue.IsZero() {
|
||||
t.Error("expected DateDue to be zero")
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumn_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "1",
|
||||
"title": "Backlog",
|
||||
"position": "1",
|
||||
"project_id": "5",
|
||||
"task_limit": "10",
|
||||
"description": "Tasks to be done"
|
||||
}`
|
||||
|
||||
var column Column
|
||||
if err := json.Unmarshal([]byte(jsonData), &column); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(column.ID) != 1 {
|
||||
t.Errorf("expected ID=1, got %d", column.ID)
|
||||
}
|
||||
if column.Title != "Backlog" {
|
||||
t.Errorf("expected Title='Backlog', got %s", column.Title)
|
||||
}
|
||||
if int(column.Position) != 1 {
|
||||
t.Errorf("expected Position=1, got %d", column.Position)
|
||||
}
|
||||
if int(column.TaskLimit) != 10 {
|
||||
t.Errorf("expected TaskLimit=10, got %d", column.TaskLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategory_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "3",
|
||||
"name": "Bug",
|
||||
"project_id": "1",
|
||||
"color_id": "red"
|
||||
}`
|
||||
|
||||
var category Category
|
||||
if err := json.Unmarshal([]byte(jsonData), &category); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(category.ID) != 3 {
|
||||
t.Errorf("expected ID=3, got %d", category.ID)
|
||||
}
|
||||
if category.Name != "Bug" {
|
||||
t.Errorf("expected Name='Bug', got %s", category.Name)
|
||||
}
|
||||
if category.ColorID != "red" {
|
||||
t.Errorf("expected ColorID='red', got %s", category.ColorID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComment_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "10",
|
||||
"task_id": "42",
|
||||
"user_id": "5",
|
||||
"date_creation": 1609459200,
|
||||
"comment": "This is a comment",
|
||||
"username": "admin",
|
||||
"name": "Admin User",
|
||||
"email": "admin@example.com"
|
||||
}`
|
||||
|
||||
var comment Comment
|
||||
if err := json.Unmarshal([]byte(jsonData), &comment); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(comment.ID) != 10 {
|
||||
t.Errorf("expected ID=10, got %d", comment.ID)
|
||||
}
|
||||
if int(comment.TaskID) != 42 {
|
||||
t.Errorf("expected TaskID=42, got %d", comment.TaskID)
|
||||
}
|
||||
if comment.Content != "This is a comment" {
|
||||
t.Errorf("expected Content='This is a comment', got %s", comment.Content)
|
||||
}
|
||||
if comment.Username != "admin" {
|
||||
t.Errorf("expected Username='admin', got %s", comment.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskLink_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "1",
|
||||
"link_id": "2",
|
||||
"task_id": "42",
|
||||
"opposite_task_id": "43",
|
||||
"label": "blocks",
|
||||
"title": "Related Task"
|
||||
}`
|
||||
|
||||
var link TaskLink
|
||||
if err := json.Unmarshal([]byte(jsonData), &link); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(link.ID) != 1 {
|
||||
t.Errorf("expected ID=1, got %d", link.ID)
|
||||
}
|
||||
if int(link.TaskID) != 42 {
|
||||
t.Errorf("expected TaskID=42, got %d", link.TaskID)
|
||||
}
|
||||
if int(link.OppositeTaskID) != 43 {
|
||||
t.Errorf("expected OppositeTaskID=43, got %d", link.OppositeTaskID)
|
||||
}
|
||||
if link.Label != "blocks" {
|
||||
t.Errorf("expected Label='blocks', got %s", link.Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskFile_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "1",
|
||||
"name": "document.pdf",
|
||||
"path": "/uploads/document.pdf",
|
||||
"is_image": "0",
|
||||
"task_id": "42",
|
||||
"date_creation": 1609459200,
|
||||
"user_id": "5",
|
||||
"size": "1048576"
|
||||
}`
|
||||
|
||||
var file TaskFile
|
||||
if err := json.Unmarshal([]byte(jsonData), &file); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(file.ID) != 1 {
|
||||
t.Errorf("expected ID=1, got %d", file.ID)
|
||||
}
|
||||
if file.Name != "document.pdf" {
|
||||
t.Errorf("expected Name='document.pdf', got %s", file.Name)
|
||||
}
|
||||
if bool(file.IsImage) {
|
||||
t.Error("expected IsImage=false")
|
||||
}
|
||||
if int64(file.Size) != 1048576 {
|
||||
t.Errorf("expected Size=1048576, got %d", file.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTag_UnmarshalJSON(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "5",
|
||||
"name": "urgent",
|
||||
"project_id": "1",
|
||||
"color_id": "red"
|
||||
}`
|
||||
|
||||
var tag Tag
|
||||
if err := json.Unmarshal([]byte(jsonData), &tag); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if int(tag.ID) != 5 {
|
||||
t.Errorf("expected ID=5, got %d", tag.ID)
|
||||
}
|
||||
if tag.Name != "urgent" {
|
||||
t.Errorf("expected Name='urgent', got %s", tag.Name)
|
||||
}
|
||||
if tag.ColorID != "red" {
|
||||
t.Errorf("expected ColorID='red', got %s", tag.ColorID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskRequest_MarshalJSON(t *testing.T) {
|
||||
req := CreateTaskRequest{
|
||||
Title: "New Task",
|
||||
ProjectID: 1,
|
||||
Description: "Task description",
|
||||
ColumnID: 2,
|
||||
Priority: 3,
|
||||
Tags: []string{"urgent", "backend"},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal error: %v", err)
|
||||
}
|
||||
|
||||
var unmarshaled map[string]any
|
||||
if err := json.Unmarshal(data, &unmarshaled); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if unmarshaled["title"] != "New Task" {
|
||||
t.Errorf("expected title='New Task', got %v", unmarshaled["title"])
|
||||
}
|
||||
if unmarshaled["project_id"].(float64) != 1 {
|
||||
t.Errorf("expected project_id=1, got %v", unmarshaled["project_id"])
|
||||
}
|
||||
tags := unmarshaled["tags"].([]any)
|
||||
if len(tags) != 2 {
|
||||
t.Errorf("expected 2 tags, got %d", len(tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskRequest_OmitEmpty(t *testing.T) {
|
||||
req := CreateTaskRequest{
|
||||
Title: "Minimal Task",
|
||||
ProjectID: 1,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal error: %v", err)
|
||||
}
|
||||
|
||||
var unmarshaled map[string]any
|
||||
if err := json.Unmarshal(data, &unmarshaled); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
// Should not have omitempty fields
|
||||
if _, exists := unmarshaled["description"]; exists {
|
||||
t.Error("description should be omitted when empty")
|
||||
}
|
||||
if _, exists := unmarshaled["color_id"]; exists {
|
||||
t.Error("color_id should be omitted when empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTaskRequest_MarshalJSON(t *testing.T) {
|
||||
title := "Updated Title"
|
||||
priority := 5
|
||||
|
||||
req := UpdateTaskRequest{
|
||||
ID: 42,
|
||||
Title: &title,
|
||||
Priority: &priority,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal error: %v", err)
|
||||
}
|
||||
|
||||
var unmarshaled map[string]any
|
||||
if err := json.Unmarshal(data, &unmarshaled); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
if unmarshaled["id"].(float64) != 42 {
|
||||
t.Errorf("expected id=42, got %v", unmarshaled["id"])
|
||||
}
|
||||
if unmarshaled["title"] != "Updated Title" {
|
||||
t.Errorf("expected title='Updated Title', got %v", unmarshaled["title"])
|
||||
}
|
||||
if unmarshaled["priority"].(float64) != 5 {
|
||||
t.Errorf("expected priority=5, got %v", unmarshaled["priority"])
|
||||
}
|
||||
|
||||
// Should not have fields that weren't set
|
||||
if _, exists := unmarshaled["description"]; exists {
|
||||
t.Error("description should be omitted when nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTaskRequest_ZeroValueVsNil(t *testing.T) {
|
||||
// Test that we can distinguish between "not set" and "set to zero"
|
||||
zero := 0
|
||||
emptyString := ""
|
||||
|
||||
req := UpdateTaskRequest{
|
||||
ID: 42,
|
||||
Priority: &zero, // Explicitly set to 0
|
||||
ColorID: &emptyString, // Explicitly set to empty string
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal error: %v", err)
|
||||
}
|
||||
|
||||
var unmarshaled map[string]any
|
||||
if err := json.Unmarshal(data, &unmarshaled); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
// These should be present even though they're zero values
|
||||
if _, exists := unmarshaled["priority"]; !exists {
|
||||
t.Error("priority should be present when explicitly set to 0")
|
||||
}
|
||||
if _, exists := unmarshaled["color_id"]; !exists {
|
||||
t.Error("color_id should be present when explicitly set to empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTask_TimestampFields(t *testing.T) {
|
||||
jsonData := `{
|
||||
"id": "1",
|
||||
"title": "Test",
|
||||
"description": "",
|
||||
"date_creation": 1609459200,
|
||||
"date_modification": 1609545600,
|
||||
"date_completed": 0,
|
||||
"date_due": 1610064000,
|
||||
"color_id": "blue",
|
||||
"project_id": "1",
|
||||
"column_id": "1",
|
||||
"owner_id": "0",
|
||||
"creator_id": "1",
|
||||
"position": "1",
|
||||
"is_active": "1",
|
||||
"score": "0",
|
||||
"category_id": "0",
|
||||
"swimlane_id": "0",
|
||||
"priority": "0",
|
||||
"reference": "",
|
||||
"recurrence_status": "0",
|
||||
"recurrence_trigger": "0",
|
||||
"recurrence_factor": "0",
|
||||
"recurrence_timeframe": "0",
|
||||
"recurrence_basedate": "0",
|
||||
"recurrence_parent": "0",
|
||||
"recurrence_child": "0"
|
||||
}`
|
||||
|
||||
var task Task
|
||||
if err := json.Unmarshal([]byte(jsonData), &task); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
// Check that creation date is parsed correctly
|
||||
expectedCreation := time.Unix(1609459200, 0)
|
||||
if !task.DateCreation.Time.Equal(expectedCreation) {
|
||||
t.Errorf("expected DateCreation=%v, got %v", expectedCreation, task.DateCreation.Time)
|
||||
}
|
||||
|
||||
// Check that completed date is zero
|
||||
if !task.DateCompleted.IsZero() {
|
||||
t.Error("expected DateCompleted to be zero")
|
||||
}
|
||||
|
||||
// Check that due date is parsed
|
||||
if task.DateDue.IsZero() {
|
||||
t.Error("expected DateDue to be set")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue