Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,14 @@ func (c *CSAPI) LeaveRoom(t ct.TestLike, roomID string) *http.Response {
return c.Do(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "leave"}, WithJSONBody(t, body))
}

// InviteRoom invites userID to the room ID, else fails the test.
// MustInviteRoom invites userID to the room ID, else fails the test.
func (c *CSAPI) MustInviteRoom(t ct.TestLike, roomID string, userID string) {
t.Helper()
res := c.InviteRoom(t, roomID, userID)
mustRespond2xx(t, res)
}

// InviteRoom invites userID to the room ID, else fails the test.
// InviteRoom invites userID to the room ID.
func (c *CSAPI) InviteRoom(t ct.TestLike, roomID string, userID string) *http.Response {
t.Helper()
// Invite the user to the room
Expand All @@ -285,6 +285,39 @@ func (c *CSAPI) InviteRoom(t ct.TestLike, roomID string, userID string) *http.Re
return c.Do(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "invite"}, WithJSONBody(t, body))
}

// MustKnockRoom will cause userID to knock on the room ID, else fails the test.
//
// Args:
// - `serverNames`: The list of servers to attempt to knock on the room through.
// These should be a resolvable addresses within the deployment network.
func (c *CSAPI) MustKnockRoom(t ct.TestLike, roomID string, serverNames []spec.ServerName) {
t.Helper()
res := c.KnockRoom(t, roomID, serverNames)
mustRespond2xx(t, res)
}

// KnockRoom will cause userID to knock on the room ID.
//
// Args:
// - `serverNames`: The list of servers to attempt to knock on the room through.
// These should be a resolvable addresses within the deployment network.
func (c *CSAPI) KnockRoom(t ct.TestLike, roomID string, serverNames []spec.ServerName) *http.Response {
t.Helper()
// construct URL query parameters
serverNameStrings := make([]string, len(serverNames))
for i, serverName := range serverNames {
serverNameStrings[i] = string(serverName)
}
query := url.Values{
"via": serverNameStrings,
}
// User knocks on the room
return c.Do(
t, "POST", []string{"_matrix", "client", "v3", "knock", roomID},
WithQueries(query), WithJSONBody(t, map[string]interface{}{}),
)
}

func (c *CSAPI) MustGetGlobalAccountData(t ct.TestLike, eventType string) *http.Response {
res := c.GetGlobalAccountData(t, eventType)
mustRespond2xx(t, res)
Expand Down
105 changes: 102 additions & 3 deletions federation/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"math/big"
"net"
"net/http"
"net/url"
"os"
"path"
"sync"
Expand All @@ -26,6 +27,7 @@ import (
"github.com/matrix-org/gomatrix"
"github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

"github.com/gorilla/mux"
Expand All @@ -35,6 +37,9 @@ import (
"github.com/matrix-org/complement/config"
"github.com/matrix-org/complement/ct"
"github.com/matrix-org/complement/internal"
"github.com/matrix-org/complement/match"
"github.com/matrix-org/complement/must"
"github.com/matrix-org/complement/should"
)

// Subset of Deployment used in federation
Expand Down Expand Up @@ -64,8 +69,9 @@ type Server struct {

directoryHandlerSetup bool
aliases map[string]string
rooms map[string]*ServerRoom
keyRing *gomatrixserverlib.KeyRing
// List of rooms known to this server
rooms map[string]*ServerRoom
keyRing *gomatrixserverlib.KeyRing
}

// EXPERIMENTAL
Expand Down Expand Up @@ -357,7 +363,7 @@ func (s *Server) MustCreateEvent(t ct.TestLike, room *ServerRoom, ev Event) goma
return pdu
}

// MustJoinRoom will make the server send a make_join and a send_join to join a room
// MustJoinRoom will make the server send a /make_join and a /send_join to join a room
// It returns the resultant room.
//
// Args:
Expand Down Expand Up @@ -443,6 +449,99 @@ func (s *Server) MustJoinRoom(t ct.TestLike, deployment FederationDeployment, re
return room
}

type knockRoom struct {
strictKnockRoomStateChecks bool
}

// KnockRoomOpt is an option for configuring how the server should knock on the room
type KnockRoomOpt func(kr *knockRoom)

// WithStrictKnockRoomStateChecks tells the server to strictly check that the received
// `knock_room_state` is valid according to the spec (c.f. MSC4311).
func WithStrictKnockRoomStateChecks() KnockRoomOpt {
return func(kr *knockRoom) {
kr.strictKnockRoomStateChecks = true
}
}

// MustKnockRoom will make the server send a /make_knock and a /send_knock to knock on a room
// It returns the resultant room.
//
// Args:
// - `remoteServer`: This should be a resolvable addresses within the deployment network.
func (s *Server) MustKnockRoom(
t ct.TestLike,
deployment FederationDeployment,
remoteServer spec.ServerName,
roomID string,
userID string,
opts ...KnockRoomOpt,
) *ServerRoom {
t.Helper()
var kr knockRoom
for _, opt := range opts {
opt(&kr)
}

origin := spec.ServerName(s.serverName)
fedClient := s.FederationClient(deployment)

makeKnockResp, err := fedClient.MakeKnock(context.Background(), origin, remoteServer, roomID, userID, SupportedRoomVersions())
if err != nil {
ct.Fatalf(t, "MustKnockRoom: make_knock failed: %v", err)
}

verImpl, err := gomatrixserverlib.GetRoomVersion(makeKnockResp.RoomVersion)
if err != nil {
ct.Fatalf(t, "MustKnockRoom: invalid room version: %v", err)
}

stateKey := userID
makeKnockResp.KnockEvent.SenderID = userID
makeKnockResp.KnockEvent.StateKey = &stateKey

eb := verImpl.NewEventBuilderFromProtoEvent(&makeKnockResp.KnockEvent)
knockEvent, err := eb.Build(time.Now(), origin, s.KeyID, s.Priv)
if err != nil {
ct.Fatalf(t, "MustKnockRoom: failed to sign event: %v", err)
}

// FIXME: Use `fedClient.SendKnock()` once it supports full PDU's vs stripped state
sendKnockPath := "/_matrix/federation/v1/send_knock/" + url.PathEscape(roomID) + "/" + url.PathEscape(knockEvent.EventID())
sendKnockReq := fclient.NewFederationRequest("PUT", origin, remoteServer, sendKnockPath)
if err := sendKnockReq.SetContent(knockEvent); err != nil {
ct.Fatalf(t, "MustKnockRoom: failed to set send_knock content: %v", err)
}
var rawResponse json.RawMessage
if err := s.SendFederationRequest(context.Background(), t, deployment, sendKnockReq, &rawResponse); err != nil {
ct.Fatalf(t, "MustKnockRoom: send_knock failed: %v", err)
}
knockResponse := gjson.ParseBytes(rawResponse)

// Strictly check that the received `knock_room_state` is valid according to the spec
// (c.f. MSC4311).
if kr.strictKnockRoomStateChecks {
must.MatchGJSON(t, knockResponse,
match.JSONArraySome("knock_room_state", func(event gjson.Result) error {
// MSC4311 also mandates that `m.room.create` event is required
return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create"))
}),
match.JSONArrayEach("knock_room_state", func(event gjson.Result) error {
// Each event should have extra fields `origin_server_ts` that indicate we're
// seeing a full PDU and not just a "stripped state event"
return should.MatchGJSON(event, match.JSONKeyPresent("origin_server_ts"))
}),
)
}

room := NewServerRoom(makeKnockResp.RoomVersion, roomID)
s.rooms[room.RoomID] = room

t.Logf("Server.MustKnockRoom knocked on room ID %s", room.RoomID)

return room
}

// Leaves a room. If this is rejecting an invite then a make_leave request is made first, before send_leave.
//
// Args:
Expand Down
31 changes: 31 additions & 0 deletions match/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,37 @@ func JSONMapEach(wantKey string, fn func(k, v gjson.Result) error) JSON {
}
}

// JSONArraySome returns a matcher which will check that `wantKey` is an array then
// loops over each item calling `fn`. If `fn` returns nil, the matcher is satisifed,
// iterating stops and we return.
//
// Will fail if the array is empty and the check never runs
func JSONArraySome(wantKey string, fn func(gjson.Result) error) JSON {
return func(body gjson.Result) error {
if wantKey != "" {
body = body.Get(wantKey)
}

if !body.Exists() {
return fmt.Errorf("JSONArraySome: missing key '%s'", wantKey)
}
if !body.IsArray() {
return fmt.Errorf("JSONArraySome: key '%s' is not an array", wantKey)
}
var satisifed bool = false
body.ForEach(func(_, val gjson.Result) bool {
err := fn(val)
satisifed = err != nil
// Stop iterating when we find a non-error
return !satisifed
})
if !satisifed {
return fmt.Errorf("JSONArraySome('%s'): unable to find item that satisfies check", wantKey)
}
return nil
}
}

// EXPERIMENTAL
// AnyOf takes 1 or more `checkers`, and builds a new checker which accepts a given
// json body iff it's accepted by at least one of the original `checkers`.
Expand Down
Loading
Loading