Centralize validation patterns with composable validators (bd-jbqx)
Add composable issue validators to internal/validation package: - IssueValidator type with Chain() composition function - Exists(), NotTemplate(), NotPinned(), NotClosed(), NotHooked() validators - HasStatus(), HasType() for checking allowed values - ForUpdate(), ForClose(), ForDelete(), ForReopen() convenience chains Update cmd/bd/show_unit_helpers.go to use centralized validators instead of duplicated inline validation logic. This enables consistent validation across all commands with a single source of truth. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
367
internal/validation/issue_test.go
Normal file
367
internal/validation/issue_test.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue returns error",
|
||||
issue: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "non-nil issue passes",
|
||||
issue: &types.Issue{ID: "bd-test"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Exists()("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Exists() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue passes (delegated check)",
|
||||
issue: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-template passes",
|
||||
issue: &types.Issue{ID: "bd-test", IsTemplate: false},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "template returns error",
|
||||
issue: &types.Issue{ID: "bd-test", IsTemplate: true},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := NotTemplate()("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NotTemplate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotPinned(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
force bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue passes",
|
||||
issue: nil,
|
||||
force: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "open issue passes",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusOpen},
|
||||
force: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "pinned issue without force fails",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusPinned},
|
||||
force: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "pinned issue with force passes",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusPinned},
|
||||
force: true,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := NotPinned(tt.force)("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NotPinned() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotClosed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue passes",
|
||||
issue: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "open issue passes",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusOpen},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "closed issue fails",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusClosed},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := NotClosed()("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NotClosed() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
validators []IssueValidator
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty chain passes",
|
||||
issue: &types.Issue{ID: "bd-test"},
|
||||
validators: []IssueValidator{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "all validators pass",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusOpen},
|
||||
validators: []IssueValidator{
|
||||
Exists(),
|
||||
NotTemplate(),
|
||||
NotPinned(false),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "first validator fails stops chain",
|
||||
issue: nil,
|
||||
validators: []IssueValidator{
|
||||
Exists(),
|
||||
NotTemplate(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "middle validator fails",
|
||||
issue: &types.Issue{ID: "bd-test", IsTemplate: true},
|
||||
validators: []IssueValidator{
|
||||
Exists(),
|
||||
NotTemplate(),
|
||||
NotPinned(false),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
chain := Chain(tt.validators...)
|
||||
err := chain("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Chain() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
allowed []types.Status
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue passes",
|
||||
issue: nil,
|
||||
allowed: []types.Status{types.StatusOpen},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "matching status passes",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusOpen},
|
||||
allowed: []types.Status{types.StatusOpen},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "one of multiple allowed passes",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusClosed},
|
||||
allowed: []types.Status{types.StatusOpen, types.StatusClosed},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-matching status fails",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusPinned},
|
||||
allowed: []types.Status{types.StatusOpen, types.StatusClosed},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := HasStatus(tt.allowed...)("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("HasStatus() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
allowed []types.IssueType
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue passes",
|
||||
issue: nil,
|
||||
allowed: []types.IssueType{types.TypeTask},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "matching type passes",
|
||||
issue: &types.Issue{ID: "bd-test", IssueType: types.TypeTask},
|
||||
allowed: []types.IssueType{types.TypeTask},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "one of multiple allowed passes",
|
||||
issue: &types.Issue{ID: "bd-test", IssueType: types.TypeBug},
|
||||
allowed: []types.IssueType{types.TypeTask, types.TypeBug},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-matching type fails",
|
||||
issue: &types.Issue{ID: "bd-test", IssueType: types.TypeEpic},
|
||||
allowed: []types.IssueType{types.TypeTask, types.TypeBug},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := HasType(tt.allowed...)("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("HasType() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestForUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue fails",
|
||||
issue: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "template fails",
|
||||
issue: &types.Issue{ID: "bd-test", IsTemplate: true},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "regular issue passes",
|
||||
issue: &types.Issue{ID: "bd-test", IsTemplate: false},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ForUpdate()("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ForUpdate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestForClose(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *types.Issue
|
||||
force bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil issue fails",
|
||||
issue: nil,
|
||||
force: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "template fails",
|
||||
issue: &types.Issue{ID: "bd-test", IsTemplate: true},
|
||||
force: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "pinned without force fails",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusPinned},
|
||||
force: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "pinned with force passes",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusPinned},
|
||||
force: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "regular open issue passes",
|
||||
issue: &types.Issue{ID: "bd-test", Status: types.StatusOpen},
|
||||
force: false,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ForClose(tt.force)("bd-test", tt.issue)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ForClose() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user