package adminapi import ( "bytes" "context" "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" "telesrv/internal/admin" "telesrv/internal/domain" "telesrv/internal/officialgifts" ) func TestAdminAPIRequiresBearerToken(t *testing.T) { srv := &Server{token: "secret", svc: fakeService{}} req := httptest.NewRequest(http.MethodPost, "/v1/accounts/set-frozen", strings.NewReader(`{}`)) rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("status=%d, want 401", rec.Code) } } func TestAdminAPISetAccountFrozen(t *testing.T) { svc := &captureFreezeService{} srv := &Server{token: "secret", svc: svc} req := httptest.NewRequest(http.MethodPost, "/v1/accounts/set-frozen", strings.NewReader(`{"command_id":"c1","actor":"ops","reason":"test","dry_run":true,"user_id":1001,"frozen":true,"freeze_until":"2030-01-02T00:00:00Z","freeze_appeal_url":"https://appeals.example.test"}`)) req.Header.Set("Authorization", "Bearer secret") rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), `"command_id":"c1"`) { t.Fatalf("body=%s", rec.Body.String()) } if svc.req.UserID != 1001 || !svc.req.Frozen || svc.req.Until.IsZero() || svc.req.AppealURL != "https://appeals.example.test" { t.Fatalf("decoded freeze request = %+v", svc.req) } } type captureModerationService struct { fakeService filter domain.ModerationCaseFilter decision domain.ModerationDecisionRequest appealReview domain.ModerationDecisionRequest } func (s *captureModerationService) ModerationCases(_ context.Context, filter domain.ModerationCaseFilter) ([]domain.ModerationCase, error) { s.filter = filter return []domain.ModerationCase{{ID: 7}}, nil } func (s *captureModerationService) DecideModerationCase(_ context.Context, request domain.ModerationDecisionRequest) (domain.ModerationCaseDetail, bool, error) { s.decision = request return domain.ModerationCaseDetail{Case: domain.ModerationCase{ID: request.CaseID}}, true, nil } func (s *captureModerationService) ReviewModerationAppeal(_ context.Context, request domain.ModerationDecisionRequest) (domain.ModerationCaseDetail, bool, error) { s.appealReview = request return domain.ModerationCaseDetail{Case: domain.ModerationCase{ID: request.CaseID}}, true, nil } func TestAdminAPIModerationQueueDecisionAndAppealReview(t *testing.T) { svc := &captureModerationService{} srv := &Server{token: "secret", svc: svc} listRequest := httptest.NewRequest( http.MethodGet, "/v1/moderation/cases?statuses=open,action_failed&assigned_to=alice&target_type=user&target_id=99&limit=25", nil, ) listRequest.Header.Set("Authorization", "Bearer secret") list := httptest.NewRecorder() srv.routes().ServeHTTP(list, listRequest) if list.Code != http.StatusOK || !strings.Contains(list.Body.String(), `"ID":7`) { t.Fatalf("list status=%d body=%s", list.Code, list.Body.String()) } if len(svc.filter.Statuses) != 2 || svc.filter.Statuses[0] != domain.ModerationCaseOpen || svc.filter.Statuses[1] != domain.ModerationCaseActionFailed || svc.filter.AssignedTo != "alice" || svc.filter.Target != (domain.Peer{Type: domain.PeerTypeUser, ID: 99}) || svc.filter.Limit != 25 { t.Fatalf("filter=%+v", svc.filter) } decisionRequest := httptest.NewRequest( http.MethodPost, "/v1/moderation/cases/7/decide", strings.NewReader(`{"expected_version":3,"actor":"alice","reason":"confirmed","command_id":"decision-7","kind":"violation","actions":[{"kind":"mark_scam","payload":{}}]}`), ) decisionRequest.Header.Set("Authorization", "Bearer secret") decision := httptest.NewRecorder() srv.routes().ServeHTTP(decision, decisionRequest) if decision.Code != http.StatusOK || !strings.Contains(decision.Body.String(), `"created":true`) || svc.decision.CaseID != 7 || svc.decision.ExpectedVersion != 3 || svc.decision.Kind != domain.ModerationDecisionViolation || len(svc.decision.Actions) != 1 || svc.decision.Actions[0].Kind != domain.ModerationActionMarkScam { t.Fatalf("decision status=%d request=%+v body=%s", decision.Code, svc.decision, decision.Body.String()) } reviewRequest := httptest.NewRequest( http.MethodPost, "/v1/moderation/cases/7/appeals/8/review", strings.NewReader(`{"expected_version":5,"actor":"bob","reason":"appeal accepted","command_id":"appeal-8","granted":true,"actions":[{"kind":"clear_peer_flags","payload":{}}]}`), ) reviewRequest.Header.Set("Authorization", "Bearer secret") review := httptest.NewRecorder() srv.routes().ServeHTTP(review, reviewRequest) if review.Code != http.StatusOK || svc.appealReview.CaseID != 7 || svc.appealReview.AppealID != 8 || svc.appealReview.Kind != domain.ModerationDecisionAppealGrant || len(svc.appealReview.Actions) != 1 || svc.appealReview.Actions[0].Kind != domain.ModerationActionClearPeerFlags { t.Fatalf("review status=%d request=%+v body=%s", review.Code, svc.appealReview, review.Body.String()) } } func TestAdminAPISetVerified(t *testing.T) { srv := &Server{token: "secret", svc: fakeService{}} req := httptest.NewRequest(http.MethodPost, "/v1/accounts/set-verified", strings.NewReader(`{"command_id":"c2","actor":"ops","reason":"official","dry_run":true,"user_id":1001,"verified":true}`)) req.Header.Set("Authorization", "Bearer secret") rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), `"command_id":"c2"`) { t.Fatalf("body=%s", rec.Body.String()) } } func TestAdminAPIGrantStars(t *testing.T) { srv := &Server{token: "secret", svc: fakeService{}} req := httptest.NewRequest(http.MethodPost, "/v1/accounts/grant-stars", strings.NewReader(`{"command_id":"c-stars","actor":"ops","reason":"manual grant","dry_run":true,"user_id":1001,"amount":500}`)) req.Header.Set("Authorization", "Bearer secret") rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), `"command_id":"c-stars"`) { t.Fatalf("body=%s", rec.Body.String()) } } func TestAdminAPISetChannelVerified(t *testing.T) { srv := &Server{token: "secret", svc: fakeService{}} req := httptest.NewRequest(http.MethodPost, "/v1/channels/set-verified", strings.NewReader(`{"command_id":"c3","actor":"ops","reason":"official","dry_run":true,"channel_id":2001,"verified":true}`)) req.Header.Set("Authorization", "Bearer secret") rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), `"command_id":"c3"`) { t.Fatalf("body=%s", rec.Body.String()) } } func TestAdminAPIImportStarGiftMultipart(t *testing.T) { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := writer.WriteField("metadata", `{"command_id":"gift-1","actor":"ops","reason":"catalog","dry_run":true,"title":"Gift","stars":50,"convert_stars":25,"enabled":true,"sort_order":3}`); err != nil { t.Fatal(err) } part, err := writer.CreateFormFile("file", "gift.lottie") if err != nil { t.Fatal(err) } animation := []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`) if _, err := part.Write(animation); err != nil { t.Fatal(err) } if err := writer.Close(); err != nil { t.Fatal(err) } svc := &captureGiftService{} srv := &Server{token: "secret", svc: svc} req := httptest.NewRequest(http.MethodPost, "/v1/gifts/import", &body) req.Header.Set("Authorization", "Bearer secret") req.Header.Set("Content-Type", writer.FormDataContentType()) rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if svc.req.CommandID != "gift-1" || svc.req.FileName != "gift.lottie" || !bytes.Equal(svc.req.Data, animation) || svc.req.Stars != 50 || svc.req.ConvertStars != 25 { t.Fatalf("decoded gift request = %+v", svc.req) } } func TestAdminAPIPublishStarGiftCollectiblesMultipart(t *testing.T) { var body bytes.Buffer writer := multipart.NewWriter(&body) metadata := `{"command_id":"pool-1","actor":"ops","reason":"pool","dry_run":true,"upgrade_stars":125,"supply_total":100,"slug_prefix":"cake","models":[{"name":"Ruby","rarity_permille":500,"sort_order":0,"file_key":"model-0"},{"name":"Sapphire","rarity_permille":500,"sort_order":1,"file_key":"model-1"}],"patterns":[{"name":"Stars","rarity_permille":500,"sort_order":0,"file_key":"pattern-0"},{"name":"Moons","rarity_permille":500,"sort_order":1,"file_key":"pattern-1"}],"backdrops":[{"name":"Night","backdrop_id":1,"center_color":1122867,"edge_color":2241348,"pattern_color":3359829,"text_color":16777215,"rarity_permille":500,"sort_order":0},{"name":"Day","backdrop_id":2,"center_color":11189196,"edge_color":7833753,"pattern_color":14544639,"text_color":1118481,"rarity_permille":500,"sort_order":1}]}` if err := writer.WriteField("metadata", metadata); err != nil { t.Fatal(err) } for key, name := range map[string]string{ "model-0": "ruby.lottie", "model-1": "sapphire.lottie", "pattern-0": "stars.tgs", "pattern-1": "moons.tgs", } { part, err := writer.CreateFormFile(key, name) if err != nil { t.Fatal(err) } if _, err := part.Write([]byte(key)); err != nil { t.Fatal(err) } } if err := writer.Close(); err != nil { t.Fatal(err) } svc := &captureCollectibleService{} srv := &Server{token: "secret", svc: svc} req := httptest.NewRequest(http.MethodPost, "/v1/gifts/11/collectibles/publish", &body) req.Header.Set("Authorization", "Bearer secret") req.Header.Set("Content-Type", writer.FormDataContentType()) rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if svc.req.GiftID != 11 || len(svc.req.Models) != 2 || svc.req.Models[0].FileName != "ruby.lottie" || string(svc.req.Patterns[0].Data) != "pattern-0" || len(svc.req.Backdrops) != 2 || svc.req.Backdrops[1].BackdropID != 2 { t.Fatalf("decoded collectible request = %+v", svc.req) } } func TestCollectiblePreviewResponsePreservesInt64AsDecimalStrings(t *testing.T) { const maxInt64 = int64(9223372036854775807) got := collectiblePreviewResponse(domain.StarGiftUpgradePreview{ GiftID: maxInt64, UpgradeStars: maxInt64, Models: []domain.StarGiftCollectibleAttribute{{ ID: maxInt64, Kind: domain.StarGiftCollectibleModel, Name: "Exact", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, OfficialDocumentID: maxInt64, }}, }) if got["gift_id"] != "9223372036854775807" || got["upgrade_stars"] != "9223372036854775807" { t.Fatalf("preview ids = %#v", got) } models, ok := got["models"].([]map[string]any) if !ok || len(models) != 1 { t.Fatalf("preview models = %#v", got["models"]) } if models[0]["id"] != "9223372036854775807" || models[0]["official_document_id"] != "9223372036854775807" { t.Fatalf("preview model ids = %#v", models[0]) } } func TestOfficialStarGiftListItemExposesExplicitCapabilities(t *testing.T) { item := officialStarGiftListItem(officialgifts.GiftSummary{ ID: 9223372036854775807, Title: "Fresh Socks", Stars: 25, ConvertStars: 10, UpgradeStars: 50, ModelCount: 10, PatternCount: 20, BackdropCount: 30, CraftedModelCount: 2, }) if item["source_gift_id"] != "9223372036854775807" || item["title"] != "Fresh Socks" || item["can_upgrade"] != true || item["can_craft"] != true { t.Fatalf("official gift item = %#v", item) } item = officialStarGiftListItem(officialgifts.GiftSummary{ ID: 1, UpgradeStars: 0, ModelCount: 1, PatternCount: 1, BackdropCount: 1, CraftedModelCount: 1, }) if item["can_upgrade"] != false || item["can_craft"] != false { t.Fatalf("unavailable official gift capabilities = %#v", item) } } type fakeService struct{} type captureFreezeService struct { fakeService req admin.SetAccountFrozenRequest } type captureGiftService struct { fakeService req admin.ImportStarGiftRequest } type captureCollectibleService struct { fakeService req admin.PublishStarGiftCollectiblesRequest } func (s *captureFreezeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) { s.req = req return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (s *captureGiftService) ImportStarGift(_ context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error) { s.req = req return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (s *captureCollectibleService) PublishStarGiftCollectibles(_ context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) { s.req = req return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) GrantPremium(_ context.Context, req admin.GrantPremiumRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) GrantStars(_ context.Context, req admin.GrantStarsRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetVerified(_ context.Context, req admin.SetVerifiedRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetChannelVerified(_ context.Context, req admin.SetChannelVerifiedRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) CreateBot(_ context.Context, req admin.CreateBotRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) DeleteBot(_ context.Context, req admin.DeleteBotRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetUserFlags(_ context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetChannelFlags(_ context.Context, req admin.SetChannelFlagsRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetSupport(_ context.Context, req admin.SetSupportRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) GiveGift(_ context.Context, req admin.GiveGiftRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetUsername(_ context.Context, req admin.SetUsernameRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetUserColor(_ context.Context, req admin.SetUserColorRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetUserEmojiStatus(_ context.Context, req admin.SetUserEmojiStatusRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetChannelSettings(_ context.Context, req admin.SetChannelSettingsRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetChannelUsername(_ context.Context, req admin.SetChannelUsernameRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetChannelColor(_ context.Context, req admin.SetChannelColorRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetChannelEmojiStatus(_ context.Context, req admin.SetChannelEmojiStatusRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) RevokeSessions(context.Context, admin.RevokeSessionsRequest) (admin.CommandResult, error) { return admin.CommandResult{}, nil } func (fakeService) DeletePrivateMessages(context.Context, admin.DeletePrivateMessagesRequest) (admin.CommandResult, error) { return admin.CommandResult{}, nil } func (fakeService) DeletePrivateHistory(context.Context, admin.DeletePrivateHistoryRequest) (admin.CommandResult, error) { return admin.CommandResult{}, nil } func (fakeService) ImportStarGift(_ context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) ImportOfficialStarGift(_ context.Context, req admin.ImportOfficialStarGiftRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) OfficialStarGifts(context.Context) ([]officialgifts.GiftSummary, error) { return nil, nil } func (fakeService) OfficialStarGiftAnimation(context.Context, string) ([]byte, bool, error) { return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil } func (fakeService) PublishStarGiftCollectibles(_ context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetStarGiftEnabled(_ context.Context, req admin.SetStarGiftEnabledRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) SetStarGiftSortOrder(_ context.Context, req admin.SetStarGiftSortOrderRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } func (fakeService) StarGiftAnimation(context.Context, int64) ([]byte, bool, error) { return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil } func (fakeService) EmojiAnimation(context.Context, int64) ([]byte, bool, error) { return []byte(`{"v":"5.7","w":100,"h":100}`), true, nil } func (fakeService) StarGiftCollectibles(context.Context, int64) (domain.StarGiftUpgradePreview, bool, error) { return domain.StarGiftUpgradePreview{}, false, nil } func (fakeService) StarGiftCollectibleAnimation(context.Context, int64, domain.StarGiftCollectibleAttributeKind, int64) ([]byte, bool, error) { return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil } func (fakeService) ModerationCases(context.Context, domain.ModerationCaseFilter) ([]domain.ModerationCase, error) { return nil, nil } func (fakeService) ModerationCase(context.Context, int64) (domain.ModerationCaseDetail, bool, error) { return domain.ModerationCaseDetail{}, false, nil } func (fakeService) ModerationReport(context.Context, int64) (domain.ModerationReport, bool, error) { return domain.ModerationReport{}, false, nil } func (fakeService) ClaimModerationCase(context.Context, int64, int64, string) (domain.ModerationCase, error) { return domain.ModerationCase{}, nil } func (fakeService) DecideModerationCase(context.Context, domain.ModerationDecisionRequest) (domain.ModerationCaseDetail, bool, error) { return domain.ModerationCaseDetail{}, true, nil } func (fakeService) SubmitModerationAppeal(context.Context, int64, int64, string) (domain.ModerationAppeal, bool, error) { return domain.ModerationAppeal{}, true, nil } func (fakeService) ReviewModerationAppeal(context.Context, domain.ModerationDecisionRequest) (domain.ModerationCaseDetail, bool, error) { return domain.ModerationCaseDetail{}, true, nil }