From c3f70320d0a21032eaf9bf32b1225b6ef2ab9af5 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sun, 25 Jan 2026 12:34:32 -0500 Subject: [PATCH] Adding tests for list handler --- cmd/api/handlers_test.go | 229 ++++++++++++++++++++++++++++++++++++-- internal/assert/assert.go | 30 +++++ 2 files changed, 247 insertions(+), 12 deletions(-) diff --git a/cmd/api/handlers_test.go b/cmd/api/handlers_test.go index 6f82cd2..66bc4b8 100644 --- a/cmd/api/handlers_test.go +++ b/cmd/api/handlers_test.go @@ -34,15 +34,11 @@ func newTestApplication(pool database.PgxIface) application { func TestHealthRoute(t *testing.T) { respRec := httptest.NewRecorder() mockPool, err := pgxmock.NewPool() - if err != nil { - t.Fatal(err) - } + assert.NilError(t, err) defer mockPool.Close() r, err := http.NewRequest(http.MethodGet, "/v1/healthcheck", nil) - if err != nil { - t.Fatal(err) - } + assert.NilError(t, err) app := newTestApplication(mockPool) app.routes().ServeHTTP(respRec, r) @@ -93,9 +89,7 @@ func TestCreateMovieHandler(t *testing.T) { rquestBody, _ := json.Marshal(movie) r, err := http.NewRequest(http.MethodPost, "/v1/movies", bytes.NewBuffer(rquestBody)) - if err != nil { - t.Fatal(err) - } + assert.NilError(t, err) app := newTestApplication(mockPool) app.routes().ServeHTTP(respRec, r) @@ -194,9 +188,7 @@ func TestCreateMovieValidatorError(t *testing.T) { t.Run(test.name, func(t *testing.T) { respRec := httptest.NewRecorder() r, err := http.NewRequest(http.MethodPost, "/v1/movies", test.input) - if err != nil { - t.Fatal(err) - } + assert.NilError(t, err) app := newTestApplication(mockPool) @@ -310,3 +302,216 @@ func TestGetMovieHandlerErrors(t *testing.T) { }) } } + +func TestListMovieHandler(t *testing.T) { + respRec := httptest.NewRecorder() + + mockPool, err := pgxmock.NewPool() + if err != nil { + t.Fatal(err) + } + defer mockPool.Close() + + movies := []data.Movie{{ + ID: 1337, + CreatedAt: time.Now(), + Title: "Batteries not included", + Year: 1987, + Runtime: data.Runtime(120), + Genres: []string{"family", "comedy"}, + Version: 1, + }, { + ID: 1338, + CreatedAt: time.Now(), + Title: "The Boy and the Heron", + Year: 2023, + Runtime: data.Runtime(140), + Genres: []string{"animation", "drama"}, + Version: 1, + }, + } + + rows := pgxmock.NewRows([]string{"id", "created_at", "title", "year", "runtime", "genres", "version"}) + for _, m := range movies { + rows.AddRow(m.ID, m.CreatedAt, m.Title, m.Year, m.Runtime, m.Genres, m.Version) + } + + mockPool.ExpectQuery(`SELECT id, created_at, title, year, runtime, genres, version + FROM movies + ORDER BY id ASC`).WillReturnRows(rows) + + r, err := http.NewRequest(http.MethodGet, "/v1/movies", nil) + assert.NilError(t, err) + + app := newTestApplication(mockPool) + app.routes().ServeHTTP(respRec, r) + + resp := respRec.Result() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Got status code %d, wanted status code %d", resp.StatusCode, http.StatusOK) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + jsonContent := make(map[string][]data.Movie) + + err = json.Unmarshal(body, &jsonContent) + assert.NilError(t, err) + + assert.Equal(t, len(jsonContent["movies"]), 2) + for i := range jsonContent["movies"] { + assert.MovieEqual(t, jsonContent["movies"][i], movies[i]) + } + + err = mockPool.ExpectationsWereMet() + assert.NilError(t, err) +} + +func TestListHandlerServerError(t *testing.T) { + respRec := httptest.NewRecorder() + mockPool, err := pgxmock.NewPool() + assert.NilError(t, err) + getAllReturnColumns := []string{"id", "created_at", "title", "year", "runtime", "genres", "version"} + errorRows := pgxmock.NewRows(getAllReturnColumns).AddRow(1, time.Now(), "will error", 2026, 120, []string{}, 1).RowError(0, fmt.Errorf("network connection lost")) + + mockPool.ExpectQuery("SELECT").WillReturnRows(errorRows) + r, err := http.NewRequest(http.MethodGet, "/v1/movies", nil) + + app := newTestApplication(mockPool) + app.routes().ServeHTTP(respRec, r) + + resp := respRec.Result() + assert.Equal(t, resp.StatusCode, http.StatusInternalServerError) + +} + +func TestListHandlerValidation(t *testing.T) { + mockPool, err := pgxmock.NewPool() + assert.NilError(t, err) + defer mockPool.Close() + + getAllReturnColumns := []string{"id", "created_at", "title", "year", "runtime", "genres", "version"} + getAllQuery := ` + SELECT id, created_at, title, year, runtime, genres, version + FROM movies + ORDER BY id ASC + ` + + testTable := []struct { + name string + query string + wantCode int + }{ + // invalid requests + { + name: "bad sort key", + query: "/v1/movies?sort=dirp", + wantCode: http.StatusUnprocessableEntity, + }, + { + name: "bad page value", + query: "/v1/movies?page=shouldbeint", + wantCode: http.StatusUnprocessableEntity, + }, + { + name: "bad page_size value", + query: "/v1/movies?page_size=shouldbeint", + wantCode: http.StatusUnprocessableEntity, + }, + { + name: "bad page too large", + query: "/v1/movies?page=1000000000", + wantCode: http.StatusUnprocessableEntity, + }, + { + name: "bad page_size too large", + query: "/v1/movies?page_size=1000", + wantCode: http.StatusUnprocessableEntity, + }, + // valid requests + { + name: "no query params", + query: "/v1/movies", + wantCode: http.StatusOK, + }, + { + name: "valid page_size", + query: "/v1/movies?page_size=50", + wantCode: http.StatusOK, + }, + { + name: "valid page", + query: "/v1/movies?page=10", + wantCode: http.StatusOK, + }, + { + name: "sort by id ascending", + query: "/v1/movies?sort=id", + wantCode: http.StatusOK, + }, + { + name: "sort by id ascending", + query: "/v1/movies?sort=-id", + wantCode: http.StatusOK, + }, + { + name: "sort by year ascending", + query: "/v1/movies?sort=year", + wantCode: http.StatusOK, + }, + { + name: "sort by year descending", + query: "/v1/movies?sort=-year", + wantCode: http.StatusOK, + }, + { + name: "sort by runtime ascending", + query: "/v1/movies?sort=runtime", + wantCode: http.StatusOK, + }, + { + name: "sort by runtime descending", + query: "/v1/movies?sort=-runtime", + wantCode: http.StatusOK, + }, + { + name: "sort by title ascending", + query: "/v1/movies?sort=title", + wantCode: http.StatusOK, + }, + { + name: "sort by title descending", + query: "/v1/movies?sort=-title", + wantCode: http.StatusOK, + }, + } + + app := newTestApplication(mockPool) + + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + respRec := httptest.NewRecorder() + mockPool, err := pgxmock.NewPool() + assert.NilError(t, err) + // assert if DB is expected to be hit + if test.wantCode == http.StatusOK { + // TODO expand return values and parameterize + // empty return is fine for validator test + mockPool.ExpectQuery(getAllQuery).WillReturnRows(mockPool.NewRows(getAllReturnColumns)) + } + + r, err := http.NewRequest(http.MethodGet, test.query, nil) + assert.NilError(t, err) + app = newTestApplication(mockPool) + app.routes().ServeHTTP(respRec, r) + resp := respRec.Result() + + assert.Equal(t, resp.StatusCode, test.wantCode) + err = mockPool.ExpectationsWereMet() + assert.NilError(t, err) + }) + + } + +} diff --git a/internal/assert/assert.go b/internal/assert/assert.go index 3cc90d5..69c5dd9 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -1,8 +1,11 @@ package assert import ( + "reflect" "strings" "testing" + + "git.runcible.io/learning/pulley/internal/data" ) // Equal a generic function to test equivalence between two values @@ -36,3 +39,30 @@ func NilError(t *testing.T, actual error) { t.Errorf("got: %v; expected: nil", actual) } } + +// MovieEqual is a helper function that compares movies attributes with +// out checking timestamps +func MovieEqual(t *testing.T, actual, expected data.Movie) { + t.Helper() // This ensures the error points to the test, not this helper line + + if actual.ID != expected.ID { + t.Errorf("ID: got %d; want %d", actual.ID, expected.ID) + } + if actual.Title != expected.Title { + t.Errorf("Title: got %q; want %q", actual.Title, expected.Title) + } + if actual.Year != expected.Year { + t.Errorf("Year: got %d; want %d", actual.Year, expected.Year) + } + if actual.Runtime != expected.Runtime { + t.Errorf("Runtime: got %v; want %v", actual.Runtime, expected.Runtime) + } + if actual.Version != expected.Version { + t.Errorf("Version: got %d; want %d", actual.Version, expected.Version) + } + + // For slices, use reflect.DeepEqual or a simple loop + if !reflect.DeepEqual(actual.Genres, expected.Genres) { + t.Errorf("Genres: got %v; want %v", actual.Genres, expected.Genres) + } +}