diff --git a/api/main.proto b/api/main.proto index e3c7ff8..6813657 100644 --- a/api/main.proto +++ b/api/main.proto @@ -1772,12 +1772,12 @@ message ResetGameLocalModeRsp {} message GameExportRsp { GameExport export = 1; } enum ScreenMode { + reserved 3, 5; + reserved "SCREEN_MODE_INTRO_DONE", "SCREEN_MODE_CONCLUSION_DONE"; SCREEN_MODE_UNSPECIFIED = 0; SCREEN_MODE_COVER = 1; SCREEN_MODE_INTRO = 2; - SCREEN_MODE_INTRO_DONE = 3; SCREEN_MODE_CONCLUSION = 4; - SCREEN_MODE_CONCLUSION_DONE = 5; SCREEN_MODE_RESULTS = 6; SCREEN_MODE_TIMER = 7; SCREEN_MODE_TEXT = 8; diff --git a/bin/evening_detective_server b/bin/evening_detective_server index d5d61d5..4e0468b 100755 Binary files a/bin/evening_detective_server and b/bin/evening_detective_server differ diff --git a/cmd/evening_detective_server/main.swagger.json b/cmd/evening_detective_server/main.swagger.json index 227f17d..2c893a7 100644 --- a/cmd/evening_detective_server/main.swagger.json +++ b/cmd/evening_detective_server/main.swagger.json @@ -6048,9 +6048,7 @@ "SCREEN_MODE_UNSPECIFIED", "SCREEN_MODE_COVER", "SCREEN_MODE_INTRO", - "SCREEN_MODE_INTRO_DONE", "SCREEN_MODE_CONCLUSION", - "SCREEN_MODE_CONCLUSION_DONE", "SCREEN_MODE_RESULTS", "SCREEN_MODE_TIMER", "SCREEN_MODE_TEXT" diff --git a/internal/app/screens.go b/internal/app/screens.go index d7855d5..8e5f18d 100644 --- a/internal/app/screens.go +++ b/internal/app/screens.go @@ -271,7 +271,7 @@ func (s *server) SetScreenMode(ctx context.Context, req *proto.SetScreenModeReq) if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } - if req == nil || req.GameID <= 0 || req.Mode == proto.ScreenMode_SCREEN_MODE_UNSPECIFIED { + if req == nil || req.GameID <= 0 || !screen_service.ValidMode(int32(req.Mode)) { return nil, status.Error(codes.InvalidArgument, "некорректный режим экрана") } if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { diff --git a/internal/app/screens_test.go b/internal/app/screens_test.go new file mode 100644 index 0000000..2a4d011 --- /dev/null +++ b/internal/app/screens_test.go @@ -0,0 +1,28 @@ +package app + +import ( + "context" + "testing" + + "evening_detective_server/internal/modules/processor_jwt" + "evening_detective_server/proto" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestSetScreenModeRejectsRemovedModes(t *testing.T) { + ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{ + Roles: []string{"organizer"}, + }) + s := &server{} + for _, mode := range []int32{3, 5} { + _, err := s.SetScreenMode(ctx, &proto.SetScreenModeReq{ + GameID: 1, + Mode: proto.ScreenMode(mode), + }) + if status.Code(err) != codes.InvalidArgument { + t.Errorf("SetScreenMode(%d) = %v, want InvalidArgument", mode, err) + } + } +} diff --git a/internal/services/screen_plan_service/service.go b/internal/services/screen_plan_service/service.go index 2281e18..d11854e 100644 --- a/internal/services/screen_plan_service/service.go +++ b/internal/services/screen_plan_service/service.go @@ -118,7 +118,7 @@ func normalizePlan(steps []Step, currentStep int) ([]Step, error) { } normalized := make([]Step, 0, len(steps)) for _, step := range steps { - if step.Mode <= 0 || step.Mode > 8 { + if !screen_service.ValidMode(step.Mode) { return nil, ErrInvalidPlan } step.Text = strings.TrimSpace(step.Text) diff --git a/internal/services/screen_plan_service/service_test.go b/internal/services/screen_plan_service/service_test.go index c26558a..a6d4eb8 100644 --- a/internal/services/screen_plan_service/service_test.go +++ b/internal/services/screen_plan_service/service_test.go @@ -1,6 +1,9 @@ package screen_plan_service -import "testing" +import ( + "errors" + "testing" +) func TestNormalizePlan(t *testing.T) { tests := []struct { @@ -16,6 +19,8 @@ func TestNormalizePlan(t *testing.T) { {Mode: 7, TimerDurationSeconds: 60}, }, }, + {name: "rejects removed intro mode", steps: []Step{{Mode: 3}}, wantErr: true}, + {name: "rejects removed conclusion mode", steps: []Step{{Mode: 5}}, wantErr: true}, { name: "rejects empty text", steps: []Step{{Mode: 8}}, @@ -36,6 +41,9 @@ func TestNormalizePlan(t *testing.T) { t.Fatalf("normalizePlan() error = %v, want error: %v", err, tt.wantErr) } if tt.wantErr { + if !errors.Is(err, ErrInvalidPlan) { + t.Fatalf("want ErrInvalidPlan, got %v", err) + } return } if steps[0].Text != "Внимание" || steps[1].TimerLabel != "Перерыв" { diff --git a/internal/services/screen_service/service.go b/internal/services/screen_service/service.go index a7ea0bf..466657c 100644 --- a/internal/services/screen_service/service.go +++ b/internal/services/screen_service/service.go @@ -7,6 +7,9 @@ import ( "evening_detective_server/internal/repos/screen_sessions_repo" ) +// ErrInvalidMode indicates an unsupported display mode. +var ErrInvalidMode = errors.New("некорректный режим экрана") + // ErrScreenNotFound indicates that a screen session was not found. var ErrScreenNotFound = errors.New("экран не найден") @@ -47,8 +50,21 @@ func (s *ScreenService) ListByGame(ctx context.Context, gameID int) ([]*ScreenSe return result, nil } +// ValidMode reports whether mode is a supported display mode. +func ValidMode(mode int32) bool { + switch mode { + case 1, 2, 4, 6, 7, 8: + return true + default: + return false + } +} + // SetMode updates the display mode for a game. func (s *ScreenService) SetMode(ctx context.Context, gameID int, mode int32) error { + if !ValidMode(mode) { + return ErrInvalidMode + } return s.repo.SetMode(ctx, gameID, mode) } diff --git a/internal/services/screen_service/service_test.go b/internal/services/screen_service/service_test.go new file mode 100644 index 0000000..e4f6ad3 --- /dev/null +++ b/internal/services/screen_service/service_test.go @@ -0,0 +1,16 @@ +package screen_service + +import ( + "context" + "errors" + "testing" +) + +func TestSetModeRejectsUnsupportedModes(t *testing.T) { + s := NewScreenService(nil) + for _, mode := range []int32{-1, 0, 3, 5, 9} { + if err := s.SetMode(context.Background(), 1, mode); !errors.Is(err, ErrInvalidMode) { + t.Errorf("SetMode(%d) = %v, want ErrInvalidMode", mode, err) + } + } +} diff --git a/migrations/20261019120000_merge_text_screen_modes.sql b/migrations/20261019120000_merge_text_screen_modes.sql new file mode 100644 index 0000000..98ce944 --- /dev/null +++ b/migrations/20261019120000_merge_text_screen_modes.sql @@ -0,0 +1,33 @@ +-- +goose Up +UPDATE game_screen_plans +SET steps = ( + SELECT jsonb_agg( + CASE WHEN step->>'mode' IN ('3', '5') THEN + step || jsonb_build_object( + 'mode', 8, + 'text', CASE step->>'mode' + WHEN '3' THEN 'Успехов' + ELSE 'Спасибо за игру!' + END + ) + ELSE step END + ORDER BY position + ) + FROM jsonb_array_elements(steps) WITH ORDINALITY AS items(step, position) +), updated_at = NOW() +WHERE EXISTS ( + SELECT 1 + FROM jsonb_array_elements(steps) AS step + WHERE step->>'mode' IN ('3', '5') +); + +UPDATE screen_sessions +SET text = CASE mode WHEN 3 THEN 'Успехов' ELSE 'Спасибо за игру!' END, + mode = 8, + command = 0, + state_version = state_version + 1, + updated_at = NOW() +WHERE mode IN (3, 5); + +-- +goose Down +-- Text screens remain valid in the previous version; original modes cannot be recovered. diff --git a/proto/main.pb.go b/proto/main.pb.go index f7e7005..fdee836 100644 --- a/proto/main.pb.go +++ b/proto/main.pb.go @@ -34,15 +34,13 @@ const ( type ScreenMode int32 const ( - ScreenMode_SCREEN_MODE_UNSPECIFIED ScreenMode = 0 - ScreenMode_SCREEN_MODE_COVER ScreenMode = 1 - ScreenMode_SCREEN_MODE_INTRO ScreenMode = 2 - ScreenMode_SCREEN_MODE_INTRO_DONE ScreenMode = 3 - ScreenMode_SCREEN_MODE_CONCLUSION ScreenMode = 4 - ScreenMode_SCREEN_MODE_CONCLUSION_DONE ScreenMode = 5 - ScreenMode_SCREEN_MODE_RESULTS ScreenMode = 6 - ScreenMode_SCREEN_MODE_TIMER ScreenMode = 7 - ScreenMode_SCREEN_MODE_TEXT ScreenMode = 8 + ScreenMode_SCREEN_MODE_UNSPECIFIED ScreenMode = 0 + ScreenMode_SCREEN_MODE_COVER ScreenMode = 1 + ScreenMode_SCREEN_MODE_INTRO ScreenMode = 2 + ScreenMode_SCREEN_MODE_CONCLUSION ScreenMode = 4 + ScreenMode_SCREEN_MODE_RESULTS ScreenMode = 6 + ScreenMode_SCREEN_MODE_TIMER ScreenMode = 7 + ScreenMode_SCREEN_MODE_TEXT ScreenMode = 8 ) // Enum value maps for ScreenMode. @@ -51,23 +49,19 @@ var ( 0: "SCREEN_MODE_UNSPECIFIED", 1: "SCREEN_MODE_COVER", 2: "SCREEN_MODE_INTRO", - 3: "SCREEN_MODE_INTRO_DONE", 4: "SCREEN_MODE_CONCLUSION", - 5: "SCREEN_MODE_CONCLUSION_DONE", 6: "SCREEN_MODE_RESULTS", 7: "SCREEN_MODE_TIMER", 8: "SCREEN_MODE_TEXT", } ScreenMode_value = map[string]int32{ - "SCREEN_MODE_UNSPECIFIED": 0, - "SCREEN_MODE_COVER": 1, - "SCREEN_MODE_INTRO": 2, - "SCREEN_MODE_INTRO_DONE": 3, - "SCREEN_MODE_CONCLUSION": 4, - "SCREEN_MODE_CONCLUSION_DONE": 5, - "SCREEN_MODE_RESULTS": 6, - "SCREEN_MODE_TIMER": 7, - "SCREEN_MODE_TEXT": 8, + "SCREEN_MODE_UNSPECIFIED": 0, + "SCREEN_MODE_COVER": 1, + "SCREEN_MODE_INTRO": 2, + "SCREEN_MODE_CONCLUSION": 4, + "SCREEN_MODE_RESULTS": 6, + "SCREEN_MODE_TIMER": 7, + "SCREEN_MODE_TEXT": 8, } ) @@ -13925,18 +13919,16 @@ const file_main_proto_rawDesc = "" + "\x0eTeamReceiptReq\x12\x16\n" + "\x06gameID\x18\x01 \x01(\x05R\x06gameID\x12\x16\n" + "\x06teamID\x18\x02 \x01(\tR\x06teamID\"\x15\n" + - "\x13PrintTeamReceiptRsp*\xf6\x01\n" + + "\x13PrintTeamReceiptRsp*\xfa\x01\n" + "\n" + "ScreenMode\x12\x1b\n" + "\x17SCREEN_MODE_UNSPECIFIED\x10\x00\x12\x15\n" + "\x11SCREEN_MODE_COVER\x10\x01\x12\x15\n" + "\x11SCREEN_MODE_INTRO\x10\x02\x12\x1a\n" + - "\x16SCREEN_MODE_INTRO_DONE\x10\x03\x12\x1a\n" + - "\x16SCREEN_MODE_CONCLUSION\x10\x04\x12\x1f\n" + - "\x1bSCREEN_MODE_CONCLUSION_DONE\x10\x05\x12\x17\n" + + "\x16SCREEN_MODE_CONCLUSION\x10\x04\x12\x17\n" + "\x13SCREEN_MODE_RESULTS\x10\x06\x12\x15\n" + "\x11SCREEN_MODE_TIMER\x10\a\x12\x14\n" + - "\x10SCREEN_MODE_TEXT\x10\b*\xb6\x01\n" + + "\x10SCREEN_MODE_TEXT\x10\b\"\x04\b\x03\x10\x03\"\x04\b\x05\x10\x05*\x16SCREEN_MODE_INTRO_DONE*\x1bSCREEN_MODE_CONCLUSION_DONE*\xb6\x01\n" + "\x10ScreenTimerState\x12\"\n" + "\x1eSCREEN_TIMER_STATE_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aSCREEN_TIMER_STATE_STOPPED\x10\x01\x12\x1e\n" +