feat: game lobby service
This commit is contained in:
@@ -0,0 +1,634 @@
|
||||
package lobby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestPublicOpenAPISpecValidates loads public-openapi.yaml and verifies it
|
||||
// is a syntactically valid OpenAPI 3.0 document.
|
||||
func TestPublicOpenAPISpecValidates(t *testing.T) {
|
||||
t.Parallel()
|
||||
loadPublicSpec(t)
|
||||
}
|
||||
|
||||
// TestInternalOpenAPISpecValidates loads internal-openapi.yaml and verifies
|
||||
// it is a syntactically valid OpenAPI 3.0 document.
|
||||
func TestInternalOpenAPISpecValidates(t *testing.T) {
|
||||
t.Parallel()
|
||||
loadInternalSpec(t)
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesGameCreateContract verifies that the game-create
|
||||
// operation has a stable operationId, correct request and response schema
|
||||
// references, and the expected required fields on CreateGameRequest.
|
||||
func TestPublicSpecFreezesGameCreateContract(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
op := getOperation(t, doc, "/api/v1/lobby/games", http.MethodPost)
|
||||
|
||||
require.Equal(t, "createGame", op.OperationID)
|
||||
assertOperationParameterRefs(t, op, "#/components/parameters/XUserID")
|
||||
assertSchemaRef(t, requestSchemaRef(t, op), "#/components/schemas/CreateGameRequest", "createGame request")
|
||||
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusCreated), "#/components/schemas/GameRecord", "createGame 201")
|
||||
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusBadRequest), "#/components/schemas/ErrorResponse", "createGame 400")
|
||||
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusForbidden), "#/components/schemas/ErrorResponse", "createGame 403")
|
||||
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusUnprocessableEntity), "#/components/schemas/ErrorResponse", "createGame 422")
|
||||
|
||||
req := componentSchemaRef(t, doc, "CreateGameRequest")
|
||||
assertRequiredFields(t, req,
|
||||
"game_name", "game_type",
|
||||
"min_players", "max_players",
|
||||
"start_gap_hours", "start_gap_players",
|
||||
"enrollment_ends_at", "turn_schedule", "target_engine_version",
|
||||
)
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesGameRecordSchema verifies that GameRecord carries the
|
||||
// full frozen field set from README.md and that optional fields are not
|
||||
// listed as required.
|
||||
func TestPublicSpecFreezesGameRecordSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
schema := componentSchemaRef(t, doc, "GameRecord")
|
||||
|
||||
assertRequiredFields(t, schema,
|
||||
"game_id", "game_name", "game_type", "owner_user_id", "status",
|
||||
"min_players", "max_players", "start_gap_hours", "start_gap_players",
|
||||
"enrollment_ends_at", "turn_schedule", "target_engine_version",
|
||||
"created_at", "updated_at",
|
||||
"current_turn", "runtime_status", "engine_health_summary",
|
||||
)
|
||||
|
||||
// Optional fields must be present in properties but not in required.
|
||||
for _, opt := range []string{"description", "started_at", "finished_at"} {
|
||||
require.Contains(t, schema.Value.Properties, opt, "GameRecord.%s must be in properties", opt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesStatusEnums verifies that the game_status enum in
|
||||
// GameRecord contains the full frozen 9-value set.
|
||||
func TestPublicSpecFreezesStatusEnums(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
assertStringEnum(t, componentSchemaRef(t, doc, "GameRecord"), "status",
|
||||
"draft", "enrollment_open", "ready_to_start", "starting",
|
||||
"start_failed", "running", "paused", "finished", "cancelled",
|
||||
)
|
||||
assertStringEnum(t, componentSchemaRef(t, doc, "GameRecord"), "game_type",
|
||||
"public", "private",
|
||||
)
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesGameLifecycleContracts verifies that every state
|
||||
// transition command has the correct operationId and returns a GameRecord on
|
||||
// success.
|
||||
func TestPublicSpecFreezesGameLifecycleContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
cases := []struct {
|
||||
path string
|
||||
operationID string
|
||||
}{
|
||||
{"/api/v1/lobby/games/{game_id}/open-enrollment", "openEnrollment"},
|
||||
{"/api/v1/lobby/games/{game_id}/ready-to-start", "manualReadyToStart"},
|
||||
{"/api/v1/lobby/games/{game_id}/start", "startGame"},
|
||||
{"/api/v1/lobby/games/{game_id}/pause", "pauseGame"},
|
||||
{"/api/v1/lobby/games/{game_id}/resume", "resumeGame"},
|
||||
{"/api/v1/lobby/games/{game_id}/cancel", "cancelGame"},
|
||||
{"/api/v1/lobby/games/{game_id}/retry-start", "retryStart"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.operationID, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
op := getOperation(t, doc, tc.path, http.MethodPost)
|
||||
require.Equal(t, tc.operationID, op.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/GameRecord",
|
||||
tc.operationID+" 200")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesApplicationContracts verifies the three application
|
||||
// operations: submit, approve, and reject.
|
||||
func TestPublicSpecFreezesApplicationContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
submitOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications", http.MethodPost)
|
||||
require.Equal(t, "submitApplication", submitOp.OperationID)
|
||||
assertSchemaRef(t, requestSchemaRef(t, submitOp), "#/components/schemas/SubmitApplicationRequest", "submit request")
|
||||
assertSchemaRef(t, responseSchemaRef(t, submitOp, http.StatusCreated), "#/components/schemas/ApplicationRecord", "submit 201")
|
||||
|
||||
req := componentSchemaRef(t, doc, "SubmitApplicationRequest")
|
||||
assertRequiredFields(t, req, "race_name")
|
||||
|
||||
approveOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve", http.MethodPost)
|
||||
require.Equal(t, "approveApplication", approveOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, approveOp, http.StatusOK), "#/components/schemas/MembershipRecord", "approve 200")
|
||||
|
||||
rejectOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject", http.MethodPost)
|
||||
require.Equal(t, "rejectApplication", rejectOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, rejectOp, http.StatusOK), "#/components/schemas/ApplicationRecord", "reject 200")
|
||||
|
||||
appRecord := componentSchemaRef(t, doc, "ApplicationRecord")
|
||||
assertRequiredFields(t, appRecord,
|
||||
"application_id", "game_id", "applicant_user_id", "race_name", "status", "created_at",
|
||||
)
|
||||
assertStringEnum(t, appRecord, "status", "submitted", "approved", "rejected")
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesInviteContracts verifies the four invite operations:
|
||||
// create, redeem, decline, and revoke.
|
||||
func TestPublicSpecFreezesInviteContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
createOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites", http.MethodPost)
|
||||
require.Equal(t, "createInvite", createOp.OperationID)
|
||||
assertSchemaRef(t, requestSchemaRef(t, createOp), "#/components/schemas/CreateInviteRequest", "create request")
|
||||
assertSchemaRef(t, responseSchemaRef(t, createOp, http.StatusCreated), "#/components/schemas/InviteRecord", "create 201")
|
||||
|
||||
req := componentSchemaRef(t, doc, "CreateInviteRequest")
|
||||
assertRequiredFields(t, req, "invitee_user_id")
|
||||
|
||||
redeemOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem", http.MethodPost)
|
||||
require.Equal(t, "redeemInvite", redeemOp.OperationID)
|
||||
assertSchemaRef(t, requestSchemaRef(t, redeemOp), "#/components/schemas/RedeemInviteRequest", "redeem request")
|
||||
assertSchemaRef(t, responseSchemaRef(t, redeemOp, http.StatusOK), "#/components/schemas/MembershipRecord", "redeem 200")
|
||||
|
||||
declineOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/decline", http.MethodPost)
|
||||
require.Equal(t, "declineInvite", declineOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, declineOp, http.StatusOK), "#/components/schemas/InviteRecord", "decline 200")
|
||||
|
||||
revokeOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/revoke", http.MethodPost)
|
||||
require.Equal(t, "revokeInvite", revokeOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, revokeOp, http.StatusOK), "#/components/schemas/InviteRecord", "revoke 200")
|
||||
|
||||
inviteRecord := componentSchemaRef(t, doc, "InviteRecord")
|
||||
assertRequiredFields(t, inviteRecord,
|
||||
"invite_id", "game_id", "inviter_user_id", "invitee_user_id", "status", "created_at", "expires_at",
|
||||
)
|
||||
assertStringEnum(t, inviteRecord, "status", "created", "redeemed", "declined", "revoked", "expired")
|
||||
|
||||
// race_name is optional on InviteRecord (set only at redeem time).
|
||||
require.Contains(t, inviteRecord.Value.Properties, "race_name", "InviteRecord.race_name must be in properties")
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesMembershipContracts verifies the membership list,
|
||||
// remove, and block operations.
|
||||
func TestPublicSpecFreezesMembershipContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
listOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/memberships", http.MethodGet)
|
||||
require.Equal(t, "listMemberships", listOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, listOp, http.StatusOK), "#/components/schemas/MembershipListResponse", "list 200")
|
||||
|
||||
removeOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove", http.MethodPost)
|
||||
require.Equal(t, "removeMember", removeOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, removeOp, http.StatusOK), "#/components/schemas/MembershipRecord", "remove 200")
|
||||
|
||||
blockOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block", http.MethodPost)
|
||||
require.Equal(t, "blockMember", blockOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, blockOp, http.StatusOK), "#/components/schemas/MembershipRecord", "block 200")
|
||||
|
||||
memberRecord := componentSchemaRef(t, doc, "MembershipRecord")
|
||||
assertRequiredFields(t, memberRecord,
|
||||
"membership_id", "game_id", "user_id", "race_name", "status", "joined_at",
|
||||
)
|
||||
assertStringEnum(t, memberRecord, "status", "active", "removed", "blocked")
|
||||
|
||||
// removed_at is optional.
|
||||
require.Contains(t, memberRecord.Value.Properties, "removed_at", "MembershipRecord.removed_at must be in properties")
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesMyListContracts verifies that the three user-facing
|
||||
// list endpoints have correct operationIds, pagination parameters, and
|
||||
// response schema references.
|
||||
func TestPublicSpecFreezesMyListContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
myGamesOp := getOperation(t, doc, "/api/v1/lobby/my/games", http.MethodGet)
|
||||
require.Equal(t, "listMyGames", myGamesOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, myGamesOp, http.StatusOK), "#/components/schemas/GameListResponse", "my/games 200")
|
||||
|
||||
myAppsOp := getOperation(t, doc, "/api/v1/lobby/my/applications", http.MethodGet)
|
||||
require.Equal(t, "listMyApplications", myAppsOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, myAppsOp, http.StatusOK), "#/components/schemas/MyApplicationListResponse", "my/applications 200")
|
||||
|
||||
myInvitesOp := getOperation(t, doc, "/api/v1/lobby/my/invites", http.MethodGet)
|
||||
require.Equal(t, "listMyInvites", myInvitesOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, myInvitesOp, http.StatusOK), "#/components/schemas/MyInviteListResponse", "my/invites 200")
|
||||
|
||||
myAppItem := componentSchemaRef(t, doc, "MyApplicationItem")
|
||||
assertRequiredFields(t, myAppItem,
|
||||
"application_id", "game_id", "applicant_user_id", "race_name",
|
||||
"status", "created_at", "game_name", "game_type",
|
||||
)
|
||||
|
||||
myInviteItem := componentSchemaRef(t, doc, "MyInviteItem")
|
||||
assertRequiredFields(t, myInviteItem,
|
||||
"invite_id", "game_id", "inviter_user_id", "invitee_user_id",
|
||||
"status", "created_at", "expires_at", "game_name", "inviter_name",
|
||||
)
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesMyRaceNamesContract verifies that the
|
||||
// self-service GET endpoint and its response schemas are wired with the
|
||||
// frozen field set.
|
||||
func TestPublicSpecFreezesMyRaceNamesContract(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
op := getOperation(t, doc, "/api/v1/lobby/my/race-names", http.MethodGet)
|
||||
require.Equal(t, "listMyRaceNames", op.OperationID)
|
||||
assertOperationParameterRefs(t, op, "#/components/parameters/XUserID")
|
||||
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK),
|
||||
"#/components/schemas/MyRaceNamesResponse", "listMyRaceNames 200")
|
||||
|
||||
resp := componentSchemaRef(t, doc, "MyRaceNamesResponse")
|
||||
assertRequiredFields(t, resp, "registered", "pending", "reservations")
|
||||
|
||||
pending := componentSchemaRef(t, doc, "PendingRaceName")
|
||||
assertRequiredFields(t, pending,
|
||||
"canonical_key", "race_name", "source_game_id", "eligible_until_ms")
|
||||
require.Contains(t, pending.Value.Properties, "reserved_at_ms",
|
||||
"PendingRaceName.reserved_at_ms must be in properties")
|
||||
|
||||
reservation := componentSchemaRef(t, doc, "RaceNameReservation")
|
||||
assertRequiredFields(t, reservation,
|
||||
"canonical_key", "race_name", "game_id", "game_status")
|
||||
require.Contains(t, reservation.Value.Properties, "reserved_at_ms",
|
||||
"RaceNameReservation.reserved_at_ms must be in properties")
|
||||
}
|
||||
|
||||
// TestPublicSpecFreezesErrorExamples verifies that the component response
|
||||
// examples use the stable error codes defined in README.md.
|
||||
func TestPublicSpecFreezesErrorExamples(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
cases := []struct {
|
||||
response string
|
||||
example string
|
||||
wantCode string
|
||||
}{
|
||||
{"InvalidRequestError", "invalidRequest", "invalid_request"},
|
||||
{"ForbiddenError", "forbidden", "forbidden"},
|
||||
{"NotFoundError", "notFound", "subject_not_found"},
|
||||
{"ConflictError", "conflict", "conflict"},
|
||||
{"InternalError", "internal", "internal_error"},
|
||||
{"ServiceUnavailableError", "unavailable", "service_unavailable"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.response, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
val := responseExampleValue(t, doc, tc.response, tc.example)
|
||||
payload, err := json.Marshal(val)
|
||||
require.NoError(t, err)
|
||||
|
||||
var envelope struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(payload, &envelope))
|
||||
require.Equal(t, tc.wantCode, envelope.Error.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// DomainPreconditionError must contain both eligibility_denied and name_taken examples.
|
||||
eligibilityVal := responseExampleValue(t, doc, "DomainPreconditionError", "eligibilityDenied")
|
||||
eligibilityPayload, err := json.Marshal(eligibilityVal)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(eligibilityPayload), "eligibility_denied")
|
||||
|
||||
nameTakenVal := responseExampleValue(t, doc, "DomainPreconditionError", "nameTaken")
|
||||
nameTakenPayload, err := json.Marshal(nameTakenVal)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(nameTakenPayload), "name_taken")
|
||||
}
|
||||
|
||||
// TestInternalSpecFreezesGMReadContracts verifies the GM-facing read
|
||||
// endpoints: internal game get and internal membership list.
|
||||
func TestInternalSpecFreezesGMReadContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadInternalSpec(t)
|
||||
|
||||
getOp := getOperation(t, doc, "/api/v1/internal/games/{game_id}", http.MethodGet)
|
||||
require.Equal(t, "internalGetGame", getOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, getOp, http.StatusOK), "#/components/schemas/GameRecord", "internalGetGame 200")
|
||||
|
||||
listOp := getOperation(t, doc, "/api/v1/internal/games/{game_id}/memberships", http.MethodGet)
|
||||
require.Equal(t, "internalListMemberships", listOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, listOp, http.StatusOK), "#/components/schemas/MembershipListResponse", "internalListMemberships 200")
|
||||
}
|
||||
|
||||
// TestInternalSpecFreezesAdminMirroredRoutes verifies that a representative
|
||||
// subset of admin-mirrored routes exist with the expected operationIds and
|
||||
// response schemas.
|
||||
func TestInternalSpecFreezesAdminMirroredRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadInternalSpec(t)
|
||||
|
||||
createOp := getOperation(t, doc, "/api/v1/lobby/games", http.MethodPost)
|
||||
require.Equal(t, "adminCreateGame", createOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, createOp, http.StatusCreated), "#/components/schemas/GameRecord", "adminCreateGame 201")
|
||||
|
||||
cancelOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/cancel", http.MethodPost)
|
||||
require.Equal(t, "adminCancelGame", cancelOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, cancelOp, http.StatusOK), "#/components/schemas/GameRecord", "adminCancelGame 200")
|
||||
|
||||
approveOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve", http.MethodPost)
|
||||
require.Equal(t, "adminApproveApplication", approveOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, approveOp, http.StatusOK), "#/components/schemas/MembershipRecord", "adminApproveApplication 200")
|
||||
|
||||
rejectOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject", http.MethodPost)
|
||||
require.Equal(t, "adminRejectApplication", rejectOp.OperationID)
|
||||
assertSchemaRef(t, responseSchemaRef(t, rejectOp, http.StatusOK), "#/components/schemas/ApplicationRecord", "adminRejectApplication 200")
|
||||
}
|
||||
|
||||
// TestPublicSpecDeclaresAllRegisteredRoutes asserts that every HTTP route
|
||||
// registered by lobby/internal/api/publichttp is declared in
|
||||
// public-openapi.yaml. The route table mirrors the mux.HandleFunc calls
|
||||
// in publichttp/{server,games,applications,invites,memberships,mylists,
|
||||
// pause_resume,racenames,ready_to_start,start}.go and must be updated
|
||||
// whenever a new public route is registered.
|
||||
func TestPublicSpecDeclaresAllRegisteredRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadPublicSpec(t)
|
||||
|
||||
for _, r := range publicHTTPRoutes() {
|
||||
t.Run(r.Method+" "+r.Path, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getOperation(t, doc, r.Path, r.Method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInternalSpecDeclaresAllRegisteredRoutes asserts that every HTTP route
|
||||
// registered by lobby/internal/api/internalhttp is declared in
|
||||
// internal-openapi.yaml. The route table mirrors the mux.HandleFunc calls
|
||||
// in internalhttp/{server,games,applications,memberships,pause_resume,
|
||||
// ready_to_start,start}.go and must be updated whenever a new internal
|
||||
// route is registered.
|
||||
func TestInternalSpecDeclaresAllRegisteredRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadInternalSpec(t)
|
||||
|
||||
for _, r := range internalHTTPRoutes() {
|
||||
t.Run(r.Method+" "+r.Path, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getOperation(t, doc, r.Path, r.Method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type httpRoute struct {
|
||||
Method string
|
||||
Path string
|
||||
}
|
||||
|
||||
func publicHTTPRoutes() []httpRoute {
|
||||
return []httpRoute{
|
||||
{http.MethodGet, "/healthz"},
|
||||
{http.MethodGet, "/readyz"},
|
||||
{http.MethodPost, "/api/v1/lobby/games"},
|
||||
{http.MethodGet, "/api/v1/lobby/games"},
|
||||
{http.MethodGet, "/api/v1/lobby/games/{game_id}"},
|
||||
{http.MethodPatch, "/api/v1/lobby/games/{game_id}"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/open-enrollment"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/cancel"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/decline"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/revoke"},
|
||||
{http.MethodGet, "/api/v1/lobby/games/{game_id}/memberships"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/pause"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/resume"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/ready-to-start"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/start"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/retry-start"},
|
||||
{http.MethodPost, "/api/v1/lobby/race-names/register"},
|
||||
{http.MethodGet, "/api/v1/lobby/my/games"},
|
||||
{http.MethodGet, "/api/v1/lobby/my/applications"},
|
||||
{http.MethodGet, "/api/v1/lobby/my/invites"},
|
||||
{http.MethodGet, "/api/v1/lobby/my/race-names"},
|
||||
}
|
||||
}
|
||||
|
||||
func internalHTTPRoutes() []httpRoute {
|
||||
return []httpRoute{
|
||||
{http.MethodGet, "/healthz"},
|
||||
{http.MethodGet, "/readyz"},
|
||||
{http.MethodGet, "/api/v1/internal/games/{game_id}"},
|
||||
{http.MethodGet, "/api/v1/internal/games/{game_id}/memberships"},
|
||||
{http.MethodPost, "/api/v1/lobby/games"},
|
||||
{http.MethodGet, "/api/v1/lobby/games"},
|
||||
{http.MethodGet, "/api/v1/lobby/games/{game_id}"},
|
||||
{http.MethodPatch, "/api/v1/lobby/games/{game_id}"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/open-enrollment"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/cancel"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject"},
|
||||
{http.MethodGet, "/api/v1/lobby/games/{game_id}/memberships"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/pause"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/resume"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/ready-to-start"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/start"},
|
||||
{http.MethodPost, "/api/v1/lobby/games/{game_id}/retry-start"},
|
||||
}
|
||||
}
|
||||
|
||||
// loadPublicSpec loads and validates lobby/api/public-openapi.yaml relative
|
||||
// to this test file.
|
||||
func loadPublicSpec(t *testing.T) *openapi3.T {
|
||||
t.Helper()
|
||||
return loadSpec(t, filepath.Join("api", "public-openapi.yaml"))
|
||||
}
|
||||
|
||||
// loadInternalSpec loads and validates lobby/api/internal-openapi.yaml
|
||||
// relative to this test file.
|
||||
func loadInternalSpec(t *testing.T) *openapi3.T {
|
||||
t.Helper()
|
||||
return loadSpec(t, filepath.Join("api", "internal-openapi.yaml"))
|
||||
}
|
||||
|
||||
func loadSpec(t *testing.T, rel string) *openapi3.T {
|
||||
t.Helper()
|
||||
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
require.FailNow(t, "runtime.Caller failed")
|
||||
}
|
||||
|
||||
specPath := filepath.Join(filepath.Dir(thisFile), rel)
|
||||
loader := openapi3.NewLoader()
|
||||
doc, err := loader.LoadFromFile(specPath)
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "load spec %s: %v", specPath, err)
|
||||
}
|
||||
if doc == nil {
|
||||
require.Failf(t, "test failed", "load spec %s: returned nil document", specPath)
|
||||
}
|
||||
if err := doc.Validate(context.Background()); err != nil {
|
||||
require.Failf(t, "test failed", "validate spec %s: %v", specPath, err)
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func getOperation(t *testing.T, doc *openapi3.T, path, method string) *openapi3.Operation {
|
||||
t.Helper()
|
||||
|
||||
if doc.Paths == nil {
|
||||
require.FailNow(t, "spec is missing paths")
|
||||
}
|
||||
pathItem := doc.Paths.Value(path)
|
||||
if pathItem == nil {
|
||||
require.Failf(t, "test failed", "spec is missing path %s", path)
|
||||
}
|
||||
op := pathItem.GetOperation(method)
|
||||
if op == nil {
|
||||
require.Failf(t, "test failed", "spec is missing %s operation for path %s", method, path)
|
||||
}
|
||||
|
||||
return op
|
||||
}
|
||||
|
||||
func requestSchemaRef(t *testing.T, op *openapi3.Operation) *openapi3.SchemaRef {
|
||||
t.Helper()
|
||||
|
||||
if op.RequestBody == nil || op.RequestBody.Value == nil {
|
||||
require.FailNow(t, "operation is missing request body")
|
||||
}
|
||||
mt := op.RequestBody.Value.Content.Get("application/json")
|
||||
if mt == nil || mt.Schema == nil {
|
||||
require.FailNow(t, "operation is missing application/json request schema")
|
||||
}
|
||||
|
||||
return mt.Schema
|
||||
}
|
||||
|
||||
func responseSchemaRef(t *testing.T, op *openapi3.Operation, status int) *openapi3.SchemaRef {
|
||||
t.Helper()
|
||||
|
||||
ref := op.Responses.Status(status)
|
||||
if ref == nil || ref.Value == nil {
|
||||
require.Failf(t, "test failed", "operation is missing %d response", status)
|
||||
}
|
||||
mt := ref.Value.Content.Get("application/json")
|
||||
if mt == nil || mt.Schema == nil {
|
||||
require.Failf(t, "test failed", "operation is missing application/json schema for %d response", status)
|
||||
}
|
||||
|
||||
return mt.Schema
|
||||
}
|
||||
|
||||
func componentSchemaRef(t *testing.T, doc *openapi3.T, name string) *openapi3.SchemaRef {
|
||||
t.Helper()
|
||||
|
||||
if doc.Components.Schemas == nil {
|
||||
require.FailNow(t, "spec is missing component schemas")
|
||||
}
|
||||
ref := doc.Components.Schemas[name]
|
||||
if ref == nil {
|
||||
require.Failf(t, "test failed", "spec is missing component schema %s", name)
|
||||
}
|
||||
|
||||
return ref
|
||||
}
|
||||
|
||||
func responseExampleValue(t *testing.T, doc *openapi3.T, responseName, exampleName string) any {
|
||||
t.Helper()
|
||||
|
||||
ref := doc.Components.Responses[responseName]
|
||||
if ref == nil || ref.Value == nil {
|
||||
require.Failf(t, "test failed", "spec is missing component response %s", responseName)
|
||||
}
|
||||
mt := ref.Value.Content.Get("application/json")
|
||||
if mt == nil {
|
||||
require.Failf(t, "test failed", "response %s is missing application/json content", responseName)
|
||||
}
|
||||
exRef := mt.Examples[exampleName]
|
||||
if exRef == nil || exRef.Value == nil {
|
||||
require.Failf(t, "test failed", "response %s is missing example %s", responseName, exampleName)
|
||||
}
|
||||
|
||||
return exRef.Value.Value
|
||||
}
|
||||
|
||||
func assertSchemaRef(t *testing.T, schemaRef *openapi3.SchemaRef, want, name string) {
|
||||
t.Helper()
|
||||
require.NotNil(t, schemaRef, "%s schema ref", name)
|
||||
require.Equal(t, want, schemaRef.Ref, "%s schema ref", name)
|
||||
}
|
||||
|
||||
func assertRequiredFields(t *testing.T, schemaRef *openapi3.SchemaRef, fields ...string) {
|
||||
t.Helper()
|
||||
require.NotNil(t, schemaRef)
|
||||
require.ElementsMatch(t, fields, schemaRef.Value.Required)
|
||||
}
|
||||
|
||||
func assertStringEnum(t *testing.T, schemaRef *openapi3.SchemaRef, property string, values ...string) {
|
||||
t.Helper()
|
||||
require.NotNil(t, schemaRef)
|
||||
|
||||
propRef := schemaRef.Value.Properties[property]
|
||||
require.NotNil(t, propRef, "schema property %s", property)
|
||||
|
||||
got := make([]string, 0, len(propRef.Value.Enum))
|
||||
for _, v := range propRef.Value.Enum {
|
||||
got = append(got, v.(string))
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, values, got)
|
||||
}
|
||||
|
||||
func assertOperationParameterRefs(t *testing.T, op *openapi3.Operation, refs ...string) {
|
||||
t.Helper()
|
||||
|
||||
got := make([]string, 0, len(op.Parameters))
|
||||
for _, p := range op.Parameters {
|
||||
got = append(got, p.Ref)
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, refs, got)
|
||||
}
|
||||
Reference in New Issue
Block a user