From 0ad8a015643ccdece40404cceea837a88c62ad90 Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Wed, 5 Nov 2025 00:13:47 +0900 Subject: [PATCH] Added test --- backend/internal/http/handlers_test.go | 191 ++++++++++++++++++ .../internal/service/episode_service_test.go | 133 +++++++++++- 2 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 backend/internal/http/handlers_test.go diff --git a/backend/internal/http/handlers_test.go b/backend/internal/http/handlers_test.go new file mode 100644 index 0000000..0b1a92a --- /dev/null +++ b/backend/internal/http/handlers_test.go @@ -0,0 +1,191 @@ +package httpapi_test + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + httpapi "watch-party-backend/internal/http" + "watch-party-backend/internal/repo" + "watch-party-backend/internal/service" + + "github.com/gin-gonic/gin" +) + +// ---- fake service implementing service.EpisodeService ---- + +type fakeSvc struct { + cur repo.Episode + getErr error + setCur repo.Episode + setErr error + moveRes repo.MoveResult + moveErr error + lastSetID int64 + lastTime string + lastMove []int64 +} + +func (f *fakeSvc) GetCurrent(ctx context.Context) (repo.Episode, error) { + return f.cur, f.getErr +} +func (f *fakeSvc) SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) { + f.lastSetID, f.lastTime = id, start + return f.setCur, f.setErr +} +func (f *fakeSvc) MoveToArchive(ctx context.Context, ids []int64) (repo.MoveResult, error) { + f.lastMove = append([]int64(nil), ids...) + return f.moveRes, f.moveErr +} + +// ---- helpers ---- + +func newRouterWithSvc(svc service.EpisodeService) *gin.Engine { + gin.SetMode(gin.TestMode) + return httpapi.NewRouter(svc) +} + +func doJSON(t *testing.T, r *gin.Engine, method, path string, body any) *httptest.ResponseRecorder { + t.Helper() + var buf bytes.Buffer + if body != nil { + if err := json.NewEncoder(&buf).Encode(body); err != nil { + t.Fatalf("encode body: %v", err) + } + } + req := httptest.NewRequest(method, path, &buf) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +// ---- tests ---- + +func TestHealthz_OK(t *testing.T) { + r := newRouterWithSvc(&fakeSvc{}) + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) + r.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("got %d", w.Code) + } +} + +func TestGetCurrent_V1_OK(t *testing.T) { + now := time.Now().UTC() + svc := &fakeSvc{ + cur: repo.Episode{ + EpNum: 7, + EpTitle: "Lucky", + SeasonName: "S2", + StartTime: "18:30:00", + PlaybackLength: "00:24:00", + DateCreated: now, + }, + } + r := newRouterWithSvc(svc) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/v1/current", nil) + r.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("got %d", w.Code) + } + var got repo.Episode + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { + t.Fatalf("json: %v", err) + } + if got.EpNum != 7 || got.EpTitle != "Lucky" { + t.Fatalf("unexpected %+v", got) + } +} + +func TestPostCurrent_BadPayload(t *testing.T) { + r := newRouterWithSvc(&fakeSvc{}) + // missing fields + w := doJSON(t, r, http.MethodPost, "/v1/current", map[string]any{"id": 1}) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", w.Code) + } +} + +func TestPostCurrent_BadTime(t *testing.T) { + svc := &fakeSvc{setErr: service.ErrInvalidTime} + r := newRouterWithSvc(svc) + w := doJSON(t, r, http.MethodPost, "/v1/current", map[string]any{ + "id": 42, "start_time": "7:0:0", + }) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d; body=%s", w.Code, w.Body.String()) + } +} + +func TestPostCurrent_OK(t *testing.T) { + want := repo.Episode{EpNum: 42, EpTitle: "Answer", StartTime: "21:07:05"} + svc := &fakeSvc{setCur: want} + r := newRouterWithSvc(svc) + w := doJSON(t, r, http.MethodPost, "/v1/current", map[string]any{ + "id": 42, "start_time": "21:07:05", + }) + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d; body=%s", w.Code, w.Body.String()) + } + var got repo.Episode + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { + t.Fatalf("json: %v", err) + } + if got.EpNum != want.EpNum || got.StartTime != want.StartTime { + t.Fatalf("unexpected %+v", got) + } + if svc.lastSetID != 42 || svc.lastTime != "21:07:05" { + t.Fatalf("service not called as expected: id=%d time=%s", svc.lastSetID, svc.lastTime) + } +} + +func TestPostArchive_Empty(t *testing.T) { + svc := &fakeSvc{moveErr: service.ErrEmptyIDs} + r := newRouterWithSvc(svc) + w := doJSON(t, r, http.MethodPost, "/v1/archive", map[string]any{}) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d; body=%s", w.Code, w.Body.String()) + } +} + +func TestPostArchive_SingleAndMultiple_OK(t *testing.T) { + svc := &fakeSvc{ + moveRes: repo.MoveResult{ + MovedIDs: []int64{1, 2}, + DeletedIDs: []int64{1, 2}, + SkippedIDs: []int64{999}, + }, + } + r := newRouterWithSvc(svc) + + // single + w1 := doJSON(t, r, http.MethodPost, "/v1/archive", map[string]any{"id": 1}) + if w1.Code != http.StatusOK { + t.Fatalf("got %d: %s", w1.Code, w1.Body.String()) + } + // multiple + w2 := doJSON(t, r, http.MethodPost, "/v1/archive", map[string]any{"ids": []int64{1, 2, 1}}) + if w2.Code != http.StatusOK { + t.Fatalf("got %d: %s", w2.Code, w2.Body.String()) + } + // check the lastMove captured by fake + if len(svc.lastMove) != 3 || svc.lastMove[0] != 1 || svc.lastMove[1] != 2 { + t.Fatalf("unexpected service ids: %+v", svc.lastMove) + } + // basic JSON fields present + var body map[string]any + if err := json.Unmarshal(w2.Body.Bytes(), &body); err != nil { + t.Fatalf("json: %v", err) + } + if body["inserted"] == nil || body["deleted"] == nil { + t.Fatalf("missing counters in response: %+v", body) + } +} diff --git a/backend/internal/service/episode_service_test.go b/backend/internal/service/episode_service_test.go index 40c870d..0f9b96d 100644 --- a/backend/internal/service/episode_service_test.go +++ b/backend/internal/service/episode_service_test.go @@ -2,6 +2,7 @@ package service_test import ( "context" + "errors" "testing" "time" @@ -9,24 +10,134 @@ import ( "watch-party-backend/internal/service" ) -type stubRepo struct { - current repo.Episode - err error +// ---- test fakes ---- + +type fakeRepo struct { + cur repo.Episode + getErr error + setErr error + moveRes repo.MoveResult + moveErr error + setCalls []struct { + id int64 + start string + } + moveCalls [][]int64 } -func (s stubRepo) GetCurrent(ctx context.Context) (repo.Episode, error) { - return s.current, s.err +func (f *fakeRepo) GetCurrent(ctx context.Context) (repo.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) (repo.MoveResult, error) { + f.moveCalls = append(f.moveCalls, ids) + return f.moveRes, f.moveErr } -func TestEpisodeService_GetCurrent(t *testing.T) { - want := repo.Episode{EpNum: 1, EpTitle: "Ep1", SeasonName: "S1", StartTime: "00:00:00", PlaybackLength: "00:24:00", DateCreated: time.Now()} - svc := service.NewEpisodeService(stubRepo{current: want}) +// ---- tests ---- + +func TestEpisodeService_GetCurrent_OK(t *testing.T) { + now := time.Now().UTC() + fr := &fakeRepo{ + cur: repo.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 error: %v", err) + t.Fatalf("unexpected err: %v", err) } - if got.EpNum != want.EpNum || got.EpTitle != want.EpTitle { - t.Fatalf("mismatch: got %+v want %+v", got, want) + 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, service.ErrInvalidTime) { + t.Fatalf("expected ErrInvalidTime, 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: repo.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, service.ErrEmptyIDs) { + t.Fatalf("expected ErrEmptyIDs, got %v", err) + } +} + +func TestEpisodeService_MoveToArchive_DedupAndOK(t *testing.T) { + fr := &fakeRepo{ + moveRes: repo.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) } }