aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/api
diff options
context:
space:
mode:
Diffstat (limited to 'backend/api')
-rw-r--r--backend/api/auth_middleware.go2
-rw-r--r--backend/api/handler.go57
-rw-r--r--backend/api/handler_test.go130
-rw-r--r--backend/api/handler_wrapper.go7
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,
},
}