feat: add OperationFailedError with actionable hints

The Kanboard API returns only true/false for many operations without
explaining why they failed. Added OperationFailedError type that
includes operation details and hints about possible causes.

Updated MoveTaskPosition and MoveTaskToProject to use this new error
type, providing users with actionable debugging information instead
of generic "failed to move task" messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Oliver Jakoubek 2026-01-27 11:12:10 +01:00
commit 449cd2626c
6 changed files with 131 additions and 5 deletions

View file

@ -3,6 +3,7 @@ package kanboard
import (
"errors"
"fmt"
"strings"
"testing"
)
@ -217,3 +218,71 @@ func TestErrorWrapping(t *testing.T) {
t.Errorf("unexpected error message: %s", wrappedTwice.Error())
}
}
func TestOperationFailedError(t *testing.T) {
tests := []struct {
name string
err *OperationFailedError
expectedSubstr []string
}{
{
name: "with hints",
err: &OperationFailedError{
Operation: "moveTaskPosition(task=42, column=5, project=1)",
Hints: []string{"task may not exist", "column may not belong to project"},
},
expectedSubstr: []string{
"moveTaskPosition",
"operation failed",
"possible causes",
"task may not exist",
"column may not belong to project",
},
},
{
name: "without hints",
err: &OperationFailedError{
Operation: "someOperation",
Hints: nil,
},
expectedSubstr: []string{"someOperation", "operation failed"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errMsg := tt.err.Error()
for _, substr := range tt.expectedSubstr {
if !containsSubstr(errMsg, substr) {
t.Errorf("error message %q should contain %q", errMsg, substr)
}
}
})
}
}
func TestIsOperationFailed(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{"OperationFailedError", &OperationFailedError{Operation: "test"}, true},
{"wrapped OperationFailedError", fmt.Errorf("call failed: %w", &OperationFailedError{Operation: "test"}), true},
{"ErrUnauthorized", ErrUnauthorized, false},
{"generic error", errors.New("some error"), false},
{"nil", nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsOperationFailed(tt.err); got != tt.expected {
t.Errorf("IsOperationFailed(%v) = %v, want %v", tt.err, got, tt.expected)
}
})
}
}
func containsSubstr(s, substr string) bool {
return strings.Contains(s, substr)
}