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