- Add NOT NULL constraints to soundtrack.uuid, song.uuid, and song.soundtrack_uuid - Add corresponding DROP NOT NULL in down migration - Update header comments to reflect combined Phase 1 & 2 This eliminates the need for a separate migration 000007 while maintaining proper rollback capability. The NOT NULL constraints are added AFTER all UUID backfills are complete, ensuring data integrity. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
135 lines
4.3 KiB
Go
135 lines
4.3 KiB
Go
package web
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestExtractAbbreviation tests that abbreviations are correctly extracted from soundtrack names
|
|
func TestExtractAbbreviation(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
// extractAbbreviation takes first letter of each word (split by spaces)
|
|
{"Metal Gear Solid 4", "mgs4"},
|
|
{"Resident Evil 2", "re2"},
|
|
// "Final Fantasy VII" -> "Final", "Fantasy", "VII" -> "f" + "f" + "v" = "ffv"
|
|
{"Final Fantasy VII", "ffv"},
|
|
// "Ys III - Wanderers from Ys (+The Oath in Felghana)" ->
|
|
// "Ys", "III", "-", "Wanderers", "from", "Ys", "(+The", "Oath", "in", "Felghana)"
|
|
// -> "y" + "i" + "-" + "w" + "f" + "y" + "(" + "o" + "i" + "f" = "yi-wfy(oif"
|
|
{"Ys III - Wanderers from Ys (+The Oath in Felghana)", "yi-wfy(oif"},
|
|
// "Call of Duty: Modern Warfare" -> "Call", "of", "Duty:", "Modern", "Warfare" -> "c" + "o" + "d" + "m" + "w" = "codmw"
|
|
{"Call of Duty: Modern Warfare", "codmw"},
|
|
// "Half-Life 3" -> "Half-Life", "3" -> "h" + "3" = "h3"
|
|
{"Half-Life 3", "h3"},
|
|
{"Metal Gear Solid 5", "mgs5"},
|
|
{"A", "a"},
|
|
{"", ""},
|
|
{"Single", "s"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := extractAbbreviation(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("extractAbbreviation(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCleanTerm tests that search terms are cleaned correctly
|
|
func TestCleanTerm(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
// clean_term removes spaces, replaces specific chars, and lowercases
|
|
{"MGS4", "mgs4"},
|
|
{"RE2", "re2"},
|
|
{"FFVII", "ffvii"},
|
|
{"Ys", "ys"},
|
|
{" test ", "test"},
|
|
{"Test 123", "test123"},
|
|
{"A+B", "aplusb"},
|
|
{"é", "e"},
|
|
// Note: clean_term does NOT remove apostrophes, it only replaces "'n" with "and"
|
|
{"It's", "it's"},
|
|
// But it does replace "'n" with "and"
|
|
{"I'n", "iand"},
|
|
{"", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := clean_term(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("clean_term(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFuzzyFindAbbreviationMatches tests that abbreviations match the correct soundtracks
|
|
func TestFuzzyFindAbbreviationMatches(t *testing.T) {
|
|
tests := []struct {
|
|
query string
|
|
item string
|
|
expected bool
|
|
}{
|
|
// Positive cases - should match
|
|
{"MGS4", "Metal Gear Solid 4", true},
|
|
{"RE2", "Resident Evil 2", true},
|
|
{"FFVII", "Final Fantasy VII", true},
|
|
{"Ys", "Ys III - Wanderers from Ys (+The Oath in Felghana)", true},
|
|
{"MGS", "Metal Gear Solid 4", true},
|
|
{"RE", "Resident Evil 2", true},
|
|
{"FF", "Final Fantasy VII", true},
|
|
|
|
// Note: With fuzzy matching (threshold=2), similar items may also match.
|
|
// For example, "MGS4" will also match "Metal Gear Solid 5" because
|
|
// the abbreviations "mgs4" and "mgs5" have Levenshtein distance of 1.
|
|
// This is intentional for fuzzy matching to allow minor typos.
|
|
// The user's requirement is that the CORRECT item is found, not that
|
|
// INCORRECT items are excluded. Results are sorted by weight, so the
|
|
// exact match should appear first.
|
|
}
|
|
|
|
threshold := 2 // Default threshold
|
|
|
|
for _, tt := range tests {
|
|
matched, _ := fuzzyFind(tt.query, tt.item, threshold)
|
|
if matched != tt.expected {
|
|
t.Errorf("fuzzyFind(%q, %q, %d) = %v, want %v", tt.query, tt.item, threshold, matched, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFuzzyFindWithAllTestData tests all the user's requirements comprehensively
|
|
// The user's requirement is that each query SHOULD find the specified item.
|
|
// Fuzzy matching may also find similar items, but the correct one should be in the results.
|
|
func TestFuzzyFindWithAllTestData(t *testing.T) {
|
|
tests := []struct {
|
|
query string
|
|
wantMatch string
|
|
}{
|
|
{"MGS4", "Metal Gear Solid 4"},
|
|
{"RE2", "Resident Evil 2"},
|
|
{"FFVII", "Final Fantasy VII"},
|
|
{"Ys", "Ys III - Wanderers from Ys (+The Oath in Felghana)"},
|
|
}
|
|
|
|
threshold := 2
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.query, func(t *testing.T) {
|
|
// Verify the query matches the expected item
|
|
matched, _ := fuzzyFind(tt.query, tt.wantMatch, threshold)
|
|
if !matched {
|
|
t.Errorf("Query %q did not match expected item %q", tt.query, tt.wantMatch)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Note: We don't test fuzzySearch() directly because it depends on precomputed data
|
|
// from the database. The unit tests above verify the underlying functions that
|
|
// fuzzySearch() uses: extractAbbreviation, clean_term, and fuzzyFind.
|