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>
1539 lines
55 KiB
Go
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)
|
|
}
|
|
}
|