package rating import ( "bytes" "os" "testing" ) // buildMinimalMP3 returns a minimal valid MP3: a 10-byte ID3v2.3 header with // no frames (tag size = 0) followed by 4096 bytes of fake audio data (0xFF 0xFB // sync word repeated, which is close enough for our purposes). func buildMinimalMP3() []byte { var buf bytes.Buffer // ID3v2.3 header: "ID3" + version 2.3.0 + flags 0 + synchsafe size 0 buf.Write([]byte{'I', 'D', '3', 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) // Fake audio data audio := make([]byte, 4096) for i := range audio { audio[i] = byte(i & 0xFF) } buf.Write(audio) return buf.Bytes() } func writeTempMP3(t *testing.T, data []byte) string { t.Helper() f, err := os.CreateTemp(t.TempDir(), "test-*.mp3") if err != nil { t.Fatalf("create temp: %v", err) } if _, err := f.Write(data); err != nil { t.Fatalf("write temp: %v", err) } f.Close() return f.Name() } func TestPopmToStars(t *testing.T) { cases := []struct{ in uint8; want int }{ {0, 0}, {1, 1}, {31, 1}, {32, 2}, {95, 2}, {96, 3}, {127, 3}, {128, 3}, {159, 3}, {160, 4}, {195, 4}, {196, 4}, {223, 4}, {224, 5}, {255, 5}, } for _, c := range cases { got := popmToStars(c.in) if got != c.want { t.Errorf("popmToStars(%d) = %d, want %d", c.in, got, c.want) } } } func TestStarsToPOPM(t *testing.T) { want := [6]uint8{0, 1, 64, 128, 196, 255} for i, w := range want { got := starsToPOPM(i) if got != w { t.Errorf("starsToPOPM(%d) = %d, want %d", i, got, w) } } } func TestId3v2AudioStart_noTag(t *testing.T) { // File with no ID3 header f, err := os.CreateTemp(t.TempDir(), "noTag-*.mp3") if err != nil { t.Fatal(err) } f.Write([]byte{0xFF, 0xFB, 0x90, 0x00}) // MP3 sync f.Close() start, err := id3v2AudioStart(f.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } if start != 0 { t.Errorf("got audioStart=%d, want 0", start) } } func TestId3v2AudioStart_withHeader(t *testing.T) { // Header with tag size 0 → audioStart should be 10 data := buildMinimalMP3() path := writeTempMP3(t, data) start, err := id3v2AudioStart(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if start != 10 { t.Errorf("got audioStart=%d, want 10", start) } } func TestSetAndGet_roundtrip(t *testing.T) { original := buildMinimalMP3() path := writeTempMP3(t, original) // File should start with no rating r, err := Get(path) if err != nil { t.Fatalf("Get before Set: %v", err) } if r != 0 { t.Errorf("expected 0 before Set, got %d", r) } // Set each star value and read back for stars := 1; stars <= 5; stars++ { if err := Set(path, stars); err != nil { t.Fatalf("Set(%d): %v", stars, err) } // File must not be truncated — must be larger than tag-only (> original size) info, err := os.Stat(path) if err != nil { t.Fatalf("stat after Set(%d): %v", stars, err) } if info.Size() < int64(len(original)) { t.Errorf("Set(%d) shrunk file: got %d bytes, original was %d", stars, info.Size(), len(original)) } got, err := Get(path) if err != nil { t.Fatalf("Get after Set(%d): %v", stars, err) } if got != stars { t.Errorf("Get after Set(%d) = %d, want %d", stars, got, stars) } } // Unrate (stars=0) must remove POPM if err := Set(path, 0); err != nil { t.Fatalf("Set(0): %v", err) } got, err := Get(path) if err != nil { t.Fatalf("Get after Set(0): %v", err) } if got != 0 { t.Errorf("Get after Set(0) = %d, want 0", got) } // Audio data must be intact after all the Set calls data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile: %v", err) } audioStart, _ := id3v2AudioStart(path) if audioStart >= int64(len(data)) { t.Fatalf("audioStart %d >= fileSize %d", audioStart, len(data)) } got4096 := data[audioStart:] orig4096 := original[10:] // original audio starts at byte 10 (tag size 0) if len(got4096) < len(orig4096) { t.Errorf("audio data truncated: got %d bytes, want %d", len(got4096), len(orig4096)) } else { // Last 4096 bytes should match original audio tail := got4096[len(got4096)-len(orig4096):] if !bytes.Equal(tail, orig4096) { t.Error("audio data corrupted after rating Set calls") } } } func TestSet_invalidStars(t *testing.T) { path := writeTempMP3(t, buildMinimalMP3()) if err := Set(path, 6); err == nil { t.Error("Set(6) should return error") } if err := Set(path, -1); err == nil { t.Error("Set(-1) should return error") } } func TestSet_notMP3(t *testing.T) { f, err := os.CreateTemp(t.TempDir(), "test-*.flac") if err != nil { t.Fatal(err) } f.Close() if err := Set(f.Name(), 3); err == nil { t.Error("Set on non-MP3 should return error") } }