Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364package xrpc
import ( "bytes" "context" "encoding/json" "errors" "log/slog" "net/http" "net/http/httptest" "testing"
"github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/api/org_tangled" "tangled.org/core/rbac" "tangled.org/core/spindle/config")
type fakeRepoWiper struct { wipedRepos []syntax.DID wipedOwners []syntax.DID repoReasons []string ownerReasons []string err error}
func (f *fakeRepoWiper) WipeRepo(ctx context.Context, repoDid syntax.DID, reason string) error { f.wipedRepos = append(f.wipedRepos, repoDid) f.repoReasons = append(f.repoReasons, reason) return f.err}
func (f *fakeRepoWiper) WipeOwner(ctx context.Context, ownerDid syntax.DID, reason string) error { f.wipedOwners = append(f.wipedOwners, ownerDid) f.ownerReasons = append(f.ownerReasons, reason) return f.err}
func setupBanTestXrpc(t *testing.T) (*Xrpc, *fakeRepoWiper, syntax.DID, syntax.DID) { t.Helper() d, e := newTestXrpcDB(t)
ownerDid := syntax.DID("did:plc:spindleowner") nonOwnerDid := syntax.DID("did:plc:otheruser")
if err := e.AddSpindle(rbac.ThisServer); err != nil { t.Fatalf("AddSpindle: %v", err) } if err := e.AddSpindleOwner(rbac.ThisServer, ownerDid.String()); err != nil { t.Fatalf("AddSpindleOwner: %v", err) }
wiper := &fakeRepoWiper{} x := &Xrpc{ Logger: slog.Default(), Db: d, Enforcer: e, Config: &config.Config{}, Wiper: wiper, }
return x, wiper, ownerDid, nonOwnerDid}
func sendBan(x *Xrpc, actor syntax.DID, input org_tangled.TempSpindleModerationBan_Input) (*httptest.ResponseRecorder, int) { body, _ := json.Marshal(input) req := httptest.NewRequest(http.MethodPost, "/"+org_tangled.TempSpindleModerationBanNSID, bytes.NewReader(body)) if actor != "" { ctx := context.WithValue(req.Context(), ActorDid, actor) req = req.WithContext(ctx) } w := httptest.NewRecorder() x.Ban(w, req) return w, w.Code}
func sendUnban(x *Xrpc, actor syntax.DID, input org_tangled.TempSpindleModerationUnban_Input) (*httptest.ResponseRecorder, int) { body, _ := json.Marshal(input) req := httptest.NewRequest(http.MethodPost, "/"+org_tangled.TempSpindleModerationUnbanNSID, bytes.NewReader(body)) if actor != "" { ctx := context.WithValue(req.Context(), ActorDid, actor) req = req.WithContext(ctx) } w := httptest.NewRecorder() x.Unban(w, req) return w, w.Code}
func sendListBans(x *Xrpc, actor syntax.DID) (*httptest.ResponseRecorder, int) { req := httptest.NewRequest(http.MethodGet, "/"+org_tangled.TempSpindleModerationListBansNSID, nil) if actor != "" { ctx := context.WithValue(req.Context(), ActorDid, actor) req = req.WithContext(ctx) } w := httptest.NewRecorder() x.ListBans(w, req) return w, w.Code}
func sendGetBan(x *Xrpc, actor syntax.DID, query string) (*httptest.ResponseRecorder, int) { path := "/" + org_tangled.TempSpindleModerationGetBanNSID if query != "" { path += "?" + query } req := httptest.NewRequest(http.MethodGet, path, nil) if actor != "" { ctx := context.WithValue(req.Context(), ActorDid, actor) req = req.WithContext(ctx) } w := httptest.NewRecorder() x.GetBan(w, req) return w, w.Code}
func TestBan_OwnerAuth(t *testing.T) { x, _, ownerDid, nonOwnerDid := setupBanTestXrpc(t)
addInput := org_tangled.TempSpindleModerationBan_Input{ Did: "did:plc:targetrepo", }
w, code := sendBan(x, nonOwnerDid, addInput) if code != http.StatusUnauthorized { t.Fatalf("expected 401 for non-owner ban add, got %d", code) }
w, code = sendBan(x, "", addInput) if code != http.StatusUnauthorized { t.Fatalf("expected 401 for missing actor ban add, got %d", code) }
w, code = sendBan(x, ownerDid, addInput) if code != http.StatusOK { t.Fatalf("expected 200 for owner ban add, got %d (body: %s)", code, w.Body.String()) }
w, code = sendListBans(x, nonOwnerDid) if code != http.StatusUnauthorized { t.Fatalf("expected 401 for non-owner ban list, got %d", code) }
w, code = sendListBans(x, "") if code != http.StatusUnauthorized { t.Fatalf("expected 401 for missing actor ban list, got %d", code) }
w, code = sendListBans(x, ownerDid) if code != http.StatusOK { t.Fatalf("expected 200 for owner ban list, got %d", code) }
removeInput := org_tangled.TempSpindleModerationUnban_Input{ Did: "did:plc:targetrepo", }
w, code = sendUnban(x, nonOwnerDid, removeInput) if code != http.StatusUnauthorized { t.Fatalf("expected 401 for non-owner ban remove, got %d", code) }
w, code = sendUnban(x, "", removeInput) if code != http.StatusUnauthorized { t.Fatalf("expected 401 for missing actor ban remove, got %d", code) }
w, code = sendUnban(x, ownerDid, removeInput) if code != http.StatusOK { t.Fatalf("expected 200 for owner ban remove, got %d", code) }}
func TestBan_ValidationErrors(t *testing.T) { x, wiper, ownerDid, _ := setupBanTestXrpc(t)
tests := []struct { name string input org_tangled.TempSpindleModerationBan_Input }{ { name: "invalid did", input: org_tangled.TempSpindleModerationBan_Input{ Did: "notadid", }, }, { name: "empty did", input: org_tangled.TempSpindleModerationBan_Input{ Did: "", }, }, }
for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { w, code := sendBan(x, ownerDid, tc.input) if code != http.StatusBadRequest { t.Fatalf("expected 400 for %s, got %d (body: %s)", tc.name, code, w.Body.String()) } bans, err := x.Db.BanList() if err != nil { t.Fatalf("BanList: %v", err) } if len(bans) != 0 { t.Fatalf("expected no ban row created for %s, got %+v", tc.name, bans) } }) }
removeTests := []struct { name string input org_tangled.TempSpindleModerationUnban_Input }{ { name: "invalid did remove", input: org_tangled.TempSpindleModerationUnban_Input{ Did: "notadid", }, }, { name: "ban not found", input: org_tangled.TempSpindleModerationUnban_Input{ Did: "did:plc:nonexistent", }, }, }
for _, tc := range removeTests { t.Run(tc.name, func(t *testing.T) { w, code := sendUnban(x, ownerDid, tc.input) if code != http.StatusBadRequest { t.Fatalf("expected 400 for %s, got %d (body: %s)", tc.name, code, w.Body.String()) } }) }
wiper.err = errors.New("wipe network error") _, code := sendBan(x, ownerDid, org_tangled.TempSpindleModerationBan_Input{ Did: "did:plc:errorrepo", }) if code != http.StatusInternalServerError { t.Fatalf("expected 500 on purge failure, got %d", code) }}
func TestBan_RoundTrip(t *testing.T) { x, wiper, ownerDid, _ := setupBanTestXrpc(t)
w, code := sendBan(x, ownerDid, org_tangled.TempSpindleModerationBan_Input{ Did: "did:plc:repo123", }) if code != http.StatusOK { t.Fatalf("expected 200 for repo ban add, got %d", code) } if len(wiper.wipedRepos) != 1 || wiper.wipedRepos[0] != "did:plc:repo123" { t.Fatalf("expected repo wipe called for did:plc:repo123, got %+v", wiper.wipedRepos) } if len(wiper.wipedOwners) != 1 || wiper.wipedOwners[0] != "did:plc:repo123" { t.Fatalf("expected owner wipe called for did:plc:repo123, got %+v", wiper.wipedOwners) } if len(wiper.repoReasons) != 1 || wiper.repoReasons[0] != "banned" { t.Fatalf("expected wipe reason 'banned', got %v", wiper.repoReasons) }
w, code = sendBan(x, ownerDid, org_tangled.TempSpindleModerationBan_Input{ Did: "did:plc:badactor", }) if code != http.StatusOK { t.Fatalf("expected 200 for owner ban add, got %d", code) } if len(wiper.wipedOwners) != 2 || wiper.wipedOwners[1] != "did:plc:badactor" { t.Fatalf("expected owner wipe called for did:plc:badactor, got %+v", wiper.wipedOwners) } if len(wiper.ownerReasons) != 2 || wiper.ownerReasons[0] != "banned" || wiper.ownerReasons[1] != "banned" { t.Fatalf("expected wipe reason 'banned' for both wipes, got %v", wiper.ownerReasons) }
w, code = sendListBans(x, ownerDid) if code != http.StatusOK { t.Fatalf("expected 200 for ban list, got %d", code) } var listOut org_tangled.TempSpindleModerationListBans_Output if err := json.Unmarshal(w.Body.Bytes(), &listOut); err != nil { t.Fatalf("failed to decode ban list output: %v", err) } if len(listOut.Bans) != 2 { t.Fatalf("expected 2 bans in list, got %d", len(listOut.Bans)) }
foundRepo := false foundOwner := false for _, b := range listOut.Bans { if b.Did == "did:plc:repo123" && b.CreatedAt != "" { foundRepo = true } if b.Did == "did:plc:badactor" && b.CreatedAt != "" { foundOwner = true } } if !foundRepo || !foundOwner { t.Fatalf("expected both repo and owner bans in list, got %+v", listOut.Bans) }
w, code = sendUnban(x, ownerDid, org_tangled.TempSpindleModerationUnban_Input{ Did: "did:plc:repo123", }) if code != http.StatusOK { t.Fatalf("expected 200 for repo ban remove, got %d", code) }
w, code = sendUnban(x, ownerDid, org_tangled.TempSpindleModerationUnban_Input{ Did: "did:plc:repo123", }) if code != http.StatusBadRequest { t.Fatalf("expected 400 for duplicate remove, got %d", code) }
w, code = sendListBans(x, ownerDid) if code != http.StatusOK { t.Fatalf("expected 200 for ban list after remove, got %d", code) } var afterRemoveOut org_tangled.TempSpindleModerationListBans_Output if err := json.Unmarshal(w.Body.Bytes(), &afterRemoveOut); err != nil { t.Fatalf("failed to decode ban list output: %v", err) } if len(afterRemoveOut.Bans) != 1 || afterRemoveOut.Bans[0].Did != "did:plc:badactor" { t.Fatalf("expected only owner ban remaining, got %+v", afterRemoveOut.Bans) }}
func TestGetBan(t *testing.T) { x, _, ownerDid, nonOwnerDid := setupBanTestXrpc(t) banned := syntax.DID("did:plc:bannedone")
if _, code := sendGetBan(x, ownerDid, "did="+banned.String()); code != http.StatusNotFound { t.Fatalf("expected 404 before the ban, got %d", code) }
if _, code := sendBan(x, ownerDid, org_tangled.TempSpindleModerationBan_Input{Did: banned.String()}); code != http.StatusOK { t.Fatalf("ban add: expected 200, got %d", code) }
w, code := sendGetBan(x, ownerDid, "did="+banned.String()) if code != http.StatusOK { t.Fatalf("expected 200 after the ban, got %d", code) } var out org_tangled.TempSpindleModerationGetBan_Output if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { t.Fatalf("decode: %v", err) } if out.Ban == nil || out.Ban.Did != banned.String() { t.Fatalf("expected the banned did echoed back, got %+v", out.Ban) } if out.Ban.CreatedAt == "" { t.Fatal("expected a ban timestamp") }
if _, code := sendGetBan(x, ownerDid, "did=notadid"); code != http.StatusBadRequest { t.Fatalf("expected 400 for a malformed did, got %d", code) } if _, code := sendGetBan(x, nonOwnerDid, "did="+banned.String()); code == http.StatusOK { t.Fatal("expected a non-owner read to be refused") }}