e71e40eef5
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
Add a second standalone promo bot to the bot container (answers /start with a localized message + a URL button into the main bot's Mini App) and gate write access in a channel's linked discussion chat: grant on join when the Telegram user is registered and neither admin-suspended nor holding a new chat_muted role, and revoke/grant on the matching moderation change for a member currently in the chat. Eligibility (registered AND NOT suspended AND NOT chat_muted; the game suspension dominates) is resolved once in the backend and reached two ways: the bot's join-time unary ResolveChatEligibility over the existing mTLS bot-link, and a backend chat_access_changed event -> gateway -> ChatGate command (idempotent; a temporary-block-expiry sweeper may over-emit). The bot guards the block/unblock path with getChatMember, since bots cannot list members. A web_app button cannot open another bot's Mini App (it signs initData with the sending bot's token), so the promo button is a t.me ?startapp URL reusing the UI's VITE_TELEGRAM_LINK. The bot must be a chat admin with the restrict-members right and chat_member in its allowed updates. No schema change: chat_muted reuses the data-driven account_roles table.
81 lines
2.5 KiB
Go
81 lines
2.5 KiB
Go
package account
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// fakeExpiryQuerier records the `since` bound of each call and replays a scripted
|
|
// result/error per call, so the sweeper's window and dispatch logic is testable
|
|
// without a database.
|
|
type fakeExpiryQuerier struct {
|
|
results [][]uuid.UUID
|
|
errs []error
|
|
sinces []time.Time
|
|
idx int
|
|
}
|
|
|
|
func (f *fakeExpiryQuerier) SuspensionsExpiredBetween(_ context.Context, since, _ time.Time) ([]uuid.UUID, error) {
|
|
f.sinces = append(f.sinces, since)
|
|
i := f.idx
|
|
f.idx++
|
|
if i < len(f.errs) && f.errs[i] != nil {
|
|
return nil, f.errs[i]
|
|
}
|
|
if i < len(f.results) {
|
|
return f.results[i], nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func newSweeper(store suspensionExpiryQuerier, onExpire func(uuid.UUID)) *SuspensionSweeper {
|
|
return &SuspensionSweeper{
|
|
store: store,
|
|
onExpire: onExpire,
|
|
log: zap.NewNop(),
|
|
since: time.Now().Add(-time.Minute).UTC(),
|
|
}
|
|
}
|
|
|
|
func TestSuspensionSweeperDispatchesAndAdvances(t *testing.T) {
|
|
id1, id2 := uuid.New(), uuid.New()
|
|
fake := &fakeExpiryQuerier{results: [][]uuid.UUID{{id1, id2}, nil}}
|
|
var got []uuid.UUID
|
|
w := newSweeper(fake, func(id uuid.UUID) { got = append(got, id) })
|
|
|
|
first := w.since
|
|
w.sweep(context.Background())
|
|
assert.Equal(t, []uuid.UUID{id1, id2}, got, "every expired account is dispatched")
|
|
assert.True(t, w.since.After(first), "the window advances on success")
|
|
|
|
// A second sweep opens the next window at the previous upper bound.
|
|
prev := w.since
|
|
w.sweep(context.Background())
|
|
require.Len(t, fake.sinces, 2)
|
|
assert.True(t, fake.sinces[1].After(fake.sinces[0]), "consecutive windows are contiguous and forward")
|
|
assert.True(t, fake.sinces[1].Equal(prev), "the next window starts at the previous upper bound")
|
|
}
|
|
|
|
func TestSuspensionSweeperKeepsWindowOnError(t *testing.T) {
|
|
fake := &fakeExpiryQuerier{errs: []error{errors.New("db down")}}
|
|
w := newSweeper(fake, func(uuid.UUID) { t.Fatal("onExpire must not run when the query fails") })
|
|
|
|
before := w.since
|
|
w.sweep(context.Background())
|
|
assert.True(t, w.since.Equal(before), "the window is retained on error so the next tick retries it")
|
|
}
|
|
|
|
func TestNewSuspensionSweeperDefaults(t *testing.T) {
|
|
w := NewSuspensionSweeper(nil, func(uuid.UUID) {}, nil)
|
|
assert.Equal(t, time.Minute, w.Interval())
|
|
assert.NotNil(t, w.log, "a nil logger is tolerated")
|
|
assert.WithinDuration(t, time.Now().UTC(), w.since, time.Second, "the first window opens at construction time")
|
|
}
|