Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions internal/domain/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package domain

import "testing"

func TestReferenceErrorNotFound(t *testing.T) {
err := &ReferenceError{
Kind: "task",
Reference: "alpha",
Matches: nil,
}
if got, want := err.Error(), `task "alpha" not found`; got != want {
t.Fatalf("Error() = %q, want %q", got, want)
}
}

func TestReferenceErrorAmbiguous(t *testing.T) {
err := &ReferenceError{
Kind: "project",
Reference: "work",
Matches: []string{"work-1", "work-2"},
}
if got, want := err.Error(), `project "work" is ambiguous: [work-1 work-2]`; got != want {
t.Fatalf("Error() = %q, want %q", got, want)
}
}
47 changes: 47 additions & 0 deletions internal/domain/filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package domain

import "testing"

func TestTaskFilterStatusCodes(t *testing.T) {
f := TaskFilter{Statuses: []TaskStatus{StatusOpen, StatusCompleted}}
got := f.StatusCodes()
want := []int{0, 2}
if len(got) != len(want) {
t.Fatalf("StatusCodes() = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("StatusCodes()[%d] = %d, want %d", i, got[i], want[i])
}
}
}

func TestTaskFilterStatusCodesEmpty(t *testing.T) {
f := TaskFilter{}
got := f.StatusCodes()
if len(got) != 0 {
t.Fatalf("StatusCodes() = %v, want empty", got)
}
}

func TestTaskFilterPriorityCodes(t *testing.T) {
f := TaskFilter{Priorities: []Priority{PriorityNone, PriorityHigh}}
got := f.PriorityCodes()
want := []int{0, 5}
if len(got) != len(want) {
t.Fatalf("PriorityCodes() = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("PriorityCodes()[%d] = %d, want %d", i, got[i], want[i])
}
}
}

func TestTaskFilterPriorityCodesEmpty(t *testing.T) {
f := TaskFilter{}
got := f.PriorityCodes()
if len(got) != 0 {
t.Fatalf("PriorityCodes() = %v, want empty", got)
}
}
35 changes: 35 additions & 0 deletions internal/domain/focus_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package domain

import "testing"

func TestFocusModeString(t *testing.T) {
cases := []struct {
mode FocusMode
want string
}{
{FocusModeTimer, "timer"},
{FocusModePomodoro, "pomodoro"},
{FocusMode(99), "unknown"},
}
for _, tc := range cases {
if got := tc.mode.String(); got != tc.want {
t.Fatalf("FocusMode(%d).String() = %q, want %q", tc.mode, got, tc.want)
}
}
}

func TestFocusStatusString(t *testing.T) {
cases := []struct {
status FocusStatus
want string
}{
{FocusStatusActive, "active"},
{FocusStatusCompleted, "completed"},
{FocusStatus(99), "unknown"},
}
for _, tc := range cases {
if got := tc.status.String(); got != tc.want {
t.Fatalf("FocusStatus(%d).String() = %q, want %q", tc.status, got, tc.want)
}
}
}
19 changes: 19 additions & 0 deletions internal/domain/habit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package domain

import "testing"

func TestHabitStatusString(t *testing.T) {
cases := []struct {
status HabitStatus
want string
}{
{HabitStatusActive, "active"},
{HabitStatusArchived, "archived"},
{HabitStatus(99), "unknown"},
}
for _, tc := range cases {
if got := tc.status.String(); got != tc.want {
t.Fatalf("HabitStatus(%d).String() = %q, want %q", tc.status, got, tc.want)
}
}
}
32 changes: 24 additions & 8 deletions internal/domain/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,35 @@ package domain
import "testing"

func TestPriorityString(t *testing.T) {
if got := PriorityHigh.String(); got != "high" {
t.Fatalf("PriorityHigh.String() = %q, want high", got)
cases := []struct {
p Priority
want string
}{
{PriorityNone, "none"},
{PriorityLow, "low"},
{PriorityMedium, "medium"},
{PriorityHigh, "high"},
{Priority(99), "unknown"},
}
if got := Priority(99).String(); got != "unknown" {
t.Fatalf("Priority(99).String() = %q, want unknown", got)
for _, tc := range cases {
if got := tc.p.String(); got != tc.want {
t.Fatalf("Priority(%d).String() = %q, want %q", tc.p, got, tc.want)
}
}
}

func TestTaskStatusString(t *testing.T) {
if got := StatusOpen.String(); got != "open" {
t.Fatalf("StatusOpen.String() = %q, want open", got)
cases := []struct {
s TaskStatus
want string
}{
{StatusOpen, "open"},
{StatusCompleted, "completed"},
{TaskStatus(99), "unknown"},
}
if got := StatusCompleted.String(); got != "completed" {
t.Fatalf("StatusCompleted.String() = %q, want completed", got)
for _, tc := range cases {
if got := tc.s.String(); got != tc.want {
t.Fatalf("TaskStatus(%d).String() = %q, want %q", tc.s, got, tc.want)
}
}
}
14 changes: 14 additions & 0 deletions internal/domain/timeparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ func TestParseUserTimeSupportsDateOnly(t *testing.T) {
t.Fatalf("got = %s, want midnight local date", got.Format("2006-01-02T15:04"))
}
}

func TestParseUserTimeSupportsRFC3339(t *testing.T) {
loc := time.FixedZone("UTC+8", 8*60*60)
got, err := ParseUserTime("2026-04-10T14:30:00+08:00", loc)
if err != nil {
t.Fatalf("ParseUserTime() error = %v", err)
}
if got.Year() != 2026 || got.Month() != 4 || got.Day() != 10 {
t.Fatalf("got = %s, want 2026-04-10", got.Format("2006-01-02"))
}
if got.Hour() != 14 || got.Minute() != 30 {
t.Fatalf("got = %s, want 14:30", got.Format("15:04"))
}
}
147 changes: 147 additions & 0 deletions internal/output/table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package output

import (
"bytes"
"strings"
"testing"
"time"

"github.com/jeely/ticktick-cli/internal/domain"
)

func TestPrintProjectsTable(t *testing.T) {
var buf bytes.Buffer
projects := []domain.Project{
{ID: "p1", Name: "Alpha", Closed: false, Kind: "TASK"},
{ID: "p2", Name: "Beta", Closed: true, Kind: "NOTE"},
}
if err := PrintProjectsTable(&buf, projects); err != nil {
t.Fatalf("PrintProjectsTable() error = %v", err)
}
out := buf.String()
if !strings.Contains(out, "Alpha") {
t.Fatalf("output missing Alpha: %s", out)
}
if !strings.Contains(out, "Beta") {
t.Fatalf("output missing Beta: %s", out)
}
}

func TestPrintProjectsTableEmpty(t *testing.T) {
var buf bytes.Buffer
if err := PrintProjectsTable(&buf, []domain.Project{}); err != nil {
t.Fatalf("PrintProjectsTable() error = %v", err)
}
out := buf.String()
if !strings.Contains(out, "ID") {
t.Fatalf("output missing header: %s", out)
}
}

func TestPrintTasksTable(t *testing.T) {
var buf bytes.Buffer
due := time.Date(2026, 5, 13, 10, 0, 0, 0, time.UTC)
tasks := []domain.Task{
{ID: "t1", Title: "Task 1", ProjectID: "p1", DueDate: &due, Priority: domain.PriorityHigh, Status: domain.StatusOpen},
}
names := map[string]string{"p1": "Project A"}
if err := PrintTasksTable(&buf, tasks, names); err != nil {
t.Fatalf("PrintTasksTable() error = %v", err)
}
out := buf.String()
if !strings.Contains(out, "Task 1") {
t.Fatalf("output missing Task 1: %s", out)
}
if !strings.Contains(out, "Project A") {
t.Fatalf("output missing project name: %s", out)
}
}

func TestPrintTasksTableUnknownProject(t *testing.T) {
var buf bytes.Buffer
tasks := []domain.Task{
{ID: "t1", Title: "Task 1", ProjectID: "p-unknown", Priority: domain.PriorityNone, Status: domain.StatusOpen},
}
if err := PrintTasksTable(&buf, tasks, map[string]string{}); err != nil {
t.Fatalf("PrintTasksTable() error = %v", err)
}
out := buf.String()
if !strings.Contains(out, "Task 1") {
t.Fatalf("output missing Task 1: %s", out)
}
}

func TestPrintFocusTable(t *testing.T) {
var buf bytes.Buffer
start := time.Date(2026, 5, 13, 9, 0, 0, 0, time.UTC)
focuses := []domain.Focus{
{ID: "f1", Title: "Focus 1", ProjectID: "p1", StartDate: &start, Mode: domain.FocusModeTimer, Status: domain.FocusStatusActive},
}
names := map[string]string{"p1": "Project A"}
if err := PrintFocusTable(&buf, focuses, names); err != nil {
t.Fatalf("PrintFocusTable() error = %v", err)
}
out := buf.String()
if !strings.Contains(out, "Focus 1") {
t.Fatalf("output missing Focus 1: %s", out)
}
}

func TestPrintHabitsTable(t *testing.T) {
var buf bytes.Buffer
habits := []domain.Habit{
{ID: "h1", Name: "Read", Goal: 30, CurrentStreak: 5, Status: domain.HabitStatusActive},
}
if err := PrintHabitsTable(&buf, habits); err != nil {
t.Fatalf("PrintHabitsTable() error = %v", err)
}
out := buf.String()
if !strings.Contains(out, "Read") {
t.Fatalf("output missing Read: %s", out)
}
if !strings.Contains(out, "active") {
t.Fatalf("output missing status: %s", out)
}
}

func TestPrintCheckinsTable(t *testing.T) {
var buf bytes.Buffer
checkins := []domain.HabitCheckin{
{HabitID: "h1", Year: 2026, Stamp: 20260513, Value: 1, Goal: 1},
}
if err := PrintCheckinsTable(&buf, checkins); err != nil {
t.Fatalf("PrintCheckinsTable() error = %v", err)
}
out := buf.String()
if !strings.Contains(out, "2026-05-13") {
t.Fatalf("output missing formatted date: %s", out)
}
}

func TestFormatCheckinStampValid(t *testing.T) {
if got, want := formatCheckinStamp(20260513), "2026-05-13"; got != want {
t.Fatalf("formatCheckinStamp(20260513) = %q, want %q", got, want)
}
}

func TestFormatCheckinStampInvalidLength(t *testing.T) {
if got, want := formatCheckinStamp(123), "123"; got != want {
t.Fatalf("formatCheckinStamp(123) = %q, want %q", got, want)
}
}

func TestFormatTimeNil(t *testing.T) {
if got, want := FormatTime(nil), ""; got != want {
t.Fatalf("FormatTime(nil) = %q, want %q", got, want)
}
}

func TestFormatTimeValue(t *testing.T) {
loc := time.FixedZone("UTC+8", 8*60*60)
v := time.Date(2026, 5, 13, 10, 30, 0, 0, loc)
got := FormatTime(&v)
// Local time depends on test environment, just verify non-empty
if got == "" {
t.Fatal("FormatTime(value) = empty, want non-empty")
}
}
7 changes: 7 additions & 0 deletions internal/ticktick/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ func TestDoJSONReturnsRemoteError(t *testing.T) {
}
}

func TestRemoteErrorString(t *testing.T) {
err := &RemoteError{StatusCode: 404, Body: "not found"}
if got, want := err.Error(), `ticktick api error: status=404 body="not found"`; got != want {
t.Fatalf("Error() = %q, want %q", got, want)
}
}

func TestDoJSONEncodesRequestJSONAndTreatsEmptySuccessAsSuccess(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.Header.Get("Content-Type"), "application/json"; got != want {
Expand Down
Loading
Loading