feat(services): Add book matching and collection services
- Add book matching service for intelligent book deduplication - Add collection service for collection management - Add test files for book matching and collections
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TestBookMatchingService_QueryBooks tests the book query functionality
|
||||
func TestBookMatchingService_QueryBooks(t *testing.T) {
|
||||
// This is a placeholder test that would need a mock database
|
||||
// For now, we'll test the matching priority logic structure
|
||||
|
||||
t.Run("Priority 1: Bookmann UUID match", func(t *testing.T) {
|
||||
// Test that UUID matching returns highest confidence
|
||||
req := &BookQueryRequest{
|
||||
Identifiers: []string{"uuid:550e8400-e29b-41d4-a716-446655440000"},
|
||||
}
|
||||
|
||||
// Would need mock DB to test actual matching
|
||||
_ = req
|
||||
t.Log("UUID matching test placeholder - requires mock database")
|
||||
})
|
||||
|
||||
t.Run("Priority 2: OPF UUID match", func(t *testing.T) {
|
||||
req := &BookQueryRequest{
|
||||
Identifiers: []string{"opf_uuid:550e8400-e29b-41d4-a716-446655440000"},
|
||||
}
|
||||
|
||||
_ = req
|
||||
t.Log("OPF UUID matching test placeholder - requires mock database")
|
||||
})
|
||||
|
||||
t.Run("Priority 3: SHA-256 match", func(t *testing.T) {
|
||||
req := &BookQueryRequest{
|
||||
SHA256: "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
|
||||
}
|
||||
|
||||
_ = req
|
||||
t.Log("SHA-256 matching test placeholder - requires mock database")
|
||||
})
|
||||
|
||||
t.Run("Priority 7: Title + author + size match", func(t *testing.T) {
|
||||
req := &BookQueryRequest{
|
||||
Title: "Test Book",
|
||||
Author: "Test Author",
|
||||
FileSize: 1024000,
|
||||
}
|
||||
|
||||
_ = req
|
||||
t.Log("Title+author+size matching test placeholder - requires mock database")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBookMatch_ConfidenceLevels tests confidence scoring
|
||||
func TestBookMatch_ConfidenceLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
match BookMatch
|
||||
confidence float64
|
||||
method string
|
||||
}{
|
||||
{
|
||||
name: "UUID match - highest confidence",
|
||||
match: BookMatch{
|
||||
Confidence: 1.0,
|
||||
MatchMethod: "uuid_match",
|
||||
},
|
||||
confidence: 1.0,
|
||||
method: "uuid_match",
|
||||
},
|
||||
{
|
||||
name: "SHA-256 match - high confidence",
|
||||
match: BookMatch{
|
||||
Confidence: 0.9,
|
||||
MatchMethod: "sha256_match",
|
||||
},
|
||||
confidence: 0.9,
|
||||
method: "sha256_match",
|
||||
},
|
||||
{
|
||||
name: "ISBN match - medium confidence",
|
||||
match: BookMatch{
|
||||
Confidence: 0.8,
|
||||
MatchMethod: "isbn_match",
|
||||
},
|
||||
confidence: 0.8,
|
||||
method: "isbn_match",
|
||||
},
|
||||
{
|
||||
name: "Title+author+size match - lower confidence",
|
||||
match: BookMatch{
|
||||
Confidence: 0.5,
|
||||
MatchMethod: "title_author_size_match",
|
||||
},
|
||||
confidence: 0.5,
|
||||
method: "title_author_size_match",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.match.Confidence != tt.confidence {
|
||||
t.Errorf("Expected confidence %f, got %f", tt.confidence, tt.match.Confidence)
|
||||
}
|
||||
|
||||
if tt.match.MatchMethod != tt.method {
|
||||
t.Errorf("Expected method %s, got %s", tt.method, tt.match.MatchMethod)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBookQueryResponse_ActionDetermination tests action logic
|
||||
func TestBookQueryResponse_ActionDetermination(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
response BookQueryResponse
|
||||
action string
|
||||
}{
|
||||
{
|
||||
name: "No matches",
|
||||
response: BookQueryResponse{
|
||||
Matches: []BookMatch{},
|
||||
},
|
||||
action: "no_match",
|
||||
},
|
||||
{
|
||||
name: "Single high-confidence match",
|
||||
response: BookQueryResponse{
|
||||
Matches: []BookMatch{
|
||||
{Confidence: 0.9, MatchMethod: "sha256_match"},
|
||||
},
|
||||
},
|
||||
action: "auto_link",
|
||||
},
|
||||
{
|
||||
name: "Single low-confidence match",
|
||||
response: BookQueryResponse{
|
||||
Matches: []BookMatch{
|
||||
{Confidence: 0.5, MatchMethod: "title_author_size_match"},
|
||||
},
|
||||
},
|
||||
action: "multiple_matches",
|
||||
},
|
||||
{
|
||||
name: "Multiple matches",
|
||||
response: BookQueryResponse{
|
||||
Matches: []BookMatch{
|
||||
{Confidence: 0.8, MatchMethod: "isbn_match"},
|
||||
{Confidence: 0.5, MatchMethod: "title_match"},
|
||||
},
|
||||
},
|
||||
action: "multiple_matches",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Determine action
|
||||
var action string
|
||||
if len(tt.response.Matches) == 0 {
|
||||
action = "no_match"
|
||||
} else if len(tt.response.Matches) == 1 && tt.response.Matches[0].Confidence > 0.7 {
|
||||
action = "auto_link"
|
||||
} else {
|
||||
action = "multiple_matches"
|
||||
}
|
||||
|
||||
if action != tt.action {
|
||||
t.Errorf("Expected action %s, got %s", tt.action, action)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetermineAction tests the action determination logic
|
||||
func TestDetermineAction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
matches []BookMatch
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "No matches",
|
||||
matches: []BookMatch{},
|
||||
expected: "no_match",
|
||||
},
|
||||
{
|
||||
name: "Single high-confidence match",
|
||||
matches: []BookMatch{
|
||||
{Confidence: 0.9, MatchMethod: "sha256_match"},
|
||||
},
|
||||
expected: "auto_link",
|
||||
},
|
||||
{
|
||||
name: "Single low-confidence match",
|
||||
matches: []BookMatch{
|
||||
{Confidence: 0.5, MatchMethod: "title_match"},
|
||||
},
|
||||
expected: "multiple_matches",
|
||||
},
|
||||
{
|
||||
name: "Multiple matches",
|
||||
matches: []BookMatch{
|
||||
{Confidence: 0.8, MatchMethod: "isbn_match"},
|
||||
{Confidence: 0.7, MatchMethod: "asin_match"},
|
||||
},
|
||||
expected: "multiple_matches",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service := &BookMatchingService{}
|
||||
result := service.determineAction(tt.matches)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected action %s, got %s", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBookQueryRequest_Validation tests request validation
|
||||
func TestBookQueryRequest_Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req BookQueryRequest
|
||||
valid bool
|
||||
reason string
|
||||
}{
|
||||
{
|
||||
name: "Valid UUID identifier",
|
||||
req: BookQueryRequest{
|
||||
Identifiers: []string{"uuid:550e8400-e29b-41d4-a716-446655440000"},
|
||||
},
|
||||
valid: true,
|
||||
reason: "",
|
||||
},
|
||||
{
|
||||
name: "Valid ISBN identifier",
|
||||
req: BookQueryRequest{
|
||||
Identifiers: []string{"isbn:9783161484100"},
|
||||
},
|
||||
valid: true,
|
||||
reason: "",
|
||||
},
|
||||
{
|
||||
name: "Valid SHA-256",
|
||||
req: BookQueryRequest{
|
||||
SHA256: "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
|
||||
},
|
||||
valid: true,
|
||||
reason: "",
|
||||
},
|
||||
{
|
||||
name: "Valid title+author+size",
|
||||
req: BookQueryRequest{
|
||||
Title: "Test Book",
|
||||
Author: "Test Author",
|
||||
FileSize: 1024000,
|
||||
},
|
||||
valid: true,
|
||||
reason: "",
|
||||
},
|
||||
{
|
||||
name: "Empty request",
|
||||
req: BookQueryRequest{},
|
||||
valid: false,
|
||||
reason: "No identifiers provided",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Basic validation logic
|
||||
valid := len(tt.req.Identifiers) > 0 ||
|
||||
tt.req.SHA256 != "" ||
|
||||
(tt.req.Title != "" && tt.req.Author != "")
|
||||
|
||||
if valid != tt.valid {
|
||||
t.Errorf("Expected valid=%v, got valid=%v. Reason: %s", tt.valid, valid, tt.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinkBookRequest_Validation tests linking request validation
|
||||
func TestLinkBookRequest_Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req LinkBookRequest
|
||||
valid bool
|
||||
reason string
|
||||
}{
|
||||
{
|
||||
name: "Valid link request",
|
||||
req: LinkBookRequest{
|
||||
MediaItemID: uuid.New(),
|
||||
ConfidenceScore: 1.0,
|
||||
DeviceFile: struct {
|
||||
FilePath string `json:"file_path"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Title string `json:"title"`
|
||||
}{
|
||||
FilePath: "/path/to/book.epub",
|
||||
SHA256: "abc123",
|
||||
Title: "Test Book",
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
reason: "",
|
||||
},
|
||||
{
|
||||
name: "Missing media item ID",
|
||||
req: LinkBookRequest{
|
||||
ConfidenceScore: 1.0,
|
||||
},
|
||||
valid: false,
|
||||
reason: "MediaItemID is required",
|
||||
},
|
||||
{
|
||||
name: "Invalid confidence score",
|
||||
req: LinkBookRequest{
|
||||
MediaItemID: uuid.New(),
|
||||
ConfidenceScore: 2.0, // Invalid, should be 0-1
|
||||
},
|
||||
valid: false,
|
||||
reason: "Confidence score must be between 0 and 1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Check if MediaItemID is valid
|
||||
valid := tt.req.MediaItemID != uuid.Nil &&
|
||||
tt.req.ConfidenceScore >= 0 &&
|
||||
tt.req.ConfidenceScore <= 1
|
||||
|
||||
if valid != tt.valid {
|
||||
t.Errorf("Expected valid=%v, got valid=%v. Reason: %s", tt.valid, valid, tt.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
func ExampleBookMatchingService_QueryBooks() {
|
||||
service := NewBookMatchingService(nil) // Would need real DB
|
||||
|
||||
req := &BookQueryRequest{
|
||||
Identifiers: []string{
|
||||
"uuid:550e8400-e29b-41d4-a716-446655440000",
|
||||
"isbn:9783161484100",
|
||||
},
|
||||
SHA256: "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
|
||||
Title: "Test Book",
|
||||
Author: "Test Author",
|
||||
FileSize: 1024000,
|
||||
}
|
||||
|
||||
_ = service
|
||||
_ = req
|
||||
// response, err := service.QueryBooks(context.Background(), req)
|
||||
// fmt.Printf("Matches: %d, Action: %s\n", len(response.Matches), response.Action)
|
||||
}
|
||||
Reference in New Issue
Block a user