package syncer import ( "os" "path/filepath" "testing" "github.com/arnef/toni-sync/internal/tonieapi" ) // fakeClient is a minimal in-memory stand-in for tonieapi.Client used in tests. type fakeClient struct { tonieSequence []tonieapi.CreativeTonie // consumed in order by GetCreativeTonie calls callIndex int uploadedTitles []string sortedTitles []string } func (f *fakeClient) GetCreativeTonie(tonieID string) (*tonieapi.CreativeTonie, error) { t := f.tonieSequence[f.callIndex] if f.callIndex < len(f.tonieSequence)-1 { f.callIndex++ } return &t, nil } func (f *fakeClient) UploadFileToTonie(tonie tonieapi.CreativeTonie, filePath, title string) error { f.uploadedTitles = append(f.uploadedTitles, title) return nil } func (f *fakeClient) SortChaptersOfTonie(tonie tonieapi.CreativeTonie, chapters []tonieapi.Chapter) error { for _, c := range chapters { f.sortedTitles = append(f.sortedTitles, c.Title) } return nil } func makeTonie(chapters []tonieapi.Chapter) tonieapi.CreativeTonie { return tonieapi.CreativeTonie{ID: "t1", HouseholdID: "h1", Name: "Test Tonie", Chapters: chapters} } func setupFolder(t *testing.T) string { t.Helper() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "01 - First.mp3"), []byte("data"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "02 - Second.mp3"), []byte("data"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("ignore me"), 0o644); err != nil { t.Fatal(err) } return dir } func TestListLocalTracksFiltersByExtension(t *testing.T) { dir := setupFolder(t) tracks, err := ListLocalTracks(dir) if err != nil { t.Fatal(err) } if len(tracks) != 2 || tracks[0].Title != "01 - First" || tracks[1].Title != "02 - Second" { t.Fatalf("unexpected tracks: %+v", tracks) } } func TestListLocalTracksMissingFolder(t *testing.T) { if _, err := ListLocalTracks(filepath.Join(t.TempDir(), "missing")); err == nil { t.Fatal("expected error for missing folder") } } func TestBuildPlanDetectsUploadNoRemoval(t *testing.T) { dir := setupFolder(t) tonie := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}}) client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{tonie}} m := Target{TonieID: "t1", Folder: dir, Prune: true} plan, err := BuildPlan(client, m) if err != nil { t.Fatal(err) } if len(plan.ToUpload) != 1 || plan.ToUpload[0].Title != "02 - Second" { t.Fatalf("expected upload of '02 - Second', got %+v", plan.ToUpload) } if len(plan.ToRemove) != 0 { t.Fatalf("expected no removal, got %+v", plan.ToRemove) } if !plan.NeedsChanges() { t.Fatal("expected NeedsChanges to be true") } } func TestBuildPlanPrunesRemovedChapters(t *testing.T) { dir := setupFolder(t) tonie := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}, {ID: "c2", Title: "stale"}}) client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{tonie}} m := Target{TonieID: "t1", Folder: dir, Prune: true} plan, err := BuildPlan(client, m) if err != nil { t.Fatal(err) } if len(plan.ToRemove) != 1 || plan.ToRemove[0].Title != "stale" { t.Fatalf("expected 'stale' to be removed, got %+v", plan.ToRemove) } } func TestBuildPlanNoPruneKeepsStaleChapters(t *testing.T) { dir := setupFolder(t) tonie := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}, {ID: "c2", Title: "stale"}}) client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{tonie}} m := Target{TonieID: "t1", Folder: dir, Prune: false} plan, err := BuildPlan(client, m) if err != nil { t.Fatal(err) } if len(plan.ToRemove) != 0 { t.Fatalf("expected no removal, got %+v", plan.ToRemove) } want := []string{"01 - First", "02 - Second", "stale"} if !equalStrings(plan.FinalOrderTitles, want) { t.Fatalf("expected order %v, got %v", want, plan.FinalOrderTitles) } } func TestBuildPlanSkipsEmptyFolder(t *testing.T) { dir := t.TempDir() // no audio files tonie := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "existing chapter"}}) client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{tonie}} m := Target{TonieID: "t1", Folder: dir, Prune: true} plan, err := BuildPlan(client, m) if err != nil { t.Fatal(err) } if !plan.Skipped { t.Fatal("expected plan to be skipped for empty folder") } if plan.NeedsChanges() { t.Fatal("expected NeedsChanges to be false for a skipped plan") } if err := ApplyPlan(client, m, plan); err != nil { t.Fatal(err) } if len(client.sortedTitles) != 0 || len(client.uploadedTitles) != 0 { t.Fatalf("expected ApplyPlan to be a no-op for a skipped plan, got uploads=%v sorted=%v", client.uploadedTitles, client.sortedTitles) } } func TestApplyPlanUploadsAndSkipsReorderWhenAlreadyMatching(t *testing.T) { dir := setupFolder(t) before := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}}) after := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}, {ID: "c2", Title: "02 - Second"}}) client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{before, after}} m := Target{TonieID: "t1", Folder: dir, Prune: true} plan, err := BuildPlan(client, m) if err != nil { t.Fatal(err) } if err := ApplyPlan(client, m, plan); err != nil { t.Fatal(err) } if len(client.uploadedTitles) != 1 || client.uploadedTitles[0] != "02 - Second" { t.Fatalf("expected upload of '02 - Second', got %v", client.uploadedTitles) } if len(client.sortedTitles) != 0 { t.Fatalf("expected no sort call since order already matches, got %v", client.sortedTitles) } } func TestApplyPlanReordersWhenOrderDiffers(t *testing.T) { dir := setupFolder(t) reversed := makeTonie([]tonieapi.Chapter{{ID: "c2", Title: "02 - Second"}, {ID: "c1", Title: "01 - First"}}) client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{reversed, reversed}} m := Target{TonieID: "t1", Folder: dir, Prune: true} plan, err := BuildPlan(client, m) if err != nil { t.Fatal(err) } if err := ApplyPlan(client, m, plan); err != nil { t.Fatal(err) } if len(client.uploadedTitles) != 0 { t.Fatalf("expected no uploads, got %v", client.uploadedTitles) } want := []string{"01 - First", "02 - Second"} if !equalStrings(client.sortedTitles, want) { t.Fatalf("expected sorted order %v, got %v", want, client.sortedTitles) } }