Files
scrabble-game/gateway/internal/transcode/transcode_link_test.go
T
Ilia Denisov 52f898ca6f
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Stage 11: account linking & merge (email + Telegram Login Widget)
Link an email (confirm-code) or Telegram (web Login Widget) to the current
account; if the identity already has its own account, merge the two into the
one in use (the current account is primary, except a guest initiator whose
durable counterpart wins). The merge runs in one transaction
(internal/accountmerge): stats + hint wallet summed, paid_account ORed,
identities/games/chat/complaints transferred, friends/blocks de-duplicated,
the secondary kept as a merged_into tombstone so a shared finished game's
no-cascade FKs hold; a shared active game blocks the merge.

- migration 00009: accounts.paid_account, merged_into, merged_at (+ jetgen)
- internal/link orchestrator; session.RevokeAllForAccount on merge
- connector ValidateLoginWidget RPC + loginwidget HMAC validator
- edge ops link.email.request/confirm/merge, link.telegram.confirm/merge;
  supersedes the Stage 8 email.bind.* surface (request never reveals 'taken'
  before the code is verified, so a probe cannot enumerate addresses)
- UI Profile link section + irreversible-merge dialog; Telegram web sign-in
- focused regression tests (merge core, guest inversion, active-game refusal,
  finished-shared-game kept), gateway transcode + connector + UI codec/e2e
- docs: PLAN, ARCHITECTURE 3/4/9, FUNCTIONAL(+ru), module READMEs
2026-06-04 11:15:14 +02:00

123 lines
4.1 KiB
Go

package transcode_test
import (
"context"
"encoding/json"
"net/http"
"testing"
flatbuffers "github.com/google/flatbuffers/go"
"scrabble/gateway/internal/connector"
"scrabble/gateway/internal/transcode"
fb "scrabble/pkg/fbs/scrabblefb"
)
func linkEmailConfirmPayload(email, code string) []byte {
b := flatbuffers.NewBuilder(64)
e := b.CreateString(email)
c := b.CreateString(code)
fb.LinkEmailConfirmStart(b)
fb.LinkEmailConfirmAddEmail(b, e)
fb.LinkEmailConfirmAddCode(b, c)
b.Finish(fb.LinkEmailConfirmEnd(b))
return b.FinishedBytes()
}
func linkTelegramPayload(data string) []byte {
b := flatbuffers.NewBuilder(64)
d := b.CreateString(data)
fb.LinkTelegramRequestStart(b)
fb.LinkTelegramRequestAddData(b, d)
b.Finish(fb.LinkTelegramRequestEnd(b))
return b.FinishedBytes()
}
func TestLinkEmailConfirmMergeRequired(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/user/link/email/confirm" {
t.Errorf("path = %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"status":"merge_required","secondary_user_id":"b-1","secondary_display_name":"Ann","secondary_games":7,"secondary_friends":3}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, ok := reg.Lookup(transcode.MsgLinkEmailConfirm)
if !ok {
t.Fatal("link.email.confirm not registered")
}
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1", Payload: linkEmailConfirmPayload("e@x.com", "123456")})
if err != nil {
t.Fatalf("handler: %v", err)
}
lr := fb.GetRootAsLinkResult(payload, 0)
if string(lr.Status()) != "merge_required" || string(lr.SecondaryUserId()) != "b-1" ||
string(lr.SecondaryDisplayName()) != "Ann" || lr.SecondaryGames() != 7 || lr.SecondaryFriends() != 3 {
t.Fatalf("link result = %q/%q/%q/%d/%d", lr.Status(), lr.SecondaryUserId(), lr.SecondaryDisplayName(), lr.SecondaryGames(), lr.SecondaryFriends())
}
if lr.Session(nil) != nil {
t.Error("a merge_required result must not carry a session")
}
}
func TestLinkEmailMergeSwitchesSession(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/user/link/email/merge" {
t.Errorf("path = %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"status":"merged","token":"tok-9","profile":{"user_id":"a-1","display_name":"Kaya","is_guest":false}}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, _ := reg.Lookup(transcode.MsgLinkEmailMerge)
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1", Payload: linkEmailConfirmPayload("e@x.com", "123456")})
if err != nil {
t.Fatalf("handler: %v", err)
}
lr := fb.GetRootAsLinkResult(payload, 0)
if string(lr.Status()) != "merged" {
t.Fatalf("status = %q, want merged", lr.Status())
}
sess := lr.Session(nil)
if sess == nil {
t.Fatal("a switched merge must carry a session")
}
if string(sess.Token()) != "tok-9" || string(sess.UserId()) != "a-1" {
t.Fatalf("session = %q/%q, want tok-9/a-1", sess.Token(), sess.UserId())
}
}
func TestLinkTelegramValidatesAndForwards(t *testing.T) {
var gotExternalID string
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/user/link/telegram" {
t.Errorf("path = %q", r.URL.Path)
}
var body struct {
ExternalID string `json:"external_id"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
gotExternalID = body.ExternalID
_, _ = w.Write([]byte(`{"status":"linked"}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, fakeValidator{user: connector.User{ExternalID: "42"}})
op, ok := reg.Lookup(transcode.MsgLinkTelegram)
if !ok {
t.Fatal("link.telegram.confirm not registered")
}
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1", Payload: linkTelegramPayload("id=42&hash=x")})
if err != nil {
t.Fatalf("handler: %v", err)
}
if gotExternalID != "42" {
t.Errorf("backend external_id = %q, want 42 (the gateway-validated id)", gotExternalID)
}
if string(fb.GetRootAsLinkResult(payload, 0).Status()) != "linked" {
t.Error("expected a linked result")
}
}