owpengram-server/cmd/telesrv-admin/botverification_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

559 lines
25 KiB
Go

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"telesrv/internal/admin"
)
// Third-party bot verification in the panel BFF. The section is separate from the
// official verification one in every dimension that matters here: its own routes,
// its own two permissions, and no overlap with verification.* in either direction.
// botVerificationRoute is one panel route with a body its handler accepts.
type panelBotVerificationRoute struct {
method string
path string
body string
}
var panelBotVerificationReadRoutes = []panelBotVerificationRoute{
{http.MethodGet, "/api/botverification/verifiers", ""},
{http.MethodGet, "/api/botverification/icons", ""},
{http.MethodGet, "/api/botverification/marks", ""},
{http.MethodGet, "/api/botverification/requests", ""},
{http.MethodGet, "/api/botverification/requests/7", ""},
{http.MethodGet, "/api/botverification/counts", ""},
{http.MethodPost, "/api/botverification/requests/7/approve", `{}`},
{http.MethodPost, "/api/botverification/requests/7/reject", `{}`},
{http.MethodPost, "/api/botverification/requests/7/revoke", `{}`},
}
var panelBotVerificationManageRoutes = []panelBotVerificationRoute{
{http.MethodPost, "/api/actions/grant-bot-verifier", `{}`},
{http.MethodPost, "/api/actions/set-bot-verifier-enabled", `{}`},
{http.MethodPost, "/api/actions/revoke-bot-verifier", `{}`},
{http.MethodPost, "/api/actions/upsert-verification-icon", `{}`},
{http.MethodPost, "/api/actions/set-verification-icon-active", `{}`},
{http.MethodPost, "/api/actions/revoke-custom-verification", `{}`},
}
func panelBotVerificationRoutes() []panelBotVerificationRoute {
out := make([]panelBotVerificationRoute, 0,
len(panelBotVerificationReadRoutes)+len(panelBotVerificationManageRoutes))
out = append(out, panelBotVerificationReadRoutes...)
return append(out, panelBotVerificationManageRoutes...)
}
func TestBotVerificationPanelRoutesRequireASession(t *testing.T) {
srv := panelServer(t, permissionAll)
for _, item := range panelBotVerificationRoutes() {
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, httptest.NewRequest(item.method, item.path, strings.NewReader(`{}`)))
if rec.Code != http.StatusUnauthorized {
t.Fatalf("%s %s status=%d, want 401", item.method, item.path, rec.Code)
}
}
}
// Every mutating route in the section is behind the double-submit CSRF token, like
// every other one in the panel: a cookie-authenticated request forged by another
// origin must not be able to appoint a verifier.
func TestBotVerificationMutationsRequireTheCSRFHeader(t *testing.T) {
srv := panelServer(t, permissionAll)
cookies, token := signIn(t, srv)
for _, item := range panelBotVerificationRoutes() {
if item.method != http.MethodPost {
continue
}
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, withCookies(
httptest.NewRequest(item.method, item.path, strings.NewReader(item.body)), cookies))
if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), csrfHeaderName) {
t.Fatalf("%s status=%d body=%s, want 403 without a csrf header", item.path, rec.Code, rec.Body.String())
}
}
// A foreign origin is refused even when the token is right.
req := withCookies(httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier",
strings.NewReader(`{}`)), cookies)
req.Header.Set(csrfHeaderName, token)
req.Header.Set("Origin", "https://evil.example")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "origin") {
t.Fatalf("foreign origin status=%d body=%s, want 403", rec.Code, rec.Body.String())
}
}
// Reads do not need the token: they change nothing, and requiring it would break
// the panel without adding protection.
func TestBotVerificationReadsDoNotNeedTheCSRFHeader(t *testing.T) {
srv := panelServer(t, permissionBotVerificationReview)
cookies, _ := signIn(t, srv)
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, withCookies(
httptest.NewRequest(http.MethodGet, "/api/botverification/verifiers", nil), cookies))
// No read store is wired in this fixture, so the gate passing is what is under
// test: 503 means the request got past authorisation and CSRF.
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("GET status=%d body=%s, want the gate passed without a token", rec.Code, rec.Body.String())
}
}
func TestBotVerificationRoutesRefuseASessionWithoutTheRight(t *testing.T) {
// A session holding only the OFFICIAL verification rights: the two mechanisms
// are separate, so it must not reach this section at all.
srv := panelServer(t, permissionVerificationReview, permissionVerificationRevoke)
cookies, token := signIn(t, srv)
check := func(item panelBotVerificationRoute, wantPermission string) {
var req *http.Request
if item.body == "" {
req = httptest.NewRequest(item.method, item.path, nil)
} else {
req = httptest.NewRequest(item.method, item.path, strings.NewReader(item.body))
req.Header.Set(csrfHeaderName, token)
}
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, withCookies(req, cookies))
if rec.Code != http.StatusForbidden {
t.Fatalf("%s %s status=%d body=%s, want 403", item.method, item.path, rec.Code, rec.Body.String())
}
var body map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("decode 403 body: %v", err)
}
if body["code"] != "FORBIDDEN" || body["permission"] != wantPermission {
t.Fatalf("%s 403 body=%+v, want %s named", item.path, body, wantPermission)
}
}
for _, item := range panelBotVerificationReadRoutes {
check(item, permissionBotVerificationReview)
}
for _, item := range panelBotVerificationManageRoutes {
check(item, permissionBotVerificationManage)
}
}
// The two halves are independent: the review right does not appoint verifiers, and
// the manage right does not decide applications.
func TestBotVerificationReviewAndManageAreIndependent(t *testing.T) {
reviewOnly := panelServer(t, permissionBotVerificationReview)
cookies, token := signIn(t, reviewOnly)
req := withCookies(httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier",
strings.NewReader(`{"reason":"partner","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"x"}`)), cookies)
req.Header.Set(csrfHeaderName, token)
rec := httptest.NewRecorder()
reviewOnly.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), permissionBotVerificationManage) {
t.Fatalf("review-only on grant status=%d body=%s, want 403 naming manage", rec.Code, rec.Body.String())
}
manageOnly := panelServer(t, permissionBotVerificationManage)
cookies, token = signIn(t, manageOnly)
req = withCookies(httptest.NewRequest(http.MethodPost, "/api/botverification/requests/7/approve",
strings.NewReader(`{"reason":"verified","confirm":true,"version":3}`)), cookies)
req.Header.Set(csrfHeaderName, token)
rec = httptest.NewRecorder()
manageOnly.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), permissionBotVerificationReview) {
t.Fatalf("manage-only on approve status=%d body=%s, want 403 naming review", rec.Code, rec.Body.String())
}
}
// A session holding the third-party rights must not reach the official section
// either: the separation is symmetric.
func TestBotVerificationSessionCannotReachTheOfficialSection(t *testing.T) {
srv := panelServer(t, permissionBotVerificationReview, permissionBotVerificationManage)
cookies, _ := signIn(t, srv)
for _, path := range []string{"/api/verification/applications", "/api/verification/counts"} {
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, withCookies(httptest.NewRequest(http.MethodGet, path, nil), cookies))
if rec.Code != http.StatusForbidden {
t.Fatalf("%s status=%d body=%s, want 403", path, rec.Code, rec.Body.String())
}
}
}
func TestApproveBotVerificationBFFForwardsActorVersionAndNote(t *testing.T) {
upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}}
api := httptest.NewServer(upstream.handler(t))
defer api.Close()
srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}}
req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/approve", strings.NewReader(`{
"reason":"the outlet checks out","confirm":true,"version":"9223372036854775807",
"internal_note":"contact came through the press office"
}`))
req.SetPathValue("id", "88")
req = requestWithActor(req, "operator")
rec := httptest.NewRecorder()
srv.handleApproveBotVerificationAPI(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if upstream.path != "/v1/botverification/requests/88/approve" {
t.Fatalf("upstream path=%q", upstream.path)
}
var got admin.ApproveBotVerificationRequest
if err := json.Unmarshal(upstream.raw, &got); err != nil {
t.Fatalf("decode forwarded approval: %v (%s)", err, upstream.raw)
}
if got.Actor != "operator" {
t.Fatalf("actor=%q, want the signed-in operator", got.Actor)
}
// The version arrives as a decimal string from the browser and must survive
// exactly: a rounded version would decide the wrong revision of the row.
if got.RequestID != 88 || got.Version != 9223372036854775807 {
t.Fatalf("forwarded approval=%+v, want the exact int64 version", got)
}
if got.InternalNote != "contact came through the press office" || got.DryRun {
t.Fatalf("forwarded approval=%+v", got)
}
if got.CommandID == "" {
t.Fatal("no command id was minted for the idempotency key")
}
}
// confirm=false is a rehearsal: nothing may be written until the operator confirms.
func TestBotVerificationBFFDefaultsToADryRun(t *testing.T) {
upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed", DryRun: true}}
api := httptest.NewServer(upstream.handler(t))
defer api.Close()
srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}}
req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/reject", strings.NewReader(
`{"reason":"not an outlet","confirm":false,"version":3}`))
req.SetPathValue("id", "88")
req = requestWithActor(req, "operator")
rec := httptest.NewRecorder()
srv.handleRejectBotVerificationAPI(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
var got admin.RejectBotVerificationRequest
if err := json.Unmarshal(upstream.raw, &got); err != nil {
t.Fatalf("decode forwarded rejection: %v", err)
}
if !got.DryRun || got.Version != 3 || got.RequestID != 88 {
t.Fatalf("forwarded rejection=%+v", got)
}
// The same on an operator action.
req = requestWithActor(httptest.NewRequest(http.MethodPost, "/api/actions/revoke-bot-verifier", strings.NewReader(
`{"reason":"programme ended","confirm":false,"bot_id":3003}`)), "operator")
rec = httptest.NewRecorder()
srv.handleRevokeBotVerifierAPI(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("revoke status=%d body=%s", rec.Code, rec.Body.String())
}
var revoke admin.RevokeBotVerifierRequest
if err := json.Unmarshal(upstream.raw, &revoke); err != nil {
t.Fatalf("decode forwarded revocation: %v", err)
}
if !revoke.DryRun || revoke.BotID != 3003 {
t.Fatalf("forwarded revocation=%+v", revoke)
}
}
func TestGrantBotVerifierBFFForwardsThePayloadAndRejectsBadShapes(t *testing.T) {
upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}}
api := httptest.NewServer(upstream.handler(t))
defer api.Close()
srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}}
req := requestWithActor(httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader(`{
"reason":"partner programme","confirm":true,
"bot_id":"9223372036854775807","icon_document_id":"9223372036854775806",
"company_name":"Example Trust","default_description":"verified by Example Trust",
"can_modify_custom_description":true,"version":"4"
}`)), "operator")
rec := httptest.NewRecorder()
srv.handleGrantBotVerifierAPI(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if upstream.path != "/v1/botverification/verifiers/grant" {
t.Fatalf("upstream path=%q", upstream.path)
}
var got admin.GrantBotVerifierRequest
if err := json.Unmarshal(upstream.raw, &got); err != nil {
t.Fatalf("decode forwarded grant: %v", err)
}
if got.BotID != 9223372036854775807 || got.IconDocumentID != 9223372036854775806 ||
got.Version != 4 || got.Actor != "operator" || got.DryRun {
t.Fatalf("forwarded grant=%+v, want the exact int64s", got)
}
if got.CompanyName != "Example Trust" || !got.CanModifyCustomDescription {
t.Fatalf("forwarded grant=%+v", got)
}
for _, payload := range []string{
`{"reason":"x","confirm":true,"bot_id":0,"icon_document_id":900,"company_name":"y"}`,
`{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":0,"company_name":"y"}`,
`{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":" "}`,
`{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"y","version":-1}`,
} {
rec := httptest.NewRecorder()
srv.handleGrantBotVerifierAPI(rec, requestWithActor(
httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader(payload)), "operator"))
if rec.Code != http.StatusBadRequest {
t.Fatalf("payload %s status=%d body=%s, want 400", payload, rec.Code, rec.Body.String())
}
}
}
func TestBotVerificationOperatorActionsForwardTheirPayloads(t *testing.T) {
upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}}
api := httptest.NewServer(upstream.handler(t))
defer api.Close()
srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}}
rec := httptest.NewRecorder()
srv.handleSetBotVerifierEnabledAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/set-bot-verifier-enabled", strings.NewReader(
`{"reason":"abuse report","confirm":true,"bot_id":"3003","enabled":false}`)), "operator"))
if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/verifiers/set-enabled" {
t.Fatalf("set-enabled status=%d path=%q body=%s", rec.Code, upstream.path, rec.Body.String())
}
var setEnabled admin.SetBotVerifierEnabledRequest
if err := json.Unmarshal(upstream.raw, &setEnabled); err != nil {
t.Fatalf("decode: %v", err)
}
if setEnabled.BotID != 3003 || setEnabled.Enabled || setEnabled.Actor != "operator" {
t.Fatalf("forwarded=%+v", setEnabled)
}
rec = httptest.NewRecorder()
srv.handleUpsertVerificationIconAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/upsert-verification-icon", strings.NewReader(
`{"reason":"new icon","confirm":true,"document_id":"9223372036854775807","name":"blue check","owner_bot_id":"3003"}`)), "operator"))
if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/icons/upsert" {
t.Fatalf("upsert-icon status=%d path=%q body=%s", rec.Code, upstream.path, rec.Body.String())
}
var icon admin.UpsertVerificationIconRequest
if err := json.Unmarshal(upstream.raw, &icon); err != nil {
t.Fatalf("decode: %v", err)
}
if icon.DocumentID != 9223372036854775807 || icon.Name != "blue check" || icon.OwnerBotID != 3003 {
t.Fatalf("forwarded=%+v", icon)
}
// owner_bot_id is optional: absent means a shared catalogue entry.
rec = httptest.NewRecorder()
srv.handleUpsertVerificationIconAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/upsert-verification-icon", strings.NewReader(
`{"reason":"new icon","confirm":true,"document_id":900,"name":"shared"}`)), "operator"))
if rec.Code != http.StatusOK {
t.Fatalf("shared icon status=%d body=%s", rec.Code, rec.Body.String())
}
// A fresh target: owner_bot_id is omitted when zero, so decoding into the
// previous value would silently keep the reserved owner.
var shared admin.UpsertVerificationIconRequest
if err := json.Unmarshal(upstream.raw, &shared); err != nil {
t.Fatalf("decode: %v", err)
}
if shared.OwnerBotID != 0 || shared.Name != "shared" {
t.Fatalf("forwarded=%+v, want a shared entry", shared)
}
rec = httptest.NewRecorder()
srv.handleSetVerificationIconActiveAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/set-verification-icon-active", strings.NewReader(
`{"reason":"retired","confirm":true,"icon_id":"501","active":false}`)), "operator"))
if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/icons/set-active" {
t.Fatalf("set-icon-active status=%d path=%q", rec.Code, upstream.path)
}
var iconActive admin.SetVerificationIconActiveRequest
if err := json.Unmarshal(upstream.raw, &iconActive); err != nil {
t.Fatalf("decode: %v", err)
}
if iconActive.IconID != 501 || iconActive.Active {
t.Fatalf("forwarded=%+v", iconActive)
}
rec = httptest.NewRecorder()
srv.handleRevokeCustomVerificationAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/revoke-custom-verification", strings.NewReader(
`{"reason":"impersonation","confirm":true,"verifier_bot_id":"3003","peer_type":"channel","peer_id":"9223372036854775807"}`)), "operator"))
if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/marks/revoke" {
t.Fatalf("revoke-mark status=%d path=%q body=%s", rec.Code, upstream.path, rec.Body.String())
}
var mark admin.RevokeCustomVerificationRequest
if err := json.Unmarshal(upstream.raw, &mark); err != nil {
t.Fatalf("decode: %v", err)
}
if mark.VerifierBotID != 3003 || mark.PeerType != "channel" || mark.PeerID != 9223372036854775807 {
t.Fatalf("forwarded=%+v", mark)
}
for _, payload := range []string{
`{"reason":"x","confirm":true,"verifier_bot_id":0,"peer_type":"channel","peer_id":5}`,
`{"reason":"x","confirm":true,"verifier_bot_id":3003,"peer_type":"chat","peer_id":5}`,
`{"reason":"x","confirm":true,"verifier_bot_id":3003,"peer_type":"","peer_id":5}`,
`{"reason":"x","confirm":true,"verifier_bot_id":3003,"peer_type":"channel","peer_id":0}`,
} {
rec := httptest.NewRecorder()
srv.handleRevokeCustomVerificationAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/revoke-custom-verification", strings.NewReader(payload)), "operator"))
if rec.Code != http.StatusBadRequest {
t.Fatalf("payload %s status=%d body=%s, want 400", payload, rec.Code, rec.Body.String())
}
}
}
// A body may not smuggle in an actor: the signed-in operator is the audit identity,
// and the strict decoder is what enforces it.
func TestBotVerificationRequestsRejectUnknownFields(t *testing.T) {
srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}}
req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/approve", strings.NewReader(
`{"reason":"ok","confirm":true,"version":3,"actor":"attacker"}`))
req.SetPathValue("id", "88")
rec := httptest.NewRecorder()
srv.handleApproveBotVerificationAPI(rec, requestWithActor(req, "operator"))
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "actor") {
t.Fatalf("status=%d body=%s, want 400 rejecting the injected actor", rec.Code, rec.Body.String())
}
// enabled is not part of the grant form: the kill switch is its own action, and
// a silently ignored field would hide that from the operator.
rec = httptest.NewRecorder()
srv.handleGrantBotVerifierAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader(
`{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"y","enabled":true}`)), "operator"))
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "enabled") {
t.Fatalf("status=%d body=%s, want 400 naming the unknown field", rec.Code, rec.Body.String())
}
}
func TestBotVerificationPathIDIsValidated(t *testing.T) {
srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}}
for _, id := range []string{"", "0", "-1", "abc"} {
req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/x/approve", strings.NewReader(
`{"reason":"ok","confirm":true,"version":3}`))
req.SetPathValue("id", id)
rec := httptest.NewRecorder()
srv.handleApproveBotVerificationAPI(rec, requestWithActor(req, "operator"))
if rec.Code != http.StatusBadRequest {
t.Fatalf("id=%q status=%d, want 400", id, rec.Code)
}
}
}
// A flattened 502 would hide the one failure the panel resolves by reloading.
func TestBotVerificationConflictReachesThePanelAs409(t *testing.T) {
upstream := &verificationUpstream{
status: http.StatusConflict,
body: admin.CommandResult{
CommandID: "c1", Status: "failed",
Error: admin.CodeCustomVerificationConflict + ": custom verification changed concurrently",
Message: "another operator changed this row first; reload it and try again",
},
}
api := httptest.NewServer(upstream.handler(t))
defer api.Close()
srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}}
req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/approve", strings.NewReader(
`{"reason":"ok","confirm":true,"version":3}`))
req.SetPathValue("id", "88")
rec := httptest.NewRecorder()
srv.handleApproveBotVerificationAPI(rec, requestWithActor(req, "operator"))
if rec.Code != http.StatusConflict {
t.Fatalf("status=%d body=%s, want 409", rec.Code, rec.Body.String())
}
var result admin.CommandResult
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
t.Fatalf("decode conflict: %v", err)
}
if !strings.Contains(result.Error, admin.CodeCustomVerificationConflict) ||
!strings.Contains(result.Message, "reload") {
t.Fatalf("result=%+v", result)
}
// The manage half too: two operators can race one verifier row.
rec = httptest.NewRecorder()
srv.handleGrantBotVerifierAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader(
`{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"y","version":3}`)), "operator"))
if rec.Code != http.StatusConflict {
t.Fatalf("grant status=%d body=%s, want 409", rec.Code, rec.Body.String())
}
// A 404 from upstream is preserved as well, so a decision on a row that is gone
// is not reported as an upstream outage.
upstream.status = http.StatusNotFound
upstream.body = admin.CommandResult{CommandID: "c2", Status: "failed",
Error: admin.CodeCustomVerificationRequestNotFound + ": custom verification request not found"}
req = httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/reject", strings.NewReader(
`{"reason":"ok","confirm":true,"version":3}`))
req.SetPathValue("id", "88")
rec = httptest.NewRecorder()
srv.handleRejectBotVerificationAPI(rec, requestWithActor(req, "operator"))
if rec.Code != http.StatusNotFound {
t.Fatalf("status=%d body=%s, want 404", rec.Code, rec.Body.String())
}
}
// An unreachable admin API is the one case with no upstream status at all.
func TestBotVerificationUnreachableUpstreamIs502(t *testing.T) {
srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}}
rec := httptest.NewRecorder()
srv.handleRevokeBotVerifierAPI(rec, requestWithActor(httptest.NewRequest(
http.MethodPost, "/api/actions/revoke-bot-verifier", strings.NewReader(
`{"reason":"x","confirm":true,"bot_id":3003}`)), "operator"))
if rec.Code != http.StatusBadGateway {
t.Fatalf("status=%d body=%s, want 502", rec.Code, rec.Body.String())
}
}
func TestBotVerificationReadFiltersAreValidatedBeforeTheStore(t *testing.T) {
// No read store: a malformed query still has to be a 400, so the panel is told
// what it got wrong whether or not the database is reachable.
srv := &server{}
cases := []struct {
handler http.HandlerFunc
path string
}{
{srv.handleCustomVerificationsAPI, "/api/botverification/marks?peer_type=chat"},
{srv.handleCustomVerificationsAPI, "/api/botverification/marks?verifier_bot_id=abc"},
{srv.handleCustomVerificationsAPI, "/api/botverification/marks?before_id=-1"},
{srv.handleCustomVerificationsAPI, "/api/botverification/marks?limit=abc"},
{srv.handleCustomVerificationRequestsAPI, "/api/botverification/requests?status=in_review"},
{srv.handleCustomVerificationRequestsAPI, "/api/botverification/requests?peer_type=chat"},
{srv.handleCustomVerificationRequestsAPI, "/api/botverification/requests?limit=-1"},
{srv.handleBotVerifiersAPI, "/api/botverification/verifiers?limit=abc"},
{srv.handleVerificationIconsAPI, "/api/botverification/icons?limit=-2"},
}
for _, item := range cases {
rec := httptest.NewRecorder()
item.handler(rec, httptest.NewRequest(http.MethodGet, item.path, nil))
if rec.Code != http.StatusBadRequest {
t.Fatalf("%s status=%d body=%s, want 400", item.path, rec.Code, rec.Body.String())
}
}
// A well-formed query with no store wired reports the store, not the query.
rec := httptest.NewRecorder()
srv.handleCustomVerificationRequestsAPI(rec, httptest.NewRequest(
http.MethodGet, "/api/botverification/requests?status=pending&peer_type=channel&limit=10", nil))
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d body=%s, want 503", rec.Code, rec.Body.String())
}
}
func TestQueryFlagReadsThePanelsBooleans(t *testing.T) {
for _, raw := range []string{"1", "true", "TRUE", " yes ", "on"} {
if !queryFlag(raw) {
t.Fatalf("queryFlag(%q) = false", raw)
}
}
for _, raw := range []string{"", "0", "false", "no", "maybe"} {
if queryFlag(raw) {
t.Fatalf("queryFlag(%q) = true", raw)
}
}
}