6db9178449
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
Non-default campaigns gain an optional colour override (background / text / link) in two sets — one for every theme, one for the dark theme only — and an "urgent" flag. - Colours ride profile.get as six trailing FlatBuffers strings on BannerCampaign (backward-compatible). The client resolves the cascade (dark <- dark ?? all, light <- all) per rendered theme and derives the strip border from the background in JS (no CSS color-mix, for the old Android WebView floor); AdBanner applies them as inline vars scoped to the strip. - Urgent is resolved entirely server-side: while any enabled, in-window urgent campaign exists, computeActiveSet returns only the urgent campaigns and bannerFor skips the eligibility gate — so a system notice reaches every viewer (paid / hint-holding / no_banner included) and preempts the ordinary feed. No wire field; it appears on each viewer's next profile.get. - Admin console (/_gm/banners): native colour pickers + a live light/dark preview of the strip, and an urgent toggle. The default campaign stays plain, enforced by the service and a DB CHECK. Migration 00009 is additive (nullable colour columns + a bool default + all-or-nothing / hex / default-plain CHECKs) — expand-contract, rollback-safe. Docs: ARCHITECTURE §10, UI_DESIGN, FUNCTIONAL (+ru). Tests: ads unit (urgent preempt + colour validation), codec + resolver unit, gateway transcode, and integration (colour round-trip + urgent bypass against real Postgres).
130 lines
5.1 KiB
Go
130 lines
5.1 KiB
Go
package transcode_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"scrabble/gateway/internal/transcode"
|
|
fb "scrabble/pkg/fbs/scrabblefb"
|
|
)
|
|
|
|
// TestProfileGetEncodesBanner verifies the gateway forwards the backend's banner
|
|
// block verbatim into the Profile payload: the campaigns (weight + resolved
|
|
// messages, in order) and the global display timings.
|
|
func TestProfileGetEncodesBanner(t *testing.T) {
|
|
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet || r.URL.Path != "/api/v1/user/profile" {
|
|
t.Errorf("unexpected %s %q", r.Method, r.URL.Path)
|
|
}
|
|
_, _ = w.Write([]byte(`{"user_id":"u-1","display_name":"Kaya","preferred_language":"en",` +
|
|
`"banner":{"campaigns":[` +
|
|
`{"weight":3,"messages":["promo-en"]},` +
|
|
`{"weight":7,"messages":["house-a","house-b"]}],` +
|
|
`"timings":{"hold_ms":60000,"edge_pause_ms":5000,"scroll_px_per_sec":40,` +
|
|
`"fade_out_ms":1000,"gap_ms":250,"fade_in_ms":1000}}}`))
|
|
})
|
|
defer cleanup()
|
|
|
|
reg := transcode.NewRegistry(backend, nil)
|
|
op, ok := reg.Lookup(transcode.MsgProfileGet)
|
|
if !ok {
|
|
t.Fatal("profile.get not registered")
|
|
}
|
|
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1"})
|
|
if err != nil {
|
|
t.Fatalf("handler: %v", err)
|
|
}
|
|
|
|
p := fb.GetRootAsProfile(payload, 0)
|
|
banner := p.Banner(nil)
|
|
if banner == nil {
|
|
t.Fatal("profile carries no banner block")
|
|
}
|
|
if got := banner.CampaignsLength(); got != 2 {
|
|
t.Fatalf("campaigns = %d, want 2", got)
|
|
}
|
|
var c fb.BannerCampaign
|
|
banner.Campaigns(&c, 0)
|
|
if c.Weight() != 3 || c.MessagesLength() != 1 || string(c.Messages(0)) != "promo-en" {
|
|
t.Errorf("campaign 0 = weight %d, %d msgs, msg0=%q", c.Weight(), c.MessagesLength(), c.Messages(0))
|
|
}
|
|
banner.Campaigns(&c, 1)
|
|
if c.Weight() != 7 || c.MessagesLength() != 2 || string(c.Messages(1)) != "house-b" {
|
|
t.Errorf("campaign 1 = weight %d, %d msgs, msg1=%q", c.Weight(), c.MessagesLength(), c.Messages(1))
|
|
}
|
|
if banner.HoldMs() != 60000 || banner.EdgePauseMs() != 5000 || banner.ScrollPxPerSec() != 40 {
|
|
t.Errorf("timings = hold %d edge %d scroll %d", banner.HoldMs(), banner.EdgePauseMs(), banner.ScrollPxPerSec())
|
|
}
|
|
if banner.FadeOutMs() != 1000 || banner.GapMs() != 250 || banner.FadeInMs() != 1000 {
|
|
t.Errorf("fade timings = out %d gap %d in %d", banner.FadeOutMs(), banner.GapMs(), banner.FadeInMs())
|
|
}
|
|
}
|
|
|
|
// TestProfileGetEncodesBannerColors verifies the gateway carries a campaign's
|
|
// optional colour overrides into the FlatBuffers payload, and leaves an absent
|
|
// colour (or an absent set) empty so the client falls back to the theme tokens.
|
|
func TestProfileGetEncodesBannerColors(t *testing.T) {
|
|
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"user_id":"u-1","display_name":"Kaya","preferred_language":"en",` +
|
|
`"banner":{"campaigns":[` +
|
|
`{"weight":1,"messages":["promo-en"],` +
|
|
`"override_bg":"#aa0000","override_fg":"#ffffff","override_link":"#ffdd00",` +
|
|
`"override_bg_dark":"#330000","override_fg_dark":"#eeeeee","override_link_dark":"#ffcc00"},` +
|
|
`{"weight":1,"messages":["plain-en"]}],` +
|
|
`"timings":{"hold_ms":60000,"edge_pause_ms":5000,"scroll_px_per_sec":40,` +
|
|
`"fade_out_ms":1000,"gap_ms":250,"fade_in_ms":1000}}}`))
|
|
})
|
|
defer cleanup()
|
|
|
|
reg := transcode.NewRegistry(backend, nil)
|
|
op, _ := reg.Lookup(transcode.MsgProfileGet)
|
|
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1"})
|
|
if err != nil {
|
|
t.Fatalf("handler: %v", err)
|
|
}
|
|
banner := fb.GetRootAsProfile(payload, 0).Banner(nil)
|
|
if banner == nil {
|
|
t.Fatal("profile carries no banner block")
|
|
}
|
|
var c fb.BannerCampaign
|
|
banner.Campaigns(&c, 0)
|
|
if got := string(c.OverrideBg()); got != "#aa0000" {
|
|
t.Errorf("override_bg = %q, want #aa0000", got)
|
|
}
|
|
if got := string(c.OverrideLink()); got != "#ffdd00" {
|
|
t.Errorf("override_link = %q, want #ffdd00", got)
|
|
}
|
|
if got := string(c.OverrideBgDark()); got != "#330000" {
|
|
t.Errorf("override_bg_dark = %q, want #330000", got)
|
|
}
|
|
if got := string(c.OverrideLinkDark()); got != "#ffcc00" {
|
|
t.Errorf("override_link_dark = %q, want #ffcc00", got)
|
|
}
|
|
// The second campaign has no override: every colour field must be absent (nil).
|
|
banner.Campaigns(&c, 1)
|
|
if c.OverrideBg() != nil || c.OverrideFg() != nil || c.OverrideLink() != nil ||
|
|
c.OverrideBgDark() != nil || c.OverrideFgDark() != nil || c.OverrideLinkDark() != nil {
|
|
t.Error("plain campaign unexpectedly carries a colour override")
|
|
}
|
|
}
|
|
|
|
// TestProfileGetNoBanner verifies an ineligible viewer's profile carries no
|
|
// banner block (the backend omits it).
|
|
func TestProfileGetNoBanner(t *testing.T) {
|
|
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"user_id":"u-1","display_name":"Kaya","preferred_language":"en"}`))
|
|
})
|
|
defer cleanup()
|
|
|
|
reg := transcode.NewRegistry(backend, nil)
|
|
op, _ := reg.Lookup(transcode.MsgProfileGet)
|
|
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1"})
|
|
if err != nil {
|
|
t.Fatalf("handler: %v", err)
|
|
}
|
|
if p := fb.GetRootAsProfile(payload, 0); p.Banner(nil) != nil {
|
|
t.Error("ineligible profile unexpectedly carries a banner block")
|
|
}
|
|
}
|