package main import ( "strconv" "testing" "time" ) // Ban = remove + the key stays out: the banned public key can't re-register // through any later invite, the member's queued scheduled messages die with // them, and the act is audited. func TestBan(t *testing.T) { ts, srv := newTestServer(t) tokAdm, uidAdm, _ := joinedMember(t, srv, "Root") if err := srv.db.setUserRole(uidAdm, "admin"); err != nil { t.Fatal(err) } tokBad, uidBad, tidBad := joinedMember(t, srv, "Spammer") if err := srv.db.setUserPublicKey(uidBad, "PUBKEY-SPAMMER", ""); err != nil { t.Fatal(err) } // A queued future message from the spammer. due := time.Now().Unix() if code, _ := modPost(t, ts.URL+"/api/schedule", `{"token":"`+tokBad+`","thread_id":"`+tidBad+`","deliver_at":`+strconv.FormatInt(due, 10)+`,"blob":"later"}`); code != 200 { t.Fatal("schedule should work pre-ban") } if code, _ := modAdminPost(t, ts.URL+"/api/admin/users/"+uidBad+"/ban", tokAdm, ""); code != 200 { t.Fatal("ban should succeed") } if srv.db.userExists(uidBad) { t.Fatal("banned member should be removed") } if due, _ := srv.db.dueScheduledMessages(1<<60, 10); len(due) != 0 { t.Fatalf("banned member's scheduled messages should die, still %d", len(due)) } if !srv.db.keyBanned("PUBKEY-SPAMMER") { t.Fatal("the key should be on the ban list") } // The same key can't re-register through a fresh invite: a new claimer // (keyless — pubkeys publish AFTER claim in the real flow) is refused the // banned key but accepted with a fresh one. joinedMember pre-registers a // key, so build the keyless joiner by hand. uidNew, err := srv.db.createUser("Reborn") if err != nil { t.Fatal(err) } if err := srv.db.createKeyInvite("inv-reborn", uidNew, "wrap", testClaimHash, 0); err != nil { t.Fatal(err) } tokNew, ok, err := srv.db.claimKeyInvite("inv-reborn", "Reborn", "", testClaimSecret) if err != nil || !ok { t.Fatalf("claim: ok=%v err=%v", ok, err) } code, _ := modPost(t, ts.URL+"/api/pubkey", `{"token":"`+tokNew+`","public_key":"PUBKEY-SPAMMER"}`) if code != 403 { t.Fatalf("banned key re-registering should be 403, got %d", code) } // A different key is fine — a ban stops the key, not the invite path. if code, _ := modPost(t, ts.URL+"/api/pubkey", `{"token":"`+tokNew+`","public_key":"PUBKEY-FRESH"}`); code != 200 { t.Fatalf("a fresh key should register, got %d", code) } // Audited. entries, err := srv.db.listAudit(0, 10) if err != nil { t.Fatal(err) } found := false for _, e := range entries { if e.Action == "member.banned" && e.SubjectID == uidBad && e.ActorID == uidAdm { found = true } } if !found { t.Fatalf("ban should be audited, got %v", entries) } } // The content-blind brakes: messages flood-stop (REST path), DM opens are // capped tighter for day-one members, and invite minting has a ceiling. // Admins are exempt from all three. func TestModerationBrakes(t *testing.T) { ts, srv := newTestServer(t) tokA, uidA, tid := joinedMember(t, srv, "Chatty") // Messages: the burst is 30; the 31st in the same instant bounces. sent := 0 for i := 0; i < 40; i++ { code, _ := modPost(t, ts.URL+"/api/messages?token="+tokA, `{"thread_id":"`+tid+`","blob":"m"}`) if code == 200 { sent++ } else if code == 429 { break } else { t.Fatalf("unexpected %d", code) } } if sent != 30 { t.Fatalf("the message brake should admit the burst of 30, admitted %d", sent) } // DM opens: a brand-new member (created just now) hits the day-one cap of 3. opened := 0 for i := 0; i < 5; i++ { _, uidT, _ := joinedMember(t, srv, "T"+strconv.Itoa(i)) code, _ := modPost(t, ts.URL+"/api/threads", dmBody(tokA, uidA, uidT)) if code == 200 { opened++ } else if code == 429 { break } else { t.Fatalf("unexpected %d on dm %d", code, i) } } if opened != 3 { t.Fatalf("a day-one member should open at most 3 DMs, opened %d", opened) } // Admins are exempt from the message brake. tokAdm, uidAdm, tidAdm := joinedMember(t, srv, "Root") if err := srv.db.setUserRole(uidAdm, "admin"); err != nil { t.Fatal(err) } for i := 0; i < 35; i++ { if code, _ := modPost(t, ts.URL+"/api/messages?token="+tokAdm, `{"thread_id":"`+tidAdm+`","blob":"m"}`); code != 200 { t.Fatalf("admin message %d should pass, got %d", i, code) } } }