Ctrl K
ComunidadGuías…

internal/store/postgres/bot_verification_integration_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

	"github.com/jackc/pgx/v5/pgxpool"

	"telesrv/internal/domain"
	"telesrv/internal/observability/dbtrace"
)

// botVerificationTestUser inserts a throwaway user row and registers the cleanup
// for everything the third-party verification tables may hang off it. Marks
// cascade from the verifier row, but the applications reference users directly,
// so they have to go first.
func botVerificationTestUser(t *testing.T, pool *pgxpool.Pool) int64 {
	t.Helper()
	ctx := context.Background()
	var id int64
	if err := pool.QueryRow(ctx, `
INSERT INTO users (access_hash, phone, first_name)
VALUES ($1, $2, 'bot verification test')
RETURNING id`, time.Now().UnixNano()&0x7fffffffffffffff, "9"+randomSuffix(t)).Scan(&id); err != nil {
		t.Fatalf("insert bot verification test user: %v", err)
	}
	t.Cleanup(func() {
		cleanupCtx := context.Background()
		_, _ = pool.Exec(cleanupCtx, `
DELETE FROM custom_verification_requests
WHERE verifier_bot_id = $1 OR applicant_user_id = $1`, id)
		_, _ = pool.Exec(cleanupCtx, `
DELETE FROM custom_verifications WHERE verifier_bot_id = $1`, id)
		_, _ = pool.Exec(cleanupCtx, `
DELETE FROM bot_verifier_settings WHERE bot_id = $1`, id)
		_, _ = pool.Exec(cleanupCtx, `DELETE FROM users WHERE id = $1`, id)
	})
	return id
}

// botVerificationTestDocumentID hands out a custom emoji document id no other
// test run collides on, because verification_icons.document_id is unique.
func botVerificationTestDocumentID(t *testing.T) int64 {
	t.Helper()
	raw, err := strconv.ParseInt(randomSuffix(t), 16, 64)
	if err != nil {
		t.Fatalf("parse random document id: %v", err)
	}
	return raw + 1
}

// botVerificationTestIcon adds a catalogue entry and removes it afterwards; the
// catalogue is deployment-wide, so it is not covered by the user cleanup.
func botVerificationTestIcon(t *testing.T, pool *pgxpool.Pool, s *BotVerificationStore, name string, ownerBotID int64) domain.VerificationIcon {
	t.Helper()
	icon, err := s.UpsertVerificationIcon(context.Background(), domain.VerificationIcon{
		DocumentID: botVerificationTestDocumentID(t),
		OwnerBotID: ownerBotID,
		Name:       name,
		Active:     true,
	})
	if err != nil {
		t.Fatalf("upsert verification icon %q: %v", name, err)
	}
	t.Cleanup(func() {
		_, _ = pool.Exec(context.Background(),
			`DELETE FROM verification_icons WHERE id = $1`, icon.ID)
	})
	return icon
}

// botVerificationTestVerifier grants verifier status the way the admin edge does.
func botVerificationTestVerifier(t *testing.T, s *BotVerificationStore, botID, iconDocumentID int64) domain.BotVerifierSettings {
	t.Helper()
	settings, err := s.UpsertBotVerifierSettings(context.Background(), domain.BotVerifierSettings{
		BotID:                      botID,
		IconDocumentID:             iconDocumentID,
		CompanyName:                fmt.Sprintf("Verifier %d", botID),
		DefaultDescription:         "verified by the test verifier",
		CanModifyCustomDescription: true,
		Enabled:                    true,
		GrantedBy:                  "operator",
		GrantReason:                "test fixture",
	})
	if err != nil {
		t.Fatalf("grant verifier %d: %v", botID, err)
	}
	return settings
}

func botVerificationTestRequest(verifier, applicant int64, peer domain.Peer, username string) domain.CustomVerificationRequest {
	return domain.CustomVerificationRequest{
		VerifierBotID:        verifier,
		ApplicantUserID:      applicant,
		Peer:                 peer,
		PeerTitle:            "Target " + username,
		PeerUsername:         username,
		Reason:               "we run the official account for this brand",
		RequestedDescription: "official brand account",
		CorrelationID:        fmt.Sprintf("corr-%d", peer.ID),
	}
}

// botVerificationTxGrant is the callback shape the app layer is expected to use:
// the mark is written through the transaction that is deciding the application,
// so the two writes commit or roll back together.
func botVerificationTxGrant() func(context.Context, domain.CustomVerificationRequest) error {
	return func(ctx context.Context, req domain.CustomVerificationRequest) error {
		tx, ok := VerificationTxFromContext(ctx)
		if !ok {
			return fmt.Errorf("decision context carries no transaction")
		}
		_, _, err := NewBotVerificationStore(tx).GrantCustomVerification(ctx,
			domain.CustomVerification{
				VerifierBotID:   req.VerifierBotID,
				Peer:            req.Peer,
				Description:     req.RequestedDescription,
				GrantedByUserID: req.ApplicantUserID,
			})
		return err
	}
}

func botVerificationTxRevoke() func(context.Context, domain.CustomVerificationRequest) error {
	return func(ctx context.Context, req domain.CustomVerificationRequest) error {
		tx, ok := VerificationTxFromContext(ctx)
		if !ok {
			return fmt.Errorf("decision context carries no transaction")
		}
		_, err := NewBotVerificationStore(tx).RevokeCustomVerification(ctx,
			req.VerifierBotID, req.Peer)
		return err
	}
}

func botVerificationUserPeer(id int64) domain.Peer {
	return domain.Peer{Type: domain.PeerTypeUser, ID: id}
}

func botVerificationChannelPeer(id int64) domain.Peer {
	return domain.Peer{Type: domain.PeerTypeChannel, ID: id}
}

// botVerificationIconIndex locates an entry in a catalogue listing. The
// catalogue is deployment-wide, so a test asserts on positions of its own rows
// rather than on the length of the page.
func botVerificationIconIndex(icons []domain.VerificationIcon, iconID int64) int {
	for i, icon := range icons {
		if icon.ID == iconID {
			return i
		}
	}
	return -1
}

// TestBotVerificationIconCataloguePostgres covers the catalogue against the real
// schema: an entry is keyed by document id, retiring one keeps it readable, and
// the listing pages newest first with the activeOnly filter honoured.
func TestBotVerificationIconCataloguePostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	s := NewBotVerificationStore(pool)

	first := botVerificationTestIcon(t, pool, s, "  Blue check  ", 0)
	if first.ID == 0 || first.Name != "Blue check" || !first.Active {
		t.Fatalf("stored icon = %+v", first)
	}
	if first.CreatedAt.IsZero() || !first.UpdatedAt.Equal(first.CreatedAt) ||
		first.CreatedAt.Location() != time.UTC {
		t.Fatalf("icon timestamps = %v / %v", first.CreatedAt, first.UpdatedAt)
	}
	second := botVerificationTestIcon(t, pool, s, "Reserved", 777)
	if second.OwnerBotID != 777 {
		t.Fatalf("reserved icon = %+v", second)
	}

	// document_id is the identity of an entry: a second upsert edits in place.
	edited, err := s.UpsertVerificationIcon(ctx, domain.VerificationIcon{
		DocumentID: first.DocumentID, OwnerBotID: 42, Name: "Blue check v2", Active: true,
	})
	if err != nil {
		t.Fatalf("re-upsert icon: %v", err)
	}
	if edited.ID != first.ID || edited.Name != "Blue check v2" || edited.OwnerBotID != 42 {
		t.Fatalf("edited icon = %+v, want id %d", edited, first.ID)
	}
	if !edited.CreatedAt.Equal(first.CreatedAt) || !edited.UpdatedAt.After(first.UpdatedAt) {
		t.Fatalf("edited timestamps = %v / %v", edited.CreatedAt, edited.UpdatedAt)
	}
	if _, err := s.UpsertVerificationIcon(ctx, domain.VerificationIcon{
		DocumentID: 0, Name: "no document",
	}); !errors.Is(err, domain.ErrVerificationIconInvalid) {
		t.Fatalf("upsert without document err = %v, want ErrVerificationIconInvalid", err)
	}

	retired, err := s.SetVerificationIconActive(ctx, second.ID, false)
	if err != nil {
		t.Fatalf("retire icon: %v", err)
	}
	if retired.Active || !retired.UpdatedAt.After(second.UpdatedAt) {
		t.Fatalf("retired icon = %+v", retired)
	}
	if _, err := s.SetVerificationIconActive(ctx, 0, false); !errors.Is(err, domain.ErrVerificationIconNotFound) {
		t.Fatalf("retire icon 0 err = %v, want ErrVerificationIconNotFound", err)
	}
	if _, err := s.SetVerificationIconActive(ctx, second.ID+1<<40, false); !errors.Is(err, domain.ErrVerificationIconNotFound) {
		t.Fatalf("retire unknown err = %v, want ErrVerificationIconNotFound", err)
	}

	byDocument, err := s.VerificationIconByDocument(ctx, second.DocumentID)
	if err != nil || byDocument.ID != second.ID || byDocument.Active {
		t.Fatalf("icon by document = %+v err=%v", byDocument, err)
	}
	if _, err := s.VerificationIconByDocument(ctx, -1); !errors.Is(err, domain.ErrVerificationIconNotFound) {
		t.Fatalf("icon by bad document err = %v, want ErrVerificationIconNotFound", err)
	}
	byID, err := s.VerificationIcon(ctx, first.ID)
	if err != nil || byID.DocumentID != first.DocumentID {
		t.Fatalf("icon by id = %+v err=%v", byID, err)
	}
	if _, err := s.VerificationIcon(ctx, first.ID+1<<40); !errors.Is(err, domain.ErrVerificationIconNotFound) {
		t.Fatalf("unknown icon err = %v, want ErrVerificationIconNotFound", err)
	}

	all, err := s.ListVerificationIcons(ctx, false, maxBotVerificationListLimit)
	if err != nil {
		t.Fatalf("list icons: %v", err)
	}
	firstAt, secondAt := botVerificationIconIndex(all, first.ID), botVerificationIconIndex(all, second.ID)
	if firstAt < 0 || secondAt < 0 || secondAt >= firstAt {
		t.Fatalf("catalogue order: first at %d, second at %d, want newest first", firstAt, secondAt)
	}
	active, err := s.ListVerificationIcons(ctx, true, maxBotVerificationListLimit)
	if err != nil {
		t.Fatalf("list active icons: %v", err)
	}
	if botVerificationIconIndex(active, first.ID) < 0 {
		t.Fatal("active catalogue lost the active icon")
	}
	if botVerificationIconIndex(active, second.ID) >= 0 {
		t.Fatal("active catalogue kept the retired icon")
	}
	if page, err := s.ListVerificationIcons(ctx, false, 1); err != nil || len(page) != 1 {
		t.Fatalf("icon page = %+v err=%v", page, err)
	}
}

// TestBotVerifierSettingsLifecyclePostgres covers verifier status: optimistic
// locking on version, the idempotent kill switch and the cascade that takes the
// marks with the verifier row while the applications stay as history.
func TestBotVerifierSettingsLifecyclePostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	s := NewBotVerificationStore(pool)
	bot := botVerificationTestUser(t, pool)
	otherBot := botVerificationTestUser(t, pool)
	applicant := botVerificationTestUser(t, pool)
	icon := botVerificationTestIcon(t, pool, s, "verifier icon", 0)

	if _, err := s.BotVerifierSettings(ctx, bot); !errors.Is(err, domain.ErrVerifierNotFound) {
		t.Fatalf("unknown verifier err = %v, want ErrVerifierNotFound", err)
	}
	if _, err := s.UpsertBotVerifierSettings(ctx, domain.BotVerifierSettings{
		BotID: bot, IconDocumentID: icon.DocumentID, CompanyName: "Acme", Version: 3,
	}); !errors.Is(err, domain.ErrVerifierNotFound) {
		t.Fatalf("versioned upsert of missing row err = %v, want ErrVerifierNotFound", err)
	}
	if _, err := s.UpsertBotVerifierSettings(ctx, domain.BotVerifierSettings{
		BotID: bot, CompanyName: "Acme",
	}); !errors.Is(err, domain.ErrVerifierSettingsInvalid) {
		t.Fatalf("iconless upsert err = %v, want ErrVerifierSettingsInvalid", err)
	}

	created := botVerificationTestVerifier(t, s, bot, icon.DocumentID)
	if created.Version != 1 || !created.Enabled || !created.CanModifyCustomDescription {
		t.Fatalf("created verifier = %+v", created)
	}
	if created.CreatedAt.IsZero() || !created.UpdatedAt.Equal(created.CreatedAt) {
		t.Fatalf("verifier timestamps = %v / %v", created.CreatedAt, created.UpdatedAt)
	}

	// Version 0 means "there is no row yet", so it loses against the stored row.
	if _, err := s.UpsertBotVerifierSettings(ctx, domain.BotVerifierSettings{
		BotID: bot, IconDocumentID: icon.DocumentID, CompanyName: "Acme",
	}); !errors.Is(err, domain.ErrCustomVerificationVersionConflict) {
		t.Fatalf("re-create err = %v, want ErrCustomVerificationVersionConflict", err)
	}
	if _, err := s.UpsertBotVerifierSettings(ctx, domain.BotVerifierSettings{
		BotID: bot, IconDocumentID: icon.DocumentID, CompanyName: "Acme", Version: 99,
	}); !errors.Is(err, domain.ErrCustomVerificationVersionConflict) {
		t.Fatalf("stale upsert err = %v, want ErrCustomVerificationVersionConflict", err)
	}

	edited, err := s.UpsertBotVerifierSettings(ctx, domain.BotVerifierSettings{
		BotID:              bot,
		IconDocumentID:     icon.DocumentID + 1,
		CompanyName:        "Acme Media",
		DefaultDescription: "checked by Acme",
		Enabled:            true,
		GrantedBy:          "operator",
		Version:            created.Version,
	})
	if err != nil {
		t.Fatalf("edit verifier: %v", err)
	}
	if edited.Version != 2 || edited.CanModifyCustomDescription ||
		edited.DefaultDescription != "checked by Acme" {
		t.Fatalf("edited verifier = %+v", edited)
	}
	if !edited.CreatedAt.Equal(created.CreatedAt) || !edited.UpdatedAt.After(created.UpdatedAt) {
		t.Fatalf("edited timestamps = %v / %v", edited.CreatedAt, edited.UpdatedAt)
	}
	// DescriptionFor is the single place the description rule lives; the stored
	// row is what feeds it.
	if _, err := edited.DescriptionFor("per-peer text"); !errors.Is(err, domain.ErrVerifierDescriptionForbidden) {
		t.Fatalf("custom description on a locked verifier err = %v, want ErrVerifierDescriptionForbidden", err)
	}
	if got, err := edited.DescriptionFor(""); err != nil || got != "checked by Acme" {
		t.Fatalf("default description = %q err=%v", got, err)
	}

	disabled, err := s.SetBotVerifierEnabled(ctx, bot, false)
	if err != nil {
		t.Fatalf("disable verifier: %v", err)
	}
	if disabled.Enabled || disabled.Version != edited.Version+1 {
		t.Fatalf("disabled verifier = %+v", disabled)
	}
	again, err := s.SetBotVerifierEnabled(ctx, bot, false)
	if err != nil || again.Version != disabled.Version {
		t.Fatalf("re-disable = v%d err=%v, want v%d", again.Version, err, disabled.Version)
	}
	if _, err := s.SetBotVerifierEnabled(ctx, otherBot, true); !errors.Is(err, domain.ErrVerifierNotFound) {
		t.Fatalf("disable non-verifier err = %v, want ErrVerifierNotFound", err)
	}
	if stored, err := s.BotVerifierSettings(ctx, bot); err != nil || stored.Enabled {
		t.Fatalf("read disabled verifier = %+v err=%v", stored, err)
	}

	enabledOther := botVerificationTestVerifier(t, s, otherBot, icon.DocumentID)
	batch, err := s.BotVerifierSettingsBatch(ctx, []int64{bot, otherBot, applicant, 0, bot})
	if err != nil {
		t.Fatalf("batch verifiers: %v", err)
	}
	if len(batch) != 2 || batch[bot].Enabled || !batch[otherBot].Enabled {
		t.Fatalf("verifier batch = %+v", batch)
	}
	if _, present := batch[applicant]; present {
		t.Fatal("batch invented verifier status for a plain user")
	}
	if empty, err := s.BotVerifierSettingsBatch(ctx, nil); err != nil || len(empty) != 0 {
		t.Fatalf("empty verifier batch = %+v err=%v", empty, err)
	}

	listed, err := s.ListBotVerifiers(ctx, false, maxBotVerificationListLimit)
	if err != nil {
		t.Fatalf("list verifiers: %v", err)
	}
	seenDisabled, seenEnabled := false, false
	for _, settings := range listed {
		seenDisabled = seenDisabled || settings.BotID == bot
		seenEnabled = seenEnabled || settings.BotID == otherBot
	}
	if !seenDisabled || !seenEnabled {
		t.Fatalf("verifier list lost a row: %+v", listed)
	}
	enabledOnly, err := s.ListBotVerifiers(ctx, true, maxBotVerificationListLimit)
	if err != nil {
		t.Fatalf("list enabled verifiers: %v", err)
	}
	for _, settings := range enabledOnly {
		if settings.BotID == bot {
			t.Fatal("enabled-only list kept a disabled verifier")
		}
	}

	// Marks cascade with the verifier row; applications do not.
	peer := botVerificationChannelPeer(applicant)
	if _, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: enabledOther.BotID, Peer: peer, Description: "cascade me",
	}); err != nil {
		t.Fatalf("grant before delete: %v", err)
	}
	req, err := s.CreateCustomVerificationRequest(ctx,
		botVerificationTestRequest(enabledOther.BotID, applicant, peer, "CascadeChannel"))
	if err != nil {
		t.Fatalf("file application before delete: %v", err)
	}
	removed, err := s.DeleteBotVerifierSettings(ctx, enabledOther.BotID)
	if err != nil || !removed {
		t.Fatalf("delete verifier: removed=%v err=%v", removed, err)
	}
	if removed, err := s.DeleteBotVerifierSettings(ctx, enabledOther.BotID); err != nil || removed {
		t.Fatalf("repeated delete: removed=%v err=%v", removed, err)
	}
	if _, err := s.CustomVerification(ctx, enabledOther.BotID, peer); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("mark after cascade err = %v, want ErrCustomVerificationNotFound", err)
	}
	if count, err := s.CountCustomVerifications(ctx, enabledOther.BotID); err != nil || count != 0 {
		t.Fatalf("mark count after cascade = %d err=%v", count, err)
	}
	if kept, err := s.CustomVerificationRequest(ctx, req.ID); err != nil || kept.ID != req.ID {
		t.Fatalf("application after cascade = %+v err=%v", kept, err)
	}
}

// TestCustomVerificationProjectionPostgres is the projection contract: exactly
// one mark exists per peer, a later verifier replaces the former one, a disabled
// verifier projects nothing while its row survives, and the batch form resolves
// every peer in a single query.
func TestCustomVerificationProjectionPostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	s := NewBotVerificationStore(pool)
	alphaBot := botVerificationTestUser(t, pool)
	betaBot := botVerificationTestUser(t, pool)
	target := botVerificationTestUser(t, pool)
	icon := botVerificationTestIcon(t, pool, s, "projection icon", 0)
	alpha := botVerificationTestVerifier(t, s, alphaBot, icon.DocumentID)
	beta := botVerificationTestVerifier(t, s, betaBot, icon.DocumentID+1)
	peer := botVerificationUserPeer(target)

	if _, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: target, Peer: peer,
	}); !errors.Is(err, domain.ErrVerifierNotFound) {
		t.Fatalf("grant by non-verifier err = %v, want ErrVerifierNotFound", err)
	}
	if _, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: alpha.BotID, Peer: domain.Peer{Type: domain.PeerTypeCommunity, ID: 5},
	}); !errors.Is(err, domain.ErrCustomVerificationTargetInvalid) {
		t.Fatalf("grant on community err = %v, want ErrCustomVerificationTargetInvalid", err)
	}
	if _, err := s.PeerVerification(ctx, peer); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("projection of an unmarked peer err = %v, want ErrCustomVerificationNotFound", err)
	}

	// The icon is denormalised from the verifier when the caller leaves it unset.
	alphaMark, created, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: alpha.BotID, Peer: peer, Description: "checked by alpha",
		GrantedByUserID: target,
	})
	if err != nil || !created {
		t.Fatalf("grant alpha mark: created=%v err=%v", created, err)
	}
	if alphaMark.IconDocumentID != alpha.IconDocumentID || alphaMark.Version != 1 ||
		alphaMark.Peer != peer {
		t.Fatalf("alpha mark = %+v", alphaMark)
	}
	betaMark, created, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: beta.BotID, Peer: peer, Description: "checked by beta",
	})
	if err != nil || !created {
		t.Fatalf("grant beta mark: created=%v err=%v", created, err)
	}
	if betaMark.ID != alphaMark.ID || betaMark.Version != alphaMark.Version+1 {
		t.Fatalf("replacement mark = %+v, want id %d v%d", betaMark, alphaMark.ID, alphaMark.Version+1)
	}
	if _, err := s.CustomVerification(ctx, alpha.BotID, peer); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("replaced alpha mark err = %v, want ErrCustomVerificationNotFound", err)
	}

	// One peer has one wire-visible mark, irrespective of how often it is read.
	for i := 0; i < 3; i++ {
		got, err := s.PeerVerification(ctx, peer)
		if err != nil {
			t.Fatalf("projection after replacement: %v", err)
		}
		if got.ID != betaMark.ID || got.Projection().Icon != beta.IconDocumentID {
			t.Fatalf("projection = %+v, want replacement mark %d", got, betaMark.ID)
		}
	}

	// Kill switch: disabling the current verifier hides the badge. The replaced
	// alpha mark must not silently reappear.
	if _, err := s.SetBotVerifierEnabled(ctx, beta.BotID, false); err != nil {
		t.Fatalf("disable beta: %v", err)
	}
	if _, err := s.PeerVerification(ctx, peer); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("projection after disabling beta err=%v, want ErrCustomVerificationNotFound", err)
	}
	if stored, err := s.CustomVerification(ctx, beta.BotID, peer); err != nil || stored.ID != betaMark.ID {
		t.Fatalf("disabled verifier lost its mark: %+v err=%v", stored, err)
	}
	if batch, err := s.PeerVerificationBatch(ctx, []domain.Peer{peer}); err != nil || len(batch) != 0 {
		t.Fatalf("batch with current verifier disabled = %+v err=%v", batch, err)
	}
	if _, err := s.SetBotVerifierEnabled(ctx, beta.BotID, true); err != nil {
		t.Fatalf("re-enable beta: %v", err)
	}
	if got, err := s.PeerVerification(ctx, peer); err != nil || got.ID != betaMark.ID {
		t.Fatalf("projection after re-enabling = %+v err=%v", got, err)
	}

	// Granting through alpha replaces beta's mark on the same peer.
	regranted, created, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: alpha.BotID, Peer: peer, IconDocumentID: icon.DocumentID + 99,
		Description: "checked by alpha, again",
	})
	if err != nil || !created {
		t.Fatalf("re-grant alpha mark: created=%v err=%v", created, err)
	}
	if regranted.ID != betaMark.ID || regranted.Version != betaMark.Version+1 ||
		regranted.IconDocumentID != icon.DocumentID+99 ||
		regranted.Description != "checked by alpha, again" {
		t.Fatalf("re-granted mark = %+v", regranted)
	}
	if regranted.CreatedAt.Before(betaMark.CreatedAt) || !regranted.UpdatedAt.After(betaMark.UpdatedAt) {
		t.Fatalf("re-granted timestamps = %v / %v", regranted.CreatedAt, regranted.UpdatedAt)
	}
	if count, err := s.CountCustomVerifications(ctx, alpha.BotID); err != nil || count != 1 {
		t.Fatalf("alpha mark count = %d err=%v", count, err)
	}
	if got, err := s.PeerVerification(ctx, peer); err != nil || got.VerifierBotID != alpha.BotID {
		t.Fatalf("projection after re-grant = %+v err=%v", got, err)
	}

	// The batch form resolves many peers with one round trip.
	second := botVerificationChannelPeer(target + 1)
	third := botVerificationUserPeer(target + 2)
	fourth := botVerificationChannelPeer(target + 3)
	unmarked := botVerificationChannelPeer(target + 4)
	secondMark, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: alpha.BotID, Peer: second, Description: "second",
	})
	if err != nil {
		t.Fatalf("grant second: %v", err)
	}
	thirdMark, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: beta.BotID, Peer: third, Description: "third",
	})
	if err != nil {
		t.Fatalf("grant third: %v", err)
	}
	fourthMark, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: beta.BotID, Peer: fourth, Description: "fourth",
	})
	if err != nil {
		t.Fatalf("grant fourth: %v", err)
	}

	tracedCtx, stats := dbtrace.WithStats(ctx)
	before := stats.Snapshot()
	batch, err := s.PeerVerificationBatch(tracedCtx, []domain.Peer{
		peer, second, third, fourth, unmarked, peer, {Type: domain.PeerTypeUser, ID: 0},
	})
	if err != nil {
		t.Fatalf("batch projection: %v", err)
	}
	// The point of the batch form: five distinct peers, one query. An N+1
	// implementation would put a round trip per peer on every serialisation.
	if spent := stats.Snapshot().Sub(before); spent.Queries != 1 {
		t.Fatalf("batch projection ran %d queries, want exactly 1", spent.Queries)
	}
	if len(batch) != 4 {
		t.Fatalf("batch projection = %+v, want 4 peers", batch)
	}
	if batch[peer].ID != regranted.ID || batch[second].ID != secondMark.ID ||
		batch[third].ID != thirdMark.ID || batch[fourth].ID != fourthMark.ID {
		t.Fatalf("batch projection picked %+v", batch)
	}
	if _, present := batch[unmarked]; present {
		t.Fatal("batch projected an unmarked peer")
	}
	if empty, err := s.PeerVerificationBatch(ctx, nil); err != nil || len(empty) != 0 {
		t.Fatalf("empty batch = %+v err=%v", empty, err)
	}

	// Disabling a verifier drops its peers from the batch too, in the same query.
	if _, err := s.SetBotVerifierEnabled(ctx, beta.BotID, false); err != nil {
		t.Fatalf("disable beta again: %v", err)
	}
	batch, err = s.PeerVerificationBatch(ctx, []domain.Peer{peer, second, third, fourth})
	if err != nil {
		t.Fatalf("batch projection after disable: %v", err)
	}
	if len(batch) != 2 || batch[peer].ID != regranted.ID || batch[second].ID != secondMark.ID {
		t.Fatalf("batch after disable = %+v", batch)
	}
	if _, err := s.SetBotVerifierEnabled(ctx, beta.BotID, true); err != nil {
		t.Fatalf("re-enable beta again: %v", err)
	}

	// Listing, filtering and keyset paging over the marks.
	mine, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		VerifierBotID: beta.BotID,
	})
	if err != nil || len(mine) != 2 || mine[0].ID != fourthMark.ID {
		t.Fatalf("verifier-filtered marks = %+v err=%v", mine, err)
	}
	page, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		VerifierBotID: beta.BotID, Limit: 2,
	})
	if err != nil || len(page) != 2 {
		t.Fatalf("mark page = %+v err=%v", page, err)
	}
	next, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		VerifierBotID: beta.BotID, Limit: 2, BeforeID: page[len(page)-1].ID,
	})
	if err != nil || len(next) != 0 {
		t.Fatalf("mark keyset tail = %+v err=%v", next, err)
	}
	channels, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		VerifierBotID: beta.BotID, PeerType: domain.PeerTypeChannel,
	})
	if err != nil || len(channels) != 1 || channels[0].Peer != fourth {
		t.Fatalf("channel-filtered marks = %+v err=%v", channels, err)
	}
	byPeer, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		PeerType: peer.Type, PeerID: peer.ID,
	})
	if err != nil || len(byPeer) != 1 {
		t.Fatalf("peer-filtered marks = %+v err=%v", byPeer, err)
	}
	byQuery, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		VerifierBotID: alpha.BotID, Query: fmt.Sprintf("%d", second.ID),
	})
	if err != nil || len(byQuery) != 1 || byQuery[0].ID != secondMark.ID {
		t.Fatalf("numeric mark query = %+v err=%v", byQuery, err)
	}
	byText, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		VerifierBotID: alpha.BotID, Query: "AGAIN",
	})
	if err != nil || len(byText) != 1 || byText[0].ID != regranted.ID {
		t.Fatalf("text mark query = %+v err=%v", byText, err)
	}
	if _, err := s.ListCustomVerifications(ctx, domain.CustomVerificationFilter{
		PeerType: domain.PeerTypeFolder,
	}); !errors.Is(err, domain.ErrCustomVerificationTargetInvalid) {
		t.Fatalf("bad peer-type filter err = %v, want ErrCustomVerificationTargetInvalid", err)
	}

	// A replaced verifier cannot revoke the current mark.
	revoked, err := s.RevokeCustomVerification(ctx, beta.BotID, peer)
	if err != nil || revoked {
		t.Fatalf("revoke replaced beta mark: revoked=%v err=%v", revoked, err)
	}
	if revoked, err := s.RevokeCustomVerification(ctx, beta.BotID, peer); err != nil || revoked {
		t.Fatalf("repeated revoke: revoked=%v err=%v", revoked, err)
	}
	if got, err := s.PeerVerification(ctx, peer); err != nil || got.ID != regranted.ID {
		t.Fatalf("projection after rejected revoke = %+v err=%v", got, err)
	}
	if revoked, err := s.RevokeCustomVerification(ctx, alpha.BotID, peer); err != nil || !revoked {
		t.Fatalf("revoke alpha mark: revoked=%v err=%v", revoked, err)
	}
	if _, err := s.RevokeCustomVerification(ctx, 0, peer); !errors.Is(err, domain.ErrCustomVerificationTargetInvalid) {
		t.Fatalf("revoke without verifier err = %v, want ErrCustomVerificationTargetInvalid", err)
	}
}

// TestCustomVerificationLimitPostgres pins the per-verifier bound: a new mark is
// refused at the limit while an existing one can still be re-described.
func TestCustomVerificationLimitPostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	s := NewBotVerificationStore(pool)
	bot := botVerificationTestUser(t, pool)
	icon := botVerificationTestIcon(t, pool, s, "limit icon", 0)
	verifier := botVerificationTestVerifier(t, s, bot, icon.DocumentID)

	// The bound is 10k marks; filling it through the store would be 10k round
	// trips, so the fixture is seeded in one statement. peer_id has no foreign
	// key, which is what makes that safe.
	if _, err := pool.Exec(ctx, `
INSERT INTO custom_verifications (
  verifier_bot_id, peer_type, peer_id, icon_document_id, description,
  created_at, updated_at, version
)
SELECT $1, 'channel', g, $2, '', $3, $3, 1
FROM generate_series(1, $4::bigint) AS g`,
		verifier.BotID, verifier.IconDocumentID, time.Now().UTC(),
		domain.MaxCustomVerificationsPerVerifier); err != nil {
		t.Fatalf("seed marks: %v", err)
	}
	count, err := s.CountCustomVerifications(ctx, verifier.BotID)
	if err != nil || count != domain.MaxCustomVerificationsPerVerifier {
		t.Fatalf("mark count = %d err=%v", count, err)
	}

	if _, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: verifier.BotID, Peer: botVerificationUserPeer(bot),
	}); !errors.Is(err, domain.ErrCustomVerificationLimit) {
		t.Fatalf("grant past the limit err = %v, want ErrCustomVerificationLimit", err)
	}
	// The bound is on creating marks, not on editing them.
	updated, created, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: verifier.BotID, Peer: botVerificationChannelPeer(7),
		Description: "still editable at the limit",
	})
	if err != nil || created {
		t.Fatalf("re-grant at the limit: created=%v err=%v", created, err)
	}
	if updated.Description != "still editable at the limit" || updated.Version != 2 {
		t.Fatalf("re-granted mark at the limit = %+v", updated)
	}
	// Freeing one slot lets the next grant through.
	if revoked, err := s.RevokeCustomVerification(ctx, verifier.BotID,
		botVerificationChannelPeer(1)); err != nil || !revoked {
		t.Fatalf("free a slot: revoked=%v err=%v", revoked, err)
	}
	if _, created, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: verifier.BotID, Peer: botVerificationUserPeer(bot),
	}); err != nil || !created {
		t.Fatalf("grant into the freed slot: created=%v err=%v", created, err)
	}
}

// TestCustomVerificationRequestQueuePostgres covers the application queue
// against the real schema: one pending application per (verifier, peer), the
// decision status machine, and the transaction that keeps an approved
// application and its mark together -- including the rollback when the callback
// fails after it has already written through the decision's transaction.
func TestCustomVerificationRequestQueuePostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	s := NewBotVerificationStore(pool)
	bot := botVerificationTestUser(t, pool)
	applicant := botVerificationTestUser(t, pool)
	target := botVerificationTestUser(t, pool)
	icon := botVerificationTestIcon(t, pool, s, "queue icon", 0)
	verifier := botVerificationTestVerifier(t, s, bot, icon.DocumentID)
	peer := botVerificationChannelPeer(target)
	// A distinct peer id, so the numeric queue search below addresses one peer
	// rather than both shapes of the same number.
	other := botVerificationUserPeer(target + 1)

	baseline, err := s.CustomVerificationRequestCounts(ctx)
	if err != nil {
		t.Fatalf("baseline queue counts: %v", err)
	}

	filed, err := s.CreateCustomVerificationRequest(ctx,
		botVerificationTestRequest(verifier.BotID, applicant, peer, "AcmeNews"))
	if err != nil {
		t.Fatalf("file application: %v", err)
	}
	if filed.Status != domain.CustomVerificationPending || filed.Version != 1 {
		t.Fatalf("filed application = %s v%d", filed.Status, filed.Version)
	}
	if !filed.ApprovedAt.IsZero() || !filed.RejectedAt.IsZero() || filed.DecidedBy != "" {
		t.Fatalf("filed application carries a decision: %+v", filed)
	}
	if filed.Peer != peer || filed.PeerUsername != "AcmeNews" ||
		filed.CorrelationID != fmt.Sprintf("corr-%d", peer.ID) {
		t.Fatalf("filed payload did not round-trip: %+v", filed)
	}

	// custom_verification_requests_pending_idx: one live application per pair.
	if _, err := s.CreateCustomVerificationRequest(ctx,
		botVerificationTestRequest(verifier.BotID, applicant, peer, "AcmeNews")); !errors.Is(err, domain.ErrCustomVerificationRequestExists) {
		t.Fatalf("duplicate pending err = %v, want ErrCustomVerificationRequestExists", err)
	}
	if _, err := s.CreateCustomVerificationRequest(ctx, domain.CustomVerificationRequest{
		VerifierBotID: verifier.BotID, ApplicantUserID: applicant, Peer: peer,
		Status: domain.CustomVerificationApproved,
	}); !errors.Is(err, domain.ErrCustomVerificationRequestInvalid) {
		t.Fatalf("pre-decided application err = %v, want ErrCustomVerificationRequestInvalid", err)
	}
	// The SQL byte bound reserves the worst-case UTF-8 width for the domain's
	// rune limit, so valid multi-byte text must behave like ASCII.
	wide := botVerificationTestRequest(verifier.BotID, applicant, botVerificationUserPeer(target+9), "Wide")
	wide.Reason = strings.Repeat("é", domain.MaxCustomVerificationReasonLength-1)
	wideFiled, err := s.CreateCustomVerificationRequest(ctx, wide)
	if err != nil {
		t.Fatalf("valid multi-byte reason: %v", err)
	}
	if _, err := pool.Exec(ctx, `DELETE FROM custom_verification_requests WHERE id = $1`, wideFiled.ID); err != nil {
		t.Fatalf("clean wide application fixture: %v", err)
	}
	if pending, err := s.PendingCustomVerificationRequest(ctx, verifier.BotID, peer); err != nil ||
		pending.ID != filed.ID {
		t.Fatalf("pending application = %+v err=%v", pending, err)
	}

	// A failing callback rolls the whole decision back, including the mark the
	// callback wrote through the decision's own transaction before it failed.
	boom := errors.New("apply exploded")
	grant := botVerificationTxGrant()
	if _, _, err := s.DecideCustomVerificationRequest(ctx, filed.ID, filed.Version,
		domain.CustomVerificationApproved, "operator", "looks good", "",
		func(ctx context.Context, req domain.CustomVerificationRequest) error {
			if err := grant(ctx, req); err != nil {
				return err
			}
			return boom
		}); !errors.Is(err, boom) {
		t.Fatalf("failing apply err = %v, want the callback error", err)
	}
	rolledBack, err := s.CustomVerificationRequest(ctx, filed.ID)
	if err != nil {
		t.Fatalf("read after rollback: %v", err)
	}
	if rolledBack.Status != domain.CustomVerificationPending ||
		rolledBack.Version != filed.Version || !rolledBack.ApprovedAt.IsZero() ||
		rolledBack.DecidedBy != "" {
		t.Fatalf("application after rollback = %+v, want pending v%d", rolledBack, filed.Version)
	}
	if _, err := s.CustomVerification(ctx, verifier.BotID, peer); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("mark after rollback err = %v, want ErrCustomVerificationNotFound", err)
	}
	if _, err := s.PeerVerification(ctx, peer); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("projection after rollback err = %v, want ErrCustomVerificationNotFound", err)
	}

	if _, _, err := s.DecideCustomVerificationRequest(ctx, filed.ID, filed.Version,
		domain.CustomVerificationApproved, "operator", "", "", nil); err == nil {
		t.Fatal("approve without a callback succeeded")
	}
	if _, _, err := s.DecideCustomVerificationRequest(ctx, filed.ID, filed.Version+7,
		domain.CustomVerificationApproved, "operator", "", "", grant); !errors.Is(err, domain.ErrCustomVerificationVersionConflict) {
		t.Fatalf("stale decision err = %v, want ErrCustomVerificationVersionConflict", err)
	}
	if _, _, err := s.DecideCustomVerificationRequest(ctx, filed.ID, filed.Version,
		domain.CustomVerificationPending, "operator", "", "", nil); !errors.Is(err, domain.ErrCustomVerificationRequestInvalid) {
		t.Fatalf("decision back to pending err = %v, want ErrCustomVerificationRequestInvalid", err)
	}
	if _, _, err := s.DecideCustomVerificationRequest(ctx, filed.ID+1<<40, filed.Version,
		domain.CustomVerificationApproved, "operator", "", "", grant); !errors.Is(err, domain.ErrCustomVerificationRequestNotFound) {
		t.Fatalf("decision on unknown application err = %v, want ErrCustomVerificationRequestNotFound", err)
	}

	approved, changed, err := s.DecideCustomVerificationRequest(ctx, filed.ID, filed.Version,
		domain.CustomVerificationApproved, "operator", "brand confirmed", "ticket 12", grant)
	if err != nil || !changed {
		t.Fatalf("approve: changed=%v err=%v", changed, err)
	}
	if approved.Status != domain.CustomVerificationApproved ||
		approved.Version != filed.Version+1 || approved.ApprovedAt.IsZero() ||
		!approved.RejectedAt.IsZero() {
		t.Fatalf("approved application = %+v", approved)
	}
	if approved.DecidedBy != "operator" || approved.DecisionReason != "brand confirmed" ||
		approved.InternalNote != "ticket 12" {
		t.Fatalf("approved decision metadata = %+v", approved)
	}
	mark, err := s.PeerVerification(ctx, peer)
	if err != nil {
		t.Fatalf("projection after approve: %v", err)
	}
	if mark.VerifierBotID != verifier.BotID || mark.Description != filed.RequestedDescription ||
		mark.IconDocumentID != verifier.IconDocumentID || mark.GrantedByUserID != applicant {
		t.Fatalf("mark after approve = %+v", mark)
	}
	if _, err := s.PendingCustomVerificationRequest(ctx, verifier.BotID, peer); !errors.Is(err, domain.ErrCustomVerificationRequestNotFound) {
		t.Fatalf("pending after approve err = %v, want ErrCustomVerificationRequestNotFound", err)
	}

	// Re-issuing the decision that already holds moves nothing and does not run
	// the callback a second time.
	repeat, changed, err := s.DecideCustomVerificationRequest(ctx, approved.ID, approved.Version,
		domain.CustomVerificationApproved, "someone else", "again", "",
		func(context.Context, domain.CustomVerificationRequest) error {
			t.Error("apply ran for a decision that already held")
			return nil
		})
	if err != nil || changed {
		t.Fatalf("repeated approve: changed=%v err=%v", changed, err)
	}
	if repeat.Version != approved.Version || repeat.DecidedBy != "operator" {
		t.Fatalf("repeated approve mutated the row: %+v", repeat)
	}

	// approved -> rejected is not in the status machine; approved -> revoked is.
	if _, _, err := s.DecideCustomVerificationRequest(ctx, approved.ID, approved.Version,
		domain.CustomVerificationRejected, "operator", "changed my mind", "", nil); !errors.Is(err, domain.ErrCustomVerificationRequestInvalid) {
		t.Fatalf("approved -> rejected err = %v, want ErrCustomVerificationRequestInvalid", err)
	}
	revoked, changed, err := s.DecideCustomVerificationRequest(ctx, approved.ID, approved.Version,
		domain.CustomVerificationRevoked, "operator", "brand asked us to", "",
		botVerificationTxRevoke())
	if err != nil || !changed {
		t.Fatalf("revoke: changed=%v err=%v", changed, err)
	}
	// The stamps are paired with the status, so leaving approved clears approved_at.
	if revoked.Status != domain.CustomVerificationRevoked ||
		revoked.Version != approved.Version+1 || !revoked.ApprovedAt.IsZero() ||
		!revoked.RejectedAt.IsZero() {
		t.Fatalf("revoked application = %+v", revoked)
	}
	if _, err := s.PeerVerification(ctx, peer); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("projection after revoke err = %v, want ErrCustomVerificationNotFound", err)
	}
	if _, _, err := s.DecideCustomVerificationRequest(ctx, revoked.ID, revoked.Version,
		domain.CustomVerificationApproved, "operator", "", "", grant); !errors.Is(err, domain.ErrCustomVerificationRequestInvalid) {
		t.Fatalf("revoked -> approved err = %v, want ErrCustomVerificationRequestInvalid", err)
	}

	// A rejection needs a reason, and the domain is what says so.
	second, err := s.CreateCustomVerificationRequest(ctx,
		botVerificationTestRequest(verifier.BotID, applicant, other, "AcmeCEO"))
	if err != nil {
		t.Fatalf("file second application: %v", err)
	}
	if _, _, err := s.DecideCustomVerificationRequest(ctx, second.ID, second.Version,
		domain.CustomVerificationRejected, "operator", "   ", "", nil); !errors.Is(err, domain.ErrVerificationReasonRequired) {
		t.Fatalf("reject without a reason err = %v, want ErrVerificationReasonRequired", err)
	}
	if stillPending, err := s.CustomVerificationRequest(ctx, second.ID); err != nil ||
		stillPending.Status != domain.CustomVerificationPending ||
		stillPending.Version != second.Version {
		t.Fatalf("application after refused rejection = %+v err=%v", stillPending, err)
	}
	rejected, changed, err := s.DecideCustomVerificationRequest(ctx, second.ID, second.Version,
		domain.CustomVerificationRejected, "operator", "not a public figure", "", nil)
	if err != nil || !changed {
		t.Fatalf("reject: changed=%v err=%v", changed, err)
	}
	if rejected.Status != domain.CustomVerificationRejected || rejected.RejectedAt.IsZero() ||
		!rejected.ApprovedAt.IsZero() {
		t.Fatalf("rejected application = %+v", rejected)
	}
	if _, err := s.CustomVerification(ctx, verifier.BotID, other); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
		t.Fatalf("rejection granted a mark: %v", err)
	}
	// A decided pair is free again: history keeps the rejection.
	reapplied, err := s.CreateCustomVerificationRequest(ctx,
		botVerificationTestRequest(verifier.BotID, applicant, other, "AcmeCEO"))
	if err != nil {
		t.Fatalf("re-apply after rejection: %v", err)
	}

	counts, err := s.CustomVerificationRequestCounts(ctx)
	if err != nil {
		t.Fatalf("queue counts: %v", err)
	}
	for status, want := range map[domain.CustomVerificationRequestStatus]int64{
		domain.CustomVerificationPending:  1,
		domain.CustomVerificationRejected: 1,
		domain.CustomVerificationRevoked:  1,
		domain.CustomVerificationApproved: 0,
	} {
		if got := counts[status] - baseline[status]; got != want {
			t.Fatalf("queue count delta for %s = %d, want %d", status, got, want)
		}
	}

	listed, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		VerifierBotID: verifier.BotID,
	})
	if err != nil || len(listed) != 3 || listed[0].ID != reapplied.ID {
		t.Fatalf("queue list = %+v err=%v", listed, err)
	}
	pendingOnly, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		VerifierBotID: verifier.BotID,
		Statuses:      []domain.CustomVerificationRequestStatus{domain.CustomVerificationPending},
	})
	if err != nil || len(pendingOnly) != 1 || pendingOnly[0].ID != reapplied.ID {
		t.Fatalf("pending queue = %+v err=%v", pendingOnly, err)
	}
	page, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		VerifierBotID: verifier.BotID, Limit: 2,
	})
	if err != nil || len(page) != 2 {
		t.Fatalf("queue page = %+v err=%v", page, err)
	}
	next, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		VerifierBotID: verifier.BotID, Limit: 2, BeforeID: page[len(page)-1].ID,
	})
	if err != nil || len(next) != 1 || next[0].ID >= page[len(page)-1].ID {
		t.Fatalf("queue keyset page = %+v err=%v", next, err)
	}
	byUsername, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		VerifierBotID: verifier.BotID, Query: "@acmec",
	})
	if err != nil || len(byUsername) != 2 {
		t.Fatalf("username query = %+v err=%v", byUsername, err)
	}
	byPeerID, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		VerifierBotID: verifier.BotID, Query: fmt.Sprintf("%d", peer.ID),
	})
	if err != nil || len(byPeerID) != 1 || byPeerID[0].ID != revoked.ID {
		t.Fatalf("numeric query = %+v err=%v", byPeerID, err)
	}
	channelsOnly, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		VerifierBotID: verifier.BotID, PeerType: domain.PeerTypeChannel,
	})
	if err != nil || len(channelsOnly) != 1 || channelsOnly[0].ID != revoked.ID {
		t.Fatalf("channel queue = %+v err=%v", channelsOnly, err)
	}
	if _, err := s.ListCustomVerificationRequests(ctx, domain.CustomVerificationRequestFilter{
		Statuses: []domain.CustomVerificationRequestStatus{"nonsense"},
	}); !errors.Is(err, domain.ErrCustomVerificationRequestInvalid) {
		t.Fatalf("bad status filter err = %v, want ErrCustomVerificationRequestInvalid", err)
	}

	history, err := s.CustomVerificationRequestsForApplicant(ctx, applicant, 0)
	if err != nil || len(history) != 3 || history[0].ID != reapplied.ID {
		t.Fatalf("applicant history = %+v err=%v", history, err)
	}
	if empty, err := s.CustomVerificationRequestsForApplicant(ctx, target, 0); err != nil ||
		len(empty) != 0 {
		t.Fatalf("other applicant history = %+v err=%v", empty, err)
	}
	if _, err := s.CustomVerificationRequestsForApplicant(ctx, 0, 0); !errors.Is(err, domain.ErrCustomVerificationRequestInvalid) {
		t.Fatalf("history for applicant 0 err = %v, want ErrCustomVerificationRequestInvalid", err)
	}
	if _, err := s.CustomVerificationRequest(ctx, reapplied.ID+1<<40); !errors.Is(err, domain.ErrCustomVerificationRequestNotFound) {
		t.Fatalf("unknown application err = %v, want ErrCustomVerificationRequestNotFound", err)
	}
}

// TestBotVerificationDescriptionsAcceptEmojiPostgres pins the app-configured
// configured custom-description limit against UTF-8 byte constraints.
func TestBotVerificationDescriptionsAcceptEmojiPostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	s := NewBotVerificationStore(pool)
	bot := botVerificationTestUser(t, pool)
	peer := domain.Peer{Type: domain.PeerTypeUser, ID: botVerificationTestUser(t, pool)}
	icon := botVerificationTestIcon(t, pool, s, "emoji icon", 0)

	emoji := strings.Repeat("🛡", domain.MaxCustomVerificationDescriptionLength)
	if utf8.RuneCountInString(emoji) > domain.MaxCustomVerificationDescriptionLength {
		t.Fatalf("test description is %d runes, over the domain limit", utf8.RuneCountInString(emoji))
	}
	if len(emoji) != 4*domain.MaxCustomVerificationDescriptionLength {
		t.Fatalf("test description is %d bytes, want UTF-8 worst case", len(emoji))
	}

	settings, err := s.UpsertBotVerifierSettings(ctx, domain.BotVerifierSettings{
		BotID: bot, IconDocumentID: icon.DocumentID, CompanyName: "Acme",
		DefaultDescription: emoji, Enabled: true, CanModifyCustomDescription: true,
	})
	if err != nil {
		t.Fatalf("verifier settings with an emoji description: %v", err)
	}
	if settings.DefaultDescription != emoji {
		t.Fatalf("stored default description was altered: %d runes back, %d in",
			utf8.RuneCountInString(settings.DefaultDescription), utf8.RuneCountInString(emoji))
	}

	// The granted mark carries the same text, so widening only the settings column
	// would move the failure one step later instead of fixing it.
	mark, _, err := s.GrantCustomVerification(ctx, domain.CustomVerification{
		VerifierBotID: bot, Peer: peer, IconDocumentID: icon.DocumentID, Description: emoji,
	})
	if err != nil {
		t.Fatalf("grant with an emoji description: %v", err)
	}
	if mark.Description != emoji {
		t.Fatalf("stored mark description was altered: %d runes back, %d in",
			utf8.RuneCountInString(mark.Description), utf8.RuneCountInString(emoji))
	}

	// And so does an application, which is where an applicant types one.
	if _, err := s.CreateCustomVerificationRequest(ctx, domain.CustomVerificationRequest{
		VerifierBotID: bot, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: botVerificationTestUser(t, pool)},
		ApplicantUserID: bot, RequestedDescription: emoji,
	}); err != nil {
		t.Fatalf("application with an emoji description: %v", err)
	}

	// Past the rune limit it is still a domain error, not a constraint error: the
	// bound moved, it did not disappear.
	tooLong := strings.Repeat("🛡", domain.MaxCustomVerificationDescriptionLength+1)
	if _, err := s.UpsertBotVerifierSettings(ctx, domain.BotVerifierSettings{
		BotID: bot, IconDocumentID: icon.DocumentID, CompanyName: "Acme",
		DefaultDescription: tooLong, Version: settings.Version,
	}); !errors.Is(err, domain.ErrVerifierSettingsInvalid) {
		t.Fatalf("over-long description err = %v, want ErrVerifierSettingsInvalid", err)
	}
}