package sync import ( "testing" "time" ) func TestPrepareWrites(t *testing.T) { baseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) tests := []struct { name string records []*PlayRecord expectedWrites int expectUnique bool }{ { name: "empty records returns nil", records: []*PlayRecord{}, expectedWrites: 0, }, { name: "single record", records: []*PlayRecord{ { TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}, }, }, expectedWrites: 1, expectUnique: true, }, { name: "multiple records same timestamp", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song C", Artists: []PlayRecordArtist{{ArtistName: "Artist C"}}, PlayedTime: Timestamp{Time: baseTime}}, }, expectedWrites: 3, expectUnique: true, }, { name: "mixed timestamps", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Second)}}, {TrackName: "Song C", Artists: []PlayRecordArtist{{ArtistName: "Artist C"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song D", Artists: []PlayRecordArtist{{ArtistName: "Artist D"}}, PlayedTime: Timestamp{Time: baseTime.Add(2 * time.Second)}}, {TrackName: "Song E", Artists: []PlayRecordArtist{{ArtistName: "Artist E"}}, PlayedTime: Timestamp{Time: baseTime}}, }, expectedWrites: 5, expectUnique: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { writes, err := prepareWrites(tt.records, RecordType) if err != nil { t.Fatalf("PrepareWrites() error = %v", err) } if tt.expectedWrites == 0 { if writes != nil { t.Errorf("PrepareWrites() = %v, want nil", writes) } return } if len(writes) != tt.expectedWrites { t.Errorf("len(writes) = %d, want %d", len(writes), tt.expectedWrites) } if tt.expectUnique { rkeys := make(map[string]bool) for _, w := range writes { rkey := w["rkey"].(string) if rkeys[rkey] { t.Errorf("duplicate rkey generated: %s", rkey) } rkeys[rkey] = true } if len(rkeys) != len(writes) { t.Errorf("got %d unique rkeys, want %d", len(rkeys), len(writes)) } } }) } } func TestPrepareWritesManyCollisions(t *testing.T) { baseTime := time.Date(2024, 1, 15, 10, 0, 0, 123456789, time.UTC) numRecords := 50 records := make([]*PlayRecord, numRecords) for i := range numRecords { records[i] = &PlayRecord{ TrackName: "Song", Artists: []PlayRecordArtist{{ArtistName: "Artist"}}, PlayedTime: Timestamp{Time: baseTime}, } } writes, err := prepareWrites(records, RecordType) if err != nil { t.Fatalf("prepareWrites() error = %v", err) } if len(writes) != numRecords { t.Errorf("len(writes) = %d, want %d", len(writes), numRecords) } rkeys := make(map[string]bool) for _, w := range writes { rkey := w["rkey"].(string) if rkeys[rkey] { t.Errorf("duplicate rkey generated: %s", rkey) } rkeys[rkey] = true } if len(rkeys) != numRecords { t.Errorf("got %d unique rkeys, want %d", len(rkeys), numRecords) } } func TestFilterNew(t *testing.T) { baseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) processedKey := CreateRecordKey(&PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}) tests := []struct { name string records []*PlayRecord existing []ExistingRecord processed map[string]bool tolerance time.Duration wantNewCount int wantNewTracks []string }{ { name: "excludes exact matches", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Minute)}}, {TrackName: "Song C", Artists: []PlayRecordArtist{{ArtistName: "Artist C"}}, PlayedTime: Timestamp{Time: baseTime.Add(2 * time.Minute)}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Minute)}}}, }, tolerance: 5 * time.Minute, wantNewCount: 2, wantNewTracks: []string{"Song A", "Song C"}, }, { name: "returns all when none exist", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Minute)}}, }, existing: []ExistingRecord{}, tolerance: 5 * time.Minute, wantNewCount: 2, wantNewTracks: []string{"Song A", "Song B"}, }, { name: "detects duplicates within tolerance", records: []*PlayRecord{ {TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: baseTime.Add(30 * time.Second)}, MusicServiceBaseDomain: MusicServiceSpotify}, {TrackName: "Different Song", Artists: []PlayRecordArtist{{ArtistName: "Different Artist"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Hour)}, MusicServiceBaseDomain: MusicServiceSpotify}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: baseTime}, MusicServiceBaseDomain: MusicServiceLastFM}}, }, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Different Song"}, }, { name: "excludes via processed map", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Minute)}}, }, existing: []ExistingRecord{}, processed: map[string]bool{ processedKey: true, }, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Song B"}, }, { name: "empty records returns nothing", records: []*PlayRecord{}, existing: []ExistingRecord{{URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}}}, tolerance: 5 * time.Minute, wantNewCount: 0, wantNewTracks: []string{}, }, { name: "nil processed map works", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, }, existing: []ExistingRecord{}, processed: nil, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Song A"}, }, { name: "nil existing records works", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, }, existing: nil, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Song A"}, }, { name: "zero tolerance requires exact time match", records: []*PlayRecord{ {TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Second)}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, tolerance: 0, wantNewCount: 1, wantNewTracks: []string{"Same Song"}, }, { name: "matches multiple existing records", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Minute)}}, {TrackName: "Song C", Artists: []PlayRecordArtist{{ArtistName: "Artist C"}}, PlayedTime: Timestamp{Time: baseTime.Add(2 * time.Minute)}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}}, {URI: "at://did:example/user/play/def", Value: &PlayRecord{TrackName: "Song C", Artists: []PlayRecordArtist{{ArtistName: "Artist C"}}, PlayedTime: Timestamp{Time: baseTime.Add(2 * time.Minute)}}}, }, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Song B"}, }, { name: "time at exact tolerance boundary matches", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime.Add(5 * time.Minute)}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, tolerance: 5 * time.Minute, wantNewCount: 0, wantNewTracks: []string{}, }, { name: "time just beyond tolerance does not match", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime.Add(5*time.Minute + time.Second)}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Song A"}, }, { name: "different artist does not match", records: []*PlayRecord{ {TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Different Artist"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Same Song"}, }, { name: "different track does not match", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: baseTime}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Song A"}, }, { name: "processed takes precedence over existing check", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, processed: map[string]bool{ processedKey: true, }, tolerance: 5 * time.Minute, wantNewCount: 0, wantNewTracks: []string{}, }, { name: "same_record_processed_and_matches_existing_returns_nothing", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, processed: map[string]bool{ processedKey: true, }, tolerance: 5 * time.Minute, wantNewCount: 0, wantNewTracks: []string{}, }, { name: "processed_skips_record_regardless_of_existing", records: []*PlayRecord{ {TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}, {TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: baseTime.Add(time.Minute)}}, }, existing: []ExistingRecord{ {URI: "at://did:example/user/play/abc", Value: &PlayRecord{TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: baseTime}}}, }, processed: map[string]bool{ processedKey: true, }, tolerance: 5 * time.Minute, wantNewCount: 1, wantNewTracks: []string{"Song B"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { newRecords := FilterNew(tt.records, tt.existing, tt.processed, tt.tolerance) if len(newRecords) != tt.wantNewCount { t.Errorf("FilterNew() returned %d records, want %d", len(newRecords), tt.wantNewCount) } wantSet := make(map[string]bool) for _, tr := range tt.wantNewTracks { wantSet[tr] = true } for _, rec := range newRecords { if !wantSet[rec.TrackName] { t.Errorf("FilterNew() returned unexpected track %q", rec.TrackName) } } }) } } func TestFindDuplicates(t *testing.T) { tests := []struct { name string records []ExistingRecord expectedDuplicateCount int }{ { name: "finds duplicates", records: []ExistingRecord{ { URI: "at://did:example:user/fm.teal.alpha.feed.play/abc123", CID: "bafyreabc123", Value: &PlayRecord{ TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}, }, }, { URI: "at://did:example:user/fm.teal.alpha.feed.play/def456", CID: "bafyreedef456", Value: &PlayRecord{ TrackName: "Same Song", Artists: []PlayRecordArtist{{ArtistName: "Same Artist"}}, PlayedTime: Timestamp{Time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}, }, }, { URI: "at://did:example:user/fm.teal.alpha.feed.play/ghi789", CID: "bafyreghi789", Value: &PlayRecord{ TrackName: "Different Song", Artists: []PlayRecordArtist{{ArtistName: "Different Artist"}}, PlayedTime: Timestamp{Time: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)}, }, }, }, expectedDuplicateCount: 1, }, { name: "returns empty for no duplicates", records: []ExistingRecord{ { URI: "at://did:example:user/fm.teal.alpha.feed.play/abc123", CID: "bafyreabc123", Value: &PlayRecord{ TrackName: "Song A", Artists: []PlayRecordArtist{{ArtistName: "Artist A"}}, PlayedTime: Timestamp{Time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}, }, }, { URI: "at://did:example:user/fm.teal.alpha.feed.play/def456", CID: "bafyreedef456", Value: &PlayRecord{ TrackName: "Song B", Artists: []PlayRecordArtist{{ArtistName: "Artist B"}}, PlayedTime: Timestamp{Time: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)}, }, }, }, expectedDuplicateCount: 0, }, { name: "handles empty slice", records: []ExistingRecord{}, expectedDuplicateCount: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { duplicates := FindDuplicates(tt.records) if len(duplicates) != tt.expectedDuplicateCount { t.Errorf("len(duplicates) = %d, want %d", len(duplicates), tt.expectedDuplicateCount) } for key, group := range duplicates { if len(group) < 2 { t.Errorf("group for key %q should have 2+ records, got %d", key, len(group)) } } }) } }