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:
parent
508c3ac6d2
commit
449cd2626c
6 changed files with 131 additions and 5 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue