package service_test import ( "context" "errors" "testing" "time" "watch-party-backend/internal/core/episode" "watch-party-backend/internal/service" ) // ---- test fakes ---- type fakeRepo struct { cur episode.Episode getErr error setErr error moveRes episode.MoveResult moveErr error setCalls []struct { id int64 start string } moveCalls [][]int64 listRes []episode.Episode listErr error createRes episode.Episode createErr error createIn []episode.NewShowInput archiveRes []episode.ArchiveEpisode archiveErr error } func (f *fakeRepo) GetCurrent(ctx context.Context) (episode.Episode, error) { return f.cur, f.getErr } func (f *fakeRepo) SetCurrent(ctx context.Context, id int64, start string) error { f.setCalls = append(f.setCalls, struct { id int64 start string }{id, start}) return f.setErr } func (f *fakeRepo) MoveToArchive(ctx context.Context, ids []int64) (episode.MoveResult, error) { f.moveCalls = append(f.moveCalls, ids) return f.moveRes, f.moveErr } func (f *fakeRepo) ListAll(ctx context.Context) ([]episode.Episode, error) { return f.listRes, f.listErr } func (f *fakeRepo) ListArchive(ctx context.Context) ([]episode.ArchiveEpisode, error) { return f.archiveRes, f.archiveErr } func (f *fakeRepo) Delete(ctx context.Context, id int64) error { return nil } func (f *fakeRepo) Create(ctx context.Context, in episode.NewShowInput) (episode.Episode, error) { f.createIn = append(f.createIn, in) return f.createRes, f.createErr } // ---- tests ---- func TestEpisodeService_GetCurrent_OK(t *testing.T) { now := time.Now().UTC() fr := &fakeRepo{ cur: episode.Episode{ EpNum: 1, EpTitle: "Pilot", SeasonName: "S1", StartTime: "20:00:00", PlaybackLength: "00:24:00", DateCreated: now, }, } svc := service.NewEpisodeService(fr) got, err := svc.GetCurrent(context.Background()) if err != nil { t.Fatalf("unexpected err: %v", err) } if got.EpNum != 1 || got.EpTitle != "Pilot" { t.Fatalf("got %+v", got) } } func TestEpisodeService_GetCurrent_PropagatesError(t *testing.T) { fr := &fakeRepo{getErr: errors.New("db down")} svc := service.NewEpisodeService(fr) if _, err := svc.GetCurrent(context.Background()); err == nil { t.Fatal("expected error") } } func TestEpisodeService_SetCurrent_RejectsBadTime(t *testing.T) { fr := &fakeRepo{} svc := service.NewEpisodeService(fr) _, err := svc.SetCurrent(context.Background(), 42, "7:0:0") // not zero-padded if err == nil || !errors.Is(err, episode.ErrInvalidStartTime) { t.Fatalf("expected ErrInvalidStartTime, got %v", err) } if len(fr.setCalls) != 0 { t.Fatalf("repo should not be called on invalid time") } } func TestEpisodeService_SetCurrent_OK(t *testing.T) { now := time.Now().UTC() fr := &fakeRepo{ cur: episode.Episode{ EpNum: 42, EpTitle: "Answer", SeasonName: "S1", StartTime: "21:07:05", PlaybackLength: "00:24:00", DateCreated: now, }, } svc := service.NewEpisodeService(fr) got, err := svc.SetCurrent(context.Background(), 42, "21:07:05") if err != nil { t.Fatalf("unexpected err: %v", err) } if got.EpNum != 42 || got.StartTime != "21:07:05" { t.Fatalf("unexpected %+v", got) } if len(fr.setCalls) != 1 || fr.setCalls[0].id != 42 || fr.setCalls[0].start != "21:07:05" { t.Fatalf("repo.SetCurrent not called as expected: %+v", fr.setCalls) } } func TestEpisodeService_MoveToArchive_EmptyIDs(t *testing.T) { fr := &fakeRepo{} svc := service.NewEpisodeService(fr) _, err := svc.MoveToArchive(context.Background(), nil) if err == nil || !errors.Is(err, episode.ErrEmptyIDs) { t.Fatalf("expected ErrEmptyIDs, got %v", err) } } func TestEpisodeService_MoveToArchive_DedupAndOK(t *testing.T) { fr := &fakeRepo{ moveRes: episode.MoveResult{ MovedIDs: []int64{1, 2}, DeletedIDs: []int64{1, 2}, SkippedIDs: []int64{999}, }, } svc := service.NewEpisodeService(fr) res, err := svc.MoveToArchive(context.Background(), []int64{1, 2, 1}) // dedupe if err != nil { t.Fatalf("unexpected: %v", err) } if len(fr.moveCalls) != 1 || len(fr.moveCalls[0]) != 2 { t.Fatalf("expected deduped call, got %+v", fr.moveCalls) } if len(res.MovedIDs) != 2 || len(res.DeletedIDs) != 2 { t.Fatalf("unexpected response: %+v", res) } } func TestEpisodeService_ListAll_OK(t *testing.T) { fr := &fakeRepo{listRes: []episode.Episode{{Id: 3}, {Id: 1}}} svc := service.NewEpisodeService(fr) got, err := svc.ListAll(context.Background()) if err != nil { t.Fatalf("unexpected: %v", err) } if len(got) != 2 || got[0].Id != 3 { t.Fatalf("bad: %+v", got) } } func TestEpisodeService_ListAll_Error(t *testing.T) { fr := &fakeRepo{listErr: errors.New("boom")} svc := service.NewEpisodeService(fr) if _, err := svc.ListAll(context.Background()); err == nil { t.Fatal("expected error") } } func TestEpisodeService_ListArchive(t *testing.T) { fr := &fakeRepo{ archiveRes: []episode.ArchiveEpisode{{Id: 1}, {Id: 2}}, } svc := service.NewEpisodeService(fr) got, err := svc.ListArchive(context.Background()) if err != nil { t.Fatalf("unexpected: %v", err) } if len(got) != 2 || got[0].Id != 1 { t.Fatalf("bad archive list: %+v", got) } } func TestEpisodeService_ListArchive_Error(t *testing.T) { fr := &fakeRepo{archiveErr: errors.New("down")} svc := service.NewEpisodeService(fr) if _, err := svc.ListArchive(context.Background()); err == nil { t.Fatal("expected error") } } func TestEpisodeService_Create_Validation(t *testing.T) { tests := []struct { name string in episode.NewShowInput want error }{ { name: "bad ep num", in: episode.NewShowInput{EpNum: 0, EpTitle: "x", SeasonName: "S1", StartTime: "10:00:00", PlaybackLength: "00:24:00"}, want: episode.ErrInvalidShowInput, }, { name: "blank title", in: episode.NewShowInput{EpNum: 1, EpTitle: " ", SeasonName: "S1", StartTime: "10:00:00", PlaybackLength: "00:24:00"}, want: episode.ErrInvalidShowInput, }, { name: "bad start", in: episode.NewShowInput{EpNum: 1, EpTitle: "x", SeasonName: "S1", StartTime: "1:0:0", PlaybackLength: "00:24:00"}, want: episode.ErrInvalidStartTime, }, { name: "bad playback format", in: episode.NewShowInput{EpNum: 1, EpTitle: "x", SeasonName: "S1", StartTime: "10:00:00", PlaybackLength: "24:00"}, want: episode.ErrInvalidPlayback, }, { name: "zero playback", in: episode.NewShowInput{EpNum: 1, EpTitle: "x", SeasonName: "S1", StartTime: "10:00:00", PlaybackLength: "00:00:00"}, want: episode.ErrInvalidPlayback, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc := service.NewEpisodeService(&fakeRepo{}) if _, err := svc.Create(context.Background(), tt.in); !errors.Is(err, tt.want) { t.Fatalf("expected %v, got %v", tt.want, err) } }) } } func TestEpisodeService_Create_OK(t *testing.T) { want := episode.Episode{Id: 10, EpNum: 1, EpTitle: "Pilot", StartTime: "10:00:00", PlaybackLength: "00:24:00"} fr := &fakeRepo{createRes: want} svc := service.NewEpisodeService(fr) got, err := svc.Create(context.Background(), episode.NewShowInput{ EpNum: 1, EpTitle: "Pilot", SeasonName: "S1", StartTime: "10:00:00", PlaybackLength: "00:24:00", }) if err != nil { t.Fatalf("unexpected: %v", err) } if got.Id != want.Id || len(fr.createIn) != 1 { t.Fatalf("repo not called or bad id: %+v, calls=%d", got, len(fr.createIn)) } if fr.createIn[0].EpTitle != "Pilot" || fr.createIn[0].EpNum != 1 { t.Fatalf("bad input passed: %+v", fr.createIn[0]) } } func TestEpisodeService_Create_DefaultStartTime(t *testing.T) { fr := &fakeRepo{createRes: episode.Episode{StartTime: "22:00:00"}} svc := service.NewEpisodeService(fr) _, err := svc.Create(context.Background(), episode.NewShowInput{ EpNum: 1, EpTitle: "Pilot", SeasonName: "S1", StartTime: "", PlaybackLength: "00:24:00", }) if err != nil { t.Fatalf("unexpected: %v", err) } if len(fr.createIn) != 1 || fr.createIn[0].StartTime != "22:00:00" { t.Fatalf("expected default start time, got %+v", fr.createIn) } }