Backend changes: - Update import paths: bookmann/internal → bookhoard/internal - Rename struct fields: BookmannUUID → BookhoardUUID - Update handler function names: mapContentIdToBookmannUUID → mapContentIdToBookhoardUUID - Update HTTP response headers: X-Bookmann-* → X-Bookhoard-* - Update service and middleware references - Update main.go imports and references This is part 2 of the project rename to Bookhoard.
416 lines
11 KiB
Go
416 lines
11 KiB
Go
package handlers
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"bookhoard/internal/database"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// Test collection rule matching with various operators
|
|
|
|
func TestEvaluateRule_Equals_StringMatch(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "genre", "equals", "Science Fiction")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_Equals_NoMatch(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Fantasy", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "genre", "equals", "Science Fiction")
|
|
assert.False(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_NotEquals_Matches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Fantasy", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "genre", "not_equals", "Science Fiction")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_NotEquals_NoMatch(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "genre", "not_equals", "Science Fiction")
|
|
assert.False(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_Contains_Matches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "author", "contains", "asimov")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_Contains_NoMatch(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "author", "contains", "clarke")
|
|
assert.False(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_NotContains_Matches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "author", "not_contains", "clarke")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_StartsWith_Matches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Series: pgtype.Text{String: "Foundation", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "series", "starts_with", "Foun")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_StartsWith_NoMatch(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Series: pgtype.Text{String: "Foundation", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "series", "starts_with", "Robots")
|
|
assert.False(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_EndsWith_Matches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Series: pgtype.Text{String: "Foundation", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "series", "ends_with", "tion")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_GreaterThan_Matches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "copyright_year", "greater_than", "2000")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_GreaterThan_NoMatch(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
CopyrightYear: pgtype.Int4{Int32: 1999, Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "copyright_year", "greater_than", "2000")
|
|
assert.False(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_LessThan_Matches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
CopyrightYear: pgtype.Int4{Int32: 1999, Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "copyright_year", "less_than", "2000")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_LessThan_NoMatch(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "copyright_year", "less_than", "2000")
|
|
assert.False(t, result)
|
|
}
|
|
|
|
func TestEvaluateRule_NullField(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{Valid: false}, // NULL
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "genre", "equals", "Science Fiction")
|
|
assert.False(t, result, "Should return false for NULL fields")
|
|
}
|
|
|
|
func TestCompareValues_CaseInsensitive(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
|
|
result := handler.compareValues("Science Fiction", "contains", "science fiction")
|
|
assert.True(t, result, "Contains should be case-insensitive")
|
|
|
|
result = handler.compareValues("Science Fiction", "contains", "SCIENCE FICTION")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
func TestCompareValues_EmptyString(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
|
|
result := handler.compareValues("", "contains", "test")
|
|
assert.False(t, result, "Empty string should not contain anything")
|
|
|
|
result = handler.compareValues("test", "contains", "")
|
|
assert.True(t, result, "Non-empty string contains empty string")
|
|
}
|
|
|
|
func TestCompareValues_NumericEdgeCases(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
|
|
result := handler.compareValues("0", "greater_than", "-1")
|
|
assert.True(t, result)
|
|
|
|
result = handler.compareValues("9999", "less_than", "10000")
|
|
assert.True(t, result)
|
|
}
|
|
|
|
// Test rule checking with multiple rules
|
|
|
|
func TestCheckRulesAgainstBook_MatchesFirstRule(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
|
|
}
|
|
|
|
rules := []map[string]interface{}{
|
|
{
|
|
"field": "genre",
|
|
"operator": "equals",
|
|
"value": "Science Fiction",
|
|
},
|
|
{
|
|
"field": "author",
|
|
"operator": "contains",
|
|
"value": "Asimov",
|
|
},
|
|
}
|
|
|
|
reason := handler.checkRulesAgainstBook(item, rules)
|
|
assert.NotEmpty(t, reason, "Should match at least one rule")
|
|
assert.Contains(t, reason, "genre")
|
|
}
|
|
|
|
func TestCheckRulesAgainstBook_MatchesSecondRule(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Fantasy", Valid: true},
|
|
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
|
|
CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true},
|
|
}
|
|
|
|
rules := []map[string]interface{}{
|
|
{
|
|
"field": "genre",
|
|
"operator": "equals",
|
|
"value": "Science Fiction",
|
|
},
|
|
{
|
|
"field": "author",
|
|
"operator": "contains",
|
|
"value": "Asimov",
|
|
},
|
|
}
|
|
|
|
reason := handler.checkRulesAgainstBook(item, rules)
|
|
assert.NotEmpty(t, reason, "Should match second rule")
|
|
assert.Contains(t, reason, "author")
|
|
}
|
|
|
|
func TestCheckRulesAgainstBook_NoMatches(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Mystery", Valid: true},
|
|
Author: pgtype.Text{String: "Agatha Christie", Valid: true},
|
|
CopyrightYear: pgtype.Int4{Int32: 1950, Valid: true},
|
|
}
|
|
|
|
rules := []map[string]interface{}{
|
|
{
|
|
"field": "genre",
|
|
"operator": "equals",
|
|
"value": "Science Fiction",
|
|
},
|
|
{
|
|
"field": "author",
|
|
"operator": "contains",
|
|
"value": "Asimov",
|
|
},
|
|
}
|
|
|
|
reason := handler.checkRulesAgainstBook(item, rules)
|
|
assert.Empty(t, reason, "Should not match any rules")
|
|
}
|
|
|
|
func TestCheckRulesAgainstBook_EmptyRules(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
|
|
}
|
|
|
|
rules := []map[string]interface{}{}
|
|
|
|
reason := handler.checkRulesAgainstBook(item, rules)
|
|
assert.Empty(t, reason, "Empty rules should return empty reason")
|
|
}
|
|
|
|
// Test complex rule scenarios
|
|
|
|
func TestComplexRule_MultipleConditions(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
|
|
// Book: "Foundation" by Isaac Asimov, Sci-Fi, 1951
|
|
item := database.ListMediaItemsRow{
|
|
Title: "Foundation",
|
|
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
|
|
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
|
|
CopyrightYear: pgtype.Int4{Int32: 1951, Valid: true},
|
|
Series: pgtype.Text{String: "Foundation", Valid: true},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
field string
|
|
operator string
|
|
value string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Match genre exactly",
|
|
field: "genre",
|
|
operator: "equals",
|
|
value: "Science Fiction",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Match author substring",
|
|
field: "author",
|
|
operator: "contains",
|
|
value: "asimov",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Match year greater than",
|
|
field: "copyright_year",
|
|
operator: "greater_than",
|
|
value: "1950",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Match year less than",
|
|
field: "copyright_year",
|
|
operator: "less_than",
|
|
value: "2000",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Match series starts with",
|
|
field: "series",
|
|
operator: "starts_with",
|
|
value: "Found",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Match series ends with",
|
|
field: "series",
|
|
operator: "ends_with",
|
|
value: "tion",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "No match for genre",
|
|
field: "genre",
|
|
operator: "equals",
|
|
value: "Fantasy",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "No match for author",
|
|
field: "author",
|
|
operator: "contains",
|
|
value: "Tolkien",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := handler.evaluateRule(item, tt.field, tt.operator, tt.value)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test edge cases and error handling
|
|
|
|
func TestEvaluateRule_InvalidOperator(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "genre", "invalid_operator", "Science Fiction")
|
|
assert.False(t, result, "Invalid operator should return false")
|
|
}
|
|
|
|
func TestEvaluateRule_NonExistentField(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
item := database.ListMediaItemsRow{
|
|
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
|
|
}
|
|
|
|
result := handler.evaluateRule(item, "nonexistent_field", "equals", "value")
|
|
assert.False(t, result, "Non-existent field should return false")
|
|
}
|
|
|
|
func TestCompareValues_InvalidOperator(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
|
|
result := handler.compareValues("test", "invalid_op", "value")
|
|
assert.False(t, result, "Invalid comparison operator should return false")
|
|
}
|
|
|
|
func TestCompareValues_NumericConversion(t *testing.T) {
|
|
handler := &CollectionHandler{}
|
|
|
|
// Valid numeric strings
|
|
result := handler.compareValues("100", "greater_than", "99")
|
|
assert.True(t, result)
|
|
|
|
result = handler.compareValues("99", "less_than", "100")
|
|
assert.True(t, result)
|
|
|
|
// Invalid numeric strings (should return false)
|
|
result = handler.compareValues("abc", "greater_than", "100")
|
|
assert.False(t, result, "Non-numeric string should not compare as greater than")
|
|
|
|
result = handler.compareValues("100", "greater_than", "xyz")
|
|
assert.False(t, result, "Non-numeric value should not allow comparison")
|
|
}
|