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-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-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-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-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-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"}
|
{"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