Created a base to build on
This commit is contained in:
63
internal/api/mock_db_test.go
Normal file
63
internal/api/mock_db_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"completed/internal/db"
|
||||
)
|
||||
|
||||
type MockQuerier struct {
|
||||
CreateGameFunc func(ctx context.Context, arg db.CreateGameParams) (db.Game, error)
|
||||
CreatePlatformFunc func(ctx context.Context, name string) (db.Platform, error)
|
||||
CreateUserFunc func(ctx context.Context, arg db.CreateUserParams) (db.User, error)
|
||||
DeleteGameFunc func(ctx context.Context, id int64) error
|
||||
DeletePlatformFunc func(ctx context.Context, id int64) error
|
||||
DeleteUserFunc func(ctx context.Context, id int64) error
|
||||
GetGameFunc func(ctx context.Context, id int64) (db.Game, error)
|
||||
GetUserFunc func(ctx context.Context, id int64) (db.User, error)
|
||||
ListGamesFunc func(ctx context.Context) ([]db.Game, error)
|
||||
ListPlatformsFunc func(ctx context.Context) ([]db.Platform, error)
|
||||
ListUsersFunc func(ctx context.Context) ([]db.User, error)
|
||||
UpdateGameFunc func(ctx context.Context, arg db.UpdateGameParams) (db.Game, error)
|
||||
UpdatePlatformFunc func(ctx context.Context, arg db.UpdatePlatformParams) (db.Platform, error)
|
||||
}
|
||||
|
||||
func (m *MockQuerier) CreateGame(ctx context.Context, arg db.CreateGameParams) (db.Game, error) {
|
||||
return m.CreateGameFunc(ctx, arg)
|
||||
}
|
||||
func (m *MockQuerier) CreatePlatform(ctx context.Context, name string) (db.Platform, error) {
|
||||
return m.CreatePlatformFunc(ctx, name)
|
||||
}
|
||||
func (m *MockQuerier) CreateUser(ctx context.Context, arg db.CreateUserParams) (db.User, error) {
|
||||
return m.CreateUserFunc(ctx, arg)
|
||||
}
|
||||
func (m *MockQuerier) DeleteGame(ctx context.Context, id int64) error {
|
||||
return m.DeleteGameFunc(ctx, id)
|
||||
}
|
||||
func (m *MockQuerier) DeletePlatform(ctx context.Context, id int64) error {
|
||||
return m.DeletePlatformFunc(ctx, id)
|
||||
}
|
||||
func (m *MockQuerier) DeleteUser(ctx context.Context, id int64) error {
|
||||
return m.DeleteUserFunc(ctx, id)
|
||||
}
|
||||
func (m *MockQuerier) GetGame(ctx context.Context, id int64) (db.Game, error) {
|
||||
return m.GetGameFunc(ctx, id)
|
||||
}
|
||||
func (m *MockQuerier) GetUser(ctx context.Context, id int64) (db.User, error) {
|
||||
return m.GetUserFunc(ctx, id)
|
||||
}
|
||||
func (m *MockQuerier) ListGames(ctx context.Context) ([]db.Game, error) {
|
||||
return m.ListGamesFunc(ctx)
|
||||
}
|
||||
func (m *MockQuerier) ListPlatforms(ctx context.Context) ([]db.Platform, error) {
|
||||
return m.ListPlatformsFunc(ctx)
|
||||
}
|
||||
func (m *MockQuerier) ListUsers(ctx context.Context) ([]db.User, error) {
|
||||
return m.ListUsersFunc(ctx)
|
||||
}
|
||||
func (m *MockQuerier) UpdateGame(ctx context.Context, arg db.UpdateGameParams) (db.Game, error) {
|
||||
return m.UpdateGameFunc(ctx, arg)
|
||||
}
|
||||
func (m *MockQuerier) UpdatePlatform(ctx context.Context, arg db.UpdatePlatformParams) (db.Platform, error) {
|
||||
return m.UpdatePlatformFunc(ctx, arg)
|
||||
}
|
||||
257
internal/api/routes.go
Normal file
257
internal/api/routes.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"completed/internal/db"
|
||||
)
|
||||
|
||||
func RegisterRoutes(api huma.API, queries db.Querier) {
|
||||
// Root/Hello Route
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "get-hello",
|
||||
Method: http.MethodGet,
|
||||
Path: "/hello",
|
||||
Summary: "Say Hello",
|
||||
Description: "Returns a friendly hello message.",
|
||||
}, func(ctx context.Context, input *struct{}) (*struct {
|
||||
Body struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
}, error) {
|
||||
resp := &struct {
|
||||
Body struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
}{}
|
||||
resp.Body.Message = "Hello from Huma + Echo!"
|
||||
return resp, nil
|
||||
})
|
||||
|
||||
registerPlatformRoutes(api, queries)
|
||||
registerGameRoutes(api, queries)
|
||||
}
|
||||
|
||||
// --- Platform Routes ---
|
||||
|
||||
// PlatformDTO ensures consistent JSON output
|
||||
type PlatformDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type PlatformInput struct {
|
||||
Name string `json:"name" doc:"Name of the platform"`
|
||||
}
|
||||
|
||||
func registerPlatformRoutes(api huma.API, queries db.Querier) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "list-platforms",
|
||||
Method: http.MethodGet,
|
||||
Path: "/platforms",
|
||||
Summary: "List Platforms",
|
||||
}, func(ctx context.Context, input *struct{}) (*struct{ Body []PlatformDTO }, error) {
|
||||
platforms, err := queries.ListPlatforms(ctx)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
dtos := make([]PlatformDTO, len(platforms))
|
||||
for i, p := range platforms {
|
||||
dtos[i] = PlatformDTO{ID: p.ID, Name: p.Name}
|
||||
}
|
||||
return &struct{ Body []PlatformDTO }{Body: dtos}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "create-platform",
|
||||
Method: http.MethodPost,
|
||||
Path: "/platforms",
|
||||
Summary: "Create Platform",
|
||||
}, func(ctx context.Context, input *struct{ Body PlatformInput }) (*struct{ Body PlatformDTO }, error) {
|
||||
platform, err := queries.CreatePlatform(ctx, input.Body.Name)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
return &struct{ Body PlatformDTO }{Body: PlatformDTO{ID: platform.ID, Name: platform.Name}}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "update-platform",
|
||||
Method: http.MethodPut,
|
||||
Path: "/platforms/{id}",
|
||||
Summary: "Update Platform",
|
||||
}, func(ctx context.Context, input *struct {
|
||||
ID int64 `path:"id"`
|
||||
Body PlatformInput
|
||||
}) (*struct{ Body PlatformDTO }, error) {
|
||||
platform, err := queries.UpdatePlatform(ctx, db.UpdatePlatformParams{
|
||||
ID: input.ID,
|
||||
Name: input.Body.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
return &struct{ Body PlatformDTO }{Body: PlatformDTO{ID: platform.ID, Name: platform.Name}}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "delete-platform",
|
||||
Method: http.MethodDelete,
|
||||
Path: "/platforms/{id}",
|
||||
Summary: "Delete Platform",
|
||||
}, func(ctx context.Context, input *struct {
|
||||
ID int64 `path:"id"`
|
||||
}) (*struct{}, error) {
|
||||
err := queries.DeletePlatform(ctx, input.ID)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
// --- Game Routes ---
|
||||
|
||||
// GameDTO for API communication (Clean JSON)
|
||||
type GameDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PlatformID int64 `json:"platform_id"`
|
||||
Score int32 `json:"score"`
|
||||
ReleaseYear string `json:"release_year" format:"date" doc:"YYYY-MM-DD"`
|
||||
Finished string `json:"finished,omitempty" format:"date" doc:"YYYY-MM-DD (optional)"`
|
||||
}
|
||||
|
||||
type GameInput struct {
|
||||
Name string `json:"name"`
|
||||
PlatformID int64 `json:"platform_id"`
|
||||
Score int32 `json:"score"`
|
||||
ReleaseYear string `json:"release_year" format:"date" doc:"YYYY-MM-DD"`
|
||||
Finished string `json:"finished,omitempty" format:"date" doc:"YYYY-MM-DD (optional)"`
|
||||
}
|
||||
|
||||
// Helper to convert DB Game to DTO
|
||||
func toGameDTO(g db.Game) GameDTO {
|
||||
dto := GameDTO{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
PlatformID: g.PlatformID,
|
||||
Score: g.Score,
|
||||
ReleaseYear: g.ReleaseYear.Time.Format("2006-01-02"),
|
||||
}
|
||||
if g.Finished.Valid {
|
||||
dto.Finished = g.Finished.Time.Format("2006-01-02")
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
// Helper to parse date string to pgtype.Date
|
||||
func parseDate(s string) (pgtype.Date, error) {
|
||||
if s == "" {
|
||||
return pgtype.Date{}, nil
|
||||
}
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
return pgtype.Date{}, err
|
||||
}
|
||||
return pgtype.Date{Time: t, Valid: true}, nil
|
||||
}
|
||||
|
||||
func registerGameRoutes(api huma.API, queries db.Querier) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "list-games",
|
||||
Method: http.MethodGet,
|
||||
Path: "/games",
|
||||
Summary: "List Games",
|
||||
}, func(ctx context.Context, input *struct{}) (*struct{ Body []GameDTO }, error) {
|
||||
games, err := queries.ListGames(ctx)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
dtos := make([]GameDTO, len(games))
|
||||
for i, g := range games {
|
||||
dtos[i] = toGameDTO(g)
|
||||
}
|
||||
return &struct{ Body []GameDTO }{Body: dtos}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "create-game",
|
||||
Method: http.MethodPost,
|
||||
Path: "/games",
|
||||
Summary: "Create Game",
|
||||
}, func(ctx context.Context, input *struct{ Body GameInput }) (*struct{ Body GameDTO }, error) {
|
||||
ry, err := parseDate(input.Body.ReleaseYear)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("Invalid release_year format (expected YYYY-MM-DD)")
|
||||
}
|
||||
fin, err := parseDate(input.Body.Finished)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("Invalid finished format (expected YYYY-MM-DD)")
|
||||
}
|
||||
|
||||
game, err := queries.CreateGame(ctx, db.CreateGameParams{
|
||||
Name: input.Body.Name,
|
||||
PlatformID: input.Body.PlatformID,
|
||||
Score: input.Body.Score,
|
||||
ReleaseYear: ry,
|
||||
Finished: fin,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
return &struct{ Body GameDTO }{Body: toGameDTO(game)}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "update-game",
|
||||
Method: http.MethodPut,
|
||||
Path: "/games/{id}",
|
||||
Summary: "Update Game",
|
||||
}, func(ctx context.Context, input *struct {
|
||||
ID int64 `path:"id"`
|
||||
Body GameInput
|
||||
}) (*struct{ Body GameDTO }, error) {
|
||||
ry, err := parseDate(input.Body.ReleaseYear)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("Invalid release_year format (expected YYYY-MM-DD)")
|
||||
}
|
||||
fin, err := parseDate(input.Body.Finished)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("Invalid finished format (expected YYYY-MM-DD)")
|
||||
}
|
||||
|
||||
game, err := queries.UpdateGame(ctx, db.UpdateGameParams{
|
||||
ID: input.ID,
|
||||
Name: input.Body.Name,
|
||||
PlatformID: input.Body.PlatformID,
|
||||
Score: input.Body.Score,
|
||||
ReleaseYear: ry,
|
||||
Finished: fin,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
return &struct{ Body GameDTO }{Body: toGameDTO(game)}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "delete-game",
|
||||
Method: http.MethodDelete,
|
||||
Path: "/games/{id}",
|
||||
Summary: "Delete Game",
|
||||
}, func(ctx context.Context, input *struct {
|
||||
ID int64 `path:"id"`
|
||||
}) (*struct{}, error) {
|
||||
err := queries.DeleteGame(ctx, input.ID)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
66
internal/api/routes_test.go
Normal file
66
internal/api/routes_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humaecho"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"completed/internal/db"
|
||||
)
|
||||
|
||||
func TestListGames(t *testing.T) {
|
||||
// 1. Setup Mock
|
||||
mockDB := &MockQuerier{
|
||||
ListGamesFunc: func(ctx context.Context) ([]db.Game, error) {
|
||||
return []db.Game{
|
||||
{ID: 1, Name: "Test Game", Score: 90},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// 2. Setup API
|
||||
e := echo.New()
|
||||
api := humaecho.New(e, huma.DefaultConfig("Test API", "1.0.0"))
|
||||
RegisterRoutes(api, mockDB)
|
||||
|
||||
// 3. Execute Request
|
||||
req := httptest.NewRequest(http.MethodGet, "/games", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
// 4. Assert
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), "Test Game")
|
||||
}
|
||||
|
||||
func TestCreatePlatform(t *testing.T) {
|
||||
// 1. Setup Mock
|
||||
mockDB := &MockQuerier{
|
||||
CreatePlatformFunc: func(ctx context.Context, name string) (db.Platform, error) {
|
||||
return db.Platform{ID: 1, Name: name}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// 2. Setup API
|
||||
e := echo.New()
|
||||
api := humaecho.New(e, huma.DefaultConfig("Test API", "1.0.0"))
|
||||
RegisterRoutes(api, mockDB)
|
||||
|
||||
// 3. Execute Request
|
||||
req := httptest.NewRequest(http.MethodPost, "/platforms", strings.NewReader(`{"name": "Sega Genesis"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
// 4. Assert
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), "Sega Genesis")
|
||||
assert.Contains(t, rec.Body.String(), `"id":1`)
|
||||
}
|
||||
Reference in New Issue
Block a user