package roomserver_test import ( "encoding/json" "errors" "testing" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/roomserver" "github.com/dariuszjaniak/warp/server/internal/storage" ) // contentOf reads a stored event's content as a map. func (f *fixture) contentOf(roomID, eventID string) map[string]any { f.t.Helper() stored, err := f.store.Event(f.t.Context(), roomID, eventID) if err != nil { f.t.Fatalf("Event: %v", err) } var body struct { Content map[string]any `json:"content"` } if err := json.Unmarshal(stored.JSON, &body); err != nil { f.t.Fatalf("the stored event is not an object: %v", err) } return body.Content } func TestRedactOwnEvent(t *testing.T) { f := newFixture(t, nil) resp := f.createRoom(roomserver.CreateRoomRequest{Preset: roomserver.PresetPublicChat}) target, err := f.sendMessage(resp.RoomID, alice, "something regrettable", "m1") if err != nil { t.Fatalf("sending the message: %v", err) } if body := f.contentOf(resp.RoomID, target); body["body"] == nil { t.Fatal("the message was stored without its body") } redaction, err := f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: alice, EventID: target, Reason: "a mistake", }) if err != nil { t.Fatalf("Redact: %v", err) } // The event is stripped in place, and still has its original ID: the // ID is the reference hash of the redacted form, so redacting an event // does not rename it. stored, err := f.store.Event(t.Context(), resp.RoomID, target) if err != nil { t.Fatalf("Event: %v", err) } if stored.RedactedBy != redaction { t.Errorf("redacted_by = %q, want %q", stored.RedactedBy, redaction) } if body := f.contentOf(resp.RoomID, target); len(body) != 0 { t.Errorf("the redacted event still has content: %v", body) } // A client reading it back is told why it is empty. clientEvent, err := f.api.Event(t.Context(), resp.RoomID, alice, target) if err != nil { t.Fatalf("Event: %v", err) } var unsigned struct { RedactedBecause struct { EventID string `json:"event_id"` } `json:"redacted_because"` } if err := json.Unmarshal(clientEvent.Unsigned, &unsigned); err != nil { t.Fatalf("the unsigned block is not an object: %v", err) } if unsigned.RedactedBecause.EventID != redaction { t.Errorf("unsigned.redacted_because names %q, want %q", unsigned.RedactedBecause.EventID, redaction) } } // TestRedactAnotherUsersEvent checks the second half of the spec's test // under "Handling redactions", which is the one that actually fires on a // server with no federation: a redaction applies when it comes from the // same server as the event it redacts, whatever the sender's power level, // because a server may always take back its own users' events. // // Every user here is on one server, so that rule covers every pair of them // and the power-level branch cannot be reached. Exercising that branch // needs an event from another server, which is Phase 5; the branch itself // is tested in internal/matrix/auth. func TestRedactAnotherUsersEvent(t *testing.T) { f := newFixture(t, nil) resp := f.createRoom(roomserver.CreateRoomRequest{ Preset: roomserver.PresetPublicChat, // carol is an ordinary member with no redact power, so if the // redaction applies it is the same-server rule that did it. PowerLevelContentOverride: json.RawMessage( `{"users":{"` + alice + `":100},"redact":50}`), }) for _, user := range []string{bob, carol} { if _, err := f.api.JoinRoom(t.Context(), roomserver.JoinRequest{ RoomID: resp.RoomID, User: user, }); err != nil { t.Fatalf("%s joining: %v", user, err) } } target, err := f.sendMessage(resp.RoomID, bob, "bob's message", "m1") if err != nil { t.Fatalf("sending the message: %v", err) } redaction, err := f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: carol, EventID: target, }) if err != nil { t.Fatalf("Redact: %v", err) } stored, err := f.store.Event(t.Context(), resp.RoomID, target) if err != nil { t.Fatalf("Event: %v", err) } if stored.RedactedBy != redaction { t.Errorf("redacted_by = %q, want %q", stored.RedactedBy, redaction) } if body := f.contentOf(resp.RoomID, target); len(body) != 0 { t.Errorf("the redacted event still has content: %v", body) } // A second redaction has nothing left to remove and must not rewrite // which redaction stripped the event. if _, err := f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: alice, EventID: target, }); err != nil { t.Fatalf("redacting twice: %v", err) } stored, err = f.store.Event(t.Context(), resp.RoomID, target) if err != nil { t.Fatalf("Event: %v", err) } if stored.RedactedBy != redaction { t.Errorf("a second redaction took over: redacted_by = %q, want %q", stored.RedactedBy, redaction) } } // TestRedactStateEventKeepsItsKeys is what the redaction algorithm is for: // stripping a membership event must not strip the membership, or the room // would forget who is in it. func TestRedactStateEventKeepsItsKeys(t *testing.T) { f := newFixture(t, nil) resp := f.createRoom(roomserver.CreateRoomRequest{Preset: roomserver.PresetPublicChat}) if _, err := f.api.JoinRoom(t.Context(), roomserver.JoinRequest{ RoomID: resp.RoomID, User: bob, }); err != nil { t.Fatalf("bob joining: %v", err) } memberEvent := f.memberEventID(resp.RoomID, bob) if _, err := f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: alice, EventID: memberEvent, }); err != nil { t.Fatalf("Redact: %v", err) } body := f.contentOf(resp.RoomID, memberEvent) if body["membership"] != event.MembershipJoin { t.Errorf("the redacted membership event lost its membership: %v", body) } // The display name is not protected, so it goes. if body["displayname"] != nil { t.Errorf("the redacted membership event kept its display name: %v", body) } // Bob is still in the room. if got := f.membership(resp.RoomID, bob); got != event.MembershipJoin { t.Errorf("bob's membership after the redaction is %q, want join", got) } } func TestRedactRefusals(t *testing.T) { f := newFixture(t, nil) resp := f.createRoom(roomserver.CreateRoomRequest{}) // An event that does not exist is a 404, not an event in the room // that refers to nothing. _, err := f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: alice, EventID: "$nope", }) if !errors.Is(err, roomserver.ErrNoSuchEvent) { t.Errorf("redacting an unknown event = %v, want ErrNoSuchEvent", err) } // A non-member cannot send a redaction at all. target, err := f.sendMessage(resp.RoomID, alice, "hello", "m1") if err != nil { t.Fatalf("sending the message: %v", err) } _, err = f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: bob, EventID: target, }) if !errors.Is(err, roomserver.ErrRejected) { t.Errorf("a stranger redacting = %v, want ErrRejected", err) } } func TestRedactIsIdempotentPerTransaction(t *testing.T) { f := newFixture(t, nil) resp := f.createRoom(roomserver.CreateRoomRequest{}) target, err := f.sendMessage(resp.RoomID, alice, "hello", "m1") if err != nil { t.Fatalf("sending the message: %v", err) } ref := &storage.TransactionRef{Localpart: "alice", DeviceID: "DEV", TxnID: "r1"} first, err := f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: alice, EventID: target, Transaction: ref, }) if err != nil { t.Fatalf("first redaction: %v", err) } again, err := f.api.Redact(t.Context(), roomserver.RedactRequest{ RoomID: resp.RoomID, Sender: alice, EventID: target, Transaction: ref, }) if err != nil { t.Fatalf("retried redaction: %v", err) } if again != first { t.Errorf("a retried redaction produced a second event: %q then %q", first, again) } }