checkvist-api/filter_test.go
Oliver Jakoubek cb30b178be Add Filter builder, Archive/Unarchive, WithRepeat, and GoDoc examples
- Implement client-side Filter builder with tag, status, due date, and search filters
- Add unit tests for Filter with performance benchmark
- Add Archive/Unarchive methods to ChecklistService
- Add WithRepeat method to TaskBuilder for recurring tasks
- Create GoDoc examples for all major functionality
2026-01-14 14:39:27 +01:00

338 lines
9.1 KiB
Go

package checkvist
import (
"testing"
"time"
)
func TestFilter_WithTag(t *testing.T) {
tasks := []Task{
{ID: 1, Content: "Task 1", TagsAsText: "important, urgent"},
{ID: 2, Content: "Task 2", TagsAsText: "urgent"},
{ID: 3, Content: "Task 3", TagsAsText: ""},
}
tests := []struct {
name string
tag string
expected []int
}{
{"filter by important", "important", []int{1}},
{"filter by urgent", "urgent", []int{1, 2}},
{"filter by nonexistent", "nonexistent", []int{}},
{"case insensitive", "IMPORTANT", []int{1}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewFilter(tasks).WithTag(tt.tag).Apply()
if len(result) != len(tt.expected) {
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
return
}
for i, task := range result {
if task.ID != tt.expected[i] {
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
}
}
})
}
}
func TestFilter_WithMultipleTags(t *testing.T) {
tasks := []Task{
{ID: 1, Content: "Task 1", TagsAsText: "important, urgent, work"},
{ID: 2, Content: "Task 2", TagsAsText: "urgent, work"},
{ID: 3, Content: "Task 3", TagsAsText: "important"},
}
tests := []struct {
name string
tags []string
expected []int
}{
{"single tag", []string{"urgent"}, []int{1, 2}},
{"two tags AND", []string{"urgent", "work"}, []int{1, 2}},
{"three tags AND", []string{"important", "urgent", "work"}, []int{1}},
{"no match", []string{"important", "urgent", "missing"}, []int{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewFilter(tasks).WithTags(tt.tags...).Apply()
if len(result) != len(tt.expected) {
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
return
}
for i, task := range result {
if task.ID != tt.expected[i] {
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
}
}
})
}
}
func TestFilter_WithStatus(t *testing.T) {
tasks := []Task{
{ID: 1, Content: "Open task", Status: StatusOpen},
{ID: 2, Content: "Closed task", Status: StatusClosed},
{ID: 3, Content: "Invalidated task", Status: StatusInvalidated},
{ID: 4, Content: "Another open", Status: StatusOpen},
}
tests := []struct {
name string
status TaskStatus
expected []int
}{
{"open tasks", StatusOpen, []int{1, 4}},
{"closed tasks", StatusClosed, []int{2}},
{"invalidated tasks", StatusInvalidated, []int{3}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewFilter(tasks).WithStatus(tt.status).Apply()
if len(result) != len(tt.expected) {
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
return
}
for i, task := range result {
if task.ID != tt.expected[i] {
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
}
}
})
}
}
func TestFilter_WithDueBefore(t *testing.T) {
now := time.Now()
yesterday := now.AddDate(0, 0, -1)
tomorrow := now.AddDate(0, 0, 1)
nextWeek := now.AddDate(0, 0, 7)
tasks := []Task{
{ID: 1, Content: "Yesterday", DueDate: &yesterday},
{ID: 2, Content: "Tomorrow", DueDate: &tomorrow},
{ID: 3, Content: "Next week", DueDate: &nextWeek},
{ID: 4, Content: "No due date", DueDate: nil},
}
result := NewFilter(tasks).WithDueBefore(now).Apply()
if len(result) != 1 {
t.Errorf("expected 1 task, got %d", len(result))
return
}
if result[0].ID != 1 {
t.Errorf("expected task ID 1, got %d", result[0].ID)
}
}
func TestFilter_WithDueAfter(t *testing.T) {
now := time.Now()
yesterday := now.AddDate(0, 0, -1)
tomorrow := now.AddDate(0, 0, 1)
nextWeek := now.AddDate(0, 0, 7)
tasks := []Task{
{ID: 1, Content: "Yesterday", DueDate: &yesterday},
{ID: 2, Content: "Tomorrow", DueDate: &tomorrow},
{ID: 3, Content: "Next week", DueDate: &nextWeek},
{ID: 4, Content: "No due date", DueDate: nil},
}
result := NewFilter(tasks).WithDueAfter(now).Apply()
if len(result) != 2 {
t.Errorf("expected 2 tasks, got %d", len(result))
return
}
if result[0].ID != 2 || result[1].ID != 3 {
t.Errorf("expected task IDs [2, 3], got [%d, %d]", result[0].ID, result[1].ID)
}
}
func TestFilter_WithDueOn(t *testing.T) {
today := time.Now().Truncate(24 * time.Hour)
todayMorning := today.Add(9 * time.Hour)
todayEvening := today.Add(18 * time.Hour)
tomorrow := today.AddDate(0, 0, 1)
tasks := []Task{
{ID: 1, Content: "Today morning", DueDate: &todayMorning},
{ID: 2, Content: "Today evening", DueDate: &todayEvening},
{ID: 3, Content: "Tomorrow", DueDate: &tomorrow},
{ID: 4, Content: "No due date", DueDate: nil},
}
result := NewFilter(tasks).WithDueOn(today).Apply()
if len(result) != 2 {
t.Errorf("expected 2 tasks, got %d", len(result))
return
}
if result[0].ID != 1 || result[1].ID != 2 {
t.Errorf("expected task IDs [1, 2], got [%d, %d]", result[0].ID, result[1].ID)
}
}
func TestFilter_WithOverdue(t *testing.T) {
today := time.Now().Truncate(24 * time.Hour)
yesterday := today.AddDate(0, 0, -1)
tomorrow := today.AddDate(0, 0, 1)
tasks := []Task{
{ID: 1, Content: "Overdue open", DueDate: &yesterday, Status: StatusOpen},
{ID: 2, Content: "Overdue closed", DueDate: &yesterday, Status: StatusClosed},
{ID: 3, Content: "Future open", DueDate: &tomorrow, Status: StatusOpen},
{ID: 4, Content: "No due date", DueDate: nil, Status: StatusOpen},
}
result := NewFilter(tasks).WithOverdue().Apply()
if len(result) != 1 {
t.Errorf("expected 1 task, got %d", len(result))
return
}
if result[0].ID != 1 {
t.Errorf("expected task ID 1, got %d", result[0].ID)
}
}
func TestFilter_WithSearch(t *testing.T) {
tasks := []Task{
{ID: 1, Content: "Buy groceries"},
{ID: 2, Content: "Call the doctor"},
{ID: 3, Content: "Review PR for grocery app"},
{ID: 4, Content: "Send email"},
}
tests := []struct {
name string
query string
expected []int
}{
{"exact match", "groceries", []int{1}},
{"partial match", "grocer", []int{1, 3}},
{"case insensitive", "CALL", []int{2}},
{"no match", "xyz", []int{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewFilter(tasks).WithSearch(tt.query).Apply()
if len(result) != len(tt.expected) {
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
return
}
for i, task := range result {
if task.ID != tt.expected[i] {
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
}
}
})
}
}
func TestFilter_Combined(t *testing.T) {
today := time.Now().Truncate(24 * time.Hour)
yesterday := today.AddDate(0, 0, -1)
tomorrow := today.AddDate(0, 0, 1)
tasks := []Task{
{ID: 1, Content: "Important task", TagsAsText: "important", Status: StatusOpen, DueDate: &yesterday},
{ID: 2, Content: "Important closed", TagsAsText: "important", Status: StatusClosed, DueDate: &yesterday},
{ID: 3, Content: "Regular task", TagsAsText: "regular", Status: StatusOpen, DueDate: &yesterday},
{ID: 4, Content: "Important future", TagsAsText: "important", Status: StatusOpen, DueDate: &tomorrow},
}
// Filter: important AND open AND due before today
result := NewFilter(tasks).
WithTag("important").
WithStatus(StatusOpen).
WithDueBefore(today).
Apply()
if len(result) != 1 {
t.Errorf("expected 1 task, got %d", len(result))
return
}
if result[0].ID != 1 {
t.Errorf("expected task ID 1, got %d", result[0].ID)
}
}
func TestFilter_EmptyFilters(t *testing.T) {
tasks := []Task{
{ID: 1, Content: "Task 1"},
{ID: 2, Content: "Task 2"},
}
result := NewFilter(tasks).Apply()
if len(result) != 2 {
t.Errorf("expected 2 tasks, got %d", len(result))
}
}
func TestFilter_EmptyTasks(t *testing.T) {
result := NewFilter([]Task{}).WithTag("test").Apply()
if len(result) != 0 {
t.Errorf("expected 0 tasks, got %d", len(result))
}
}
func TestFilter_Performance_1000Tasks(t *testing.T) {
// Create 1000 tasks with various attributes
tasks := make([]Task, 1000)
now := time.Now()
for i := 0; i < 1000; i++ {
due := now.AddDate(0, 0, i-500) // -500 to +499 days from now
tasks[i] = Task{
ID: i + 1,
Content: "Task with some content to search through",
TagsAsText: "tag1, tag2, tag3",
Status: TaskStatus(i % 3),
DueDate: &due,
}
}
// Mark important tasks
for i := 0; i < 100; i++ {
tasks[i].TagsAsText = "important, urgent"
}
start := time.Now()
// Run a complex filter
result := NewFilter(tasks).
WithTag("important").
WithStatus(StatusOpen).
WithDueBefore(now).
WithSearch("content").
Apply()
elapsed := time.Since(start)
// Performance target: <10ms for 1000 tasks
if elapsed > 10*time.Millisecond {
t.Errorf("filter took %v, expected <10ms", elapsed)
}
// Verify we got some results
if len(result) == 0 {
t.Log("No matching tasks found, but performance test passed")
}
}
func TestFilter_TagsMap(t *testing.T) {
// Test that Tags map is also checked
tasks := []Task{
{ID: 1, Content: "Task 1", Tags: Tags{"important": true}, TagsAsText: ""},
{ID: 2, Content: "Task 2", Tags: nil, TagsAsText: "important"},
}
result := NewFilter(tasks).WithTag("important").Apply()
if len(result) != 2 {
t.Errorf("expected 2 tasks, got %d", len(result))
}
}