generated from VLADIMIR/template
up
This commit is contained in:
+17
-1
@@ -675,6 +675,7 @@ service EveningDetectiveServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each request creates a fresh setup package, replacing any unfinished preparation.
|
||||||
rpc StartGameExport(StartGameExportReq) returns (GameExportRsp) {
|
rpc StartGameExport(StartGameExportReq) returns (GameExportRsp) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/api/games/{ID}/export"
|
post: "/api/games/{ID}/export"
|
||||||
@@ -682,6 +683,14 @@ service EveningDetectiveServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release the transfer without discarding ready packages or their future results.
|
||||||
|
rpc ResetGameLocalMode(ResetGameLocalModeReq) returns (ResetGameLocalModeRsp) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/api/games/{ID}/local-mode/reset"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
rpc GetGameExport(GetGameExportReq) returns (GameExportRsp) {
|
rpc GetGameExport(GetGameExportReq) returns (GameExportRsp) {
|
||||||
option (google.api.http) = {get: "/api/games/{ID}/export"};
|
option (google.api.http) = {get: "/api/games/{ID}/export"};
|
||||||
}
|
}
|
||||||
@@ -1719,7 +1728,7 @@ message Game {
|
|||||||
// GET /api/games/{ID}/export/download (not an HttpBody RPC).
|
// GET /api/games/{ID}/export/download (not an HttpBody RPC).
|
||||||
message GameExport {
|
message GameExport {
|
||||||
string ID = 1;
|
string ID = 1;
|
||||||
string state = 2; // preparing, ready or failed; preparing/ready lock game writes.
|
string state = 2; // preparing, ready or failed; ready indicates a completed package.
|
||||||
string packageID = 3;
|
string packageID = 3;
|
||||||
int64 size = 4;
|
int64 size = 4;
|
||||||
string sha256 = 5;
|
string sha256 = 5;
|
||||||
@@ -1736,6 +1745,13 @@ message StartGameExportReq { int32 ID = 1; }
|
|||||||
|
|
||||||
message GetGameExportReq { int32 ID = 1; }
|
message GetGameExportReq { int32 ID = 1; }
|
||||||
|
|
||||||
|
message ResetGameLocalModeReq {
|
||||||
|
int32 ID = 1;
|
||||||
|
string exportID = 2; // Expected latest export, protecting against stale confirmations.
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResetGameLocalModeRsp {}
|
||||||
|
|
||||||
message GameExportRsp { GameExport export = 1; }
|
message GameExportRsp { GameExport export = 1; }
|
||||||
|
|
||||||
enum ScreenMode {
|
enum ScreenMode {
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
|
"summary": "Each request creates a fresh setup package, replacing any unfinished preparation.",
|
||||||
"operationId": "EveningDetectiveServer_StartGameExport",
|
"operationId": "EveningDetectiveServer_StartGameExport",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -761,6 +762,46 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/games/{ID}/local-mode/reset": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Release the transfer without discarding ready packages or their future results.",
|
||||||
|
"operationId": "EveningDetectiveServer_ResetGameLocalMode",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A successful response.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/evening_detective_serverResetGameLocalModeRsp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "An unexpected error response.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/rpcStatus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ID",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/EveningDetectiveServerResetGameLocalModeBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"EveningDetectiveServer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/games/{ID}/pause": {
|
"/api/games/{ID}/pause": {
|
||||||
"put": {
|
"put": {
|
||||||
"summary": "Остановить игру",
|
"summary": "Остановить игру",
|
||||||
@@ -4209,6 +4250,15 @@
|
|||||||
"EveningDetectiveServerResetGameBody": {
|
"EveningDetectiveServerResetGameBody": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"EveningDetectiveServerResetGameLocalModeBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exportID": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expected latest export, protecting against stale confirmations."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"EveningDetectiveServerSaveScreenPlanBody": {
|
"EveningDetectiveServerSaveScreenPlanBody": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -5043,7 +5093,7 @@
|
|||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "preparing, ready or failed; preparing/ready lock game writes."
|
"description": "preparing, ready or failed; ready indicates a completed package."
|
||||||
},
|
},
|
||||||
"packageID": {
|
"packageID": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -5831,6 +5881,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverResetGameLocalModeRsp": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"evening_detective_serverResetGameRsp": {
|
"evening_detective_serverResetGameRsp": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,6 +51,20 @@ func (s *server) GetGameExport(ctx context.Context, req *proto.GetGameExportReq)
|
|||||||
return &proto.GameExportRsp{Export: mapGameExport(result)}, nil
|
return &proto.GameExportRsp{Export: mapGameExport(result)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *server) ResetGameLocalMode(ctx context.Context, req *proto.ResetGameLocalModeReq) (*proto.ResetGameLocalModeRsp, error) {
|
||||||
|
claims, err := claimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !canExportGame(claims) {
|
||||||
|
return nil, status.Error(codes.PermissionDenied, "permission denied")
|
||||||
|
}
|
||||||
|
if err := s.gameExportService.ResetLocalMode(ctx, int(req.GetID()), req.GetExportID()); err != nil {
|
||||||
|
return nil, gameExportErrorStatus(err)
|
||||||
|
}
|
||||||
|
return &proto.ResetGameLocalModeRsp{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GameExportDownloadHandler authenticates with the existing JWT processor and streams private ZIPs.
|
// GameExportDownloadHandler authenticates with the existing JWT processor and streams private ZIPs.
|
||||||
func GameExportDownloadHandler(jwt processor_jwt.IProcessorJWT, exports *game_export_service.GameExportService) http.Handler {
|
func GameExportDownloadHandler(jwt processor_jwt.IProcessorJWT, exports *game_export_service.GameExportService) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ func gameExportErrorStatus(err error) error {
|
|||||||
return status.Error(codes.FailedPrecondition, game_export_service.ErrVenueMode.Error())
|
return status.Error(codes.FailedPrecondition, game_export_service.ErrVenueMode.Error())
|
||||||
case errors.Is(err, game_export_service.ErrNotReady):
|
case errors.Is(err, game_export_service.ErrNotReady):
|
||||||
return status.Error(codes.FailedPrecondition, game_export_service.ErrNotReady.Error())
|
return status.Error(codes.FailedPrecondition, game_export_service.ErrNotReady.Error())
|
||||||
case errors.Is(err, game_export_service.ErrNotExportable):
|
case errors.Is(err, game_export_service.ErrExportChanged):
|
||||||
return status.Error(codes.FailedPrecondition, game_export_service.ErrNotExportable.Error())
|
return status.Error(codes.Aborted, game_export_service.ErrExportChanged.Error())
|
||||||
case errors.Is(err, game_export_service.ErrInvalidGameID):
|
case errors.Is(err, game_export_service.ErrInvalidGameID):
|
||||||
return status.Error(codes.InvalidArgument, game_export_service.ErrInvalidGameID.Error())
|
return status.Error(codes.InvalidArgument, game_export_service.ErrInvalidGameID.Error())
|
||||||
|
case errors.Is(err, game_export_service.ErrInvalidExportID):
|
||||||
|
return status.Error(codes.InvalidArgument, game_export_service.ErrInvalidExportID.Error())
|
||||||
default:
|
default:
|
||||||
log.Printf("game export API: %v", err)
|
log.Printf("game export API: %v", err)
|
||||||
return status.Error(codes.Internal, "Не удалось выполнить операцию экспорта игры")
|
return status.Error(codes.Internal, "Не удалось выполнить операцию экспорта игры")
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ func TestExportRolesAndVenueMode(t *testing.T) {
|
|||||||
if status.Code(err) != tc.code {
|
if status.Code(err) != tc.code {
|
||||||
t.Fatalf("status access: %v", err)
|
t.Fatalf("status access: %v", err)
|
||||||
}
|
}
|
||||||
|
_, err = server.ResetGameLocalMode(ctx, &proto.ResetGameLocalModeReq{ID: 1, ExportID: "export"})
|
||||||
|
if status.Code(err) != tc.code {
|
||||||
|
t.Fatalf("reset access: %v", err)
|
||||||
|
}
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/games/1/export/download", nil)
|
request := httptest.NewRequest(http.MethodGet, "/api/games/1/export/download", nil)
|
||||||
request.SetPathValue("id", "1")
|
request.SetPathValue("id", "1")
|
||||||
if tc.role != "" {
|
if tc.role != "" {
|
||||||
@@ -71,7 +75,8 @@ func TestExportErrorsAreSafe(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"game missing", game_export_service.ErrGameNotFound, codes.NotFound},
|
{"game missing", game_export_service.ErrGameNotFound, codes.NotFound},
|
||||||
{"export missing", game_export_service.ErrExportNotFound, codes.NotFound},
|
{"export missing", game_export_service.ErrExportNotFound, codes.NotFound},
|
||||||
{"progress", game_export_service.ErrNotExportable, codes.FailedPrecondition},
|
{"changed export", game_export_service.ErrExportChanged, codes.Aborted},
|
||||||
|
{"invalid export", game_export_service.ErrInvalidExportID, codes.InvalidArgument},
|
||||||
{"not ready", game_export_service.ErrNotReady, codes.FailedPrecondition},
|
{"not ready", game_export_service.ErrNotReady, codes.FailedPrecondition},
|
||||||
{"venue", game_export_service.ErrVenueMode, codes.FailedPrecondition},
|
{"venue", game_export_service.ErrVenueMode, codes.FailedPrecondition},
|
||||||
{"invalid game", game_export_service.ErrInvalidGameID, codes.InvalidArgument},
|
{"invalid game", game_export_service.ErrInvalidGameID, codes.InvalidArgument},
|
||||||
|
|||||||
@@ -104,3 +104,27 @@ func (f exportFixture) start(t *testing.T, id string) *Attempt {
|
|||||||
requireNoError(t, err)
|
requireNoError(t, err)
|
||||||
return attempt
|
return attempt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// releaseMigration exercises transactional schema changes without querying application data.
|
||||||
|
func (f exportFixture) releaseMigration(t *testing.T, down bool) error {
|
||||||
|
t.Helper()
|
||||||
|
raw, err := os.ReadFile("../../../migrations/20261017120000_release_game_exports.sql")
|
||||||
|
requireNoError(t, err)
|
||||||
|
parts := strings.Split(string(raw), "-- +goose Down")
|
||||||
|
sql := parts[0]
|
||||||
|
if down {
|
||||||
|
sql = parts[1]
|
||||||
|
}
|
||||||
|
tx, err := f.pool.Begin(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(context.Background()) }()
|
||||||
|
if _, err := tx.Exec(
|
||||||
|
context.Background(),
|
||||||
|
sql,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit(context.Background())
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrGameNotFound = errors.New("игра не найдена")
|
ErrGameNotFound = errors.New("игра не найдена")
|
||||||
ErrNotExportable = errors.New("экспорт доступен только для игры в черновике без дат проведения и прогресса")
|
ErrExportChanged = errors.New("экспорт игры изменился; обновите страницу и повторите действие")
|
||||||
ErrExportNotFound = errors.New("экспорт игры ещё не запущен")
|
ErrExportNotFound = errors.New("экспорт игры ещё не запущен")
|
||||||
ErrOwnershipLost = errors.New("владение экспортом потеряно")
|
ErrOwnershipLost = errors.New("владение экспортом потеряно")
|
||||||
)
|
)
|
||||||
@@ -45,79 +45,43 @@ func NewGameExportsRepo(pool *pgxpool.Pool) *GameExportsRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start locks the game before inspecting progress, then saves all package data in one SQL snapshot.
|
// Start serializes replacements on the game and saves a fresh, consistent package snapshot.
|
||||||
func (r *GameExportsRepo) Start(ctx context.Context, gameID, userID int, id, source string) (*Attempt, error) {
|
func (r *GameExportsRepo) Start(ctx context.Context, gameID, userID int, id, source string) (*Attempt, error) {
|
||||||
tx, err := r.pool.Begin(ctx)
|
tx, err := r.pool.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() { _ = tx.Rollback(ctx) }()
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
var status string
|
var lockedID int
|
||||||
var started, ended *time.Time
|
|
||||||
err = tx.QueryRow(
|
err = tx.QueryRow(
|
||||||
ctx,
|
ctx,
|
||||||
`SELECT
|
`SELECT
|
||||||
status,
|
id
|
||||||
started_at,
|
|
||||||
ended_at
|
|
||||||
FROM games
|
FROM games
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
FOR UPDATE`,
|
FOR UPDATE`,
|
||||||
gameID,
|
gameID,
|
||||||
).Scan(&status, &started, &ended)
|
).Scan(&lockedID)
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return nil, ErrGameNotFound
|
return nil, ErrGameNotFound
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
existing, err := readAttempt(ctx, tx, gameID, true, true)
|
// Retain ready packages for late results; fence unfinished workers before replacing them.
|
||||||
if err == nil {
|
_, err = tx.Exec(
|
||||||
return existing, tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
if !errors.Is(err, ErrExportNotFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if status != "draft" || started != nil || ended != nil {
|
|
||||||
return nil, ErrNotExportable
|
|
||||||
}
|
|
||||||
var progress bool
|
|
||||||
err = tx.QueryRow(
|
|
||||||
ctx,
|
ctx,
|
||||||
`SELECT
|
`UPDATE game_exports
|
||||||
EXISTS (
|
SET released_at = clock_timestamp(),
|
||||||
SELECT
|
state = CASE WHEN state = 'preparing' THEN 'failed' ELSE state END,
|
||||||
1
|
failure = CASE WHEN state = 'preparing' THEN 'Подготовка заменена новым пакетом.' ELSE failure END,
|
||||||
FROM teams t
|
owner_id = NULL, lease_until = NULL
|
||||||
WHERE t.game_id = $1 AND (
|
WHERE game_id = $1 AND released_at IS NULL AND state IN ('preparing', 'ready')`,
|
||||||
EXISTS (
|
|
||||||
SELECT
|
|
||||||
1
|
|
||||||
FROM actions a
|
|
||||||
WHERE a.team_id = t.id
|
|
||||||
)
|
|
||||||
OR EXISTS (
|
|
||||||
SELECT
|
|
||||||
1
|
|
||||||
FROM applications a
|
|
||||||
WHERE a.team_id = t.id
|
|
||||||
)
|
|
||||||
OR EXISTS (
|
|
||||||
SELECT
|
|
||||||
1
|
|
||||||
FROM team_answers a
|
|
||||||
WHERE a.team_id = t.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)`,
|
|
||||||
gameID,
|
gameID,
|
||||||
).Scan(&progress)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if progress {
|
|
||||||
return nil, ErrNotExportable
|
|
||||||
}
|
|
||||||
snapshot, err := captureSnapshot(ctx, tx, gameID)
|
snapshot, err := captureSnapshot(ctx, tx, gameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -147,6 +111,75 @@ func (r *GameExportsRepo) Get(ctx context.Context, gameID int) (*Attempt, error)
|
|||||||
return readAttempt(ctx, r.pool, gameID, false, false)
|
return readAttempt(ctx, r.pool, gameID, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetLocalMode releases only the expected latest export, preserving ready archives for late results.
|
||||||
|
func (r *GameExportsRepo) ResetLocalMode(ctx context.Context, gameID int, exportID string) error {
|
||||||
|
tx, err := r.pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
|
var lockedID int
|
||||||
|
err = tx.QueryRow(
|
||||||
|
ctx,
|
||||||
|
`SELECT
|
||||||
|
id
|
||||||
|
FROM games
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE`,
|
||||||
|
gameID,
|
||||||
|
).Scan(&lockedID)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrGameNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var latestID, state string
|
||||||
|
var releasedAt *time.Time
|
||||||
|
err = tx.QueryRow(
|
||||||
|
ctx,
|
||||||
|
`SELECT
|
||||||
|
id,
|
||||||
|
state,
|
||||||
|
released_at
|
||||||
|
FROM game_exports
|
||||||
|
WHERE game_id = $1
|
||||||
|
ORDER BY created_at DESC, id
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE`,
|
||||||
|
gameID,
|
||||||
|
).Scan(&latestID, &state, &releasedAt)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrExportNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if latestID != exportID {
|
||||||
|
return ErrExportChanged
|
||||||
|
}
|
||||||
|
if releasedAt != nil {
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
|
if state != "ready" && state != "preparing" {
|
||||||
|
return ErrExportChanged
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`UPDATE game_exports
|
||||||
|
SET released_at = clock_timestamp(),
|
||||||
|
state = CASE WHEN state = 'preparing' THEN 'failed' ELSE state END,
|
||||||
|
failure = CASE WHEN state = 'preparing' THEN 'Подготовка отменена при сбросе локального режима.' ELSE failure END,
|
||||||
|
owner_id = NULL, lease_until = NULL
|
||||||
|
WHERE id = $1`,
|
||||||
|
exportID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// IRowReader is implemented by both the pool and a transaction.
|
// IRowReader is implemented by both the pool and a transaction.
|
||||||
type IRowReader interface {
|
type IRowReader interface {
|
||||||
QueryRow(context.Context, string, ...any) pgx.Row
|
QueryRow(context.Context, string, ...any) pgx.Row
|
||||||
@@ -169,7 +202,7 @@ func readAttempt(ctx context.Context, reader IRowReader, gameID int, active, wit
|
|||||||
object_key,
|
object_key,
|
||||||
created_at
|
created_at
|
||||||
FROM game_exports
|
FROM game_exports
|
||||||
WHERE game_id = $1 AND (NOT $2 OR state IN ('preparing', 'ready'))
|
WHERE game_id = $1 AND (NOT $2 OR (state IN ('preparing', 'ready') AND released_at IS NULL))
|
||||||
ORDER BY created_at DESC, id
|
ORDER BY created_at DESC, id
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
gameID,
|
gameID,
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ func TestExportSnapshotAndEditableGame(t *testing.T) {
|
|||||||
requireNoError(t, mapRepo.Update(ctx, game_map_repo.Item{GameID: f.gameID, Kind: "team", ItemID: f.teamID, X: 10}))
|
requireNoError(t, mapRepo.Update(ctx, game_map_repo.Item{GameID: f.gameID, Kind: "team", ItemID: f.teamID, X: 10}))
|
||||||
_, err = reviews.Moderate(ctx, review.ID, "approved")
|
_, err = reviews.Moderate(ctx, review.ID, "approved")
|
||||||
requireNoError(t, err)
|
requireNoError(t, err)
|
||||||
same, err := f.exports.Start(ctx, f.gameID, f.userID, "not-a-new-attempt", "test")
|
same, err := f.exports.Get(ctx, f.gameID)
|
||||||
requireNoError(t, err)
|
requireNoError(t, err)
|
||||||
if same.ID != attempt.ID || !bytes.Equal(same.Snapshot, attempt.Snapshot) {
|
if same.ID != attempt.ID {
|
||||||
t.Fatal("online edits changed an existing export snapshot")
|
t.Fatal("online edits changed an existing export snapshot")
|
||||||
}
|
}
|
||||||
requireNoError(t, f.scenarios.UpdateScenarioByID(ctx, f.scenarioID, "Edited shared story", "", ""))
|
requireNoError(t, f.scenarios.UpdateScenarioByID(ctx, f.scenarioID, "Edited shared story", "", ""))
|
||||||
@@ -163,12 +163,9 @@ func TestExportConcurrentStartAndTeamWrites(t *testing.T) {
|
|||||||
if changeErr != nil {
|
if changeErr != nil {
|
||||||
t.Fatal(changeErr)
|
t.Fatal(changeErr)
|
||||||
}
|
}
|
||||||
var id string
|
ids := map[string]bool{}
|
||||||
for attempt := range attempts {
|
for attempt := range attempts {
|
||||||
if id != "" && id != attempt.ID {
|
ids[attempt.ID] = true
|
||||||
t.Fatal("more than one active export")
|
|
||||||
}
|
|
||||||
id = attempt.ID
|
|
||||||
var snapshot struct {
|
var snapshot struct {
|
||||||
Teams []struct {
|
Teams []struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -179,11 +176,26 @@ func TestExportConcurrentStartAndTeamWrites(t *testing.T) {
|
|||||||
t.Fatalf("unexpected team snapshot: %s", snapshot.Teams[0].Name)
|
t.Fatalf("unexpected team snapshot: %s", snapshot.Teams[0].Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(ids) != 8 {
|
||||||
|
t.Fatalf("expected fresh attempts, got %d", len(ids))
|
||||||
|
}
|
||||||
|
current, err := f.exports.Get(ctx, f.gameID)
|
||||||
|
requireNoError(t, err)
|
||||||
|
claimed, err := f.exports.Claim(ctx, "worker")
|
||||||
|
requireNoError(t, err)
|
||||||
|
if claimed.ID != current.ID {
|
||||||
|
t.Fatal("worker claimed an obsolete attempt")
|
||||||
|
}
|
||||||
|
_, err = f.exports.Claim(ctx, "other-worker")
|
||||||
|
if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
t.Fatalf("more than one active export: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExportRejectsProgressAndRunningGames(t *testing.T) {
|
func TestExportAllowsProgressAndRunningGames(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
mutate func(exportFixture) error
|
mutate func(exportFixture) error
|
||||||
@@ -205,9 +217,7 @@ func TestExportRejectsProgressAndRunningGames(t *testing.T) {
|
|||||||
f := newExportFixture(t)
|
f := newExportFixture(t)
|
||||||
requireNoError(t, tc.mutate(f))
|
requireNoError(t, tc.mutate(f))
|
||||||
_, err := f.exports.Start(context.Background(), f.gameID, f.userID, "attempt", "https://game.example")
|
_, err := f.exports.Start(context.Background(), f.gameID, f.userID, "attempt", "https://game.example")
|
||||||
if !errors.Is(err, ErrNotExportable) {
|
requireNoError(t, err)
|
||||||
t.Fatalf("export allowed: %v", err)
|
|
||||||
}
|
|
||||||
requireNoError(t, f.teams.UpdateTeam(context.Background(), f.teamID, "Still writable"))
|
requireNoError(t, f.teams.UpdateTeam(context.Background(), f.teamID, "Still writable"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package game_exports_repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResetLocalModePreservesReadyPackageAndGame(t *testing.T) {
|
||||||
|
for _, name := range []string{"ResetLocalModePreservesReadyPackageAndGame"} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
f := newExportFixture(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
attempt := f.start(t, "ready-attempt")
|
||||||
|
_, err := f.exports.Claim(ctx, "worker")
|
||||||
|
requireNoError(t, err)
|
||||||
|
requireNoError(t, f.exports.Complete(ctx, attempt.ID, "worker", "package", "digest", 123))
|
||||||
|
before, err := f.games.GetGameByID(ctx, f.gameID)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if before.Export == nil {
|
||||||
|
t.Fatal("missing local mode before reset")
|
||||||
|
}
|
||||||
|
requireNoError(t, f.exports.ResetLocalMode(ctx, f.gameID, attempt.ID))
|
||||||
|
requireNoError(t, f.exports.ResetLocalMode(ctx, f.gameID, attempt.ID))
|
||||||
|
after, err := f.games.GetGameByID(ctx, f.gameID)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if after.Export != nil || after.Status != before.Status || after.Name != before.Name {
|
||||||
|
t.Fatal("reset retained local mode or changed the game")
|
||||||
|
}
|
||||||
|
games, err := f.games.GetGames(ctx)
|
||||||
|
requireNoError(t, err)
|
||||||
|
for _, game := range games {
|
||||||
|
if game.ID == f.gameID && game.Export != nil {
|
||||||
|
t.Fatal("game list retained local mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saved, err := f.exports.Get(ctx, f.gameID)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if saved.ID != attempt.ID || saved.State != "ready" || saved.PackageID != "package" {
|
||||||
|
t.Fatal("reset damaged the package needed for late results")
|
||||||
|
}
|
||||||
|
next := f.start(t, "new-attempt")
|
||||||
|
if err := f.exports.ResetLocalMode(ctx, f.gameID, attempt.ID); !errors.Is(err, ErrExportChanged) {
|
||||||
|
t.Fatalf("stale confirmation accepted: %v", err)
|
||||||
|
}
|
||||||
|
current, err := f.exports.Get(ctx, f.gameID)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if current.ID != next.ID || current.State != "preparing" {
|
||||||
|
t.Fatal("stale confirmation changed new export")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResetAndReplacementFencePreparingWorker(t *testing.T) {
|
||||||
|
for _, replace := range []bool{false, true} {
|
||||||
|
t.Run(map[bool]string{false: "reset", true: "replace"}[replace], func(t *testing.T) {
|
||||||
|
f := newExportFixture(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
attempt := f.start(t, "old")
|
||||||
|
_, err := f.exports.Claim(ctx, "worker")
|
||||||
|
requireNoError(t, err)
|
||||||
|
if replace {
|
||||||
|
f.start(t, "new")
|
||||||
|
} else {
|
||||||
|
requireNoError(t, f.exports.ResetLocalMode(ctx, f.gameID, attempt.ID))
|
||||||
|
}
|
||||||
|
if err := f.exports.Complete(ctx, attempt.ID, "worker", "package", "digest", 123); !errors.Is(err, ErrOwnershipLost) {
|
||||||
|
t.Fatalf("cancelled worker published: %v", err)
|
||||||
|
}
|
||||||
|
if err := f.exports.Heartbeat(ctx, attempt.ID, "worker"); !errors.Is(err, ErrOwnershipLost) {
|
||||||
|
t.Fatalf("cancelled worker renewed lease: %v", err)
|
||||||
|
}
|
||||||
|
keys, err := f.exports.CleanupKeys(ctx)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if len(keys) != 1 || keys[0] != attempt.ObjectKey {
|
||||||
|
t.Fatalf("cancelled object cleanup lost: %v", keys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResetConcurrentReplacement(t *testing.T) {
|
||||||
|
for _, name := range []string{"ResetConcurrentReplacement"} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
f := newExportFixture(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
attempt := f.start(t, "old")
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var resetErr, startErr error
|
||||||
|
wg.Add(2)
|
||||||
|
go func() { defer wg.Done(); resetErr = f.exports.ResetLocalMode(ctx, f.gameID, attempt.ID) }()
|
||||||
|
go func() { defer wg.Done(); _, startErr = f.exports.Start(ctx, f.gameID, f.userID, "new", "source") }()
|
||||||
|
wg.Wait()
|
||||||
|
requireNoError(t, startErr)
|
||||||
|
if resetErr != nil && !errors.Is(resetErr, ErrExportChanged) {
|
||||||
|
t.Fatal(resetErr)
|
||||||
|
}
|
||||||
|
game, err := f.games.GetGameByID(ctx, f.gameID)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if game.Export == nil || game.Export.ID != "new" {
|
||||||
|
t.Fatal("concurrent reset released the new export")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResetMissingGameOrExport(t *testing.T) {
|
||||||
|
for _, name := range []string{"ResetMissingGameOrExport"} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
f := newExportFixture(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := f.exports.ResetLocalMode(ctx, f.gameID+1, "missing"); !errors.Is(err, ErrGameNotFound) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.exports.ResetLocalMode(ctx, f.gameID, "missing"); !errors.Is(err, ErrExportNotFound) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleaseMigrationRollback(t *testing.T) {
|
||||||
|
for _, name := range []string{"ReleaseMigrationRollback"} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
f := newExportFixture(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
requireNoError(t, f.releaseMigration(t, true))
|
||||||
|
requireNoError(t, f.releaseMigration(t, false))
|
||||||
|
attempt := f.start(t, "first")
|
||||||
|
_, err := f.exports.Claim(ctx, "worker")
|
||||||
|
requireNoError(t, err)
|
||||||
|
requireNoError(t, f.exports.Complete(ctx, attempt.ID, "worker", "package-1", "digest", 1))
|
||||||
|
second := f.start(t, "second")
|
||||||
|
_, err = f.exports.Claim(ctx, "worker")
|
||||||
|
requireNoError(t, err)
|
||||||
|
requireNoError(t, f.exports.Complete(ctx, second.ID, "worker", "package-2", "digest", 1))
|
||||||
|
// Rollback must refuse incompatible history without removing the current export or schema.
|
||||||
|
if err := f.releaseMigration(t, true); err == nil {
|
||||||
|
t.Fatal("rollback allowed incompatible ready history")
|
||||||
|
}
|
||||||
|
current, err := f.exports.Get(ctx, f.gameID)
|
||||||
|
requireNoError(t, err)
|
||||||
|
if current.ID != second.ID || current.State != "ready" {
|
||||||
|
t.Fatal("rollback damaged export history")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ func (r *GamesRepo) GetGameByID(
|
|||||||
games.started_at,
|
games.started_at,
|
||||||
games.ended_at,
|
games.ended_at,
|
||||||
(SELECT
|
(SELECT
|
||||||
CASE WHEN EXISTS (
|
CASE WHEN e.released_at IS NOT NULL OR EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM game_returns r
|
FROM game_returns r
|
||||||
WHERE r.export_id = e.id
|
WHERE r.export_id = e.id
|
||||||
@@ -152,7 +152,7 @@ func (r *GamesRepo) GetGames(
|
|||||||
games.format,
|
games.format,
|
||||||
games.status,
|
games.status,
|
||||||
(SELECT
|
(SELECT
|
||||||
CASE WHEN EXISTS (
|
CASE WHEN e.released_at IS NOT NULL OR EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM game_returns r
|
FROM game_returns r
|
||||||
WHERE r.export_id = e.id
|
WHERE r.export_id = e.id
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func TestAnswerAccessPostgres(t *testing.T) {
|
|||||||
defer pool.Close()
|
defer pool.Close()
|
||||||
_, err = pool.Exec(ctx, `
|
_, err = pool.Exec(ctx, `
|
||||||
CREATE TEMP TABLE game_returns(export_id TEXT);
|
CREATE TEMP TABLE game_returns(export_id TEXT);
|
||||||
CREATE TEMP TABLE game_exports(id TEXT, game_id INT, state TEXT, package_id TEXT, size BIGINT, sha256 TEXT, failure TEXT, created_at TIMESTAMPTZ);
|
CREATE TEMP TABLE game_exports(id TEXT, game_id INT, state TEXT, package_id TEXT, size BIGINT, sha256 TEXT, failure TEXT, created_at TIMESTAMPTZ, released_at TIMESTAMPTZ);
|
||||||
CREATE TEMP TABLE games (id INT, name TEXT, description TEXT, location TEXT, start_at TIMESTAMP,
|
CREATE TEMP TABLE games (id INT, name TEXT, description TEXT, location TEXT, start_at TIMESTAMP,
|
||||||
format TEXT, status TEXT, scenario_id INT, started_at TIMESTAMP, ended_at TIMESTAMP);
|
format TEXT, status TEXT, scenario_id INT, started_at TIMESTAMP, ended_at TIMESTAMP);
|
||||||
CREATE TEMP TABLE teams (id UUID, name TEXT, captain_email TEXT, participants_count INT,
|
CREATE TEMP TABLE teams (id UUID, name TEXT, captain_email TEXT, participants_count INT,
|
||||||
|
|||||||
@@ -218,16 +218,22 @@ func TestExportBuildsSavedSnapshotAcrossWorkerRestart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
first := verifyPackage(t, f, restartedWorker)
|
first := verifyPackage(t, f, restartedWorker)
|
||||||
verifyExportedGameReads(t, f, restartedWorker, original.ID)
|
verifyExportedGameReads(t, f, restartedWorker, original.ID)
|
||||||
repeated, err := restartedWorker.Start(context.Background(), f.gameID, f.userID)
|
|
||||||
check(t, err)
|
|
||||||
if repeated.ID != original.ID || repeated.State != "ready" {
|
|
||||||
t.Fatal("ready export not idempotent")
|
|
||||||
}
|
|
||||||
_, second := download(t, f.service(storage), f.gameID)
|
_, second := download(t, f.service(storage), f.gameID)
|
||||||
if !bytes.Equal(first, second) {
|
if !bytes.Equal(first, second) {
|
||||||
t.Fatal("download changed after restart")
|
t.Fatal("download changed after restart")
|
||||||
}
|
}
|
||||||
assertAbortedDownload(t, f, restartedWorker, first)
|
assertAbortedDownload(t, f, restartedWorker, first)
|
||||||
|
repeated, err := restartedWorker.Start(context.Background(), f.gameID, f.userID)
|
||||||
|
check(t, err)
|
||||||
|
if repeated.ID == original.ID || repeated.State != "preparing" {
|
||||||
|
t.Fatal("new preparation reused old export")
|
||||||
|
}
|
||||||
|
_, err = restartedWorker.ProcessNext(context.Background())
|
||||||
|
check(t, err)
|
||||||
|
_, changed := download(t, restartedWorker, f.gameID)
|
||||||
|
if bytes.Equal(first, changed) {
|
||||||
|
t.Fatal("new preparation reused old bytes")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package game_export_service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"evening_detective_server/internal/repos"
|
||||||
|
"evening_detective_server/internal/repos/actions_repo"
|
||||||
|
"evening_detective_server/internal/repos/game_imports_repo"
|
||||||
|
"evening_detective_server/internal/repos/game_screen_plans_repo"
|
||||||
|
"evening_detective_server/internal/repos/team_answers_repo"
|
||||||
|
"evening_detective_server/internal/services/game_export_service"
|
||||||
|
"evening_detective_server/internal/services/game_import_service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResetLocalModeValidation(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
venue bool
|
||||||
|
gameID int
|
||||||
|
exportID string
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{"venue", true, 1, "export", game_export_service.ErrVenueMode},
|
||||||
|
{"invalid game", false, 0, "export", game_export_service.ErrInvalidGameID},
|
||||||
|
{"empty export", false, 1, " ", game_export_service.ErrInvalidExportID},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
service := game_export_service.NewGameExportService(nil, nil, nil, nil, "", tc.venue)
|
||||||
|
if err := service.ResetLocalMode(context.Background(), tc.gameID, tc.exportID); !errors.Is(err, tc.want) {
|
||||||
|
t.Fatalf("unexpected validation: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExportAnyStatusProducesFreshSetup(t *testing.T) {
|
||||||
|
for _, format := range []string{"online", "offline"} {
|
||||||
|
for _, state := range []string{"draft", "run", "pause", "finish"} {
|
||||||
|
t.Run(format+"/"+state, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
f := newServiceFixture(t)
|
||||||
|
check(t, f.games.UpdateGame(ctx, f.gameID, "Test game", "Description", nil, time.Now(), int32(f.scenarioID), format))
|
||||||
|
if state != "draft" {
|
||||||
|
check(t, f.games.StartGame(ctx, f.gameID))
|
||||||
|
if state == "pause" {
|
||||||
|
check(t, f.games.PauseGame(ctx, f.gameID))
|
||||||
|
}
|
||||||
|
if state == "finish" {
|
||||||
|
check(t, f.games.FinishGame(ctx, f.gameID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := actions_repo.NewActionsRepo(f.pool).AddAction(ctx, f.teamID, "p", false)
|
||||||
|
check(t, err)
|
||||||
|
check(t, team_answers_repo.NewTeamAnswersRepo(f.pool).UpsertAnswers(ctx, f.teamID, []repos.TeamAnswer{{QuestionCode: "q", Text: "Saved answer"}}))
|
||||||
|
_, err = game_screen_plans_repo.NewGameScreenPlansRepo(f.pool).Save(ctx, f.gameID,
|
||||||
|
[]game_screen_plans_repo.Step{{Mode: 1}, {Mode: 2}}, 1)
|
||||||
|
check(t, err)
|
||||||
|
before, err := f.games.GetGameByID(ctx, f.gameID)
|
||||||
|
check(t, err)
|
||||||
|
storage := &memoryStorage{objects: map[string][]byte{}}
|
||||||
|
exporter := f.service(storage)
|
||||||
|
_, err = exporter.Start(ctx, f.gameID, f.userID)
|
||||||
|
check(t, err)
|
||||||
|
_, err = exporter.ProcessNext(ctx)
|
||||||
|
check(t, err)
|
||||||
|
_, raw := download(t, exporter, f.gameID)
|
||||||
|
bundle, err := f.codec.Unpack(ctx, bytes.NewReader(raw), int64(len(raw)), t.TempDir())
|
||||||
|
check(t, err)
|
||||||
|
defer func() { _ = os.RemoveAll(bundle.Dir) }()
|
||||||
|
game := bundle.Data.Game
|
||||||
|
if game.Status != "draft" || game.StartedAt != nil || game.EndedAt != nil || game.Format != format ||
|
||||||
|
bundle.Data.Teams[0].Status != "register" || bundle.Data.ScreenPlan.CurrentStep != 0 {
|
||||||
|
t.Fatalf("package retained source progress: %+v", bundle.Data)
|
||||||
|
}
|
||||||
|
after, err := f.games.GetGameByID(ctx, f.gameID)
|
||||||
|
check(t, err)
|
||||||
|
before.Export, after.Export = nil, nil
|
||||||
|
if !reflect.DeepEqual(before, after) {
|
||||||
|
t.Fatal("export mutated the source game")
|
||||||
|
}
|
||||||
|
actions, err := actions_repo.NewActionsRepo(f.pool).GetActionsByTeamID(ctx, f.teamID)
|
||||||
|
check(t, err)
|
||||||
|
answers, err := team_answers_repo.NewTeamAnswersRepo(f.pool).GetAnswersByTeamID(ctx, f.teamID)
|
||||||
|
check(t, err)
|
||||||
|
if len(actions) != 1 || len(answers) != 1 {
|
||||||
|
t.Fatal("export removed source progress")
|
||||||
|
}
|
||||||
|
// Import into an independent venue schema using the real importer.
|
||||||
|
venue := newServiceFixture(t)
|
||||||
|
check(t, venue.teams.DeleteTeam(ctx, venue.teamID))
|
||||||
|
check(t, venue.games.DeleteGameByID(ctx, venue.gameID))
|
||||||
|
importer := game_import_service.NewGameImportService(game_imports_repo.NewGameImportsRepo(venue.pool), venue.codec,
|
||||||
|
&memoryStorage{objects: map[string][]byte{}}, true)
|
||||||
|
imported, err := importer.Import(ctx, bytes.NewReader(raw), venue.userID)
|
||||||
|
check(t, err)
|
||||||
|
local, err := venue.games.GetGameByID(ctx, imported.GameID)
|
||||||
|
check(t, err)
|
||||||
|
if local.Status != "draft" || local.StartedAt != nil || local.EndedAt != nil {
|
||||||
|
t.Fatalf("import did not start fresh: %+v", local)
|
||||||
|
}
|
||||||
|
actions, err = actions_repo.NewActionsRepo(venue.pool).GetActionsByTeamID(ctx, f.teamID)
|
||||||
|
check(t, err)
|
||||||
|
answers, err = team_answers_repo.NewTeamAnswersRepo(venue.pool).GetAnswersByTeamID(ctx, f.teamID)
|
||||||
|
check(t, err)
|
||||||
|
if len(actions) != 0 || len(answers) != 0 {
|
||||||
|
t.Fatal("import carried source progress")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultsFromFreshPackageOfFinishedGame(t *testing.T) {
|
||||||
|
for _, format := range []string{"online", "offline"} {
|
||||||
|
t.Run(format, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
f := newReturnFixture(t, format)
|
||||||
|
check(t, f.games.StartGame(ctx, f.gameID))
|
||||||
|
check(t, f.games.FinishGame(ctx, f.gameID))
|
||||||
|
exporter := f.service(f.storage)
|
||||||
|
_, err := exporter.Start(ctx, f.gameID, f.userID)
|
||||||
|
check(t, err)
|
||||||
|
_, err = exporter.ProcessNext(ctx)
|
||||||
|
check(t, err)
|
||||||
|
export, _ := download(t, exporter, f.gameID)
|
||||||
|
f.manifest.SourcePackageID = export.PackageID
|
||||||
|
f.manifest.SourceSHA256 = export.SHA256
|
||||||
|
check(t, exporter.ResetLocalMode(ctx, f.gameID, export.ID))
|
||||||
|
_, err = f.apply(f.pack(t))
|
||||||
|
check(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLateResultsAfterResetAndNewExport(t *testing.T) {
|
||||||
|
for _, format := range []string{"online", "offline"} {
|
||||||
|
for _, next := range []string{"none", "preparing", "ready"} {
|
||||||
|
t.Run(format+"/"+next, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
f := newReturnFixture(t, format)
|
||||||
|
raw := f.pack(t)
|
||||||
|
exporter := f.service(f.storage)
|
||||||
|
export, err := exporter.Get(ctx, f.gameID)
|
||||||
|
check(t, err)
|
||||||
|
check(t, exporter.ResetLocalMode(ctx, f.gameID, export.ID))
|
||||||
|
if next != "none" {
|
||||||
|
_, err = exporter.Start(ctx, f.gameID, f.userID)
|
||||||
|
check(t, err)
|
||||||
|
if next == "ready" {
|
||||||
|
_, err = exporter.ProcessNext(ctx)
|
||||||
|
check(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(t, exporter.Maintain(ctx))
|
||||||
|
_, err = f.apply(raw)
|
||||||
|
check(t, err)
|
||||||
|
game, err := f.games.GetGameByID(ctx, f.gameID)
|
||||||
|
check(t, err)
|
||||||
|
if game.Status != "finish" || (next == "none" && game.Export != nil) {
|
||||||
|
t.Fatal("late results were lost or restored released local mode")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResetConcurrentResults(t *testing.T) {
|
||||||
|
for _, name := range []string{"ResetConcurrentResults"} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
f := newReturnFixture(t, "online")
|
||||||
|
ctx := context.Background()
|
||||||
|
exporter := f.service(f.storage)
|
||||||
|
export, err := exporter.Get(ctx, f.gameID)
|
||||||
|
check(t, err)
|
||||||
|
raw := f.pack(t)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errs := make(chan error, 2)
|
||||||
|
wg.Add(2)
|
||||||
|
go func() { defer wg.Done(); errs <- exporter.ResetLocalMode(ctx, f.gameID, export.ID) }()
|
||||||
|
go func() { defer wg.Done(); _, err := f.apply(raw); errs <- err }()
|
||||||
|
wg.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
check(t, err)
|
||||||
|
}
|
||||||
|
game, err := f.games.GetGameByID(ctx, f.gameID)
|
||||||
|
check(t, err)
|
||||||
|
if game.Export != nil || game.Status != "finish" {
|
||||||
|
t.Fatalf("reset and return conflict: %+v", game)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package game_export_service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"evening_detective_server/internal/repos"
|
"evening_detective_server/internal/repos"
|
||||||
"evening_detective_server/internal/repos/game_exports_repo"
|
"evening_detective_server/internal/repos/game_exports_repo"
|
||||||
)
|
)
|
||||||
@@ -12,12 +13,13 @@ type GameExport = repos.GameExport
|
|||||||
var (
|
var (
|
||||||
ErrGameNotFound = game_exports_repo.ErrGameNotFound
|
ErrGameNotFound = game_exports_repo.ErrGameNotFound
|
||||||
ErrExportNotFound = game_exports_repo.ErrExportNotFound
|
ErrExportNotFound = game_exports_repo.ErrExportNotFound
|
||||||
ErrNotExportable = game_exports_repo.ErrNotExportable
|
ErrExportChanged = game_exports_repo.ErrExportChanged
|
||||||
)
|
)
|
||||||
|
|
||||||
// IGameExportsRepo is the worker's persistence boundary, including publication fencing.
|
// IGameExportsRepo is the worker's persistence boundary, including publication fencing.
|
||||||
type IGameExportsRepo interface {
|
type IGameExportsRepo interface {
|
||||||
Start(context.Context, int, int, string, string) (*game_exports_repo.Attempt, error)
|
Start(context.Context, int, int, string, string) (*game_exports_repo.Attempt, error)
|
||||||
|
ResetLocalMode(context.Context, int, string) error
|
||||||
Get(context.Context, int) (*game_exports_repo.Attempt, error)
|
Get(context.Context, int) (*game_exports_repo.Attempt, error)
|
||||||
Claim(context.Context, string) (*game_exports_repo.Attempt, error)
|
Claim(context.Context, string) (*game_exports_repo.Attempt, error)
|
||||||
Heartbeat(context.Context, string, string) error
|
Heartbeat(context.Context, string, string) error
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ var (
|
|||||||
ErrVenueMode = errors.New("экспорт игры доступен только на онлайн-сервере")
|
ErrVenueMode = errors.New("экспорт игры доступен только на онлайн-сервере")
|
||||||
ErrNotReady = errors.New("пакет игры ещё не готов")
|
ErrNotReady = errors.New("пакет игры ещё не готов")
|
||||||
ErrInvalidGameID = errors.New("некорректный идентификатор игры")
|
ErrInvalidGameID = errors.New("некорректный идентификатор игры")
|
||||||
|
ErrInvalidExportID = errors.New("не указан идентификатор экспорта")
|
||||||
)
|
)
|
||||||
|
|
||||||
type GameExportService struct {
|
type GameExportService struct {
|
||||||
@@ -82,6 +83,19 @@ func (s *GameExportService) Get(ctx context.Context, gameID int) (*repos.GameExp
|
|||||||
return &attempt.GameExport, nil
|
return &attempt.GameExport, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *GameExportService) ResetLocalMode(ctx context.Context, gameID int, exportID string) error {
|
||||||
|
if s.venue {
|
||||||
|
return ErrVenueMode
|
||||||
|
}
|
||||||
|
if gameID <= 0 {
|
||||||
|
return ErrInvalidGameID
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(exportID) == "" {
|
||||||
|
return ErrInvalidExportID
|
||||||
|
}
|
||||||
|
return s.repo.ResetLocalMode(ctx, gameID, exportID)
|
||||||
|
}
|
||||||
|
|
||||||
// OpenDownload always opens the same stored bytes and never changes game or export state.
|
// OpenDownload always opens the same stored bytes and never changes game or export state.
|
||||||
func (s *GameExportService) OpenDownload(ctx context.Context, gameID int) (*repos.GameExport, io.ReadCloser, error) {
|
func (s *GameExportService) OpenDownload(ctx context.Context, gameID int) (*repos.GameExport, io.ReadCloser, error) {
|
||||||
if s.venue {
|
if s.venue {
|
||||||
@@ -225,6 +239,16 @@ func (s *GameExportService) prepare(ctx context.Context, attempt *game_exports_r
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Export setup for a fresh game without mutating the recorded source snapshot.
|
||||||
|
snapshot.Data.Game.Status = "draft"
|
||||||
|
snapshot.Data.Game.StartedAt = nil
|
||||||
|
snapshot.Data.Game.EndedAt = nil
|
||||||
|
for i := range snapshot.Data.Teams {
|
||||||
|
snapshot.Data.Teams[i].Status = "register"
|
||||||
|
}
|
||||||
|
if snapshot.Data.ScreenPlan != nil {
|
||||||
|
snapshot.Data.ScreenPlan.CurrentStep = 0
|
||||||
|
}
|
||||||
dir, err := os.MkdirTemp("", "game-export-"+attempt.ID+"-")
|
dir, err := os.MkdirTemp("", "game-export-"+attempt.ID+"-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func TestServicesLifecyclePostgres(t *testing.T) {
|
|||||||
_, err = pool.Exec(ctx, `
|
_, err = pool.Exec(ctx, `
|
||||||
CREATE TEMP TABLE users(id INT, username TEXT);
|
CREATE TEMP TABLE users(id INT, username TEXT);
|
||||||
CREATE TEMP TABLE game_returns(export_id TEXT);
|
CREATE TEMP TABLE game_returns(export_id TEXT);
|
||||||
CREATE TEMP TABLE game_exports(id TEXT, game_id INT, state TEXT, package_id TEXT, size BIGINT, sha256 TEXT, failure TEXT, created_at TIMESTAMPTZ);
|
CREATE TEMP TABLE game_exports(id TEXT, game_id INT, state TEXT, package_id TEXT, size BIGINT, sha256 TEXT, failure TEXT, created_at TIMESTAMPTZ, released_at TIMESTAMPTZ);
|
||||||
CREATE TEMP TABLE scenarios(id INT PRIMARY KEY, playlist_id INT, name TEXT, description TEXT, image TEXT, scenario TEXT,
|
CREATE TEMP TABLE scenarios(id INT PRIMARY KEY, playlist_id INT, name TEXT, description TEXT, image TEXT, scenario TEXT,
|
||||||
author_id INT, status TEXT DEFAULT 'draft', updated_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(), published_at TIMESTAMP, is_deleted BOOLEAN DEFAULT FALSE);
|
author_id INT, status TEXT DEFAULT 'draft', updated_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(), published_at TIMESTAMP, is_deleted BOOLEAN DEFAULT FALSE);
|
||||||
CREATE TEMP TABLE games(id SERIAL PRIMARY KEY, name TEXT, description TEXT, location TEXT, start_at TIMESTAMP, scenario_id INT,
|
CREATE TEMP TABLE games(id SERIAL PRIMARY KEY, name TEXT, description TEXT, location TEXT, start_at TIMESTAMP, scenario_id INT,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func TestFinishedTeamStoryPostgres(t *testing.T) {
|
|||||||
defer pool.Close()
|
defer pool.Close()
|
||||||
_, err = pool.Exec(ctx, `
|
_, err = pool.Exec(ctx, `
|
||||||
CREATE TEMP TABLE game_returns(export_id TEXT);
|
CREATE TEMP TABLE game_returns(export_id TEXT);
|
||||||
CREATE TEMP TABLE game_exports(id TEXT, game_id INT, state TEXT, package_id TEXT, size BIGINT, sha256 TEXT, failure TEXT, created_at TIMESTAMPTZ);
|
CREATE TEMP TABLE game_exports(id TEXT, game_id INT, state TEXT, package_id TEXT, size BIGINT, sha256 TEXT, failure TEXT, created_at TIMESTAMPTZ, released_at TIMESTAMPTZ);
|
||||||
CREATE TEMP TABLE games (id INT, name TEXT, description TEXT, location TEXT, start_at TIMESTAMP,
|
CREATE TEMP TABLE games (id INT, name TEXT, description TEXT, location TEXT, start_at TIMESTAMP,
|
||||||
format TEXT, status TEXT, scenario_id INT, started_at TIMESTAMP, ended_at TIMESTAMP);
|
format TEXT, status TEXT, scenario_id INT, started_at TIMESTAMP, ended_at TIMESTAMP);
|
||||||
CREATE TEMP TABLE teams (id UUID, name TEXT, captain_email TEXT, participants_count INT,
|
CREATE TEMP TABLE teams (id UUID, name TEXT, captain_email TEXT, participants_count INT,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE game_exports ADD COLUMN released_at TIMESTAMPTZ;
|
||||||
|
DROP INDEX game_exports_active;
|
||||||
|
CREATE UNIQUE INDEX game_exports_active ON game_exports(game_id)
|
||||||
|
WHERE state IN ('preparing', 'ready') AND released_at IS NULL;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- Refuse rollback if retained ready exports cannot fit the old unique index.
|
||||||
|
DROP INDEX game_exports_active;
|
||||||
|
CREATE UNIQUE INDEX game_exports_active ON game_exports(game_id) WHERE state IN ('preparing', 'ready');
|
||||||
|
ALTER TABLE game_exports DROP COLUMN released_at;
|
||||||
+886
-789
File diff suppressed because it is too large
Load Diff
@@ -2271,6 +2271,51 @@ func local_request_EveningDetectiveServer_StartGameExport_0(ctx context.Context,
|
|||||||
return msg, metadata, err
|
return msg, metadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func request_EveningDetectiveServer_ResetGameLocalMode_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
|
var (
|
||||||
|
protoReq ResetGameLocalModeReq
|
||||||
|
metadata runtime.ServerMetadata
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
if req.Body != nil {
|
||||||
|
_, _ = io.Copy(io.Discard, req.Body)
|
||||||
|
}
|
||||||
|
val, ok := pathParams["ID"]
|
||||||
|
if !ok {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
|
||||||
|
}
|
||||||
|
protoReq.ID, err = runtime.Int32(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
|
||||||
|
}
|
||||||
|
msg, err := client.ResetGameLocalMode(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||||
|
return msg, metadata, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func local_request_EveningDetectiveServer_ResetGameLocalMode_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
|
var (
|
||||||
|
protoReq ResetGameLocalModeReq
|
||||||
|
metadata runtime.ServerMetadata
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
val, ok := pathParams["ID"]
|
||||||
|
if !ok {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
|
||||||
|
}
|
||||||
|
protoReq.ID, err = runtime.Int32(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
|
||||||
|
}
|
||||||
|
msg, err := server.ResetGameLocalMode(ctx, &protoReq)
|
||||||
|
return msg, metadata, err
|
||||||
|
}
|
||||||
|
|
||||||
func request_EveningDetectiveServer_GetGameExport_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
func request_EveningDetectiveServer_GetGameExport_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
var (
|
var (
|
||||||
protoReq GetGameExportReq
|
protoReq GetGameExportReq
|
||||||
@@ -5712,6 +5757,26 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti
|
|||||||
}
|
}
|
||||||
forward_EveningDetectiveServer_StartGameExport_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
forward_EveningDetectiveServer_StartGameExport_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
})
|
})
|
||||||
|
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_ResetGameLocalMode_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
|
defer cancel()
|
||||||
|
var stream runtime.ServerTransportStream
|
||||||
|
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||||
|
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||||
|
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/ResetGameLocalMode", runtime.WithHTTPPathPattern("/api/games/{ID}/local-mode/reset"))
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, md, err := local_request_EveningDetectiveServer_ResetGameLocalMode_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||||
|
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||||
|
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forward_EveningDetectiveServer_ResetGameLocalMode_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetGameExport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetGameExport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -7843,6 +7908,23 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti
|
|||||||
}
|
}
|
||||||
forward_EveningDetectiveServer_StartGameExport_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
forward_EveningDetectiveServer_StartGameExport_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
})
|
})
|
||||||
|
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_ResetGameLocalMode_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
|
defer cancel()
|
||||||
|
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||||
|
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/ResetGameLocalMode", runtime.WithHTTPPathPattern("/api/games/{ID}/local-mode/reset"))
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, md, err := request_EveningDetectiveServer_ResetGameLocalMode_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||||
|
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forward_EveningDetectiveServer_ResetGameLocalMode_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetGameExport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetGameExport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -8777,6 +8859,7 @@ var (
|
|||||||
pattern_EveningDetectiveServer_DownloadGameResultsPackage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "ID", "results-package", "download"}, ""))
|
pattern_EveningDetectiveServer_DownloadGameResultsPackage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "ID", "results-package", "download"}, ""))
|
||||||
pattern_EveningDetectiveServer_ApplyGameResultsPackage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "ID", "results-package", "apply"}, ""))
|
pattern_EveningDetectiveServer_ApplyGameResultsPackage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "ID", "results-package", "apply"}, ""))
|
||||||
pattern_EveningDetectiveServer_StartGameExport_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "ID", "export"}, ""))
|
pattern_EveningDetectiveServer_StartGameExport_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "ID", "export"}, ""))
|
||||||
|
pattern_EveningDetectiveServer_ResetGameLocalMode_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "ID", "local-mode", "reset"}, ""))
|
||||||
pattern_EveningDetectiveServer_GetGameExport_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "ID", "export"}, ""))
|
pattern_EveningDetectiveServer_GetGameExport_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "ID", "export"}, ""))
|
||||||
pattern_EveningDetectiveServer_IssueGameTickets_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "tickets"}, ""))
|
pattern_EveningDetectiveServer_IssueGameTickets_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "tickets"}, ""))
|
||||||
pattern_EveningDetectiveServer_GetTicket_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "tickets", "token"}, ""))
|
pattern_EveningDetectiveServer_GetTicket_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "tickets", "token"}, ""))
|
||||||
@@ -8894,6 +8977,7 @@ var (
|
|||||||
forward_EveningDetectiveServer_DownloadGameResultsPackage_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_DownloadGameResultsPackage_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_ApplyGameResultsPackage_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_ApplyGameResultsPackage_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_StartGameExport_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_StartGameExport_0 = runtime.ForwardResponseMessage
|
||||||
|
forward_EveningDetectiveServer_ResetGameLocalMode_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_GetGameExport_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_GetGameExport_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_IssueGameTickets_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_IssueGameTickets_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_GetTicket_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_GetTicket_0 = runtime.ForwardResponseMessage
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const (
|
|||||||
EveningDetectiveServer_DownloadGameResultsPackage_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadGameResultsPackage"
|
EveningDetectiveServer_DownloadGameResultsPackage_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadGameResultsPackage"
|
||||||
EveningDetectiveServer_ApplyGameResultsPackage_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/ApplyGameResultsPackage"
|
EveningDetectiveServer_ApplyGameResultsPackage_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/ApplyGameResultsPackage"
|
||||||
EveningDetectiveServer_StartGameExport_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/StartGameExport"
|
EveningDetectiveServer_StartGameExport_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/StartGameExport"
|
||||||
|
EveningDetectiveServer_ResetGameLocalMode_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/ResetGameLocalMode"
|
||||||
EveningDetectiveServer_GetGameExport_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGameExport"
|
EveningDetectiveServer_GetGameExport_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGameExport"
|
||||||
EveningDetectiveServer_IssueGameTickets_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/IssueGameTickets"
|
EveningDetectiveServer_IssueGameTickets_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/IssueGameTickets"
|
||||||
EveningDetectiveServer_GetTicket_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTicket"
|
EveningDetectiveServer_GetTicket_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTicket"
|
||||||
@@ -214,7 +215,10 @@ type EveningDetectiveServerClient interface {
|
|||||||
// Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit.
|
// Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit.
|
||||||
DownloadGameResultsPackage(ctx context.Context, in *GameResultsPackageReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
|
DownloadGameResultsPackage(ctx context.Context, in *GameResultsPackageReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
|
||||||
ApplyGameResultsPackage(ctx context.Context, in *ApplyGameResultsPackageReq, opts ...grpc.CallOption) (*GameResultsPackageRsp, error)
|
ApplyGameResultsPackage(ctx context.Context, in *ApplyGameResultsPackageReq, opts ...grpc.CallOption) (*GameResultsPackageRsp, error)
|
||||||
|
// Each request creates a fresh setup package, replacing any unfinished preparation.
|
||||||
StartGameExport(ctx context.Context, in *StartGameExportReq, opts ...grpc.CallOption) (*GameExportRsp, error)
|
StartGameExport(ctx context.Context, in *StartGameExportReq, opts ...grpc.CallOption) (*GameExportRsp, error)
|
||||||
|
// Release the transfer without discarding ready packages or their future results.
|
||||||
|
ResetGameLocalMode(ctx context.Context, in *ResetGameLocalModeReq, opts ...grpc.CallOption) (*ResetGameLocalModeRsp, error)
|
||||||
GetGameExport(ctx context.Context, in *GetGameExportReq, opts ...grpc.CallOption) (*GameExportRsp, error)
|
GetGameExport(ctx context.Context, in *GetGameExportReq, opts ...grpc.CallOption) (*GameExportRsp, error)
|
||||||
IssueGameTickets(ctx context.Context, in *IssueGameTicketsReq, opts ...grpc.CallOption) (*IssueGameTicketsRsp, error)
|
IssueGameTickets(ctx context.Context, in *IssueGameTicketsReq, opts ...grpc.CallOption) (*IssueGameTicketsRsp, error)
|
||||||
GetTicket(ctx context.Context, in *GetTicketReq, opts ...grpc.CallOption) (*GetTicketRsp, error)
|
GetTicket(ctx context.Context, in *GetTicketReq, opts ...grpc.CallOption) (*GetTicketRsp, error)
|
||||||
@@ -912,6 +916,16 @@ func (c *eveningDetectiveServerClient) StartGameExport(ctx context.Context, in *
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *eveningDetectiveServerClient) ResetGameLocalMode(ctx context.Context, in *ResetGameLocalModeReq, opts ...grpc.CallOption) (*ResetGameLocalModeRsp, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ResetGameLocalModeRsp)
|
||||||
|
err := c.cc.Invoke(ctx, EveningDetectiveServer_ResetGameLocalMode_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *eveningDetectiveServerClient) GetGameExport(ctx context.Context, in *GetGameExportReq, opts ...grpc.CallOption) (*GameExportRsp, error) {
|
func (c *eveningDetectiveServerClient) GetGameExport(ctx context.Context, in *GetGameExportReq, opts ...grpc.CallOption) (*GameExportRsp, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(GameExportRsp)
|
out := new(GameExportRsp)
|
||||||
@@ -1494,7 +1508,10 @@ type EveningDetectiveServerServer interface {
|
|||||||
// Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit.
|
// Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit.
|
||||||
DownloadGameResultsPackage(context.Context, *GameResultsPackageReq) (*httpbody.HttpBody, error)
|
DownloadGameResultsPackage(context.Context, *GameResultsPackageReq) (*httpbody.HttpBody, error)
|
||||||
ApplyGameResultsPackage(context.Context, *ApplyGameResultsPackageReq) (*GameResultsPackageRsp, error)
|
ApplyGameResultsPackage(context.Context, *ApplyGameResultsPackageReq) (*GameResultsPackageRsp, error)
|
||||||
|
// Each request creates a fresh setup package, replacing any unfinished preparation.
|
||||||
StartGameExport(context.Context, *StartGameExportReq) (*GameExportRsp, error)
|
StartGameExport(context.Context, *StartGameExportReq) (*GameExportRsp, error)
|
||||||
|
// Release the transfer without discarding ready packages or their future results.
|
||||||
|
ResetGameLocalMode(context.Context, *ResetGameLocalModeReq) (*ResetGameLocalModeRsp, error)
|
||||||
GetGameExport(context.Context, *GetGameExportReq) (*GameExportRsp, error)
|
GetGameExport(context.Context, *GetGameExportReq) (*GameExportRsp, error)
|
||||||
IssueGameTickets(context.Context, *IssueGameTicketsReq) (*IssueGameTicketsRsp, error)
|
IssueGameTickets(context.Context, *IssueGameTicketsReq) (*IssueGameTicketsRsp, error)
|
||||||
GetTicket(context.Context, *GetTicketReq) (*GetTicketRsp, error)
|
GetTicket(context.Context, *GetTicketReq) (*GetTicketRsp, error)
|
||||||
@@ -1751,6 +1768,9 @@ func (UnimplementedEveningDetectiveServerServer) ApplyGameResultsPackage(context
|
|||||||
func (UnimplementedEveningDetectiveServerServer) StartGameExport(context.Context, *StartGameExportReq) (*GameExportRsp, error) {
|
func (UnimplementedEveningDetectiveServerServer) StartGameExport(context.Context, *StartGameExportReq) (*GameExportRsp, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method StartGameExport not implemented")
|
return nil, status.Error(codes.Unimplemented, "method StartGameExport not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedEveningDetectiveServerServer) ResetGameLocalMode(context.Context, *ResetGameLocalModeReq) (*ResetGameLocalModeRsp, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ResetGameLocalMode not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedEveningDetectiveServerServer) GetGameExport(context.Context, *GetGameExportReq) (*GameExportRsp, error) {
|
func (UnimplementedEveningDetectiveServerServer) GetGameExport(context.Context, *GetGameExportReq) (*GameExportRsp, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetGameExport not implemented")
|
return nil, status.Error(codes.Unimplemented, "method GetGameExport not implemented")
|
||||||
}
|
}
|
||||||
@@ -3060,6 +3080,24 @@ func _EveningDetectiveServer_StartGameExport_Handler(srv interface{}, ctx contex
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _EveningDetectiveServer_ResetGameLocalMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ResetGameLocalModeReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(EveningDetectiveServerServer).ResetGameLocalMode(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: EveningDetectiveServer_ResetGameLocalMode_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(EveningDetectiveServerServer).ResetGameLocalMode(ctx, req.(*ResetGameLocalModeReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _EveningDetectiveServer_GetGameExport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _EveningDetectiveServer_GetGameExport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(GetGameExportReq)
|
in := new(GetGameExportReq)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -4237,6 +4275,10 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "StartGameExport",
|
MethodName: "StartGameExport",
|
||||||
Handler: _EveningDetectiveServer_StartGameExport_Handler,
|
Handler: _EveningDetectiveServer_StartGameExport_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ResetGameLocalMode",
|
||||||
|
Handler: _EveningDetectiveServer_ResetGameLocalMode_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "GetGameExport",
|
MethodName: "GetGameExport",
|
||||||
Handler: _EveningDetectiveServer_GetGameExport_Handler,
|
Handler: _EveningDetectiveServer_GetGameExport_Handler,
|
||||||
|
|||||||
Reference in New Issue
Block a user