Refactor episode management: introduce core episode model and update service/repo interfaces

This commit is contained in:
Nik Afiq 2025-12-02 20:47:46 +09:00
parent 5f081d3eb9
commit f8837fed9f
7 changed files with 119 additions and 103 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.env .env
.cache
.DS_Store .DS_Store
*/node_modules */node_modules

View File

@ -0,0 +1,40 @@
package episode
import (
"context"
"errors"
"time"
)
var (
ErrNotFound = errors.New("episode not found")
ErrInvalidStartTime = errors.New("invalid start_time (expected HH:MM:SS)")
ErrEmptyIDs = errors.New("ids must not be empty")
)
// Episode represents a single show/episode in the system.
type Episode struct {
Id int
EpNum int
EpTitle string
SeasonName string
StartTime string
PlaybackLength string
DateCreated time.Time
}
// MoveResult describes what happened during an archive move operation.
type MoveResult struct {
MovedIDs []int64
DeletedIDs []int64
SkippedIDs []int64
}
// Repository is the storage port that the application layer depends on.
type Repository interface {
GetCurrent(ctx context.Context) (Episode, error)
SetCurrent(ctx context.Context, id int64, startHHMMSS string) error
MoveToArchive(ctx context.Context, ids []int64) (MoveResult, error)
ListAll(ctx context.Context) ([]Episode, error)
Delete(ctx context.Context, id int64) error
}

View File

@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"time" "time"
"watch-party-backend/internal/repo" "watch-party-backend/internal/core/episode"
"watch-party-backend/internal/service" "watch-party-backend/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -58,7 +58,7 @@ func getCurrentHandler(svc service.EpisodeService) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
cur, err := svc.GetCurrent(c.Request.Context()) cur, err := svc.GetCurrent(c.Request.Context())
if err != nil { if err != nil {
if err == repo.ErrNotFound { if errors.Is(err, episode.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "no current row found"}) c.JSON(http.StatusNotFound, gin.H{"error": "no current row found"})
return return
} }
@ -90,11 +90,11 @@ func setCurrentHandler(svc service.EpisodeService) gin.HandlerFunc {
} }
cur, err := svc.SetCurrent(c.Request.Context(), req.ID, req.StartTime) cur, err := svc.SetCurrent(c.Request.Context(), req.ID, req.StartTime)
if err != nil { if err != nil {
switch err { switch {
case service.ErrInvalidTime: case errors.Is(err, episode.ErrInvalidStartTime):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
case repo.ErrNotFound: case errors.Is(err, episode.ErrNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": "id not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "id not found"})
return return
default: default:
@ -132,7 +132,7 @@ func moveToArchiveHandler(svc service.EpisodeService) gin.HandlerFunc {
res, err := svc.MoveToArchive(c.Request.Context(), ids) res, err := svc.MoveToArchive(c.Request.Context(), ids)
if err != nil { if err != nil {
if err == service.ErrEmptyIDs { if errors.Is(err, episode.ErrEmptyIDs) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
@ -190,7 +190,7 @@ func deleteShowsHandler(svc service.EpisodeService) gin.HandlerFunc {
} }
err = svc.Delete(c.Request.Context(), id) err = svc.Delete(c.Request.Context(), id)
if err != nil { if err != nil {
if errors.Is(err, repo.ErrNotFound) { if errors.Is(err, episode.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "id not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "id not found"})
return return
} }

View File

@ -9,8 +9,8 @@ import (
"testing" "testing"
"time" "time"
"watch-party-backend/internal/core/episode"
httpapi "watch-party-backend/internal/http" httpapi "watch-party-backend/internal/http"
"watch-party-backend/internal/repo"
"watch-party-backend/internal/service" "watch-party-backend/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -19,36 +19,39 @@ import (
// ---- fake service implementing service.EpisodeService ---- // ---- fake service implementing service.EpisodeService ----
type fakeSvc struct { type fakeSvc struct {
cur repo.Episode cur episode.Episode
getErr error getErr error
setCur repo.Episode setCur episode.Episode
setErr error setErr error
moveRes repo.MoveResult moveRes episode.MoveResult
moveErr error moveErr error
lastSetID int64 lastSetID int64
lastTime string lastTime string
lastMove []int64 lastMove []int64
} }
func (f *fakeSvc) GetCurrent(ctx context.Context) (repo.Episode, error) { func (f *fakeSvc) GetCurrent(ctx context.Context) (episode.Episode, error) {
return f.cur, f.getErr return f.cur, f.getErr
} }
func (f *fakeSvc) SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) { func (f *fakeSvc) SetCurrent(ctx context.Context, id int64, start string) (episode.Episode, error) {
f.lastSetID, f.lastTime = id, start f.lastSetID, f.lastTime = id, start
return f.setCur, f.setErr return f.setCur, f.setErr
} }
func (f *fakeSvc) MoveToArchive(ctx context.Context, ids []int64) (repo.MoveResult, error) { func (f *fakeSvc) MoveToArchive(ctx context.Context, ids []int64) (episode.MoveResult, error) {
f.lastMove = append([]int64(nil), ids...) f.lastMove = append([]int64(nil), ids...)
return f.moveRes, f.moveErr return f.moveRes, f.moveErr
} }
func (f *fakeSvc) ListAll(ctx context.Context) ([]repo.Episode, error) { func (f *fakeSvc) ListAll(ctx context.Context) ([]episode.Episode, error) {
if f.moveErr != nil { if f.moveErr != nil {
return nil, f.moveErr return nil, f.moveErr
} }
return []repo.Episode{ return []episode.Episode{
{Id: 10, EpTitle: "X"}, {Id: 10, EpTitle: "X"},
}, nil }, nil
} }
func (f *fakeSvc) Delete(ctx context.Context, id int64) error {
return nil
}
// ---- helpers ---- // ---- helpers ----
@ -87,7 +90,7 @@ func TestHealthz_OK(t *testing.T) {
func TestGetCurrent_V1_OK(t *testing.T) { func TestGetCurrent_V1_OK(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
svc := &fakeSvc{ svc := &fakeSvc{
cur: repo.Episode{ cur: episode.Episode{
EpNum: 7, EpNum: 7,
EpTitle: "Lucky", EpTitle: "Lucky",
SeasonName: "S2", SeasonName: "S2",
@ -99,12 +102,12 @@ func TestGetCurrent_V1_OK(t *testing.T) {
r := newRouterWithSvc(svc) r := newRouterWithSvc(svc)
w := httptest.NewRecorder() w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/v1/current", nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/current", nil)
r.ServeHTTP(w, req) r.ServeHTTP(w, req)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Fatalf("got %d", w.Code) t.Fatalf("got %d", w.Code)
} }
var got repo.Episode var got episode.Episode
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json: %v", err) t.Fatalf("json: %v", err)
} }
@ -116,16 +119,16 @@ func TestGetCurrent_V1_OK(t *testing.T) {
func TestPostCurrent_BadPayload(t *testing.T) { func TestPostCurrent_BadPayload(t *testing.T) {
r := newRouterWithSvc(&fakeSvc{}) r := newRouterWithSvc(&fakeSvc{})
// missing fields // missing fields
w := doJSON(t, r, http.MethodPost, "/v1/current", map[string]any{"id": 1}) w := doJSON(t, r, http.MethodPost, "/api/v1/current", map[string]any{"id": 1})
if w.Code != http.StatusBadRequest { if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code) t.Fatalf("expected 400, got %d", w.Code)
} }
} }
func TestPostCurrent_BadTime(t *testing.T) { func TestPostCurrent_BadTime(t *testing.T) {
svc := &fakeSvc{setErr: service.ErrInvalidTime} svc := &fakeSvc{setErr: episode.ErrInvalidStartTime}
r := newRouterWithSvc(svc) r := newRouterWithSvc(svc)
w := doJSON(t, r, http.MethodPost, "/v1/current", map[string]any{ w := doJSON(t, r, http.MethodPost, "/api/v1/current", map[string]any{
"id": 42, "start_time": "7:0:0", "id": 42, "start_time": "7:0:0",
}) })
if w.Code != http.StatusBadRequest { if w.Code != http.StatusBadRequest {
@ -134,16 +137,16 @@ func TestPostCurrent_BadTime(t *testing.T) {
} }
func TestPostCurrent_OK(t *testing.T) { func TestPostCurrent_OK(t *testing.T) {
want := repo.Episode{EpNum: 42, EpTitle: "Answer", StartTime: "21:07:05"} want := episode.Episode{EpNum: 42, EpTitle: "Answer", StartTime: "21:07:05"}
svc := &fakeSvc{setCur: want} svc := &fakeSvc{setCur: want}
r := newRouterWithSvc(svc) r := newRouterWithSvc(svc)
w := doJSON(t, r, http.MethodPost, "/v1/current", map[string]any{ w := doJSON(t, r, http.MethodPost, "/api/v1/current", map[string]any{
"id": 42, "start_time": "21:07:05", "id": 42, "start_time": "21:07:05",
}) })
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d; body=%s", w.Code, w.Body.String()) t.Fatalf("expected 200, got %d; body=%s", w.Code, w.Body.String())
} }
var got repo.Episode var got episode.Episode
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json: %v", err) t.Fatalf("json: %v", err)
} }
@ -156,9 +159,9 @@ func TestPostCurrent_OK(t *testing.T) {
} }
func TestPostArchive_Empty(t *testing.T) { func TestPostArchive_Empty(t *testing.T) {
svc := &fakeSvc{moveErr: service.ErrEmptyIDs} svc := &fakeSvc{moveErr: episode.ErrEmptyIDs}
r := newRouterWithSvc(svc) r := newRouterWithSvc(svc)
w := doJSON(t, r, http.MethodPost, "/v1/archive", map[string]any{}) w := doJSON(t, r, http.MethodPost, "/api/v1/archive", map[string]any{})
if w.Code != http.StatusBadRequest { if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d; body=%s", w.Code, w.Body.String()) t.Fatalf("expected 400, got %d; body=%s", w.Code, w.Body.String())
} }
@ -166,7 +169,7 @@ func TestPostArchive_Empty(t *testing.T) {
func TestPostArchive_SingleAndMultiple_OK(t *testing.T) { func TestPostArchive_SingleAndMultiple_OK(t *testing.T) {
svc := &fakeSvc{ svc := &fakeSvc{
moveRes: repo.MoveResult{ moveRes: episode.MoveResult{
MovedIDs: []int64{1, 2}, MovedIDs: []int64{1, 2},
DeletedIDs: []int64{1, 2}, DeletedIDs: []int64{1, 2},
SkippedIDs: []int64{999}, SkippedIDs: []int64{999},
@ -175,12 +178,12 @@ func TestPostArchive_SingleAndMultiple_OK(t *testing.T) {
r := newRouterWithSvc(svc) r := newRouterWithSvc(svc)
// single // single
w1 := doJSON(t, r, http.MethodPost, "/v1/archive", map[string]any{"id": 1}) w1 := doJSON(t, r, http.MethodPost, "/api/v1/archive", map[string]any{"id": 1})
if w1.Code != http.StatusOK { if w1.Code != http.StatusOK {
t.Fatalf("got %d: %s", w1.Code, w1.Body.String()) t.Fatalf("got %d: %s", w1.Code, w1.Body.String())
} }
// multiple // multiple
w2 := doJSON(t, r, http.MethodPost, "/v1/archive", map[string]any{"ids": []int64{1, 2, 1}}) w2 := doJSON(t, r, http.MethodPost, "/api/v1/archive", map[string]any{"ids": []int64{1, 2, 1}})
if w2.Code != http.StatusOK { if w2.Code != http.StatusOK {
t.Fatalf("got %d: %s", w2.Code, w2.Body.String()) t.Fatalf("got %d: %s", w2.Code, w2.Body.String())
} }

View File

@ -3,47 +3,22 @@ package repo
import ( import (
"context" "context"
"errors" "errors"
"time"
"watch-party-backend/internal/core/episode"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
var ErrNotFound = errors.New("not found")
type Episode struct {
Id int `json:"id"`
EpNum int `json:"ep_num"`
EpTitle string `json:"ep_title"`
SeasonName string `json:"season_name"`
StartTime string `json:"start_time"`
PlaybackLength string `json:"playback_length"`
DateCreated time.Time `json:"date_created"`
}
type MoveResult struct {
MovedIDs []int64 `json:"moved_ids"`
DeletedIDs []int64 `json:"deleted_ids"`
SkippedIDs []int64 `json:"skipped_ids"`
}
type EpisodeRepository interface {
GetCurrent(ctx context.Context) (Episode, error)
SetCurrent(ctx context.Context, id int64, startHHMMSS string) error
MoveToArchive(ctx context.Context, ids []int64) (MoveResult, error)
ListAll(ctx context.Context) ([]Episode, error)
Delete(ctx context.Context, id int64) error
}
type pgxEpisodeRepo struct { type pgxEpisodeRepo struct {
pool *pgxpool.Pool pool *pgxpool.Pool
} }
func NewEpisodeRepo(pool *pgxpool.Pool) EpisodeRepository { func NewEpisodeRepo(pool *pgxpool.Pool) episode.Repository {
return &pgxEpisodeRepo{pool: pool} return &pgxEpisodeRepo{pool: pool}
} }
func (r *pgxEpisodeRepo) ListAll(ctx context.Context) ([]Episode, error) { func (r *pgxEpisodeRepo) ListAll(ctx context.Context) ([]episode.Episode, error) {
const q = ` const q = `
SELECT SELECT
id, id,
@ -62,9 +37,9 @@ ORDER BY id DESC;
} }
defer rows.Close() defer rows.Close()
var out []Episode var out []episode.Episode
for rows.Next() { for rows.Next() {
var e Episode var e episode.Episode
if err := rows.Scan(&e.Id, &e.EpNum, &e.EpTitle, &e.SeasonName, &e.StartTime, &e.PlaybackLength, &e.DateCreated); err != nil { if err := rows.Scan(&e.Id, &e.EpNum, &e.EpTitle, &e.SeasonName, &e.StartTime, &e.PlaybackLength, &e.DateCreated); err != nil {
return nil, err return nil, err
} }
@ -73,7 +48,7 @@ ORDER BY id DESC;
return out, rows.Err() return out, rows.Err()
} }
func (r *pgxEpisodeRepo) GetCurrent(ctx context.Context) (Episode, error) { func (r *pgxEpisodeRepo) GetCurrent(ctx context.Context) (episode.Episode, error) {
const q = ` const q = `
SELECT SELECT
id, id,
@ -88,15 +63,15 @@ WHERE current_ep = true
ORDER BY id DESC ORDER BY id DESC
LIMIT 1; LIMIT 1;
` `
var e Episode var e episode.Episode
err := r.pool.QueryRow(ctx, q).Scan( err := r.pool.QueryRow(ctx, q).Scan(
&e.Id, &e.EpNum, &e.EpTitle, &e.SeasonName, &e.StartTime, &e.PlaybackLength, &e.DateCreated, &e.Id, &e.EpNum, &e.EpTitle, &e.SeasonName, &e.StartTime, &e.PlaybackLength, &e.DateCreated,
) )
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return Episode{}, ErrNotFound return episode.Episode{}, episode.ErrNotFound
} }
return Episode{}, err return episode.Episode{}, err
} }
return e, nil return e, nil
} }
@ -119,14 +94,14 @@ func (r *pgxEpisodeRepo) SetCurrent(ctx context.Context, id int64, startHHMMSS s
return err return err
} }
if ct.RowsAffected() == 0 { if ct.RowsAffected() == 0 {
return ErrNotFound return episode.ErrNotFound
} }
return tx.Commit(ctx) return tx.Commit(ctx)
} }
func (r *pgxEpisodeRepo) MoveToArchive(ctx context.Context, ids []int64) (MoveResult, error) { func (r *pgxEpisodeRepo) MoveToArchive(ctx context.Context, ids []int64) (episode.MoveResult, error) {
res := MoveResult{} res := episode.MoveResult{}
if len(ids) == 0 { if len(ids) == 0 {
return res, nil return res, nil
} }
@ -207,7 +182,7 @@ func (r *pgxEpisodeRepo) Delete(ctx context.Context, id int64) error {
return err return err
} }
if cmdTag.RowsAffected() == 0 { if cmdTag.RowsAffected() == 0 {
return ErrNotFound return episode.ErrNotFound
} }
return nil return nil
} }

View File

@ -2,41 +2,35 @@ package service
import ( import (
"context" "context"
"errors"
"regexp" "regexp"
"time" "time"
"watch-party-backend/internal/repo" "watch-party-backend/internal/core/episode"
)
var (
ErrInvalidTime = errors.New("invalid start_time (expected HH:MM:SS)")
ErrEmptyIDs = errors.New("ids must not be empty")
) )
type EpisodeService interface { type EpisodeService interface {
GetCurrent(ctx context.Context) (repo.Episode, error) GetCurrent(ctx context.Context) (episode.Episode, error)
SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) SetCurrent(ctx context.Context, id int64, start string) (episode.Episode, error)
MoveToArchive(ctx context.Context, ids []int64) (repo.MoveResult, error) MoveToArchive(ctx context.Context, ids []int64) (episode.MoveResult, error)
ListAll(ctx context.Context) ([]repo.Episode, error) ListAll(ctx context.Context) ([]episode.Episode, error)
Delete(ctx context.Context, id int64) error Delete(ctx context.Context, id int64) error
} }
type episodeService struct { type episodeService struct {
repo repo.EpisodeRepository repo episode.Repository
} }
func NewEpisodeService(r repo.EpisodeRepository) EpisodeService { func NewEpisodeService(r episode.Repository) EpisodeService {
return &episodeService{repo: r} return &episodeService{repo: r}
} }
func (s *episodeService) ListAll(ctx context.Context) ([]repo.Episode, error) { func (s *episodeService) ListAll(ctx context.Context) ([]episode.Episode, error) {
c, cancel := context.WithTimeout(ctx, 5*time.Second) c, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
return s.repo.ListAll(c) return s.repo.ListAll(c)
} }
func (s *episodeService) GetCurrent(ctx context.Context) (repo.Episode, error) { func (s *episodeService) GetCurrent(ctx context.Context) (episode.Episode, error) {
c, cancel := context.WithTimeout(ctx, 3*time.Second) c, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() defer cancel()
return s.repo.GetCurrent(c) return s.repo.GetCurrent(c)
@ -44,19 +38,19 @@ func (s *episodeService) GetCurrent(ctx context.Context) (repo.Episode, error) {
var hhmmss = regexp.MustCompile(`^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$`) var hhmmss = regexp.MustCompile(`^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$`)
func (s *episodeService) SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) { func (s *episodeService) SetCurrent(ctx context.Context, id int64, start string) (episode.Episode, error) {
if !hhmmss.MatchString(start) { if !hhmmss.MatchString(start) {
return repo.Episode{}, ErrInvalidTime return episode.Episode{}, episode.ErrInvalidStartTime
} }
c, cancel := context.WithTimeout(ctx, 5*time.Second) c, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
if err := s.repo.SetCurrent(c, id, start); err != nil { if err := s.repo.SetCurrent(c, id, start); err != nil {
return repo.Episode{}, err return episode.Episode{}, err
} }
return s.repo.GetCurrent(c) return s.repo.GetCurrent(c)
} }
func (s *episodeService) MoveToArchive(ctx context.Context, ids []int64) (repo.MoveResult, error) { func (s *episodeService) MoveToArchive(ctx context.Context, ids []int64) (episode.MoveResult, error) {
uniq := make([]int64, 0, len(ids)) uniq := make([]int64, 0, len(ids))
seen := make(map[int64]struct{}, len(ids)) seen := make(map[int64]struct{}, len(ids))
for _, id := range ids { for _, id := range ids {
@ -66,7 +60,7 @@ func (s *episodeService) MoveToArchive(ctx context.Context, ids []int64) (repo.M
} }
} }
if len(uniq) == 0 { if len(uniq) == 0 {
return repo.MoveResult{}, ErrEmptyIDs return episode.MoveResult{}, episode.ErrEmptyIDs
} }
c, cancel := context.WithTimeout(ctx, 10*time.Second) c, cancel := context.WithTimeout(ctx, 10*time.Second)

View File

@ -6,28 +6,28 @@ import (
"testing" "testing"
"time" "time"
"watch-party-backend/internal/repo" "watch-party-backend/internal/core/episode"
"watch-party-backend/internal/service" "watch-party-backend/internal/service"
) )
// ---- test fakes ---- // ---- test fakes ----
type fakeRepo struct { type fakeRepo struct {
cur repo.Episode cur episode.Episode
getErr error getErr error
setErr error setErr error
moveRes repo.MoveResult moveRes episode.MoveResult
moveErr error moveErr error
setCalls []struct { setCalls []struct {
id int64 id int64
start string start string
} }
moveCalls [][]int64 moveCalls [][]int64
listRes []repo.Episode listRes []episode.Episode
listErr error listErr error
} }
func (f *fakeRepo) GetCurrent(ctx context.Context) (repo.Episode, error) { func (f *fakeRepo) GetCurrent(ctx context.Context) (episode.Episode, error) {
return f.cur, f.getErr return f.cur, f.getErr
} }
func (f *fakeRepo) SetCurrent(ctx context.Context, id int64, start string) error { func (f *fakeRepo) SetCurrent(ctx context.Context, id int64, start string) error {
@ -37,20 +37,23 @@ func (f *fakeRepo) SetCurrent(ctx context.Context, id int64, start string) error
}{id, start}) }{id, start})
return f.setErr return f.setErr
} }
func (f *fakeRepo) MoveToArchive(ctx context.Context, ids []int64) (repo.MoveResult, error) { func (f *fakeRepo) MoveToArchive(ctx context.Context, ids []int64) (episode.MoveResult, error) {
f.moveCalls = append(f.moveCalls, ids) f.moveCalls = append(f.moveCalls, ids)
return f.moveRes, f.moveErr return f.moveRes, f.moveErr
} }
func (f *fakeRepo) ListAll(ctx context.Context) ([]repo.Episode, error) { func (f *fakeRepo) ListAll(ctx context.Context) ([]episode.Episode, error) {
return f.listRes, f.listErr return f.listRes, f.listErr
} }
func (f *fakeRepo) Delete(ctx context.Context, id int64) error {
return nil
}
// ---- tests ---- // ---- tests ----
func TestEpisodeService_GetCurrent_OK(t *testing.T) { func TestEpisodeService_GetCurrent_OK(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
fr := &fakeRepo{ fr := &fakeRepo{
cur: repo.Episode{ cur: episode.Episode{
EpNum: 1, EpNum: 1,
EpTitle: "Pilot", EpTitle: "Pilot",
SeasonName: "S1", SeasonName: "S1",
@ -83,8 +86,8 @@ func TestEpisodeService_SetCurrent_RejectsBadTime(t *testing.T) {
svc := service.NewEpisodeService(fr) svc := service.NewEpisodeService(fr)
_, err := svc.SetCurrent(context.Background(), 42, "7:0:0") // not zero-padded _, err := svc.SetCurrent(context.Background(), 42, "7:0:0") // not zero-padded
if err == nil || !errors.Is(err, service.ErrInvalidTime) { if err == nil || !errors.Is(err, episode.ErrInvalidStartTime) {
t.Fatalf("expected ErrInvalidTime, got %v", err) t.Fatalf("expected ErrInvalidStartTime, got %v", err)
} }
if len(fr.setCalls) != 0 { if len(fr.setCalls) != 0 {
t.Fatalf("repo should not be called on invalid time") t.Fatalf("repo should not be called on invalid time")
@ -94,7 +97,7 @@ func TestEpisodeService_SetCurrent_RejectsBadTime(t *testing.T) {
func TestEpisodeService_SetCurrent_OK(t *testing.T) { func TestEpisodeService_SetCurrent_OK(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
fr := &fakeRepo{ fr := &fakeRepo{
cur: repo.Episode{ cur: episode.Episode{
EpNum: 42, EpNum: 42,
EpTitle: "Answer", EpTitle: "Answer",
SeasonName: "S1", SeasonName: "S1",
@ -121,14 +124,14 @@ func TestEpisodeService_MoveToArchive_EmptyIDs(t *testing.T) {
fr := &fakeRepo{} fr := &fakeRepo{}
svc := service.NewEpisodeService(fr) svc := service.NewEpisodeService(fr)
_, err := svc.MoveToArchive(context.Background(), nil) _, err := svc.MoveToArchive(context.Background(), nil)
if err == nil || !errors.Is(err, service.ErrEmptyIDs) { if err == nil || !errors.Is(err, episode.ErrEmptyIDs) {
t.Fatalf("expected ErrEmptyIDs, got %v", err) t.Fatalf("expected ErrEmptyIDs, got %v", err)
} }
} }
func TestEpisodeService_MoveToArchive_DedupAndOK(t *testing.T) { func TestEpisodeService_MoveToArchive_DedupAndOK(t *testing.T) {
fr := &fakeRepo{ fr := &fakeRepo{
moveRes: repo.MoveResult{ moveRes: episode.MoveResult{
MovedIDs: []int64{1, 2}, MovedIDs: []int64{1, 2},
DeletedIDs: []int64{1, 2}, DeletedIDs: []int64{1, 2},
SkippedIDs: []int64{999}, SkippedIDs: []int64{999},
@ -148,7 +151,7 @@ func TestEpisodeService_MoveToArchive_DedupAndOK(t *testing.T) {
} }
func TestEpisodeService_ListAll_OK(t *testing.T) { func TestEpisodeService_ListAll_OK(t *testing.T) {
fr := &fakeRepo{listRes: []repo.Episode{{Id: 3}, {Id: 1}}} fr := &fakeRepo{listRes: []episode.Episode{{Id: 3}, {Id: 1}}}
svc := service.NewEpisodeService(fr) svc := service.NewEpisodeService(fr)
got, err := svc.ListAll(context.Background()) got, err := svc.ListAll(context.Background())
if err != nil { if err != nil {