| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "strconv" |
| 5 | "testing" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | // Ban = remove + the key stays out: the banned public key can't re-register |
| 10 | // through any later invite, the member's queued scheduled messages die with |
| 11 | // them, and the act is audited. |
| 12 | func TestBan(t *testing.T) { |
| 13 | ts, srv := newTestServer(t) |
| 14 | |
| 15 | tokAdm, uidAdm, _ := joinedMember(t, srv, "Root") |
| 16 | if err := srv.db.setUserRole(uidAdm, "admin"); err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | tokBad, uidBad, tidBad := joinedMember(t, srv, "Spammer") |
| 20 | if err := srv.db.setUserPublicKey(uidBad, "PUBKEY-SPAMMER", ""); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | // A queued future message from the spammer. |
| 24 | due := time.Now().Unix() |
| 25 | if code, _ := modPost(t, ts.URL+"/api/schedule", |
| 26 | `{"token":"`+tokBad+`","thread_id":"`+tidBad+`","deliver_at":`+strconv.FormatInt(due, 10)+`,"blob":"later"}`); code != 200 { |
| 27 | t.Fatal("schedule should work pre-ban") |
| 28 | } |
| 29 | |
| 30 | if code, _ := modAdminPost(t, ts.URL+"/api/admin/users/"+uidBad+"/ban", tokAdm, ""); code != 200 { |
| 31 | t.Fatal("ban should succeed") |
| 32 | } |
| 33 | if srv.db.userExists(uidBad) { |
| 34 | t.Fatal("banned member should be removed") |
| 35 | } |
| 36 | if due, _ := srv.db.dueScheduledMessages(1<<60, 10); len(due) != 0 { |
| 37 | t.Fatalf("banned member's scheduled messages should die, still %d", len(due)) |
| 38 | } |
| 39 | if !srv.db.keyBanned("PUBKEY-SPAMMER") { |
| 40 | t.Fatal("the key should be on the ban list") |
| 41 | } |
| 42 | |
| 43 | // The same key can't re-register through a fresh invite: a new claimer |
| 44 | // (keyless — pubkeys publish AFTER claim in the real flow) is refused the |
| 45 | // banned key but accepted with a fresh one. joinedMember pre-registers a |
| 46 | // key, so build the keyless joiner by hand. |
| 47 | uidNew, err := srv.db.createUser("Reborn") |
| 48 | if err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | if err := srv.db.createKeyInvite("inv-reborn", uidNew, "wrap", testClaimHash, 0); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | tokNew, ok, err := srv.db.claimKeyInvite("inv-reborn", "Reborn", "", testClaimSecret) |
| 55 | if err != nil || !ok { |
| 56 | t.Fatalf("claim: ok=%v err=%v", ok, err) |
| 57 | } |
| 58 | code, _ := modPost(t, ts.URL+"/api/pubkey", `{"token":"`+tokNew+`","public_key":"PUBKEY-SPAMMER"}`) |
| 59 | if code != 403 { |
| 60 | t.Fatalf("banned key re-registering should be 403, got %d", code) |
| 61 | } |
| 62 | // A different key is fine — a ban stops the key, not the invite path. |
| 63 | if code, _ := modPost(t, ts.URL+"/api/pubkey", `{"token":"`+tokNew+`","public_key":"PUBKEY-FRESH"}`); code != 200 { |
| 64 | t.Fatalf("a fresh key should register, got %d", code) |
| 65 | } |
| 66 | |
| 67 | // Audited. |
| 68 | entries, err := srv.db.listAudit(0, 10) |
| 69 | if err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | found := false |
| 73 | for _, e := range entries { |
| 74 | if e.Action == "member.banned" && e.SubjectID == uidBad && e.ActorID == uidAdm { |
| 75 | found = true |
| 76 | } |
| 77 | } |
| 78 | if !found { |
| 79 | t.Fatalf("ban should be audited, got %v", entries) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | // The content-blind brakes: messages flood-stop (REST path), DM opens are |
| 84 | // capped tighter for day-one members, and invite minting has a ceiling. |
| 85 | // Admins are exempt from all three. |
| 86 | func TestModerationBrakes(t *testing.T) { |
| 87 | ts, srv := newTestServer(t) |
| 88 | |
| 89 | tokA, uidA, tid := joinedMember(t, srv, "Chatty") |
| 90 | // Messages: the burst is 30; the 31st in the same instant bounces. |
| 91 | sent := 0 |
| 92 | for i := 0; i < 40; i++ { |
| 93 | code, _ := modPost(t, ts.URL+"/api/messages?token="+tokA, `{"thread_id":"`+tid+`","blob":"m"}`) |
| 94 | if code == 200 { |
| 95 | sent++ |
| 96 | } else if code == 429 { |
| 97 | break |
| 98 | } else { |
| 99 | t.Fatalf("unexpected %d", code) |
| 100 | } |
| 101 | } |
| 102 | if sent != 30 { |
| 103 | t.Fatalf("the message brake should admit the burst of 30, admitted %d", sent) |
| 104 | } |
| 105 | |
| 106 | // DM opens: a brand-new member (created just now) hits the day-one cap of 3. |
| 107 | opened := 0 |
| 108 | for i := 0; i < 5; i++ { |
| 109 | _, uidT, _ := joinedMember(t, srv, "T"+strconv.Itoa(i)) |
| 110 | code, _ := modPost(t, ts.URL+"/api/threads", dmBody(tokA, uidA, uidT)) |
| 111 | if code == 200 { |
| 112 | opened++ |
| 113 | } else if code == 429 { |
| 114 | break |
| 115 | } else { |
| 116 | t.Fatalf("unexpected %d on dm %d", code, i) |
| 117 | } |
| 118 | } |
| 119 | if opened != 3 { |
| 120 | t.Fatalf("a day-one member should open at most 3 DMs, opened %d", opened) |
| 121 | } |
| 122 | |
| 123 | // Admins are exempt from the message brake. |
| 124 | tokAdm, uidAdm, tidAdm := joinedMember(t, srv, "Root") |
| 125 | if err := srv.db.setUserRole(uidAdm, "admin"); err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | for i := 0; i < 35; i++ { |
| 129 | if code, _ := modPost(t, ts.URL+"/api/messages?token="+tokAdm, `{"thread_id":"`+tidAdm+`","blob":"m"}`); code != 200 { |
| 130 | t.Fatalf("admin message %d should pass, got %d", i, code) |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | |