owpengram-server/internal/app/botverification/service_test.go
Egor Egorov fff8de783a
feat: add NFT usernames and bot verification (#22)
Implements collectible usernames, official verification workflows, and third-party bot verification after maintainer protocol and migration review.

The composite activity/moderation rating remains an admin-only read model; Telegram Stars Rating wire fields stay unset pending a dedicated official-semantics implementation.

Reviewed-Head: 2796345775ea0f908fb7734601e5e1dee4b653b9
Original-Head: fa082b892fd5180c9c9bc53c81c21cf5d250a75b

Co-authored-by: Egor Egorov <business.egor.sg@gmail.com>
2026-07-28 01:18:00 +08:00

1539 lines
55 KiB
Go

package botverification
import (
"context"
"errors"
"strings"
"testing"
"time"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
// The fixtures are one deployment: a verifier bot owned by one account, a second
// bot owned by the same account, a stranger, a plain user peer and a channel that
// account created.
const (
fixtureVerifierBot = int64(100)
fixtureOwnedBot = int64(101)
fixtureOwner = int64(7)
fixtureStranger = int64(8)
fixtureUserPeer = int64(500)
fixtureChannel = int64(900)
fixtureIconDoc = int64(4242)
fixtureOtherDoc = int64(4343)
)
// ---------------------------------------------------------------------------
// Fakes
// ---------------------------------------------------------------------------
type memberKey struct {
channelID int64
userID int64
}
// fakeDirectory is the peer-resolution port. It answers exactly like the
// aggregates do: an unknown channel is domain.ErrChannelInvalid and a
// non-membership is domain.ErrUserNotParticipant, so the service's error folding
// is exercised rather than bypassed.
type fakeDirectory struct {
users map[int64]domain.User
channels map[int64]domain.Channel
owners map[int64]int64
members map[memberKey]domain.ChannelMember
}
func (f *fakeDirectory) AdminUser(_ context.Context, userID int64) (domain.User, bool, error) {
user, ok := f.users[userID]
return user, ok, nil
}
func (f *fakeDirectory) OwnsBot(_ context.Context, ownerUserID, botUserID int64) (bool, error) {
owner, ok := f.owners[botUserID]
return ok && owner == ownerUserID, nil
}
func (f *fakeDirectory) GetChannelByID(_ context.Context, channelID int64) (domain.Channel, error) {
channel, ok := f.channels[channelID]
if !ok {
return domain.Channel{}, domain.ErrChannelInvalid
}
return channel, nil
}
func (f *fakeDirectory) GetParticipant(_ context.Context, _, channelID, participantUserID int64) (domain.ChannelMember, error) {
member, ok := f.members[memberKey{channelID: channelID, userID: participantUserID}]
if !ok {
return domain.ChannelMember{}, domain.ErrUserNotParticipant
}
return member, nil
}
type fakePeerNotifier struct {
peers []domain.Peer
err error
}
func (f *fakePeerNotifier) NotifyPeerBotVerification(_ context.Context, peer domain.Peer) error {
f.peers = append(f.peers, peer)
return f.err
}
func (f *fakePeerNotifier) count() int { return len(f.peers) }
type fakeIconResolver struct {
documents map[int64]domain.Document
calls int
err error
}
func (f *fakeIconResolver) GetDocuments(_ context.Context, ids []int64) ([]domain.Document, error) {
f.calls++
if f.err != nil {
return nil, f.err
}
out := make([]domain.Document, 0, len(ids))
for _, id := range ids {
if document, ok := f.documents[id]; ok {
out = append(out, document)
}
}
return out, nil
}
type applicantCall struct {
recipientUserID int64
request domain.CustomVerificationRequest
}
type fakeApplicantNotifier struct {
calls []applicantCall
err error
}
func (f *fakeApplicantNotifier) SendVerificationDecision(_ context.Context, recipientUserID int64, req domain.CustomVerificationRequest) error {
f.calls = append(f.calls, applicantCall{recipientUserID: recipientUserID, request: req})
return f.err
}
type fakeLimiter struct {
allow bool
keys []string
err error
}
func (f *fakeLimiter) Allow(_ context.Context, key string, _ int, _ time.Duration) (bool, int, error) {
f.keys = append(f.keys, key)
if f.err != nil {
return false, 0, f.err
}
if f.allow {
return true, 0, nil
}
return false, 60, nil
}
func (f *fakeLimiter) AllowN(ctx context.Context, key string, _, limit int, window time.Duration) (bool, int, error) {
return f.Allow(ctx, key, limit, window)
}
// fakeMarkApplier stands in for the process's transaction-aware writer: it
// records that the decision callbacks went through the port and then delegates to
// the store, which is what a real adapter does when the context carries no
// transaction.
type fakeMarkApplier struct {
store *memory.BotVerificationStore
grants int
revokes int
}
func (f *fakeMarkApplier) GrantCustomVerification(ctx context.Context, mark domain.CustomVerification) (domain.CustomVerification, bool, error) {
f.grants++
return f.store.GrantCustomVerification(ctx, mark)
}
func (f *fakeMarkApplier) RevokeCustomVerification(ctx context.Context, verifierBotID int64, peer domain.Peer) (bool, error) {
f.revokes++
return f.store.RevokeCustomVerification(ctx, verifierBotID, peer)
}
func customEmojiDocument(id int64) domain.Document {
return domain.Document{
ID: id,
AccessHash: id * 3,
MimeType: "application/x-tgsticker",
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrCustomEmoji, Alt: "✅"}},
}
}
// ---------------------------------------------------------------------------
// Harness
// ---------------------------------------------------------------------------
type harness struct {
t *testing.T
svc *Service
store *memory.BotVerificationStore
dir *fakeDirectory
peers *fakePeerNotifier
docs *fakeIconResolver
applicant *fakeApplicantNotifier
limiter *fakeLimiter
}
func newHarness(t *testing.T, opts ...Option) *harness {
t.Helper()
h := &harness{
t: t,
store: memory.NewBotVerificationStore(),
dir: &fakeDirectory{
users: map[int64]domain.User{
fixtureVerifierBot: {ID: fixtureVerifierBot, FirstName: "Acme Verifier", Username: "acmeverifierbot", Bot: true, BotInfoVersion: 1},
fixtureOwnedBot: {ID: fixtureOwnedBot, FirstName: "Acme Helper", Username: "acmehelperbot", Bot: true, BotInfoVersion: 1},
fixtureOwner: {ID: fixtureOwner, FirstName: "Ann", LastName: "Owner", Username: "annowner"},
fixtureStranger: {ID: fixtureStranger, FirstName: "Sam", LastName: "Stranger", Username: "samstranger"},
fixtureUserPeer: {ID: fixtureUserPeer, FirstName: "Target", LastName: "Account", Username: "targetaccount"},
},
channels: map[int64]domain.Channel{
fixtureChannel: {ID: fixtureChannel, CreatorUserID: fixtureOwner, Title: "Acme Channel", Username: "acmechannel", Broadcast: true},
},
owners: map[int64]int64{
fixtureVerifierBot: fixtureOwner,
fixtureOwnedBot: fixtureOwner,
},
members: map[memberKey]domain.ChannelMember{},
},
peers: &fakePeerNotifier{},
docs: &fakeIconResolver{documents: map[int64]domain.Document{fixtureIconDoc: customEmojiDocument(fixtureIconDoc)}},
applicant: &fakeApplicantNotifier{},
limiter: &fakeLimiter{allow: true},
}
base := []Option{
WithStore(h.store),
WithPeerDirectory(h.dir),
WithPeerNotifier(h.peers),
WithIconResolver(h.docs),
WithApplicantNotifier(h.applicant),
WithRateLimiter(h.limiter, 5, 24*time.Hour),
}
h.svc = NewService(append(base, opts...)...)
return h
}
// seedIcon writes a catalogue entry straight to the store, so a test can set up
// an icon state the service itself would refuse to create.
func (h *harness) seedIcon(documentID, ownerBotID int64, active bool) domain.VerificationIcon {
h.t.Helper()
icon, err := h.store.UpsertVerificationIcon(context.Background(), domain.VerificationIcon{
DocumentID: documentID,
OwnerBotID: ownerBotID,
Name: "Acme badge",
Active: active,
})
if err != nil {
h.t.Fatalf("seed icon: %v", err)
}
return icon
}
func verifierSettings(enabled, canModify bool) domain.BotVerifierSettings {
return domain.BotVerifierSettings{
BotID: fixtureVerifierBot,
IconDocumentID: fixtureIconDoc,
CompanyName: "Acme Inc",
DefaultDescription: "Verified by Acme",
CanModifyCustomDescription: canModify,
Enabled: enabled,
GrantedBy: "operator",
GrantReason: "partner programme",
}
}
// seedVerifier writes verifier status straight to the store, bypassing
// GrantVerifier's own checks.
func (h *harness) seedVerifier(settings domain.BotVerifierSettings) domain.BotVerifierSettings {
h.t.Helper()
stored, err := h.store.UpsertBotVerifierSettings(context.Background(), settings)
if err != nil {
h.t.Fatalf("seed verifier: %v", err)
}
return stored
}
func (h *harness) mark(verifierBotID int64, peer domain.Peer) (domain.CustomVerification, bool) {
h.t.Helper()
mark, err := h.store.CustomVerification(context.Background(), verifierBotID, peer)
if errors.Is(err, domain.ErrCustomVerificationNotFound) {
return domain.CustomVerification{}, false
}
if err != nil {
h.t.Fatalf("read mark: %v", err)
}
return mark, true
}
func userPeer(id int64) domain.Peer { return domain.Peer{Type: domain.PeerTypeUser, ID: id} }
func chanPeer(id int64) domain.Peer { return domain.Peer{Type: domain.PeerTypeChannel, ID: id} }
// ---------------------------------------------------------------------------
// bots.setCustomVerification: one refusal per test
// ---------------------------------------------------------------------------
func TestSetCustomVerificationRefusesNonVerifier(t *testing.T) {
h := newHarness(t)
changed, err := h.svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
})
if !errors.Is(err, domain.ErrVerifierForbidden) {
t.Fatalf("err = %v, want ErrVerifierForbidden", err)
}
if changed {
t.Fatal("changed = true for a bot that is not a verifier")
}
if _, found := h.mark(fixtureVerifierBot, userPeer(fixtureUserPeer)); found {
t.Fatal("a mark was written by a non-verifier")
}
}
func TestSetCustomVerificationRefusesDisabledVerifier(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(false, true))
_, err := h.svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
})
// The kill switch is indistinguishable from "never was a verifier", by design.
if !errors.Is(err, domain.ErrVerifierForbidden) {
t.Fatalf("err = %v, want ErrVerifierForbidden", err)
}
if _, found := h.mark(fixtureVerifierBot, userPeer(fixtureUserPeer)); found {
t.Fatal("a disabled verifier granted a mark")
}
}
func TestSetCustomVerificationRefusesForeignCaller(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
_, err := h.svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
// Neither the bot itself nor its owner.
CallerUserID: fixtureStranger,
})
if !errors.Is(err, domain.ErrVerifierForbidden) {
t.Fatalf("err = %v, want ErrVerifierForbidden", err)
}
if _, found := h.mark(fixtureVerifierBot, userPeer(fixtureUserPeer)); found {
t.Fatal("a stranger granted a mark through somebody else's verifier")
}
}
func TestSetCustomVerificationAcceptsOwnerCaller(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
changed, err := h.svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureOwner,
})
if err != nil || !changed {
t.Fatalf("SetCustomVerification by owner = %v/%v, want true/nil", changed, err)
}
mark, found := h.mark(fixtureVerifierBot, userPeer(fixtureUserPeer))
if !found {
t.Fatal("owner call did not grant the mark")
}
if mark.GrantedByUserID != fixtureOwner {
t.Fatalf("GrantedByUserID = %d, want the calling owner %d", mark.GrantedByUserID, fixtureOwner)
}
}
func TestSetCustomVerificationRefusesForbiddenDescription(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, false))
_, err := h.svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CustomDescription: "hand-written text",
CallerUserID: fixtureVerifierBot,
})
if !errors.Is(err, domain.ErrVerifierDescriptionForbidden) {
t.Fatalf("err = %v, want ErrVerifierDescriptionForbidden", err)
}
if _, found := h.mark(fixtureVerifierBot, userPeer(fixtureUserPeer)); found {
t.Fatal("the mark was written despite the forbidden description")
}
}
func TestSetCustomVerificationRefusesInvalidPeer(t *testing.T) {
for name, peer := range map[string]domain.Peer{
"unknown user": userPeer(4242),
"unknown channel": chanPeer(4242),
// A built-in account's identity is seeded, not granted by a third party.
"system account": userPeer(domain.VerifyBotUserID),
// Not a peer kind that can carry a mark at all (peer_type CHECK).
"community peer": {Type: domain.PeerTypeCommunity, ID: 12},
"zero id": userPeer(0),
} {
t.Run(name, func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
_, err := h.svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: peer,
Enabled: true,
CallerUserID: fixtureVerifierBot,
})
if !errors.Is(err, domain.ErrCustomVerificationTargetInvalid) {
t.Fatalf("err = %v, want ErrCustomVerificationTargetInvalid", err)
}
if h.peers.count() != 0 {
t.Fatalf("peer pushes = %d, want none", h.peers.count())
}
})
}
}
// TestSetCustomVerificationGrantsRevokesAndRepeats walks the whole RPC contract:
// the icon comes from the settings, an identical re-apply reports changed=false,
// an edited description does move, and a revoke is idempotent.
func TestSetCustomVerificationGrantsRevokesAndRepeats(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
peer := chanPeer(fixtureChannel)
request := domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: peer,
Enabled: true,
CallerUserID: fixtureVerifierBot,
}
changed, err := h.svc.SetCustomVerification(ctx, request)
if err != nil || !changed {
t.Fatalf("first grant = %v/%v, want true/nil", changed, err)
}
mark, found := h.mark(fixtureVerifierBot, peer)
if !found {
t.Fatal("first grant stored no mark")
}
if mark.IconDocumentID != fixtureIconDoc {
t.Fatalf("IconDocumentID = %d, want the verifier's configured icon %d", mark.IconDocumentID, fixtureIconDoc)
}
if mark.Description != "Verified by Acme" {
t.Fatalf("Description = %q, want the operator default", mark.Description)
}
if h.peers.count() != 1 || h.peers.peers[0] != peer {
t.Fatalf("peer pushes = %v, want exactly one for %v", h.peers.peers, peer)
}
// Identical re-apply: nothing moved, so the RPC answers Bool false and no push
// is emitted.
changed, err = h.svc.SetCustomVerification(ctx, request)
if err != nil {
t.Fatalf("repeat grant: %v", err)
}
if changed {
t.Fatal("repeat grant reported changed = true")
}
if h.peers.count() != 1 {
t.Fatalf("peer pushes = %d after a no-op repeat, want 1", h.peers.count())
}
// A different description is a real change.
edited := request
edited.CustomDescription = "Acme partner since 2019"
changed, err = h.svc.SetCustomVerification(ctx, edited)
if err != nil || !changed {
t.Fatalf("description edit = %v/%v, want true/nil", changed, err)
}
if mark, _ := h.mark(fixtureVerifierBot, peer); mark.Description != "Acme partner since 2019" {
t.Fatalf("Description = %q after the edit", mark.Description)
}
if h.peers.count() != 2 {
t.Fatalf("peer pushes = %d after the edit, want 2", h.peers.count())
}
revoke := domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: peer,
CallerUserID: fixtureVerifierBot,
}
changed, err = h.svc.SetCustomVerification(ctx, revoke)
if err != nil || !changed {
t.Fatalf("revoke = %v/%v, want true/nil", changed, err)
}
if _, found := h.mark(fixtureVerifierBot, peer); found {
t.Fatal("the mark survived the revoke")
}
if h.peers.count() != 3 {
t.Fatalf("peer pushes = %d after the revoke, want 3", h.peers.count())
}
changed, err = h.svc.SetCustomVerification(ctx, revoke)
if err != nil {
t.Fatalf("repeat revoke: %v", err)
}
if changed {
t.Fatal("repeat revoke reported changed = true")
}
if h.peers.count() != 3 {
t.Fatalf("peer pushes = %d after the repeat revoke, want 3", h.peers.count())
}
}
// TestSetCustomVerificationRevokesVanishedPeer pins the asymmetry between the two
// directions: granting needs a resolvable peer, stripping does not. A peer that was
// deleted after being marked is exactly the case where a verifier most needs to be
// able to take its badge back.
func TestSetCustomVerificationRevokesVanishedPeer(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
if _, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
}); err != nil {
t.Fatalf("grant: %v", err)
}
// The account is gone from the directory by the time the verifier revokes.
delete(h.dir.users, fixtureUserPeer)
changed, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
CallerUserID: fixtureVerifierBot,
})
if err != nil || !changed {
t.Fatalf("revoke of a vanished peer = %v/%v, want true/nil", changed, err)
}
if _, found := h.mark(fixtureVerifierBot, userPeer(fixtureUserPeer)); found {
t.Fatal("the mark survived")
}
}
// TestSetCustomVerificationSurvivesPushFailure pins the "data first, push second"
// rule: a broken notifier must not turn a committed mark into an RPC error.
func TestSetCustomVerificationSurvivesPushFailure(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
h.peers.err = errors.New("router is gone")
changed, err := h.svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
})
if err != nil || !changed {
t.Fatalf("grant with a failing push = %v/%v, want true/nil", changed, err)
}
if _, found := h.mark(fixtureVerifierBot, userPeer(fixtureUserPeer)); !found {
t.Fatal("the mark was not stored")
}
}
func TestSetCustomVerificationEnforcesPerVerifierBound(t *testing.T) {
h := newHarness(t, WithMaxPerVerifier(1))
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
if _, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
}); err != nil {
t.Fatalf("first grant: %v", err)
}
_, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: chanPeer(fixtureChannel),
Enabled: true,
CallerUserID: fixtureVerifierBot,
})
if !errors.Is(err, domain.ErrCustomVerificationLimit) {
t.Fatalf("err = %v, want ErrCustomVerificationLimit", err)
}
// An existing mark stays re-describable at the bound: the budget is spent on
// new marks only.
if _, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CustomDescription: "still a partner",
CallerUserID: fixtureVerifierBot,
}); err != nil {
t.Fatalf("re-describe at the bound: %v", err)
}
}
// ---------------------------------------------------------------------------
// Verifier status
// ---------------------------------------------------------------------------
// TestGrantVerifierRefusesBeforeWriting is the ordering guarantee: every check
// runs before the store is touched, so a refused grant leaves no verifier row and
// pushes nothing.
func TestGrantVerifierRefusesBeforeWriting(t *testing.T) {
for name, test := range map[string]struct {
setup func(h *harness)
settings domain.BotVerifierSettings
want error
}{
"not a bot": {
setup: func(h *harness) { h.seedIcon(fixtureIconDoc, 0, true) },
settings: func() domain.BotVerifierSettings {
settings := verifierSettings(true, true)
settings.BotID = fixtureUserPeer
return settings
}(),
want: domain.ErrBotNotFound,
},
"unknown account": {
setup: func(h *harness) { h.seedIcon(fixtureIconDoc, 0, true) },
settings: func() domain.BotVerifierSettings {
settings := verifierSettings(true, true)
settings.BotID = 31337
return settings
}(),
want: domain.ErrBotNotFound,
},
"built-in system bot": {
setup: func(h *harness) { h.seedIcon(fixtureIconDoc, 0, true) },
settings: func() domain.BotVerifierSettings {
settings := verifierSettings(true, true)
settings.BotID = domain.VerifyBotUserID
return settings
}(),
want: domain.ErrVerificationTargetSystem,
},
"icon absent from the catalogue": {
setup: func(h *harness) {},
settings: verifierSettings(true, true),
want: domain.ErrVerificationIconNotFound,
},
"icon retired": {
setup: func(h *harness) { h.seedIcon(fixtureIconDoc, 0, false) },
settings: verifierSettings(true, true),
want: domain.ErrVerificationIconInactive,
},
"icon reserved for another verifier": {
setup: func(h *harness) { h.seedIcon(fixtureIconDoc, fixtureOwnedBot, true) },
settings: verifierSettings(true, true),
want: domain.ErrVerificationIconNotFound,
},
"document does not exist": {
setup: func(h *harness) {
h.seedIcon(fixtureIconDoc, 0, true)
// The catalogue entry is fine; the document it names is gone, so the
// badge would render as nothing at all.
h.docs.documents = map[int64]domain.Document{}
},
settings: verifierSettings(true, true),
want: domain.ErrVerificationIconInvalid,
},
"document is not a custom emoji": {
setup: func(h *harness) {
h.seedIcon(fixtureIconDoc, 0, true)
h.docs.documents = map[int64]domain.Document{
fixtureIconDoc: {ID: fixtureIconDoc, MimeType: "video/mp4"},
}
},
settings: verifierSettings(true, true),
want: domain.ErrVerificationIconInvalid,
},
} {
t.Run(name, func(t *testing.T) {
h := newHarness(t)
test.setup(h)
_, err := h.svc.GrantVerifier(context.Background(), test.settings)
if !errors.Is(err, test.want) {
t.Fatalf("err = %v, want %v", err, test.want)
}
if _, err := h.store.BotVerifierSettings(context.Background(), test.settings.BotID); !errors.Is(err, domain.ErrVerifierNotFound) {
t.Fatalf("verifier row after a refused grant: %v, want ErrVerifierNotFound", err)
}
if h.peers.count() != 0 {
t.Fatalf("peer pushes = %d after a refused grant, want none", h.peers.count())
}
})
}
}
func TestGrantVerifierStoresAndPushesTheBot(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
ctx := context.Background()
stored, err := h.svc.GrantVerifier(ctx, verifierSettings(true, true))
if err != nil {
t.Fatalf("GrantVerifier: %v", err)
}
if stored.BotID != fixtureVerifierBot || stored.Version != 1 || !stored.Enabled {
t.Fatalf("stored = %+v, want the first version of an enabled verifier", stored)
}
if h.docs.calls != 1 {
t.Fatalf("icon resolver calls = %d, want 1", h.docs.calls)
}
// Only the verifier's own botInfo is pushed; its marked peers converge on their
// next authoritative read.
if h.peers.count() != 1 || h.peers.peers[0] != userPeer(fixtureVerifierBot) {
t.Fatalf("peer pushes = %v, want exactly one for the bot itself", h.peers.peers)
}
// The built-in @verifierbot is the one system account this status exists for.
verifier := verifierSettings(true, true)
verifier.BotID = domain.VerifierBotUserID
if _, err := h.svc.GrantVerifier(ctx, verifier); err != nil {
t.Fatalf("GrantVerifier(@verifierbot): %v", err)
}
}
func TestSetVerifierEnabledAndRevoke(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
granted := h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
// A flip to the value already stored burns no version and pushes nothing.
same, err := h.svc.SetVerifierEnabled(ctx, fixtureVerifierBot, true)
if err != nil {
t.Fatalf("SetVerifierEnabled(no-op): %v", err)
}
if same.Version != granted.Version || h.peers.count() != 0 {
t.Fatalf("no-op flip: version %d->%d, pushes %d", granted.Version, same.Version, h.peers.count())
}
disabled, err := h.svc.SetVerifierEnabled(ctx, fixtureVerifierBot, false)
if err != nil {
t.Fatalf("SetVerifierEnabled(false): %v", err)
}
if disabled.Enabled || disabled.Version == granted.Version {
t.Fatalf("disabled = %+v, want enabled=false at a new version", disabled)
}
if h.peers.count() != 1 || h.peers.peers[0] != userPeer(fixtureVerifierBot) {
t.Fatalf("peer pushes = %v, want one for the bot", h.peers.peers)
}
removed, err := h.svc.RevokeVerifier(ctx, fixtureVerifierBot)
if err != nil || !removed {
t.Fatalf("RevokeVerifier = %v/%v, want true/nil", removed, err)
}
if _, err := h.store.BotVerifierSettings(ctx, fixtureVerifierBot); !errors.Is(err, domain.ErrVerifierNotFound) {
t.Fatalf("verifier row after revoke: %v", err)
}
// A repeated revoke is a no-op rather than an error.
removed, err = h.svc.RevokeVerifier(ctx, fixtureVerifierBot)
if err != nil || removed {
t.Fatalf("repeat RevokeVerifier = %v/%v, want false/nil", removed, err)
}
if h.peers.count() != 2 {
t.Fatalf("peer pushes = %d, want 2 (disable + revoke)", h.peers.count())
}
}
// ---------------------------------------------------------------------------
// Icon catalogue
// ---------------------------------------------------------------------------
func TestUpsertIconRequiresAnExistingDocument(t *testing.T) {
h := newHarness(t)
h.docs.documents = map[int64]domain.Document{}
ctx := context.Background()
_, err := h.svc.UpsertIcon(ctx, domain.VerificationIcon{DocumentID: fixtureIconDoc, Name: "Acme badge", Active: true})
if !errors.Is(err, domain.ErrVerificationIconInvalid) {
t.Fatalf("err = %v, want ErrVerificationIconInvalid", err)
}
icons, err := h.store.ListVerificationIcons(ctx, false, 0)
if err != nil {
t.Fatalf("ListVerificationIcons: %v", err)
}
if len(icons) != 0 {
t.Fatalf("catalogue = %+v, want nothing written for an unresolvable document", icons)
}
h.docs.documents[fixtureIconDoc] = customEmojiDocument(fixtureIconDoc)
stored, err := h.svc.UpsertIcon(ctx, domain.VerificationIcon{DocumentID: fixtureIconDoc, Name: " Acme badge ", Active: true})
if err != nil {
t.Fatalf("UpsertIcon: %v", err)
}
if stored.ID <= 0 || stored.Name != "Acme badge" || !stored.Active {
t.Fatalf("stored = %+v, want a trimmed active entry", stored)
}
retired, err := h.svc.SetIconActive(ctx, stored.ID, false)
if err != nil {
t.Fatalf("SetIconActive: %v", err)
}
if retired.Active {
t.Fatal("SetIconActive(false) left the entry active")
}
active, err := h.svc.Icons(ctx, true, 0)
if err != nil {
t.Fatalf("Icons: %v", err)
}
if len(active) != 0 {
t.Fatalf("active icons = %+v, want none after retiring the only entry", active)
}
}
func TestUpsertIconWithoutResolverIsAConfigurationError(t *testing.T) {
svc := NewService(WithStore(memory.NewBotVerificationStore()))
_, err := svc.UpsertIcon(context.Background(), domain.VerificationIcon{DocumentID: fixtureIconDoc, Name: "Acme", Active: true})
if err == nil || !strings.Contains(err.Error(), "icon resolver is not configured") {
t.Fatalf("err = %v, want the resolver configuration error", err)
}
}
// ---------------------------------------------------------------------------
// Applications
// ---------------------------------------------------------------------------
func (h *harness) createRequest(applicantUserID int64, peer domain.Peer, description string) (domain.CustomVerificationRequest, error) {
return h.svc.CreateRequest(context.Background(), domain.CustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
ApplicantUserID: applicantUserID,
Peer: peer,
PeerTitle: "TITLE THE CLIENT MADE UP",
PeerUsername: "clientmadethisup",
Reason: "we are a partner",
RequestedDescription: description,
})
}
func TestCreateRequestChecks(t *testing.T) {
t.Run("unknown verifier", func(t *testing.T) {
h := newHarness(t)
if _, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrVerifierForbidden) {
t.Fatalf("err = %v, want ErrVerifierForbidden", err)
}
})
t.Run("disabled verifier", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(false, true))
if _, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrVerifierForbidden) {
t.Fatalf("err = %v, want ErrVerifierForbidden", err)
}
})
t.Run("invalid target", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, err := h.createRequest(fixtureOwner, chanPeer(1234), ""); !errors.Is(err, domain.ErrCustomVerificationTargetInvalid) {
t.Fatalf("err = %v, want ErrCustomVerificationTargetInvalid", err)
}
})
t.Run("applicant does not control the channel", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, err := h.createRequest(fixtureStranger, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrVerificationNotOwner) {
t.Fatalf("err = %v, want ErrVerificationNotOwner", err)
}
})
t.Run("plain member does not control the channel", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
h.dir.members[memberKey{channelID: fixtureChannel, userID: fixtureStranger}] = domain.ChannelMember{
ChannelID: fixtureChannel,
UserID: fixtureStranger,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberActive,
}
if _, err := h.createRequest(fixtureStranger, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrVerificationNotOwner) {
t.Fatalf("err = %v, want ErrVerificationNotOwner", err)
}
})
t.Run("administrator with change_info controls the channel", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
h.dir.members[memberKey{channelID: fixtureChannel, userID: fixtureStranger}] = domain.ChannelMember{
ChannelID: fixtureChannel,
UserID: fixtureStranger,
Role: domain.ChannelRoleAdmin,
Status: domain.ChannelMemberActive,
AdminRights: domain.ChannelAdminRights{ChangeInfo: true},
}
if _, err := h.createRequest(fixtureStranger, chanPeer(fixtureChannel), ""); err != nil {
t.Fatalf("CreateRequest by a change_info admin: %v", err)
}
})
t.Run("bot owner controls the bot", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, err := h.createRequest(fixtureOwner, userPeer(fixtureOwnedBot), ""); err != nil {
t.Fatalf("CreateRequest for an owned bot: %v", err)
}
})
t.Run("stranger does not control the bot", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, err := h.createRequest(fixtureStranger, userPeer(fixtureOwnedBot), ""); !errors.Is(err, domain.ErrVerificationNotOwner) {
t.Fatalf("err = %v, want ErrVerificationNotOwner", err)
}
})
t.Run("user files for their own account", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, err := h.createRequest(fixtureUserPeer, userPeer(fixtureUserPeer), ""); err != nil {
t.Fatalf("CreateRequest for own account: %v", err)
}
})
t.Run("peer already marked by this verifier", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, _, err := h.store.GrantCustomVerification(context.Background(), domain.CustomVerification{
VerifierBotID: fixtureVerifierBot,
Peer: chanPeer(fixtureChannel),
IconDocumentID: fixtureIconDoc,
Description: "Verified by Acme",
}); err != nil {
t.Fatalf("seed mark: %v", err)
}
if _, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrCustomVerificationRequestExists) {
t.Fatalf("err = %v, want ErrCustomVerificationRequestExists", err)
}
})
t.Run("second pending application", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), ""); err != nil {
t.Fatalf("first CreateRequest: %v", err)
}
if _, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrCustomVerificationRequestExists) {
t.Fatalf("err = %v, want ErrCustomVerificationRequestExists", err)
}
})
t.Run("description this verifier may not apply", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, false))
if _, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "our own wording"); !errors.Is(err, domain.ErrVerifierDescriptionForbidden) {
t.Fatalf("err = %v, want ErrVerifierDescriptionForbidden", err)
}
})
t.Run("rate limited", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
h.limiter.allow = false
if _, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrVerificationRateLimited) {
t.Fatalf("err = %v, want ErrVerificationRateLimited", err)
}
if len(h.limiter.keys) != 1 || !strings.HasPrefix(h.limiter.keys[0], requestRateLimitKeyPrefix) {
t.Fatalf("limiter keys = %v, want one namespaced per-applicant key", h.limiter.keys)
}
pending, err := h.store.ListCustomVerificationRequests(context.Background(), domain.CustomVerificationRequestFilter{})
if err != nil {
t.Fatalf("ListCustomVerificationRequests: %v", err)
}
if len(pending) != 0 {
t.Fatalf("stored applications = %+v, want none when the budget is spent", pending)
}
})
t.Run("a refused application spends no budget", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
if _, err := h.createRequest(fixtureStranger, chanPeer(fixtureChannel), ""); !errors.Is(err, domain.ErrVerificationNotOwner) {
t.Fatalf("err = %v, want ErrVerificationNotOwner", err)
}
if len(h.limiter.keys) != 0 {
t.Fatalf("limiter keys = %v, want the budget untouched by a refusal", h.limiter.keys)
}
})
t.Run("snapshot comes from the directory", func(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
stored, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
if stored.PeerTitle != "Acme Channel" || stored.PeerUsername != "acmechannel" {
t.Fatalf("snapshot = %q/%q, want the resolved channel identity", stored.PeerTitle, stored.PeerUsername)
}
if stored.Status != domain.CustomVerificationPending || stored.Version != 1 {
t.Fatalf("stored = %+v, want a pending first version", stored)
}
})
}
// TestApproveGrantsMarkAndNotifiesOnce is the decision contract: the mark and the
// approval land together, each notifier fires exactly once, and a retry does
// neither again.
func TestApproveGrantsMarkAndNotifiesOnce(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
filed, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "Acme partner")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
decided, changed, err := h.svc.Approve(ctx, filed.ID, filed.Version, "operator", "checked the docs", "internal note")
if err != nil || !changed {
t.Fatalf("Approve = %v/%v, want true/nil", changed, err)
}
if decided.Status != domain.CustomVerificationApproved {
t.Fatalf("status = %q, want approved", decided.Status)
}
mark, found := h.mark(fixtureVerifierBot, chanPeer(fixtureChannel))
if !found {
t.Fatal("approval did not grant the mark")
}
if mark.IconDocumentID != fixtureIconDoc || mark.Description != "Acme partner" {
t.Fatalf("mark = %+v, want the verifier icon and the requested description", mark)
}
if h.peers.count() != 1 || h.peers.peers[0] != chanPeer(fixtureChannel) {
t.Fatalf("peer pushes = %v, want exactly one for the target", h.peers.peers)
}
if len(h.applicant.calls) != 1 {
t.Fatalf("applicant notifications = %d, want 1", len(h.applicant.calls))
}
if h.applicant.calls[0].recipientUserID != fixtureOwner ||
h.applicant.calls[0].request.Status != domain.CustomVerificationApproved {
t.Fatalf("applicant call = %+v, want the approved decision to the applicant", h.applicant.calls[0])
}
// Retried approval: already approved, so nothing is written and nobody is told
// twice.
again, changed, err := h.svc.Approve(ctx, filed.ID, decided.Version, "operator", "checked the docs", "")
if err != nil {
t.Fatalf("repeat Approve: %v", err)
}
if changed {
t.Fatal("repeat Approve reported changed = true")
}
if again.Version != decided.Version {
t.Fatalf("version %d -> %d on a repeat approval", decided.Version, again.Version)
}
if h.peers.count() != 1 || len(h.applicant.calls) != 1 {
t.Fatalf("notifications after the repeat: %d peer / %d applicant, want 1/1", h.peers.count(), len(h.applicant.calls))
}
}
func TestApproveRefusesDisabledVerifier(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
filed, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
// The operator switches the verifier off while the application waits.
if _, err := h.svc.SetVerifierEnabled(ctx, fixtureVerifierBot, false); err != nil {
t.Fatalf("SetVerifierEnabled: %v", err)
}
if _, _, err := h.svc.Approve(ctx, filed.ID, filed.Version, "operator", "", ""); !errors.Is(err, domain.ErrVerifierForbidden) {
t.Fatalf("err = %v, want ErrVerifierForbidden", err)
}
if _, found := h.mark(fixtureVerifierBot, chanPeer(fixtureChannel)); found {
t.Fatal("a disabled verifier granted a mark through the review path")
}
if stored, err := h.store.CustomVerificationRequest(ctx, filed.ID); err != nil || stored.Status != domain.CustomVerificationPending {
t.Fatalf("application = %+v / %v, want it still pending", stored, err)
}
}
func TestApproveIsVersionChecked(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
filed, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
if _, _, err := h.svc.Approve(ctx, filed.ID, filed.Version+1, "operator", "", ""); !errors.Is(err, domain.ErrCustomVerificationVersionConflict) {
t.Fatalf("err = %v, want ErrCustomVerificationVersionConflict", err)
}
if _, found := h.mark(fixtureVerifierBot, chanPeer(fixtureChannel)); found {
t.Fatal("a stale approval granted the mark")
}
}
func TestRejectRequiresAReason(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
filed, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
if _, _, err := h.svc.Reject(ctx, filed.ID, filed.Version, "operator", " ", "note"); !errors.Is(err, domain.ErrVerificationReasonRequired) {
t.Fatalf("err = %v, want ErrVerificationReasonRequired", err)
}
if stored, err := h.store.CustomVerificationRequest(ctx, filed.ID); err != nil || stored.Status != domain.CustomVerificationPending {
t.Fatalf("application = %+v / %v, want it untouched", stored, err)
}
if len(h.applicant.calls) != 0 {
t.Fatalf("applicant notifications = %d, want none", len(h.applicant.calls))
}
decided, changed, err := h.svc.Reject(ctx, filed.ID, filed.Version, "operator", "not a partner", "note")
if err != nil || !changed {
t.Fatalf("Reject = %v/%v, want true/nil", changed, err)
}
if decided.Status != domain.CustomVerificationRejected || decided.DecisionReason != "not a partner" {
t.Fatalf("decided = %+v, want a rejection carrying its reason", decided)
}
// A rejection touches no peer state, so only the applicant hears about it.
if h.peers.count() != 0 {
t.Fatalf("peer pushes = %d, want none for a rejection", h.peers.count())
}
if len(h.applicant.calls) != 1 || h.applicant.calls[0].request.Status != domain.CustomVerificationRejected {
t.Fatalf("applicant calls = %+v, want one rejection notice", h.applicant.calls)
}
if _, found := h.mark(fixtureVerifierBot, chanPeer(fixtureChannel)); found {
t.Fatal("a rejection granted a mark")
}
}
// TestDecisionsWriteThroughTheMarkApplier pins the wiring the atomicity of a
// decision depends on: the grant and the revocation both go through the injected
// writer, which in production is the one that picks the decision's transaction out
// of the context.
func TestDecisionsWriteThroughTheMarkApplier(t *testing.T) {
h := newHarness(t)
applier := &fakeMarkApplier{store: h.store}
h.svc = NewService(
WithStore(h.store),
WithPeerDirectory(h.dir),
WithPeerNotifier(h.peers),
WithIconResolver(h.docs),
WithApplicantNotifier(h.applicant),
WithRateLimiter(h.limiter, 5, 24*time.Hour),
WithMarkApplier(applier),
)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
filed, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
approved, _, err := h.svc.Approve(ctx, filed.ID, filed.Version, "operator", "ok", "")
if err != nil {
t.Fatalf("Approve: %v", err)
}
if applier.grants != 1 {
t.Fatalf("applier grants = %d, want 1", applier.grants)
}
if _, _, err := h.svc.RevokeRequest(ctx, approved.ID, approved.Version, "operator", "ended", ""); err != nil {
t.Fatalf("RevokeRequest: %v", err)
}
if applier.revokes != 1 {
t.Fatalf("applier revokes = %d, want 1", applier.revokes)
}
// The plain RPC path is not a decision, so it does not go through the applier.
if _, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
}); err != nil {
t.Fatalf("SetCustomVerification: %v", err)
}
if applier.grants != 1 {
t.Fatalf("applier grants = %d after the RPC path, want it untouched", applier.grants)
}
}
func TestRevokeRequestRemovesTheMark(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
filed, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
approved, _, err := h.svc.Approve(ctx, filed.ID, filed.Version, "operator", "ok", "")
if err != nil {
t.Fatalf("Approve: %v", err)
}
if _, _, err := h.svc.RevokeRequest(ctx, approved.ID, approved.Version, "operator", "", ""); !errors.Is(err, domain.ErrVerificationReasonRequired) {
t.Fatalf("err = %v, want ErrVerificationReasonRequired", err)
}
revoked, changed, err := h.svc.RevokeRequest(ctx, approved.ID, approved.Version, "operator", "partner agreement ended", "")
if err != nil || !changed {
t.Fatalf("RevokeRequest = %v/%v, want true/nil", changed, err)
}
if revoked.Status != domain.CustomVerificationRevoked {
t.Fatalf("status = %q, want revoked", revoked.Status)
}
if _, found := h.mark(fixtureVerifierBot, chanPeer(fixtureChannel)); found {
t.Fatal("the mark survived the revocation")
}
if h.peers.count() != 2 {
t.Fatalf("peer pushes = %d, want 2 (approve + revoke)", h.peers.count())
}
if len(h.applicant.calls) != 2 || h.applicant.calls[1].request.Status != domain.CustomVerificationRevoked {
t.Fatalf("applicant calls = %+v, want an approval and a revocation notice", h.applicant.calls)
}
}
func TestRevokeMarkAndListings(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
if _, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
}); err != nil {
t.Fatalf("SetCustomVerification: %v", err)
}
marks, err := h.svc.Marks(ctx, domain.CustomVerificationFilter{VerifierBotID: fixtureVerifierBot})
if err != nil {
t.Fatalf("Marks: %v", err)
}
if len(marks) != 1 {
t.Fatalf("Marks = %+v, want one", marks)
}
verifiers, err := h.svc.Verifiers(ctx, true, 0)
if err != nil {
t.Fatalf("Verifiers: %v", err)
}
if len(verifiers) != 1 || verifiers[0].BotID != fixtureVerifierBot {
t.Fatalf("Verifiers = %+v, want the one enabled verifier", verifiers)
}
removed, err := h.svc.RevokeMark(ctx, fixtureVerifierBot, userPeer(fixtureUserPeer))
if err != nil || !removed {
t.Fatalf("RevokeMark = %v/%v, want true/nil", removed, err)
}
removed, err = h.svc.RevokeMark(ctx, fixtureVerifierBot, userPeer(fixtureUserPeer))
if err != nil || removed {
t.Fatalf("repeat RevokeMark = %v/%v, want false/nil", removed, err)
}
if _, err := h.svc.Marks(ctx, domain.CustomVerificationFilter{PeerType: domain.PeerTypeCommunity}); !errors.Is(err, domain.ErrCustomVerificationTargetInvalid) {
t.Fatalf("Marks with an unmarkable peer type: %v", err)
}
}
func TestProjectionAndQueueReads(t *testing.T) {
h := newHarness(t)
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
filed, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
if err != nil {
t.Fatalf("CreateRequest: %v", err)
}
if _, _, err := h.svc.Approve(ctx, filed.ID, filed.Version, "operator", "ok", ""); err != nil {
t.Fatalf("Approve: %v", err)
}
mark, err := h.svc.PeerVerification(ctx, chanPeer(fixtureChannel))
if err != nil {
t.Fatalf("PeerVerification: %v", err)
}
if mark.Projection().Icon != fixtureIconDoc || mark.Projection().BotID != fixtureVerifierBot {
t.Fatalf("projection = %+v", mark.Projection())
}
batch, err := h.svc.PeerVerificationBatch(ctx, []domain.Peer{chanPeer(fixtureChannel), userPeer(fixtureUserPeer)})
if err != nil {
t.Fatalf("PeerVerificationBatch: %v", err)
}
if len(batch) != 1 {
t.Fatalf("batch = %+v, want only the marked peer", batch)
}
settings, err := h.svc.VerifierSettings(ctx, fixtureVerifierBot)
if err != nil || settings.BotID != fixtureVerifierBot {
t.Fatalf("VerifierSettings = %+v / %v", settings, err)
}
settingsBatch, err := h.svc.VerifierSettingsBatch(ctx, []int64{fixtureVerifierBot, fixtureOwnedBot})
if err != nil {
t.Fatalf("VerifierSettingsBatch: %v", err)
}
if len(settingsBatch) != 1 {
t.Fatalf("settings batch = %+v, want only the verifier", settingsBatch)
}
counts, err := h.svc.RequestCounts(ctx)
if err != nil {
t.Fatalf("RequestCounts: %v", err)
}
if counts[domain.CustomVerificationApproved] != 1 {
t.Fatalf("counts = %+v, want one approved", counts)
}
history, err := h.svc.ApplicantRequests(ctx, fixtureOwner, 0)
if err != nil || len(history) != 1 {
t.Fatalf("ApplicantRequests = %+v / %v", history, err)
}
if _, err := h.svc.Request(ctx, filed.ID); err != nil {
t.Fatalf("Request: %v", err)
}
if _, err := h.svc.PendingRequest(ctx, fixtureVerifierBot, chanPeer(fixtureChannel)); !errors.Is(err, domain.ErrCustomVerificationRequestNotFound) {
t.Fatalf("PendingRequest after the decision: %v, want not found", err)
}
if _, err := h.svc.Requests(ctx, domain.CustomVerificationRequestFilter{Statuses: []domain.CustomVerificationRequestStatus{"nonsense"}}); !errors.Is(err, domain.ErrCustomVerificationRequestInvalid) {
t.Fatalf("Requests with a bogus status: %v", err)
}
}
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
// TestDisabledConfigRefusesMutations pins the flag's scope: every write refuses
// with ErrDisabled, while the marks already granted keep projecting so a peer's
// badge does not silently vanish when an operator turns the feature off.
func TestDisabledConfigRefusesMutations(t *testing.T) {
h := newHarness(t, WithEnabled(false))
h.seedIcon(fixtureIconDoc, 0, true)
h.seedVerifier(verifierSettings(true, true))
ctx := context.Background()
if _, _, err := h.store.GrantCustomVerification(ctx, domain.CustomVerification{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
IconDocumentID: fixtureIconDoc,
Description: "Verified by Acme",
}); err != nil {
t.Fatalf("seed mark: %v", err)
}
if h.svc.Enabled() || h.svc.Ready() {
t.Fatal("a disabled service reports itself enabled")
}
for name, call := range map[string]func() error{
"SetCustomVerification": func() error {
_, err := h.svc.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: chanPeer(fixtureChannel),
Enabled: true,
CallerUserID: fixtureVerifierBot,
})
return err
},
"GrantVerifier": func() error {
_, err := h.svc.GrantVerifier(ctx, verifierSettings(true, true))
return err
},
"SetVerifierEnabled": func() error {
_, err := h.svc.SetVerifierEnabled(ctx, fixtureVerifierBot, false)
return err
},
"RevokeVerifier": func() error {
_, err := h.svc.RevokeVerifier(ctx, fixtureVerifierBot)
return err
},
"UpsertIcon": func() error {
_, err := h.svc.UpsertIcon(ctx, domain.VerificationIcon{DocumentID: fixtureIconDoc, Name: "Acme", Active: true})
return err
},
"SetIconActive": func() error {
_, err := h.svc.SetIconActive(ctx, 1, false)
return err
},
"RevokeMark": func() error {
_, err := h.svc.RevokeMark(ctx, fixtureVerifierBot, userPeer(fixtureUserPeer))
return err
},
"CreateRequest": func() error {
_, err := h.createRequest(fixtureOwner, chanPeer(fixtureChannel), "")
return err
},
"Approve": func() error {
_, _, err := h.svc.Approve(ctx, 1, 1, "operator", "", "")
return err
},
"Reject": func() error {
_, _, err := h.svc.Reject(ctx, 1, 1, "operator", "no", "")
return err
},
"RevokeRequest": func() error {
_, _, err := h.svc.RevokeRequest(ctx, 1, 1, "operator", "no", "")
return err
},
} {
t.Run(name, func(t *testing.T) {
if err := call(); !errors.Is(err, ErrDisabled) {
t.Fatalf("err = %v, want ErrDisabled", err)
}
})
}
if h.peers.count() != 0 || len(h.applicant.calls) != 0 {
t.Fatalf("a disabled service notified: %d peer / %d applicant", h.peers.count(), len(h.applicant.calls))
}
// Reads still answer: the badge already granted keeps rendering, and the panel
// can still audit what exists.
if _, err := h.svc.PeerVerification(ctx, userPeer(fixtureUserPeer)); err != nil {
t.Fatalf("PeerVerification while disabled: %v", err)
}
if icons, err := h.svc.Icons(ctx, false, 0); err != nil || len(icons) != 1 {
t.Fatalf("Icons while disabled = %+v / %v", icons, err)
}
if verifiers, err := h.svc.Verifiers(ctx, false, 0); err != nil || len(verifiers) != 1 {
t.Fatalf("Verifiers while disabled = %+v / %v", verifiers, err)
}
}
// TestNilAndUnconfiguredServiceAreSafe covers the two shapes a process can hand
// out by accident: no service at all, and a service with no store.
func TestNilAndUnconfiguredServiceAreSafe(t *testing.T) {
ctx := context.Background()
var nilService *Service
if nilService.Enabled() || nilService.Ready() {
t.Fatal("a nil service reports itself enabled")
}
if _, err := nilService.PeerVerification(ctx, userPeer(fixtureUserPeer)); !errors.Is(err, domain.ErrCustomVerificationNotFound) {
t.Fatalf("nil PeerVerification: %v, want ErrCustomVerificationNotFound", err)
}
batch, err := nilService.PeerVerificationBatch(ctx, []domain.Peer{userPeer(fixtureUserPeer)})
if err != nil || len(batch) != 0 {
t.Fatalf("nil PeerVerificationBatch = %+v / %v, want an empty map", batch, err)
}
if _, err := nilService.VerifierSettings(ctx, fixtureVerifierBot); !errors.Is(err, domain.ErrVerifierNotFound) {
t.Fatalf("nil VerifierSettings: %v, want ErrVerifierNotFound", err)
}
if settings, err := nilService.VerifierSettingsBatch(ctx, []int64{fixtureVerifierBot}); err != nil || len(settings) != 0 {
t.Fatalf("nil VerifierSettingsBatch = %+v / %v", settings, err)
}
if _, err := nilService.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{}); !errors.Is(err, ErrDisabled) {
t.Fatalf("nil SetCustomVerification: %v, want ErrDisabled", err)
}
// Nil notifier hooks are ignored rather than panicking.
nilService.SetPeerNotifier(&fakePeerNotifier{})
nilService.SetApplicantNotifier(&fakeApplicantNotifier{})
// Enabled but storeless: every entry point says what is missing instead of
// dereferencing nil.
bare := NewService(WithPeerDirectory(&fakeDirectory{}), WithIconResolver(&fakeIconResolver{}))
if !bare.Enabled() || bare.Ready() {
t.Fatal("a storeless service reports itself ready")
}
for name, call := range map[string]func() error{
"SetCustomVerification": func() error {
_, err := bare.SetCustomVerification(ctx, domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
CallerUserID: fixtureVerifierBot,
})
return err
},
"GrantVerifier": func() error {
_, err := bare.GrantVerifier(ctx, verifierSettings(true, true))
return err
},
"Icons": func() error {
_, err := bare.Icons(ctx, false, 0)
return err
},
"Requests": func() error {
_, err := bare.Requests(ctx, domain.CustomVerificationRequestFilter{})
return err
},
} {
t.Run(name, func(t *testing.T) {
err := call()
if err == nil || !strings.Contains(err.Error(), "store is not configured") {
t.Fatalf("err = %v, want the store configuration error", err)
}
})
}
}
// TestSetCustomVerificationWithoutDirectoryFails pins that a security check which
// cannot run refuses instead of passing.
func TestSetCustomVerificationWithoutDirectoryFails(t *testing.T) {
st := memory.NewBotVerificationStore()
if _, err := st.UpsertVerificationIcon(context.Background(), domain.VerificationIcon{DocumentID: fixtureIconDoc, Name: "Acme", Active: true}); err != nil {
t.Fatalf("seed icon: %v", err)
}
if _, err := st.UpsertBotVerifierSettings(context.Background(), verifierSettings(true, true)); err != nil {
t.Fatalf("seed verifier: %v", err)
}
svc := NewService(WithStore(st))
_, err := svc.SetCustomVerification(context.Background(), domain.SetCustomVerificationRequest{
VerifierBotID: fixtureVerifierBot,
Peer: userPeer(fixtureUserPeer),
Enabled: true,
// Not the bot itself, so ownership has to be resolved -- and cannot be.
CallerUserID: fixtureOwner,
})
if err == nil || !strings.Contains(err.Error(), "bot directory is not configured") {
t.Fatalf("err = %v, want the bot directory configuration error", err)
}
}