diff options
Diffstat (limited to 'backend/api')
| -rw-r--r-- | backend/api/auth_middleware.go | 2 | ||||
| -rw-r--r-- | backend/api/handler.go | 57 | ||||
| -rw-r--r-- | backend/api/handler_test.go | 130 | ||||
| -rw-r--r-- | backend/api/handler_wrapper.go | 7 |
4 files changed, 159 insertions, 37 deletions
diff --git a/backend/api/auth_middleware.go b/backend/api/auth_middleware.go index d721f1d..0b0dfc8 100644 --- a/backend/api/auth_middleware.go +++ b/backend/api/auth_middleware.go @@ -12,7 +12,7 @@ import ( type sessionIDContextKey struct{} type userContextKey struct{} -func SessionCookieMiddleware(q *db.Queries) echo.MiddlewareFunc { +func SessionCookieMiddleware(q db.Querier) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cookie, err := c.Cookie("albatross_session") diff --git a/backend/api/handler.go b/backend/api/handler.go index 3fe7e3c..7efacf3 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -11,7 +11,6 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" - "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" "github.com/oapi-codegen/nullable" @@ -20,10 +19,15 @@ import ( "albatross-2026-backend/db" ) +type AuthenticatorInterface interface { + Login(ctx context.Context, username, password string) (int, error) +} + type Handler struct { - q *db.Queries - pool *pgxpool.Pool + q db.Querier + txm db.TxManager hub GameHubInterface + auth AuthenticatorInterface conf *config.Config } @@ -47,7 +51,7 @@ func (r postLoginCookieResponse) VisitPostLoginResponse(w http.ResponseWriter) e func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) { username := request.Body.Username password := request.Body.Password - userID, err := auth.Login(ctx, h.q, h.pool, username, password) + userID, err := h.auth.Login(ctx, username, password) if err != nil { slog.Error("login failed", "error", err) var msg string @@ -419,40 +423,29 @@ func (h *Handler) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySu }, nil } - tx, err := h.pool.Begin(ctx) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - defer func() { - if err := tx.Rollback(ctx); err != nil && err != pgx.ErrTxClosed { - slog.Error("failed to rollback transaction", "error", err) + var submissionID int32 + err = h.txm.RunInTx(ctx, func(qtx db.Querier) error { + if err := qtx.UpdateCodeAndStatus(ctx, db.UpdateCodeAndStatusParams{ + GameID: int32(gameID), + UserID: user.UserID, + Code: code, + Status: "running", + }); err != nil { + return err } - }() - - qtx := h.q.WithTx(tx) - err = qtx.UpdateCodeAndStatus(ctx, db.UpdateCodeAndStatusParams{ - GameID: int32(gameID), - UserID: user.UserID, - Code: code, - Status: "running", - }) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - submissionID, err := qtx.CreateSubmission(ctx, db.CreateSubmissionParams{ - GameID: int32(gameID), - UserID: user.UserID, - Code: code, - CodeSize: int32(codeSize), + var err error + submissionID, err = qtx.CreateSubmission(ctx, db.CreateSubmissionParams{ + GameID: int32(gameID), + UserID: user.UserID, + Code: code, + CodeSize: int32(codeSize), + }) + return err }) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - if err := tx.Commit(ctx); err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - err = h.hub.EnqueueTestTasks(ctx, int(submissionID), gameID, int(user.UserID), language, code) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) diff --git a/backend/api/handler_test.go b/backend/api/handler_test.go new file mode 100644 index 0000000..47ad92c --- /dev/null +++ b/backend/api/handler_test.go @@ -0,0 +1,130 @@ +package api + +import ( + "context" + "errors" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + + "albatross-2026-backend/config" + "albatross-2026-backend/db" +) + +// mockQuerier implements db.Querier for testing. +type mockQuerier struct { + db.Querier + getGameByIDFunc func(ctx context.Context, gameID int32) (db.GetGameByIDRow, error) +} + +func (m *mockQuerier) GetGameByID(ctx context.Context, gameID int32) (db.GetGameByIDRow, error) { + if m.getGameByIDFunc != nil { + return m.getGameByIDFunc(ctx, gameID) + } + return db.GetGameByIDRow{}, pgx.ErrNoRows +} + +// mockTxManager implements db.TxManager for testing. +type mockTxManager struct{} + +func (m *mockTxManager) RunInTx(ctx context.Context, fn func(q db.Querier) error) error { + return fn(&mockQuerier{}) +} + +// mockGameHub implements GameHubInterface for testing. +type mockGameHub struct { + calcCodeSizeResult int + enqueueErr error +} + +func (m *mockGameHub) CalcCodeSize(_ string, _ string) int { + return m.calcCodeSizeResult +} + +func (m *mockGameHub) EnqueueTestTasks(_ context.Context, _, _, _ int, _, _ string) error { + return m.enqueueErr +} + +// mockAuthenticator implements AuthenticatorInterface for testing. +type mockAuthenticator struct { + loginResult int + loginErr error +} + +func (m *mockAuthenticator) Login(_ context.Context, _, _ string) (int, error) { + return m.loginResult, m.loginErr +} + +func TestPostGamePlaySubmit_GameNotFound(t *testing.T) { + h := Handler{ + q: &mockQuerier{}, + txm: &mockTxManager{}, + hub: &mockGameHub{}, + auth: &mockAuthenticator{}, + conf: &config.Config{}, + } + user := &db.User{UserID: 1} + resp, err := h.PostGamePlaySubmit(context.Background(), PostGamePlaySubmitRequestObject{ + GameID: 999, + Body: &PostGamePlaySubmitJSONRequestBody{Code: "test"}, + }, user) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := resp.(PostGamePlaySubmit404JSONResponse); !ok { + t.Errorf("expected 404 response, got %T", resp) + } +} + +func TestPostGamePlaySubmit_GameNotRunning(t *testing.T) { + h := Handler{ + q: &mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + StartedAt: pgtype.Timestamp{ + Valid: false, + }, + }, nil + }, + }, + txm: &mockTxManager{}, + hub: &mockGameHub{calcCodeSizeResult: 10}, + auth: &mockAuthenticator{}, + conf: &config.Config{}, + } + user := &db.User{UserID: 1} + resp, err := h.PostGamePlaySubmit(context.Background(), PostGamePlaySubmitRequestObject{ + GameID: 1, + Body: &PostGamePlaySubmitJSONRequestBody{Code: "<?php echo 1;"}, + }, user) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r, ok := resp.(PostGamePlaySubmit403JSONResponse); !ok { + t.Errorf("expected 403 response, got %T", resp) + } else if r.Message != "Game is not running" { + t.Errorf("unexpected message: %s", r.Message) + } +} + +func TestPostLogin_AuthFailure(t *testing.T) { + h := Handler{ + q: &mockQuerier{}, + txm: &mockTxManager{}, + hub: &mockGameHub{}, + auth: &mockAuthenticator{loginErr: errors.New("invalid credentials")}, + conf: &config.Config{}, + } + resp, err := h.PostLogin(context.Background(), PostLoginRequestObject{ + Body: &PostLoginJSONRequestBody{Username: "user", Password: "wrong"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := resp.(PostLogin401JSONResponse); !ok { + t.Errorf("expected 401 response, got %T", resp) + } +} diff --git a/backend/api/handler_wrapper.go b/backend/api/handler_wrapper.go index 7448d13..28b89e4 100644 --- a/backend/api/handler_wrapper.go +++ b/backend/api/handler_wrapper.go @@ -5,8 +5,6 @@ package api import ( "context" - "github.com/jackc/pgx/v5/pgxpool" - "albatross-2026-backend/config" "albatross-2026-backend/db" ) @@ -17,12 +15,13 @@ type HandlerWrapper struct { impl Handler } -func NewHandler(queries *db.Queries, pool *pgxpool.Pool, hub GameHubInterface, conf *config.Config) *HandlerWrapper { +func NewHandler(queries db.Querier, txm db.TxManager, hub GameHubInterface, auth AuthenticatorInterface, conf *config.Config) *HandlerWrapper { return &HandlerWrapper{ impl: Handler{ q: queries, - pool: pool, + txm: txm, hub: hub, + auth: auth, conf: conf, }, } |
