Stage 17 round 6 (#10 backend): enforce chat only on your turn
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
PostMessage now rejects a chat sent on a finished game or when it is not the sender's turn (ErrChatNotYourTurn -> 409 chat_not_your_turn), matching the UI where the message field is hidden off-turn and only the nudge shows. Existing chat tests post on the to-move seat and are unaffected; adds an off-turn-rejection integration test + the dto mapping case + the UI error message.
This commit is contained in:
@@ -314,6 +314,20 @@ func TestChatRejectsBadContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestChatOnlyOnYourTurn checks chat is allowed only on the sender's own turn (Stage 17):
|
||||
// the player to move can post, the waiting player gets ErrChatNotYourTurn.
|
||||
func TestChatOnlyOnYourTurn(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := newSocialService()
|
||||
gameID, seats := newGameWithSeats(t, 2) // seat 0 is to move at the opening
|
||||
if _, err := svc.PostMessage(ctx, gameID, seats[1], "hi", ""); !errors.Is(err, social.ErrChatNotYourTurn) {
|
||||
t.Fatalf("off-turn chat = %v, want ErrChatNotYourTurn", err)
|
||||
}
|
||||
if _, err := svc.PostMessage(ctx, gameID, seats[0], "hi", ""); err != nil {
|
||||
t.Fatalf("on-turn chat = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNudgeRulesAndRateLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := newSocialService()
|
||||
|
||||
@@ -48,6 +48,7 @@ func TestStatusForError(t *testing.T) {
|
||||
"not your turn": {game.ErrNotYourTurn, http.StatusConflict, "not_your_turn"},
|
||||
"nudge own turn": {social.ErrNudgeOnOwnTurn, http.StatusConflict, "nudge_own_turn"},
|
||||
"nudge too soon": {social.ErrNudgeTooSoon, http.StatusConflict, "nudge_too_soon"},
|
||||
"chat off turn": {social.ErrChatNotYourTurn, http.StatusConflict, "chat_not_your_turn"},
|
||||
"illegal play": {engine.ErrIllegalPlay, http.StatusUnprocessableEntity, "illegal_play"},
|
||||
"email taken": {account.ErrEmailTaken, http.StatusConflict, "email_taken"},
|
||||
"code mismatch": {account.ErrCodeMismatch, http.StatusUnauthorized, "code_invalid"},
|
||||
|
||||
@@ -207,6 +207,8 @@ func statusForError(err error) (int, string) {
|
||||
// A too-frequent nudge is a distinct, non-content rejection — the UI must say
|
||||
// "don't rush the player so often", not the chat content-rejection message.
|
||||
return http.StatusConflict, "nudge_too_soon"
|
||||
case errors.Is(err, social.ErrChatNotYourTurn):
|
||||
return http.StatusConflict, "chat_not_your_turn"
|
||||
case errors.Is(err, social.ErrSelfRelation):
|
||||
return http.StatusBadRequest, "self_relation"
|
||||
case errors.Is(err, social.ErrRequestExists):
|
||||
|
||||
@@ -49,13 +49,22 @@ type Message struct {
|
||||
// rune limit, and free of links/emails/phone numbers (the content filter). The
|
||||
// gateway-forwarded senderIP is validated and stored for moderation.
|
||||
func (svc *Service) PostMessage(ctx context.Context, gameID, senderID uuid.UUID, body, senderIP string) (Message, error) {
|
||||
seats, _, _, err := svc.games.Participants(ctx, gameID)
|
||||
seats, toMove, status, err := svc.games.Participants(ctx, gameID)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
if !slices.Contains(seats, senderID) {
|
||||
idx := slices.Index(seats, senderID)
|
||||
if idx < 0 {
|
||||
return Message{}, ErrNotParticipant
|
||||
}
|
||||
// Chat is allowed only on the sender's own turn in an active game; the opponent's-turn
|
||||
// control is the nudge (Stage 17).
|
||||
if status != statusActive {
|
||||
return Message{}, ErrGameNotActive
|
||||
}
|
||||
if idx != toMove {
|
||||
return Message{}, ErrChatNotYourTurn
|
||||
}
|
||||
sender, err := svc.accounts.GetByID(ctx, senderID)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
|
||||
@@ -67,6 +67,10 @@ var (
|
||||
ErrNudgeTooSoon = errors.New("social: a nudge was already sent in the last hour")
|
||||
// ErrGameNotActive is returned when a nudge is attempted on a finished game.
|
||||
ErrGameNotActive = errors.New("social: game is not active")
|
||||
// ErrChatNotYourTurn is returned when a chat message is sent while it is not the
|
||||
// sender's turn — chat is allowed only on your own turn (the opponent's-turn control
|
||||
// is the nudge, Stage 17).
|
||||
ErrChatNotYourTurn = errors.New("social: cannot chat while it is not your turn")
|
||||
)
|
||||
|
||||
// Service is the social domain. It is the only writer of the friendships, blocks
|
||||
|
||||
@@ -158,6 +158,7 @@ export const en = {
|
||||
'error.no_hint_available': 'No options with your letters.',
|
||||
'error.chat_rejected': 'Message rejected (too long or contains contact info).',
|
||||
'error.nudge_too_soon': "Please don't rush your opponent so often.",
|
||||
'error.chat_not_your_turn': 'You can chat only on your turn.',
|
||||
'error.game_finished': 'This game is finished.',
|
||||
'error.not_a_player': 'You are not a player in this game.',
|
||||
'error.already_queued': 'You are already in the queue.',
|
||||
|
||||
@@ -159,6 +159,7 @@ export const ru: Record<MessageKey, string> = {
|
||||
'error.no_hint_available': 'Нет вариантов с вашим набором.',
|
||||
'error.chat_rejected': 'Сообщение отклонено (слишком длинное или содержит контакты).',
|
||||
'error.nudge_too_soon': 'Не стоит торопить соперника так часто.',
|
||||
'error.chat_not_your_turn': 'Писать в чат можно только в свой ход.',
|
||||
'error.game_finished': 'Эта игра уже завершена.',
|
||||
'error.not_a_player': 'Вы не участник этой игры.',
|
||||
'error.already_queued': 'Вы уже в очереди.',
|
||||
|
||||
Reference in New Issue
Block a user