From 1f7c7695eb0c067418ce263fe8a71adbc2a65bbe Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:18:01 +0100 Subject: [PATCH 1/2] Room version 12 tests --- go.mod | 12 +- go.sum | 50 ++-- tests/v12/main_test.go | 65 +++++ tests/v12/msc4289_test.go | 595 ++++++++++++++++++++++++++++++++++++++ tests/v12/msc4291_test.go | 285 ++++++++++++++++++ tests/v12/msc4297_test.go | 462 +++++++++++++++++++++++++++++ tests/v12/msc4311_test.go | 52 ++++ 7 files changed, 1494 insertions(+), 27 deletions(-) create mode 100644 tests/v12/main_test.go create mode 100644 tests/v12/msc4289_test.go create mode 100644 tests/v12/msc4291_test.go create mode 100644 tests/v12/msc4297_test.go create mode 100644 tests/v12/msc4311_test.go diff --git a/go.mod b/go.mod index 3345e74e..32ba8f5e 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,12 @@ require ( github.com/docker/go-connections v0.4.0 github.com/gorilla/mux v1.8.0 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 - github.com/matrix-org/gomatrixserverlib v0.0.0-20250119093516-0a1b2bafb5cf + github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98 github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 github.com/sirupsen/logrus v1.9.3 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.38.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 gonum.org/v1/plot v0.11.0 ) @@ -35,9 +35,11 @@ require ( github.com/go-pdf/fpdf v0.6.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/hashicorp/go-set/v3 v3.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/oleiade/lane/v2 v2.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -50,9 +52,9 @@ require ( go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect gotest.tools/v3 v3.0.3 // indirect ) diff --git a/go.sum b/go.sum index 983d9beb..5d1b01fc 100644 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -64,23 +64,27 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjw github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA= +github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= -github.com/matrix-org/gomatrixserverlib v0.0.0-20250119093516-0a1b2bafb5cf h1:NcRPAlNWXSMrYBOw9oBEX7z5uQxIKA1m/eo51DYQ7KM= -github.com/matrix-org/gomatrixserverlib v0.0.0-20250119093516-0a1b2bafb5cf/go.mod h1:lcYW5K+XQ1MSNUhFJAWXx3oeErkl4f3BohYDboc9vJw= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98 h1:AH19nhwaPYCRddS/s7LgKS+fhntFXg2qG47uFAjwFJ4= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66/go.mod h1:iBI1foelCqA09JJgPV0FYz4qA5dUXYOxMi57FxKBdd4= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw= +github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= @@ -95,6 +99,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/shoenig/test v1.11.0 h1:NoPa5GIoBwuqzIviCrnUJa+t5Xb4xi5Z+zODJnIDsEQ= +github.com/shoenig/test v1.11.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -102,8 +108,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -135,8 +141,8 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -150,20 +156,20 @@ golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -173,13 +179,13 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -188,8 +194,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tests/v12/main_test.go b/tests/v12/main_test.go new file mode 100644 index 00000000..3843d618 --- /dev/null +++ b/tests/v12/main_test.go @@ -0,0 +1,65 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/federation" + "github.com/matrix-org/gomatrixserverlib" +) + +const roomVersion12 = "12" + +var V12ServerRoom = federation.ServerRoomImplCustom{ + ProtoEventCreatorFn: ProtoEventCreator, +} + +func TestMain(m *testing.M) { + complement.TestMain(m, "v12") +} + +// Override how Complement makes proto events so we can conditionally disable/enable the inclusion of the create event +// depending on whether we're running in combined mode or not. +// Complement also doesn't set the room version correctly on the ProtoEvent as this was a new addition to GMSL. +func ProtoEventCreator(def federation.ServerRoomImpl, room *federation.ServerRoom, ev federation.Event) (*gomatrixserverlib.ProtoEvent, error) { + var prevEvents interface{} + if ev.PrevEvents != nil { + // We deliberately want to set the prev events. + prevEvents = ev.PrevEvents + } else { + // No other prev events were supplied so we'll just + // use the forward extremities of the room, which is + // the usual behaviour. + prevEvents = room.ForwardExtremities + } + proto := gomatrixserverlib.ProtoEvent{ + SenderID: ev.Sender, + Depth: int64(room.Depth + 1), // depth starts at 1 + Type: ev.Type, + StateKey: ev.StateKey, + RoomID: room.RoomID, + PrevEvents: prevEvents, + AuthEvents: ev.AuthEvents, + Redacts: ev.Redacts, + Version: gomatrixserverlib.MustGetRoomVersion(room.Version), + } + if err := proto.SetContent(ev.Content); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) + } + if err := proto.SetUnsigned(ev.Content); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) + } + if proto.AuthEvents == nil { + var stateNeeded gomatrixserverlib.StateNeeded + // this does the right thing for v12 + stateNeeded, err := gomatrixserverlib.StateNeededForProtoEvent(&proto) + if err != nil { + return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) + } + // we never include the create event if the HS supports MSC4291 + stateNeeded.Create = false + proto.AuthEvents = room.AuthEvents(stateNeeded) + } + return &proto, nil +} diff --git a/tests/v12/msc4289_test.go b/tests/v12/msc4289_test.go new file mode 100644 index 00000000..32a621b4 --- /dev/null +++ b/tests/v12/msc4289_test.go @@ -0,0 +1,595 @@ +package tests + +import ( + "encoding/json" + "math" + "testing" + "time" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/b" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/federation" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/tidwall/gjson" +) + +var maxCanonicalJSONInt = math.Pow(2, 53) - 1 + +// Test that the creator can kick an admin created both via +// trusted_private_chat and by explicit promotion, including beyond PL100. +// Also checks the creator isn't in the PL event. +func TestMSC4289PrivilegedRoomCreators(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + + kickBob := func(roomID string) { + t.Helper() + alice.MustDo(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": bob.UserID, + }), + ) + } + + t.Run("PL event is missing creator in users map", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("users", map[string]any{})) + }) + + t.Run("m.room.tombstone needs PL150 in the PL event", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("events."+client.GjsonEscape("m.room.tombstone"), 150)) + }) + + t.Run("creator cannot set self in PL event", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ + "users": map[string]int{ + alice.UserID: 100, + }, + })) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) + + t.Run("creator can kick admin", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: 100, + }, + }, + }) + kickBob(roomID) + }) + t.Run("creator can kick admin above PL100", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: 949342, + }, + }, + }) + kickBob(roomID) + }) + t.Run("creator can kick admin at JSON max value", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt, + }, + }, + }) + kickBob(roomID) + }) + // technically not a MSC4289 thing but implementations may set the creator PL to be + // above the value expressible in canonical JSON to implement "infinite". + t.Run("power level cannot be set beyond max canonical JSON int", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + resp := alice.Do( + t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, + client.WithJSONBody(t, map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt + 1, + }, + }), + ) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) + t.Run("admin with >PL100 cannot kick creator", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt, + }, + }, + }) + resp := bob.Do(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": alice.UserID, + }), + ) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 403, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_FORBIDDEN"), + }, + }) + }) + t.Run("admin with >PL100 sorts after the room creator for state resolution", func(t *testing.T) { + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + plEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob: 9493420, + }, + }, + }) + room.WaiterForEvent(plEventID).Waitf(t, 5*time.Second, "failed to see PL event giving bob >PL100") + + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Content: map[string]any{ + "join_rule": spec.Invite, + }, + }) + // Bob concurrently sets the join rule to 'knock'. + // State resolution will apply power events (join rules) from highest PL to lowest + // so ensure the end result is Bob's 'knock'. + bobJREvent := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Content: map[string]any{ + "join_rule": spec.Knock, + }, + PrevEvents: []string{plEventID}, + Sender: bob, + }) + room.AddEvent(bobJREvent) + + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{bobJREvent.JSON()}, nil) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, bobJREvent.EventID())) + joinRuleContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomJoinRules, "") + must.MatchGJSON(t, joinRuleContent, match.JSONKeyEqual("join_rule", "knock")) + }) + // Some servers may apply validation to ensure the creator appears in the power_level_content_override, + // which for v12 rooms is wrong. + t.Run("power_level_content_override can be set", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + "power_level_content_override": map[string]any{ + "users": map[string]int{ + bob.UserID: 100, + }, + }, + }) + plContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, plContent, match.JSONKeyEqual("users", map[string]float64{ + bob.UserID: 100, + })) + }) + t.Run("power_level_content_override cannot set the room creator", func(t *testing.T) { + resp := alice.CreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + "power_level_content_override": map[string]any{ + "users": map[string]int{ + alice.UserID: 100, + }, + }, + }) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) +} + +// Check that additional_creators works in the happy case +func TestMSC4289PrivilegedRoomCreators_Additional(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "creation_content": map[string]any{ + "additional_creators": []string{bob.UserID}, + }, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + // we should not be able to kick bob + res := alice.Do(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": bob.UserID, + }), + ) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: 403, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_FORBIDDEN"), + }, + }) + // Bob should be able to do privileged operations like set the room name + bob.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomName, + StateKey: b.Ptr(""), + Content: map[string]any{ + "name": "Bob's room name", + }, + }) + // Bob should not be able to be inserted into content.users in the PL event + // because they are in additional_creators + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ + "users": map[string]int{ + bob.UserID: 100, + }, + })) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) +} + +func TestMSC4289PrivilegedRoomCreators_InvitedAreCreators(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "trusted_private_chat", + "is_direct": true, + "invite": []string{bob.UserID}, + }) + createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, match.JSONKeyEqual("additional_creators", []string{bob.UserID})) +} + +// Ensure that trusted_private_chat handling doesn't replace additional_creators +func TestMSC4289PrivilegedRoomCreators_AdditionalCreatorsAndInvited(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "charlie", + }) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "trusted_private_chat", + "is_direct": true, + "invite": []string{bob.UserID}, + "creation_content": map[string]any{ + "additional_creators": []string{charlie.UserID}, + }, + }) + createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, + match.JSONCheckOff("additional_creators", []interface{}{bob.UserID, charlie.UserID}, match.CheckOffMapper(func(r gjson.Result) interface{} { + return r.Str + })), + ) +} + +// Check that 'additional_creators' is validated correctly. +func TestMSC4289PrivilegedRoomCreators_AdditionalValidation(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + + testCases := []struct { + Name string + AdditionalCreators any + WantSuccess bool + }{ + { + Name: "additional_creators isn't an array", + AdditionalCreators: "not-an-array", + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't strings", + AdditionalCreators: []any{"@foo:example.com", 42}, + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't user ID strings", + AdditionalCreators: []any{"@foo:example.com", "not-a-user-id"}, + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't valid user ID strings (domain)", + AdditionalCreators: []any{"@invalid:dom$ain$.com"}, + WantSuccess: false, + }, + { + Name: "additional_creators are valid", + AdditionalCreators: []any{"@foo:example.com", "@bar:baz.code"}, + WantSuccess: true, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + resp := alice.CreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "creation_content": map[string]any{ + "additional_creators": tc.AdditionalCreators, + }, + }) + if tc.WantSuccess { + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 200, + }) + } else { + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + } + }) + } +} + +func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "charlie", + }) + + testCases := []struct { + name string + initialCreator *client.CSAPI + initialAdditionalCreators []string + initialVersion string + initialUserPLs map[string]int + entitiyDoingUpgrade *client.CSAPI + newAdditionalCreators []string + // assertions + wantAdditionalCreators []string + wantNewUsersMap map[string]int64 + }{ + { + name: "non-creator admins can upgrade v11 rooms to v12", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + bob.UserID: 100, + }, + entitiyDoingUpgrade: bob, + wantAdditionalCreators: []string{}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "non-creator admins can upgrade v11 rooms to v12 with additional moderators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + bob.UserID: 100, + charlie.UserID: 100, + }, + entitiyDoingUpgrade: bob, + wantAdditionalCreators: []string{}, + wantNewUsersMap: map[string]int64{ + charlie.UserID: 100, + }, + }, + { + name: "non-creator admins can upgrade v12 rooms to v12 with different creators", + initialCreator: alice, + initialVersion: roomVersion12, + initialUserPLs: map[string]int{ + bob.UserID: 150, // bob has enough permission to upgrade + }, + entitiyDoingUpgrade: bob, + newAdditionalCreators: []string{charlie.UserID}, + wantAdditionalCreators: []string{charlie.UserID}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "non-creator admins can upgrade v12 rooms to v12 with different creators with additional moderators", + initialCreator: alice, + initialVersion: roomVersion12, + initialUserPLs: map[string]int{ + bob.UserID: 150, // bob has enough permission to upgrade + charlie.UserID: 50, // gets removed as he will become an additional creator + }, + entitiyDoingUpgrade: bob, + newAdditionalCreators: []string{charlie.UserID}, + wantAdditionalCreators: []string{charlie.UserID}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "creator admins can upgrade v11 rooms to v12 with additional_creators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + alice.UserID: 100, + bob.UserID: 100, + }, + entitiyDoingUpgrade: alice, + newAdditionalCreators: []string{bob.UserID}, + wantAdditionalCreators: []string{bob.UserID}, + wantNewUsersMap: map[string]int64{}, // both alice and bob are removed as they are now creators. + }, + { + name: "creator admins can upgrade v11 rooms to v12 with additional_creators and moderators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + alice.UserID: 100, + bob.UserID: 100, + charlie.UserID: 50, + }, + entitiyDoingUpgrade: alice, + newAdditionalCreators: []string{bob.UserID}, + wantAdditionalCreators: []string{bob.UserID}, + wantNewUsersMap: map[string]int64{ + charlie.UserID: 50, + }, + }, + } + + for _, tc := range testCases { + createBody := map[string]interface{}{ + "room_version": tc.initialVersion, + "preset": "public_chat", + } + if tc.initialAdditionalCreators != nil { + must.Equal(t, tc.initialVersion, roomVersion12, "can only set additional_creators on v12") + createBody["additional_creators"] = tc.initialAdditionalCreators + } + roomID := tc.initialCreator.MustCreateRoom(t, createBody) + alice.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + bob.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + charlie.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + tc.initialCreator.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": tc.initialUserPLs, + }, + }) + upgradeBody := map[string]any{ + "new_version": roomVersion12, + } + if tc.newAdditionalCreators != nil { + upgradeBody["additional_creators"] = tc.newAdditionalCreators + } + res := tc.entitiyDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) + newRoomID := must.ParseJSON(t, res.Body).Get("replacement_room").Str + // New Create event assertions + createContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + createAssertions := []match.JSON{ + match.JSONKeyEqual("room_version", roomVersion12), + } + if tc.wantAdditionalCreators != nil { + if len(tc.wantAdditionalCreators) > 0 { + createAssertions = append(createAssertions, match.JSONKeyEqual("additional_creators", tc.wantAdditionalCreators)) + } else { + createAssertions = append(createAssertions, match.JSONKeyMissing("additional_creators")) + } + } + must.MatchGJSON( + t, createContent, createAssertions..., + ) + // New PL assertions + plContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") + if tc.wantNewUsersMap != nil { + plContent.Get("users").ForEach(func(key, v gjson.Result) bool { + gotVal := v.Int() + wantVal, ok := tc.wantNewUsersMap[key.Str] + if !ok { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v but want it missing", tc.name, key.Str, gotVal) + return true + } + if gotVal != wantVal { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v want %v", tc.name, key.Str, gotVal, wantVal) + } + delete(tc.wantNewUsersMap, key.Str) + return true + }) + if len(tc.wantNewUsersMap) > 0 { + ct.Errorf(t, "%s: upgraded room PL content missed these users %v", tc.name, tc.wantNewUsersMap) + } + } + t.Logf("OK: %v", tc.name) + } +} diff --git a/tests/v12/msc4291_test.go b/tests/v12/msc4291_test.go new file mode 100644 index 00000000..a47c1c71 --- /dev/null +++ b/tests/v12/msc4291_test.go @@ -0,0 +1,285 @@ +package tests + +import ( + "fmt" + "net/url" + "slices" + "testing" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/b" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/federation" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/tidwall/gjson" +) + +// Test that the room ID is in fact the hash of the create event. +func TestMSC4291RoomIDAsHashOfCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + assertCreateEventIsRoomID(t, alice, roomID) +} + +func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + + createEvent := room.CurrentState(spec.MRoomCreate, "") + if createEvent == nil { + ct.Fatalf(t, "missing create event from /send_join response") + } + t.Logf("Create event is %s", createEvent.EventID()) + createEventID := createEvent.EventID() + must.Equal(t, + roomID, fmt.Sprintf("!%s", createEventID[1:]), // swap $ for ! + "room ID was not the hash of the create event ID", + ) + + for _, event := range room.Timeline { + rawAuthEvents := gjson.GetBytes(event.JSON(), "auth_events") + must.Equal(t, rawAuthEvents.IsArray(), true, "auth_events key is missing / not an array") + var authEventIDs []string + for _, rawAuthEventID := range rawAuthEvents.Array() { + authEventIDs = append(authEventIDs, rawAuthEventID.Str) + } + t.Logf("create=%v authEventIDs=>%v", createEvent.EventID(), authEventIDs) + if slices.Contains(authEventIDs, createEvent.EventID()) { + ct.Fatalf(t, "Event %s (%s) contains the create event in auth_events: %v", event.EventID(), event.Type(), authEventIDs) + } + must.Equal(t, event.RoomID().String(), roomID, fmt.Sprintf("event %s room ID mismatch: got %v want %v", event.EventID(), event.RoomID(), roomID)) + } +} + +// Test that /upgrade also makes a room where the create event ID is the room ID +func TestMSC4291RoomIDAsHashOfCreateEvent_UpgradedRooms(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + + testCases := []struct { + initialVersion string + }{ + { + initialVersion: roomVersion12, + }, + { + initialVersion: "11", + }, + { + initialVersion: "10", + }, + } + for _, tc := range testCases { + oldRoomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": tc.initialVersion, + "preset": "public_chat", + }) + bob.MustJoinRoom(t, oldRoomID, []spec.ServerName{"hs1"}) + res := alice.MustDo(t, "POST", []string{ + "_matrix", "client", "v3", "rooms", oldRoomID, "upgrade", + }, client.WithJSONBody(t, map[string]any{ + "new_version": roomVersion12, + })) + newRoomID := gjson.GetBytes(client.ParseJSON(t, res), "replacement_room").Str + t.Logf("upgraded from %s (%s) to %s (%s)", tc.initialVersion, oldRoomID, roomVersion12, newRoomID) + assertCreateEventIsRoomID(t, alice, newRoomID) + tombstoneContent := alice.MustGetStateEventContent(t, oldRoomID, "m.room.tombstone", "") + must.MatchGJSON(t, tombstoneContent, match.JSONKeyEqual("replacement_room", newRoomID)) + createContent := alice.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, match.JSONKeyEqual("predecessor.room_id", oldRoomID), match.JSONKeyMissing("predecessor.event_id")) + } +} + +// Ensure that clients cannot send an m.room.create event in an existing room. +func TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + for _, version := range []string{"11", roomVersion12} { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": version, + }) + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomCreate, ""}, client.WithJSONBody(t, map[string]any{ + "room_version": version, + // some homeservers may not create a new event if the content exactly matches the prior state, + // so just add some entropy. + "entropy": 100, + })) + must.MatchResponse(t, resp, match.HTTPResponse{StatusCode: 400}) + } +} + +// Test that all CS APIs that return events include the room_id for the create event, +// with the exception of /sync as that always removes room IDs. +func TestMSC4291RoomIDAsHashOfCreateEvent_RoomIDIsOnCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + eventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Hello", + }, + }) + createEventID := "$" + roomID[1:] + + testCases := []struct { + name string + path []string + qps url.Values + extractCreateEvent func(resp gjson.Result) *gjson.Result + }{ + { + name: "/state", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "state"}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/messages", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "messages"}, + qps: url.Values{ + "dir": {"b"}, + "limit": {"100"}, + }, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("chunk").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/event/{eventID}", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "event", createEventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + return &resp + }, + }, + { + name: "/context direct", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", createEventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + ev := resp.Get("event") + return &ev + }, + }, + { + name: "/context indirect", + qps: url.Values{ + "limit": {"100"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("events_before").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/context state", + qps: url.Values{ + "limit": {"100"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("state").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/state?format=event", + qps: url.Values{ + "format": {"event"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.create", ""}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + return &resp + }, + }, + } + for _, tc := range testCases { + opts := []client.RequestOpt{} + if tc.qps != nil { + opts = append(opts, client.WithQueries(tc.qps)) + } + resp := alice.MustDo(t, "GET", tc.path, opts...) + body := must.ParseJSON(t, resp.Body) + createEvent := tc.extractCreateEvent(body) + if createEvent == nil { + ct.Errorf(t, "%s: failed to find create event", tc.name) + continue + } + must.Equal(t, createEvent.Get("room_id").Str, roomID, fmt.Sprintf("%s: create event is missing room ID", tc.name)) + } +} + +func assertCreateEventIsRoomID(t ct.TestLike, client *client.CSAPI, roomID string) (createEventID string) { + t.Helper() + res := client.MustDo(t, "GET", []string{ + "_matrix", "client", "v3", "rooms", roomID, "state", + }) + stateEvents := must.ParseJSON(t, res.Body) + stateEvents.ForEach(func(_, value gjson.Result) bool { + if value.Get("type").Str == spec.MRoomCreate && value.Get("state_key").Str == "" { + createEventID = value.Get("event_id").Str + return false + } + return true + }) + if createEventID == "" { + ct.Fatalf(t, "failed to find create event ID from /state respone: %v", stateEvents.Raw) + } + must.Equal(t, + roomID, fmt.Sprintf("!%s", createEventID[1:]), + "room ID was not the hash of the create event ID", + ) + return createEventID +} diff --git a/tests/v12/msc4297_test.go b/tests/v12/msc4297_test.go new file mode 100644 index 00000000..715b3983 --- /dev/null +++ b/tests/v12/msc4297_test.go @@ -0,0 +1,462 @@ +package tests + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/b" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/federation" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/matrix-org/complement/runtime" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/gomatrixserverlib/fclient" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" + "github.com/tidwall/gjson" +) + +// Test that v2.1 has implemented starting from the empty set not the unconflicted set +// This test assumes a few things about the underlying server implementation: +// - It eventually gives up calling /get_missing_events for some n < 250 and hits /state or /state_ids for the historical state. +// - It does not call /event_auth but may call /event/{eventID} +// - On encountering DAG gaps, the current state is the resolution of all the forwards extremities for each section. +// In other words, the server calculates the current state as the merger of (what_we_knew_before, what_we_know_now), +// despite there being no events with >1 prev_events. +// +// The scenario in this test is similar to the one in the MSC but different in two key ways: +// - To force incorrect state, "Charlie changes display name" happens 250 times to force a /state{_ids} request. +// In the MSC this only happened once. +// - "Bob changes display name" does not exist. We rely on the server calculating the current state as the +// merging of the forwards extremitiy before the gap and the forwards extremity after the gap, so +// in other words we apply state resolution to (Alice leave, 250th Charlie display name change). +func TestMSC4297StateResolutionV2_1_starts_from_empty_set(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + charlie := srv.UserID("charlie") + + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + joinRulePublic := room.CurrentState(spec.MRoomJoinRules, "") + aliceJoin := room.CurrentState(spec.MRoomMember, alice.UserID) + synchronisationEventID := bob.SendEventSynced(t, room.RoomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "can you hear me charlie?", + }, + }) + room.WaiterForEvent(synchronisationEventID).Waitf(t, 5*time.Second, "failed to see synchronisation event, is federation working?") + + // Alice makes the room invite-only then leaves + joinRuleInviteOnlyEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Sender: alice.UserID, + Content: map[string]interface{}{ + "join_rule": "invite", + }, + }) + room.WaiterForEvent(joinRuleInviteOnlyEventID).Waitf(t, 5*time.Second, "failed to see invite join rule event") + + alice.MustLeaveRoom(t, roomID) + // Wait for Charlie to see it + time.Sleep(time.Second) + aliceLeaveEvent := room.CurrentState(spec.MRoomMember, alice.UserID) + if membership, err := aliceLeaveEvent.Membership(); err != nil || membership != spec.Leave { + ct.Fatalf(t, "failed to see Alice leave the room, alice event is %s", string(aliceLeaveEvent.JSON())) + } + + // Now only Bob (server under test) and Charlie (Complement server) are left in the room. + // Charlie is going to send an event with unknown prev_event, causing /get_missing_events + // until eventually /state_ids is hit. When it is, we'll return incorrect room state, claiming + // that the current join rule is public, not invite. This will cause the join rules to get conflicted + // and replayed. V2 would base the checks off the unconflicted state, and since all servers agree + // that Alice=leave it would start like that, making the join rule transitions invalid and causing the + // room to have no join rule at all. V2.1 fixes this by loading the auth_events of the event being replayed + // which correctly has Alice joined. Alice isn't automatically re-joined to the room though because the + // last step of the algorithm is to apply the unconflicted state on top of the resolved conflicts, without + // any extra checks. + + // We don't know how far back server impls will go, so let's use 250 as a large enough value. + charlieEvents := make([]gomatrixserverlib.PDU, 250) + eventIDToIndex := make(map[string]int) + for i := range charlieEvents { + ev := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &charlie, + Sender: charlie, + Content: map[string]interface{}{ + "membership": spec.Join, + "displayname": fmt.Sprintf("Charlie %d", i), + }, + }) + room.AddEvent(ev) + charlieEvents[i] = ev + eventIDToIndex[ev.EventID()] = i + } + + seenStateIDs := helpers.NewWaiter() + // process requests to walk back through charlie's display name changes + srv.Mux().HandleFunc( + "/_matrix/federation/v1/get_missing_events/{roomID}", + srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, charlieEvents)), + ) + + getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { + t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + // Find the correct state for charlie + // we want to return the state before the requested event hence -1 + stateEvents = append(stateEvents, charlieEvents[eventIDToIndex[atEventID]-1]) + // charlie's auth events are everything prior to this + authChain = append(authChain, charlieEvents[:eventIDToIndex[atEventID]-1]...) + + stateEvents = append(stateEvents, + room.CurrentState(spec.MRoomCreate, ""), + room.CurrentState(spec.MRoomPowerLevels, ""), + room.CurrentState(spec.MRoomMember, bob.UserID), + joinRulePublic, // This is wrong, and should be invite. + aliceLeaveEvent, + ) + authChain = append(authChain, aliceJoin) + return + } + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state_ids/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChainIDs []string `json:"auth_chain_ids"` + PDUIDs []string `json:"pdu_ids"` + }{ + AuthChainIDs: asEventIDs(authChain), + PDUIDs: asEventIDs(stateEvents), + }, + } + }), + ) + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` + PDUs gomatrixserverlib.EventJSONs `json:"pdus"` + }{ + AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), + PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), + }, + } + }), + ) + + // Send the last event to trigger /get_missing_events and /state_ids + finalEvent := charlieEvents[len(charlieEvents)-1] + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ + finalEvent.JSON(), + }, nil) + + seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") + + // wait until bob sees the final event + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) + + // the join rules should be `invite`. + // Servers which do not implement v2.1 will get a HTTP 404 here. + content := bob.MustGetStateEventContent(t, room.RoomID, spec.MRoomJoinRules, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("join_rule", "invite")) +} + +func TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + creator := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "creator"}) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + charlie := srv.UserID("charlie") + eve := srv.UserID("eve") + zara := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "zara"}) + + // We play every event from Problem B from the MSC except for Eve's events. We separate out + // Alice and Creator roles for combined MSC compatibility. + roomID := creator.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + creator.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + }, + }, + }) + alice.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + firstPowerLevelEvent := room.CurrentState(spec.MRoomPowerLevels, "") + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + bob.UserID: 50, + }, + }, + }) + pl3EventID := bob.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + bob.UserID: 50, + charlie: 50, + }, + }, + }) + zara.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + + // Now Eve will join from the third PL event + eveJoinEvent := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &eve, + Sender: eve, + Content: map[string]interface{}{ + "membership": spec.Join, + }, + PrevEvents: []string{ + pl3EventID, + }, + }) + room.AddEvent(eveJoinEvent) + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{eveJoinEvent.JSON()}, nil) + aliceSince := alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, eveJoinEvent.EventID())) + + // Now change Eve's display name many times to force partial synchronisation + + // We don't know how far back server impls will go, so let's use 250 as a large enough value. + eveEvents := make([]gomatrixserverlib.PDU, 250) + eventIDToIndex := make(map[string]int) + prevEvents := eveJoinEvent.EventID() + for i := range eveEvents { + ev := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &eve, + Sender: eve, + Content: map[string]interface{}{ + "membership": spec.Join, + "displayname": fmt.Sprintf("Eve %d", i), + }, + PrevEvents: []string{prevEvents}, + }) + room.AddEvent(ev) + eveEvents[i] = ev + eventIDToIndex[ev.EventID()] = i + prevEvents = ev.EventID() + } + + seenStateIDs := helpers.NewWaiter() + + // process requests to walk back through eve's display name changes + srv.Mux().HandleFunc( + "/_matrix/federation/v1/get_missing_events/{roomID}", + srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, eveEvents)), + ) + + getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { + t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + // Find the correct state for eve + // we want to return the state before the requested event hence -1 + stateEvents = append(stateEvents, eveEvents[eventIDToIndex[atEventID]-1]) + // charlie's auth events are everything prior to this + authChain = append(authChain, eveEvents[:eventIDToIndex[atEventID]-1]...) + + stateEvents = append(stateEvents, + room.CurrentState(spec.MRoomCreate, ""), + room.CurrentState(spec.MRoomMember, alice.UserID), + firstPowerLevelEvent, // This is wrong, and should be the 3rd PL event. + room.CurrentState(spec.MRoomJoinRules, ""), + room.CurrentState(spec.MRoomMember, bob.UserID), + room.CurrentState(spec.MRoomMember, charlie), + ) + authChain = append(authChain, eveJoinEvent) + return + } + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state_ids/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChainIDs []string `json:"auth_chain_ids"` + PDUIDs []string `json:"pdu_ids"` + }{ + AuthChainIDs: asEventIDs(authChain), + PDUIDs: asEventIDs(stateEvents), + }, + } + }), + ) + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` + PDUs gomatrixserverlib.EventJSONs `json:"pdus"` + }{ + AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), + PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), + }, + } + }), + ) + + // Send the last event to trigger /get_missing_events and /state_ids + finalEvent := eveEvents[len(eveEvents)-1] + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ + finalEvent.JSON(), + }, nil) + + seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") + + // wait until bob sees the final event + alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) + + // the power levels should be the 3rd one (Alice:PL100, Bob:PL50, Charlie:PL59), not the 1st (Alice: PL100). + // Servers which do not implement v2.1 will see the 1st one. + content := alice.MustGetStateEventContent(t, room.RoomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(alice.UserID), 100)) + // v2 Servers will fail here as bob does not exist in this map because it reset to an earlier event. + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(bob.UserID), 50)) + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(charlie), 50)) +} + +func getMissingEventsHandler(t ct.TestLike, room *federation.ServerRoom, eventIDToIndex map[string]int, events []gomatrixserverlib.PDU) func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + return func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /get_missing_events for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + latestEventID := gjson.ParseBytes(fr.Content()).Get("latest_events").Array()[0].Str + j, ok := eventIDToIndex[latestEventID] + if !ok { + ct.Fatalf(t, "received /get_missing_events with latest=%s which is unknown", latestEventID) + } + t.Logf("received /get_missing_events with latest=%s (i=%d)", latestEventID, j) + // return 10 earlier events. + slice := events[j-10 : j] + for _, ev := range slice { + t.Logf("/get_missing_events returning %v (i=%v)", ev.EventID(), eventIDToIndex[ev.EventID()]) + } + return util.JSONResponse{ + Code: 200, + JSON: struct { + Events gomatrixserverlib.EventJSONs `json:"events"` + }{ + Events: gomatrixserverlib.NewEventJSONsFromEvents(slice), + }, + } + } +} + +func asEventIDs(pdus []gomatrixserverlib.PDU) []string { + eventIDs := make([]string, len(pdus)) + for i := range pdus { + eventIDs[i] = pdus[i].EventID() + } + return eventIDs +} diff --git a/tests/v12/msc4311_test.go b/tests/v12/msc4311_test.go new file mode 100644 index 00000000..c7550a8d --- /dev/null +++ b/tests/v12/msc4311_test.go @@ -0,0 +1,52 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/matrix-org/complement/runtime" + "github.com/matrix-org/gomatrixserverlib/spec" +) + +func TestMSC4311FullCreateEventOnStrippedState(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet + deployment := complement.Deploy(t, 2) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) + remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + for _, target := range []*client.CSAPI{local, remote} { + t.Logf("checking %s", target.UserID) + alice.MustInviteRoom(t, roomID, target.UserID) + resp, _ := target.MustSync(t, client.SyncReq{}) + inviteState := resp.Get( + fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)), + ) + must.NotEqual(t, len(inviteState.Array()), 0, "no events in invite_state") + // find the create event + found := false + for _, ev := range inviteState.Array() { + if ev.Get("type").Str == spec.MRoomCreate { + found = true + // we should have extra fields + must.MatchGJSON(t, ev, + match.JSONKeyPresent("origin_server_ts"), + ) + } + } + if !found { + ct.Errorf(t, "failed to find create event in invite_state") + } + } + +} From c795011e1e6cae4f4abea607295219d8baf9e581 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:03:44 +0100 Subject: [PATCH 2/2] Move to singular file --- go.mod | 2 +- go.sum | 2 + tests/v12/main_test.go | 65 -- tests/v12/msc4289_test.go | 595 ---------------- tests/v12/msc4291_test.go | 285 -------- tests/v12/msc4297_test.go | 462 ------------ tests/v12/msc4311_test.go | 52 -- tests/v12_test.go | 1395 +++++++++++++++++++++++++++++++++++++ 8 files changed, 1398 insertions(+), 1460 deletions(-) delete mode 100644 tests/v12/main_test.go delete mode 100644 tests/v12/msc4289_test.go delete mode 100644 tests/v12/msc4291_test.go delete mode 100644 tests/v12/msc4297_test.go delete mode 100644 tests/v12/msc4311_test.go create mode 100644 tests/v12_test.go diff --git a/go.mod b/go.mod index 32ba8f5e..567b2d00 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/docker/go-connections v0.4.0 github.com/gorilla/mux v1.8.0 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 - github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98 + github.com/matrix-org/gomatrixserverlib v0.0.0-20250811173135-d6bb4bdbc2ac github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 github.com/sirupsen/logrus v1.9.3 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index 5d1b01fc..eef584a7 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98 h1:AH19nhwaPYCRddS/s7LgKS+fhntFXg2qG47uFAjwFJ4= github.com/matrix-org/gomatrixserverlib v0.0.0-20250811171307-390dbaa8de98/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811173135-d6bb4bdbc2ac h1:iQeN/RdPauAZQQIaUY40eWgxQKUrbWxtuFHARvYV0E4= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250811173135-d6bb4bdbc2ac/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66/go.mod h1:iBI1foelCqA09JJgPV0FYz4qA5dUXYOxMi57FxKBdd4= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= diff --git a/tests/v12/main_test.go b/tests/v12/main_test.go deleted file mode 100644 index 3843d618..00000000 --- a/tests/v12/main_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package tests - -import ( - "fmt" - "testing" - - "github.com/matrix-org/complement" - "github.com/matrix-org/complement/federation" - "github.com/matrix-org/gomatrixserverlib" -) - -const roomVersion12 = "12" - -var V12ServerRoom = federation.ServerRoomImplCustom{ - ProtoEventCreatorFn: ProtoEventCreator, -} - -func TestMain(m *testing.M) { - complement.TestMain(m, "v12") -} - -// Override how Complement makes proto events so we can conditionally disable/enable the inclusion of the create event -// depending on whether we're running in combined mode or not. -// Complement also doesn't set the room version correctly on the ProtoEvent as this was a new addition to GMSL. -func ProtoEventCreator(def federation.ServerRoomImpl, room *federation.ServerRoom, ev federation.Event) (*gomatrixserverlib.ProtoEvent, error) { - var prevEvents interface{} - if ev.PrevEvents != nil { - // We deliberately want to set the prev events. - prevEvents = ev.PrevEvents - } else { - // No other prev events were supplied so we'll just - // use the forward extremities of the room, which is - // the usual behaviour. - prevEvents = room.ForwardExtremities - } - proto := gomatrixserverlib.ProtoEvent{ - SenderID: ev.Sender, - Depth: int64(room.Depth + 1), // depth starts at 1 - Type: ev.Type, - StateKey: ev.StateKey, - RoomID: room.RoomID, - PrevEvents: prevEvents, - AuthEvents: ev.AuthEvents, - Redacts: ev.Redacts, - Version: gomatrixserverlib.MustGetRoomVersion(room.Version), - } - if err := proto.SetContent(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) - } - if err := proto.SetUnsigned(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) - } - if proto.AuthEvents == nil { - var stateNeeded gomatrixserverlib.StateNeeded - // this does the right thing for v12 - stateNeeded, err := gomatrixserverlib.StateNeededForProtoEvent(&proto) - if err != nil { - return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) - } - // we never include the create event if the HS supports MSC4291 - stateNeeded.Create = false - proto.AuthEvents = room.AuthEvents(stateNeeded) - } - return &proto, nil -} diff --git a/tests/v12/msc4289_test.go b/tests/v12/msc4289_test.go deleted file mode 100644 index 32a621b4..00000000 --- a/tests/v12/msc4289_test.go +++ /dev/null @@ -1,595 +0,0 @@ -package tests - -import ( - "encoding/json" - "math" - "testing" - "time" - - "github.com/matrix-org/complement" - "github.com/matrix-org/complement/b" - "github.com/matrix-org/complement/client" - "github.com/matrix-org/complement/ct" - "github.com/matrix-org/complement/federation" - "github.com/matrix-org/complement/helpers" - "github.com/matrix-org/complement/match" - "github.com/matrix-org/complement/must" - "github.com/matrix-org/gomatrixserverlib/spec" - "github.com/tidwall/gjson" -) - -var maxCanonicalJSONInt = math.Pow(2, 53) - 1 - -// Test that the creator can kick an admin created both via -// trusted_private_chat and by explicit promotion, including beyond PL100. -// Also checks the creator isn't in the PL event. -func TestMSC4289PrivilegedRoomCreators(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - }) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - }) - - kickBob := func(roomID string) { - t.Helper() - alice.MustDo(t, - "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, - client.WithJSONBody(t, map[string]any{ - "user_id": bob.UserID, - }), - ) - } - - t.Run("PL event is missing creator in users map", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - }) - content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") - must.MatchGJSON(t, content, match.JSONKeyEqual("users", map[string]any{})) - }) - - t.Run("m.room.tombstone needs PL150 in the PL event", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - }) - content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") - must.MatchGJSON(t, content, match.JSONKeyEqual("events."+client.GjsonEscape("m.room.tombstone"), 150)) - }) - - t.Run("creator cannot set self in PL event", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - }) - resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ - "users": map[string]int{ - alice.UserID: 100, - }, - })) - must.MatchResponse(t, resp, match.HTTPResponse{ - StatusCode: 400, - }) - }) - - t.Run("creator can kick admin", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "invite": []string{bob.UserID}, - }) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]interface{}{ - "users": map[string]any{ - bob.UserID: 100, - }, - }, - }) - kickBob(roomID) - }) - t.Run("creator can kick admin above PL100", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "invite": []string{bob.UserID}, - }) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]interface{}{ - "users": map[string]any{ - bob.UserID: 949342, - }, - }, - }) - kickBob(roomID) - }) - t.Run("creator can kick admin at JSON max value", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "invite": []string{bob.UserID}, - }) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]interface{}{ - "users": map[string]any{ - bob.UserID: maxCanonicalJSONInt, - }, - }, - }) - kickBob(roomID) - }) - // technically not a MSC4289 thing but implementations may set the creator PL to be - // above the value expressible in canonical JSON to implement "infinite". - t.Run("power level cannot be set beyond max canonical JSON int", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - "invite": []string{bob.UserID}, - }) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - resp := alice.Do( - t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, - client.WithJSONBody(t, map[string]interface{}{ - "users": map[string]any{ - bob.UserID: maxCanonicalJSONInt + 1, - }, - }), - ) - must.MatchResponse(t, resp, match.HTTPResponse{ - StatusCode: 400, - }) - }) - t.Run("admin with >PL100 cannot kick creator", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "invite": []string{bob.UserID}, - }) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]interface{}{ - "users": map[string]any{ - bob.UserID: maxCanonicalJSONInt, - }, - }, - }) - resp := bob.Do(t, - "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, - client.WithJSONBody(t, map[string]any{ - "user_id": alice.UserID, - }), - ) - must.MatchResponse(t, resp, match.HTTPResponse{ - StatusCode: 403, - JSON: []match.JSON{ - match.JSONKeyEqual("errcode", "M_FORBIDDEN"), - }, - }) - }) - t.Run("admin with >PL100 sorts after the room creator for state resolution", func(t *testing.T) { - srv := federation.NewServer(t, deployment, - federation.HandleKeyRequests(), - federation.HandleMakeSendJoinRequests(), - federation.HandleTransactionRequests(nil, nil), - federation.HandleEventRequests(), - ) - srv.UnexpectedRequestsAreErrors = false - cancel := srv.Listen() - defer cancel() - bob := srv.UserID("bob") - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) - plEventID := alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]interface{}{ - "users": map[string]any{ - bob: 9493420, - }, - }, - }) - room.WaiterForEvent(plEventID).Waitf(t, 5*time.Second, "failed to see PL event giving bob >PL100") - - alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomJoinRules, - StateKey: b.Ptr(""), - Content: map[string]any{ - "join_rule": spec.Invite, - }, - }) - // Bob concurrently sets the join rule to 'knock'. - // State resolution will apply power events (join rules) from highest PL to lowest - // so ensure the end result is Bob's 'knock'. - bobJREvent := srv.MustCreateEvent(t, room, federation.Event{ - Type: spec.MRoomJoinRules, - StateKey: b.Ptr(""), - Content: map[string]any{ - "join_rule": spec.Knock, - }, - PrevEvents: []string{plEventID}, - Sender: bob, - }) - room.AddEvent(bobJREvent) - - srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{bobJREvent.JSON()}, nil) - alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, bobJREvent.EventID())) - joinRuleContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomJoinRules, "") - must.MatchGJSON(t, joinRuleContent, match.JSONKeyEqual("join_rule", "knock")) - }) - // Some servers may apply validation to ensure the creator appears in the power_level_content_override, - // which for v12 rooms is wrong. - t.Run("power_level_content_override can be set", func(t *testing.T) { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "invite": []string{bob.UserID}, - "power_level_content_override": map[string]any{ - "users": map[string]int{ - bob.UserID: 100, - }, - }, - }) - plContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") - must.MatchGJSON(t, plContent, match.JSONKeyEqual("users", map[string]float64{ - bob.UserID: 100, - })) - }) - t.Run("power_level_content_override cannot set the room creator", func(t *testing.T) { - resp := alice.CreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "invite": []string{bob.UserID}, - "power_level_content_override": map[string]any{ - "users": map[string]int{ - alice.UserID: 100, - }, - }, - }) - must.MatchResponse(t, resp, match.HTTPResponse{ - StatusCode: 400, - }) - }) -} - -// Check that additional_creators works in the happy case -func TestMSC4289PrivilegedRoomCreators_Additional(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - }) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - }) - - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - "creation_content": map[string]any{ - "additional_creators": []string{bob.UserID}, - }, - }) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - // we should not be able to kick bob - res := alice.Do(t, - "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, - client.WithJSONBody(t, map[string]any{ - "user_id": bob.UserID, - }), - ) - must.MatchResponse(t, res, match.HTTPResponse{ - StatusCode: 403, - JSON: []match.JSON{ - match.JSONKeyEqual("errcode", "M_FORBIDDEN"), - }, - }) - // Bob should be able to do privileged operations like set the room name - bob.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomName, - StateKey: b.Ptr(""), - Content: map[string]any{ - "name": "Bob's room name", - }, - }) - // Bob should not be able to be inserted into content.users in the PL event - // because they are in additional_creators - resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ - "users": map[string]int{ - bob.UserID: 100, - }, - })) - must.MatchResponse(t, resp, match.HTTPResponse{ - StatusCode: 400, - }) -} - -func TestMSC4289PrivilegedRoomCreators_InvitedAreCreators(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - }) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - }) - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "trusted_private_chat", - "is_direct": true, - "invite": []string{bob.UserID}, - }) - createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") - must.MatchGJSON(t, createContent, match.JSONKeyEqual("additional_creators", []string{bob.UserID})) -} - -// Ensure that trusted_private_chat handling doesn't replace additional_creators -func TestMSC4289PrivilegedRoomCreators_AdditionalCreatorsAndInvited(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - }) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - }) - charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "charlie", - }) - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "trusted_private_chat", - "is_direct": true, - "invite": []string{bob.UserID}, - "creation_content": map[string]any{ - "additional_creators": []string{charlie.UserID}, - }, - }) - createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") - must.MatchGJSON(t, createContent, - match.JSONCheckOff("additional_creators", []interface{}{bob.UserID, charlie.UserID}, match.CheckOffMapper(func(r gjson.Result) interface{} { - return r.Str - })), - ) -} - -// Check that 'additional_creators' is validated correctly. -func TestMSC4289PrivilegedRoomCreators_AdditionalValidation(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - }) - - testCases := []struct { - Name string - AdditionalCreators any - WantSuccess bool - }{ - { - Name: "additional_creators isn't an array", - AdditionalCreators: "not-an-array", - WantSuccess: false, - }, - { - Name: "additional_creators elements aren't strings", - AdditionalCreators: []any{"@foo:example.com", 42}, - WantSuccess: false, - }, - { - Name: "additional_creators elements aren't user ID strings", - AdditionalCreators: []any{"@foo:example.com", "not-a-user-id"}, - WantSuccess: false, - }, - { - Name: "additional_creators elements aren't valid user ID strings (domain)", - AdditionalCreators: []any{"@invalid:dom$ain$.com"}, - WantSuccess: false, - }, - { - Name: "additional_creators are valid", - AdditionalCreators: []any{"@foo:example.com", "@bar:baz.code"}, - WantSuccess: true, - }, - } - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - resp := alice.CreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - "creation_content": map[string]any{ - "additional_creators": tc.AdditionalCreators, - }, - }) - if tc.WantSuccess { - must.MatchResponse(t, resp, match.HTTPResponse{ - StatusCode: 200, - }) - } else { - must.MatchResponse(t, resp, match.HTTPResponse{ - StatusCode: 400, - }) - } - }) - } -} - -func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - }) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - }) - charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "charlie", - }) - - testCases := []struct { - name string - initialCreator *client.CSAPI - initialAdditionalCreators []string - initialVersion string - initialUserPLs map[string]int - entitiyDoingUpgrade *client.CSAPI - newAdditionalCreators []string - // assertions - wantAdditionalCreators []string - wantNewUsersMap map[string]int64 - }{ - { - name: "non-creator admins can upgrade v11 rooms to v12", - initialCreator: alice, - initialVersion: "11", - initialUserPLs: map[string]int{ - bob.UserID: 100, - }, - entitiyDoingUpgrade: bob, - wantAdditionalCreators: []string{}, - wantNewUsersMap: map[string]int64{}, - }, - { - name: "non-creator admins can upgrade v11 rooms to v12 with additional moderators", - initialCreator: alice, - initialVersion: "11", - initialUserPLs: map[string]int{ - bob.UserID: 100, - charlie.UserID: 100, - }, - entitiyDoingUpgrade: bob, - wantAdditionalCreators: []string{}, - wantNewUsersMap: map[string]int64{ - charlie.UserID: 100, - }, - }, - { - name: "non-creator admins can upgrade v12 rooms to v12 with different creators", - initialCreator: alice, - initialVersion: roomVersion12, - initialUserPLs: map[string]int{ - bob.UserID: 150, // bob has enough permission to upgrade - }, - entitiyDoingUpgrade: bob, - newAdditionalCreators: []string{charlie.UserID}, - wantAdditionalCreators: []string{charlie.UserID}, - wantNewUsersMap: map[string]int64{}, - }, - { - name: "non-creator admins can upgrade v12 rooms to v12 with different creators with additional moderators", - initialCreator: alice, - initialVersion: roomVersion12, - initialUserPLs: map[string]int{ - bob.UserID: 150, // bob has enough permission to upgrade - charlie.UserID: 50, // gets removed as he will become an additional creator - }, - entitiyDoingUpgrade: bob, - newAdditionalCreators: []string{charlie.UserID}, - wantAdditionalCreators: []string{charlie.UserID}, - wantNewUsersMap: map[string]int64{}, - }, - { - name: "creator admins can upgrade v11 rooms to v12 with additional_creators", - initialCreator: alice, - initialVersion: "11", - initialUserPLs: map[string]int{ - alice.UserID: 100, - bob.UserID: 100, - }, - entitiyDoingUpgrade: alice, - newAdditionalCreators: []string{bob.UserID}, - wantAdditionalCreators: []string{bob.UserID}, - wantNewUsersMap: map[string]int64{}, // both alice and bob are removed as they are now creators. - }, - { - name: "creator admins can upgrade v11 rooms to v12 with additional_creators and moderators", - initialCreator: alice, - initialVersion: "11", - initialUserPLs: map[string]int{ - alice.UserID: 100, - bob.UserID: 100, - charlie.UserID: 50, - }, - entitiyDoingUpgrade: alice, - newAdditionalCreators: []string{bob.UserID}, - wantAdditionalCreators: []string{bob.UserID}, - wantNewUsersMap: map[string]int64{ - charlie.UserID: 50, - }, - }, - } - - for _, tc := range testCases { - createBody := map[string]interface{}{ - "room_version": tc.initialVersion, - "preset": "public_chat", - } - if tc.initialAdditionalCreators != nil { - must.Equal(t, tc.initialVersion, roomVersion12, "can only set additional_creators on v12") - createBody["additional_creators"] = tc.initialAdditionalCreators - } - roomID := tc.initialCreator.MustCreateRoom(t, createBody) - alice.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) - bob.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) - charlie.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) - tc.initialCreator.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]interface{}{ - "users": tc.initialUserPLs, - }, - }) - upgradeBody := map[string]any{ - "new_version": roomVersion12, - } - if tc.newAdditionalCreators != nil { - upgradeBody["additional_creators"] = tc.newAdditionalCreators - } - res := tc.entitiyDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) - newRoomID := must.ParseJSON(t, res.Body).Get("replacement_room").Str - // New Create event assertions - createContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") - createAssertions := []match.JSON{ - match.JSONKeyEqual("room_version", roomVersion12), - } - if tc.wantAdditionalCreators != nil { - if len(tc.wantAdditionalCreators) > 0 { - createAssertions = append(createAssertions, match.JSONKeyEqual("additional_creators", tc.wantAdditionalCreators)) - } else { - createAssertions = append(createAssertions, match.JSONKeyMissing("additional_creators")) - } - } - must.MatchGJSON( - t, createContent, createAssertions..., - ) - // New PL assertions - plContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") - if tc.wantNewUsersMap != nil { - plContent.Get("users").ForEach(func(key, v gjson.Result) bool { - gotVal := v.Int() - wantVal, ok := tc.wantNewUsersMap[key.Str] - if !ok { - ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v but want it missing", tc.name, key.Str, gotVal) - return true - } - if gotVal != wantVal { - ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v want %v", tc.name, key.Str, gotVal, wantVal) - } - delete(tc.wantNewUsersMap, key.Str) - return true - }) - if len(tc.wantNewUsersMap) > 0 { - ct.Errorf(t, "%s: upgraded room PL content missed these users %v", tc.name, tc.wantNewUsersMap) - } - } - t.Logf("OK: %v", tc.name) - } -} diff --git a/tests/v12/msc4291_test.go b/tests/v12/msc4291_test.go deleted file mode 100644 index a47c1c71..00000000 --- a/tests/v12/msc4291_test.go +++ /dev/null @@ -1,285 +0,0 @@ -package tests - -import ( - "fmt" - "net/url" - "slices" - "testing" - - "github.com/matrix-org/complement" - "github.com/matrix-org/complement/b" - "github.com/matrix-org/complement/client" - "github.com/matrix-org/complement/ct" - "github.com/matrix-org/complement/federation" - "github.com/matrix-org/complement/helpers" - "github.com/matrix-org/complement/match" - "github.com/matrix-org/complement/must" - "github.com/matrix-org/gomatrixserverlib/spec" - "github.com/tidwall/gjson" -) - -// Test that the room ID is in fact the hash of the create event. -func TestMSC4291RoomIDAsHashOfCreateEvent(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - }) - assertCreateEventIsRoomID(t, alice, roomID) -} - -func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) - - srv := federation.NewServer(t, deployment, - federation.HandleKeyRequests(), - federation.HandleMakeSendJoinRequests(), - federation.HandleTransactionRequests(nil, nil), - federation.HandleEventRequests(), - ) - srv.UnexpectedRequestsAreErrors = false - cancel := srv.Listen() - defer cancel() - bob := srv.UserID("bob") - - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) - - createEvent := room.CurrentState(spec.MRoomCreate, "") - if createEvent == nil { - ct.Fatalf(t, "missing create event from /send_join response") - } - t.Logf("Create event is %s", createEvent.EventID()) - createEventID := createEvent.EventID() - must.Equal(t, - roomID, fmt.Sprintf("!%s", createEventID[1:]), // swap $ for ! - "room ID was not the hash of the create event ID", - ) - - for _, event := range room.Timeline { - rawAuthEvents := gjson.GetBytes(event.JSON(), "auth_events") - must.Equal(t, rawAuthEvents.IsArray(), true, "auth_events key is missing / not an array") - var authEventIDs []string - for _, rawAuthEventID := range rawAuthEvents.Array() { - authEventIDs = append(authEventIDs, rawAuthEventID.Str) - } - t.Logf("create=%v authEventIDs=>%v", createEvent.EventID(), authEventIDs) - if slices.Contains(authEventIDs, createEvent.EventID()) { - ct.Fatalf(t, "Event %s (%s) contains the create event in auth_events: %v", event.EventID(), event.Type(), authEventIDs) - } - must.Equal(t, event.RoomID().String(), roomID, fmt.Sprintf("event %s room ID mismatch: got %v want %v", event.EventID(), event.RoomID(), roomID)) - } -} - -// Test that /upgrade also makes a room where the create event ID is the room ID -func TestMSC4291RoomIDAsHashOfCreateEvent_UpgradedRooms(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) - - testCases := []struct { - initialVersion string - }{ - { - initialVersion: roomVersion12, - }, - { - initialVersion: "11", - }, - { - initialVersion: "10", - }, - } - for _, tc := range testCases { - oldRoomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": tc.initialVersion, - "preset": "public_chat", - }) - bob.MustJoinRoom(t, oldRoomID, []spec.ServerName{"hs1"}) - res := alice.MustDo(t, "POST", []string{ - "_matrix", "client", "v3", "rooms", oldRoomID, "upgrade", - }, client.WithJSONBody(t, map[string]any{ - "new_version": roomVersion12, - })) - newRoomID := gjson.GetBytes(client.ParseJSON(t, res), "replacement_room").Str - t.Logf("upgraded from %s (%s) to %s (%s)", tc.initialVersion, oldRoomID, roomVersion12, newRoomID) - assertCreateEventIsRoomID(t, alice, newRoomID) - tombstoneContent := alice.MustGetStateEventContent(t, oldRoomID, "m.room.tombstone", "") - must.MatchGJSON(t, tombstoneContent, match.JSONKeyEqual("replacement_room", newRoomID)) - createContent := alice.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") - must.MatchGJSON(t, createContent, match.JSONKeyEqual("predecessor.room_id", oldRoomID), match.JSONKeyMissing("predecessor.event_id")) - } -} - -// Ensure that clients cannot send an m.room.create event in an existing room. -func TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) - for _, version := range []string{"11", roomVersion12} { - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": version, - }) - resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomCreate, ""}, client.WithJSONBody(t, map[string]any{ - "room_version": version, - // some homeservers may not create a new event if the content exactly matches the prior state, - // so just add some entropy. - "entropy": 100, - })) - must.MatchResponse(t, resp, match.HTTPResponse{StatusCode: 400}) - } -} - -// Test that all CS APIs that return events include the room_id for the create event, -// with the exception of /sync as that always removes room IDs. -func TestMSC4291RoomIDAsHashOfCreateEvent_RoomIDIsOnCreateEvent(t *testing.T) { - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - }) - eventID := alice.SendEventSynced(t, roomID, b.Event{ - Type: "m.room.message", - Content: map[string]interface{}{ - "msgtype": "m.text", - "body": "Hello", - }, - }) - createEventID := "$" + roomID[1:] - - testCases := []struct { - name string - path []string - qps url.Values - extractCreateEvent func(resp gjson.Result) *gjson.Result - }{ - { - name: "/state", - path: []string{"_matrix", "client", "v3", "rooms", roomID, "state"}, - extractCreateEvent: func(resp gjson.Result) *gjson.Result { - for _, ev := range resp.Array() { - if ev.Get("type").Str == spec.MRoomCreate { - return &ev - } - } - return nil - }, - }, - { - name: "/messages", - path: []string{"_matrix", "client", "v3", "rooms", roomID, "messages"}, - qps: url.Values{ - "dir": {"b"}, - "limit": {"100"}, - }, - extractCreateEvent: func(resp gjson.Result) *gjson.Result { - for _, ev := range resp.Get("chunk").Array() { - if ev.Get("type").Str == spec.MRoomCreate { - return &ev - } - } - return nil - }, - }, - { - name: "/event/{eventID}", - path: []string{"_matrix", "client", "v3", "rooms", roomID, "event", createEventID}, - extractCreateEvent: func(resp gjson.Result) *gjson.Result { - return &resp - }, - }, - { - name: "/context direct", - path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", createEventID}, - extractCreateEvent: func(resp gjson.Result) *gjson.Result { - ev := resp.Get("event") - return &ev - }, - }, - { - name: "/context indirect", - qps: url.Values{ - "limit": {"100"}, - }, - path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, - extractCreateEvent: func(resp gjson.Result) *gjson.Result { - for _, ev := range resp.Get("events_before").Array() { - if ev.Get("type").Str == spec.MRoomCreate { - return &ev - } - } - return nil - }, - }, - { - name: "/context state", - qps: url.Values{ - "limit": {"100"}, - }, - path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, - extractCreateEvent: func(resp gjson.Result) *gjson.Result { - for _, ev := range resp.Get("state").Array() { - if ev.Get("type").Str == spec.MRoomCreate { - return &ev - } - } - return nil - }, - }, - { - name: "/state?format=event", - qps: url.Values{ - "format": {"event"}, - }, - path: []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.create", ""}, - extractCreateEvent: func(resp gjson.Result) *gjson.Result { - return &resp - }, - }, - } - for _, tc := range testCases { - opts := []client.RequestOpt{} - if tc.qps != nil { - opts = append(opts, client.WithQueries(tc.qps)) - } - resp := alice.MustDo(t, "GET", tc.path, opts...) - body := must.ParseJSON(t, resp.Body) - createEvent := tc.extractCreateEvent(body) - if createEvent == nil { - ct.Errorf(t, "%s: failed to find create event", tc.name) - continue - } - must.Equal(t, createEvent.Get("room_id").Str, roomID, fmt.Sprintf("%s: create event is missing room ID", tc.name)) - } -} - -func assertCreateEventIsRoomID(t ct.TestLike, client *client.CSAPI, roomID string) (createEventID string) { - t.Helper() - res := client.MustDo(t, "GET", []string{ - "_matrix", "client", "v3", "rooms", roomID, "state", - }) - stateEvents := must.ParseJSON(t, res.Body) - stateEvents.ForEach(func(_, value gjson.Result) bool { - if value.Get("type").Str == spec.MRoomCreate && value.Get("state_key").Str == "" { - createEventID = value.Get("event_id").Str - return false - } - return true - }) - if createEventID == "" { - ct.Fatalf(t, "failed to find create event ID from /state respone: %v", stateEvents.Raw) - } - must.Equal(t, - roomID, fmt.Sprintf("!%s", createEventID[1:]), - "room ID was not the hash of the create event ID", - ) - return createEventID -} diff --git a/tests/v12/msc4297_test.go b/tests/v12/msc4297_test.go deleted file mode 100644 index 715b3983..00000000 --- a/tests/v12/msc4297_test.go +++ /dev/null @@ -1,462 +0,0 @@ -package tests - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/matrix-org/complement" - "github.com/matrix-org/complement/b" - "github.com/matrix-org/complement/client" - "github.com/matrix-org/complement/ct" - "github.com/matrix-org/complement/federation" - "github.com/matrix-org/complement/helpers" - "github.com/matrix-org/complement/match" - "github.com/matrix-org/complement/must" - "github.com/matrix-org/complement/runtime" - "github.com/matrix-org/gomatrixserverlib" - "github.com/matrix-org/gomatrixserverlib/fclient" - "github.com/matrix-org/gomatrixserverlib/spec" - "github.com/matrix-org/util" - "github.com/tidwall/gjson" -) - -// Test that v2.1 has implemented starting from the empty set not the unconflicted set -// This test assumes a few things about the underlying server implementation: -// - It eventually gives up calling /get_missing_events for some n < 250 and hits /state or /state_ids for the historical state. -// - It does not call /event_auth but may call /event/{eventID} -// - On encountering DAG gaps, the current state is the resolution of all the forwards extremities for each section. -// In other words, the server calculates the current state as the merger of (what_we_knew_before, what_we_know_now), -// despite there being no events with >1 prev_events. -// -// The scenario in this test is similar to the one in the MSC but different in two key ways: -// - To force incorrect state, "Charlie changes display name" happens 250 times to force a /state{_ids} request. -// In the MSC this only happened once. -// - "Bob changes display name" does not exist. We rely on the server calculating the current state as the -// merging of the forwards extremitiy before the gap and the forwards extremity after the gap, so -// in other words we apply state resolution to (Alice leave, 250th Charlie display name change). -func TestMSC4297StateResolutionV2_1_starts_from_empty_set(t *testing.T) { - runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - srv := federation.NewServer(t, deployment, - federation.HandleKeyRequests(), - federation.HandleMakeSendJoinRequests(), - federation.HandleTransactionRequests(nil, nil), - federation.HandleEventRequests(), - ) - srv.UnexpectedRequestsAreErrors = false - cancel := srv.Listen() - defer cancel() - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) - charlie := srv.UserID("charlie") - - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) - joinRulePublic := room.CurrentState(spec.MRoomJoinRules, "") - aliceJoin := room.CurrentState(spec.MRoomMember, alice.UserID) - synchronisationEventID := bob.SendEventSynced(t, room.RoomID, b.Event{ - Type: "m.room.message", - Content: map[string]interface{}{ - "msgtype": "m.text", - "body": "can you hear me charlie?", - }, - }) - room.WaiterForEvent(synchronisationEventID).Waitf(t, 5*time.Second, "failed to see synchronisation event, is federation working?") - - // Alice makes the room invite-only then leaves - joinRuleInviteOnlyEventID := alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomJoinRules, - StateKey: b.Ptr(""), - Sender: alice.UserID, - Content: map[string]interface{}{ - "join_rule": "invite", - }, - }) - room.WaiterForEvent(joinRuleInviteOnlyEventID).Waitf(t, 5*time.Second, "failed to see invite join rule event") - - alice.MustLeaveRoom(t, roomID) - // Wait for Charlie to see it - time.Sleep(time.Second) - aliceLeaveEvent := room.CurrentState(spec.MRoomMember, alice.UserID) - if membership, err := aliceLeaveEvent.Membership(); err != nil || membership != spec.Leave { - ct.Fatalf(t, "failed to see Alice leave the room, alice event is %s", string(aliceLeaveEvent.JSON())) - } - - // Now only Bob (server under test) and Charlie (Complement server) are left in the room. - // Charlie is going to send an event with unknown prev_event, causing /get_missing_events - // until eventually /state_ids is hit. When it is, we'll return incorrect room state, claiming - // that the current join rule is public, not invite. This will cause the join rules to get conflicted - // and replayed. V2 would base the checks off the unconflicted state, and since all servers agree - // that Alice=leave it would start like that, making the join rule transitions invalid and causing the - // room to have no join rule at all. V2.1 fixes this by loading the auth_events of the event being replayed - // which correctly has Alice joined. Alice isn't automatically re-joined to the room though because the - // last step of the algorithm is to apply the unconflicted state on top of the resolved conflicts, without - // any extra checks. - - // We don't know how far back server impls will go, so let's use 250 as a large enough value. - charlieEvents := make([]gomatrixserverlib.PDU, 250) - eventIDToIndex := make(map[string]int) - for i := range charlieEvents { - ev := srv.MustCreateEvent(t, room, federation.Event{ - Type: spec.MRoomMember, - StateKey: &charlie, - Sender: charlie, - Content: map[string]interface{}{ - "membership": spec.Join, - "displayname": fmt.Sprintf("Charlie %d", i), - }, - }) - room.AddEvent(ev) - charlieEvents[i] = ev - eventIDToIndex[ev.EventID()] = i - } - - seenStateIDs := helpers.NewWaiter() - // process requests to walk back through charlie's display name changes - srv.Mux().HandleFunc( - "/_matrix/federation/v1/get_missing_events/{roomID}", - srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, charlieEvents)), - ) - - getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { - t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) - // Find the correct state for charlie - // we want to return the state before the requested event hence -1 - stateEvents = append(stateEvents, charlieEvents[eventIDToIndex[atEventID]-1]) - // charlie's auth events are everything prior to this - authChain = append(authChain, charlieEvents[:eventIDToIndex[atEventID]-1]...) - - stateEvents = append(stateEvents, - room.CurrentState(spec.MRoomCreate, ""), - room.CurrentState(spec.MRoomPowerLevels, ""), - room.CurrentState(spec.MRoomMember, bob.UserID), - joinRulePublic, // This is wrong, and should be invite. - aliceLeaveEvent, - ) - authChain = append(authChain, aliceJoin) - return - } - srv.Mux().HandleFunc( - "/_matrix/federation/v1/state_ids/{roomID}", - srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { - if pathParams["roomID"] != room.RoomID { - t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) - return util.JSONResponse{ - Code: 400, - JSON: "wrong room", - } - } - defer seenStateIDs.Finish() - req, err := fr.HTTPRequest() - must.NotError(t, "failed to get fed http reqest", err) - atEventID := req.URL.Query().Get("event_id") - t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) - authChain, stateEvents := getIncorrectState(atEventID) - return util.JSONResponse{ - Code: 200, - JSON: struct { - AuthChainIDs []string `json:"auth_chain_ids"` - PDUIDs []string `json:"pdu_ids"` - }{ - AuthChainIDs: asEventIDs(authChain), - PDUIDs: asEventIDs(stateEvents), - }, - } - }), - ) - srv.Mux().HandleFunc( - "/_matrix/federation/v1/state/{roomID}", - srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { - if pathParams["roomID"] != room.RoomID { - t.Errorf("Received /state for the wrong room: %s", room.RoomID) - return util.JSONResponse{ - Code: 400, - JSON: "wrong room", - } - } - defer seenStateIDs.Finish() - req, err := fr.HTTPRequest() - must.NotError(t, "failed to get fed http reqest", err) - atEventID := req.URL.Query().Get("event_id") - t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) - authChain, stateEvents := getIncorrectState(atEventID) - return util.JSONResponse{ - Code: 200, - JSON: struct { - AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` - PDUs gomatrixserverlib.EventJSONs `json:"pdus"` - }{ - AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), - PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), - }, - } - }), - ) - - // Send the last event to trigger /get_missing_events and /state_ids - finalEvent := charlieEvents[len(charlieEvents)-1] - srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ - finalEvent.JSON(), - }, nil) - - seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") - - // wait until bob sees the final event - bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) - - // the join rules should be `invite`. - // Servers which do not implement v2.1 will get a HTTP 404 here. - content := bob.MustGetStateEventContent(t, room.RoomID, spec.MRoomJoinRules, "") - must.MatchGJSON(t, content, match.JSONKeyEqual("join_rule", "invite")) -} - -func TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph(t *testing.T) { - runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes - deployment := complement.Deploy(t, 1) - defer deployment.Destroy(t) - srv := federation.NewServer(t, deployment, - federation.HandleKeyRequests(), - federation.HandleMakeSendJoinRequests(), - federation.HandleTransactionRequests(nil, nil), - federation.HandleEventRequests(), - ) - srv.UnexpectedRequestsAreErrors = false - cancel := srv.Listen() - defer cancel() - creator := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "creator"}) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) - bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) - charlie := srv.UserID("charlie") - eve := srv.UserID("eve") - zara := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "zara"}) - - // We play every event from Problem B from the MSC except for Eve's events. We separate out - // Alice and Creator roles for combined MSC compatibility. - roomID := creator.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) - creator.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]any{ - "users": map[string]any{ - alice.UserID: 100, - }, - }, - }) - alice.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) - firstPowerLevelEvent := room.CurrentState(spec.MRoomPowerLevels, "") - alice.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]any{ - "users": map[string]any{ - alice.UserID: 100, - bob.UserID: 50, - }, - }, - }) - pl3EventID := bob.SendEventSynced(t, roomID, b.Event{ - Type: spec.MRoomPowerLevels, - StateKey: b.Ptr(""), - Content: map[string]any{ - "users": map[string]any{ - alice.UserID: 100, - bob.UserID: 50, - charlie: 50, - }, - }, - }) - zara.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - - // Now Eve will join from the third PL event - eveJoinEvent := srv.MustCreateEvent(t, room, federation.Event{ - Type: spec.MRoomMember, - StateKey: &eve, - Sender: eve, - Content: map[string]interface{}{ - "membership": spec.Join, - }, - PrevEvents: []string{ - pl3EventID, - }, - }) - room.AddEvent(eveJoinEvent) - srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{eveJoinEvent.JSON()}, nil) - aliceSince := alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, eveJoinEvent.EventID())) - - // Now change Eve's display name many times to force partial synchronisation - - // We don't know how far back server impls will go, so let's use 250 as a large enough value. - eveEvents := make([]gomatrixserverlib.PDU, 250) - eventIDToIndex := make(map[string]int) - prevEvents := eveJoinEvent.EventID() - for i := range eveEvents { - ev := srv.MustCreateEvent(t, room, federation.Event{ - Type: spec.MRoomMember, - StateKey: &eve, - Sender: eve, - Content: map[string]interface{}{ - "membership": spec.Join, - "displayname": fmt.Sprintf("Eve %d", i), - }, - PrevEvents: []string{prevEvents}, - }) - room.AddEvent(ev) - eveEvents[i] = ev - eventIDToIndex[ev.EventID()] = i - prevEvents = ev.EventID() - } - - seenStateIDs := helpers.NewWaiter() - - // process requests to walk back through eve's display name changes - srv.Mux().HandleFunc( - "/_matrix/federation/v1/get_missing_events/{roomID}", - srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, eveEvents)), - ) - - getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { - t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) - // Find the correct state for eve - // we want to return the state before the requested event hence -1 - stateEvents = append(stateEvents, eveEvents[eventIDToIndex[atEventID]-1]) - // charlie's auth events are everything prior to this - authChain = append(authChain, eveEvents[:eventIDToIndex[atEventID]-1]...) - - stateEvents = append(stateEvents, - room.CurrentState(spec.MRoomCreate, ""), - room.CurrentState(spec.MRoomMember, alice.UserID), - firstPowerLevelEvent, // This is wrong, and should be the 3rd PL event. - room.CurrentState(spec.MRoomJoinRules, ""), - room.CurrentState(spec.MRoomMember, bob.UserID), - room.CurrentState(spec.MRoomMember, charlie), - ) - authChain = append(authChain, eveJoinEvent) - return - } - srv.Mux().HandleFunc( - "/_matrix/federation/v1/state_ids/{roomID}", - srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { - if pathParams["roomID"] != room.RoomID { - t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) - return util.JSONResponse{ - Code: 400, - JSON: "wrong room", - } - } - defer seenStateIDs.Finish() - req, err := fr.HTTPRequest() - must.NotError(t, "failed to get fed http reqest", err) - atEventID := req.URL.Query().Get("event_id") - t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) - authChain, stateEvents := getIncorrectState(atEventID) - return util.JSONResponse{ - Code: 200, - JSON: struct { - AuthChainIDs []string `json:"auth_chain_ids"` - PDUIDs []string `json:"pdu_ids"` - }{ - AuthChainIDs: asEventIDs(authChain), - PDUIDs: asEventIDs(stateEvents), - }, - } - }), - ) - srv.Mux().HandleFunc( - "/_matrix/federation/v1/state/{roomID}", - srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { - if pathParams["roomID"] != room.RoomID { - t.Errorf("Received /state for the wrong room: %s", room.RoomID) - return util.JSONResponse{ - Code: 400, - JSON: "wrong room", - } - } - defer seenStateIDs.Finish() - req, err := fr.HTTPRequest() - must.NotError(t, "failed to get fed http reqest", err) - atEventID := req.URL.Query().Get("event_id") - t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) - authChain, stateEvents := getIncorrectState(atEventID) - return util.JSONResponse{ - Code: 200, - JSON: struct { - AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` - PDUs gomatrixserverlib.EventJSONs `json:"pdus"` - }{ - AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), - PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), - }, - } - }), - ) - - // Send the last event to trigger /get_missing_events and /state_ids - finalEvent := eveEvents[len(eveEvents)-1] - srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ - finalEvent.JSON(), - }, nil) - - seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") - - // wait until bob sees the final event - alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) - - // the power levels should be the 3rd one (Alice:PL100, Bob:PL50, Charlie:PL59), not the 1st (Alice: PL100). - // Servers which do not implement v2.1 will see the 1st one. - content := alice.MustGetStateEventContent(t, room.RoomID, spec.MRoomPowerLevels, "") - must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(alice.UserID), 100)) - // v2 Servers will fail here as bob does not exist in this map because it reset to an earlier event. - must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(bob.UserID), 50)) - must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(charlie), 50)) -} - -func getMissingEventsHandler(t ct.TestLike, room *federation.ServerRoom, eventIDToIndex map[string]int, events []gomatrixserverlib.PDU) func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { - return func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { - if pathParams["roomID"] != room.RoomID { - t.Errorf("Received /get_missing_events for the wrong room: %s", room.RoomID) - return util.JSONResponse{ - Code: 400, - JSON: "wrong room", - } - } - latestEventID := gjson.ParseBytes(fr.Content()).Get("latest_events").Array()[0].Str - j, ok := eventIDToIndex[latestEventID] - if !ok { - ct.Fatalf(t, "received /get_missing_events with latest=%s which is unknown", latestEventID) - } - t.Logf("received /get_missing_events with latest=%s (i=%d)", latestEventID, j) - // return 10 earlier events. - slice := events[j-10 : j] - for _, ev := range slice { - t.Logf("/get_missing_events returning %v (i=%v)", ev.EventID(), eventIDToIndex[ev.EventID()]) - } - return util.JSONResponse{ - Code: 200, - JSON: struct { - Events gomatrixserverlib.EventJSONs `json:"events"` - }{ - Events: gomatrixserverlib.NewEventJSONsFromEvents(slice), - }, - } - } -} - -func asEventIDs(pdus []gomatrixserverlib.PDU) []string { - eventIDs := make([]string, len(pdus)) - for i := range pdus { - eventIDs[i] = pdus[i].EventID() - } - return eventIDs -} diff --git a/tests/v12/msc4311_test.go b/tests/v12/msc4311_test.go deleted file mode 100644 index c7550a8d..00000000 --- a/tests/v12/msc4311_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package tests - -import ( - "fmt" - "testing" - - "github.com/matrix-org/complement" - "github.com/matrix-org/complement/client" - "github.com/matrix-org/complement/ct" - "github.com/matrix-org/complement/helpers" - "github.com/matrix-org/complement/match" - "github.com/matrix-org/complement/must" - "github.com/matrix-org/complement/runtime" - "github.com/matrix-org/gomatrixserverlib/spec" -) - -func TestMSC4311FullCreateEventOnStrippedState(t *testing.T) { - runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet - deployment := complement.Deploy(t, 2) - defer deployment.Destroy(t) - alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) - local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) - remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) - for _, target := range []*client.CSAPI{local, remote} { - t.Logf("checking %s", target.UserID) - alice.MustInviteRoom(t, roomID, target.UserID) - resp, _ := target.MustSync(t, client.SyncReq{}) - inviteState := resp.Get( - fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)), - ) - must.NotEqual(t, len(inviteState.Array()), 0, "no events in invite_state") - // find the create event - found := false - for _, ev := range inviteState.Array() { - if ev.Get("type").Str == spec.MRoomCreate { - found = true - // we should have extra fields - must.MatchGJSON(t, ev, - match.JSONKeyPresent("origin_server_ts"), - ) - } - } - if !found { - ct.Errorf(t, "failed to find create event in invite_state") - } - } - -} diff --git a/tests/v12_test.go b/tests/v12_test.go new file mode 100644 index 00000000..70c86f53 --- /dev/null +++ b/tests/v12_test.go @@ -0,0 +1,1395 @@ +package tests + +import ( + "encoding/json" + "fmt" + "math" + "net/url" + "slices" + "testing" + "time" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/b" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/federation" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/matrix-org/complement/runtime" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/gomatrixserverlib/fclient" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" + "github.com/tidwall/gjson" +) + +var maxCanonicalJSONInt = math.Pow(2, 53) - 1 + +const roomVersion12 = "12" + +var V12ServerRoom = federation.ServerRoomImplCustom{ + ProtoEventCreatorFn: Protov12EventCreator, +} + +// Override how Complement makes proto events so we can conditionally disable/enable the inclusion of the create event +// depending on whether we're running in combined mode or not. +// Complement also doesn't set the room version correctly on the ProtoEvent as this was a new addition to GMSL. +func Protov12EventCreator(def federation.ServerRoomImpl, room *federation.ServerRoom, ev federation.Event) (*gomatrixserverlib.ProtoEvent, error) { + var prevEvents interface{} + if ev.PrevEvents != nil { + // We deliberately want to set the prev events. + prevEvents = ev.PrevEvents + } else { + // No other prev events were supplied so we'll just + // use the forward extremities of the room, which is + // the usual behaviour. + prevEvents = room.ForwardExtremities + } + proto := gomatrixserverlib.ProtoEvent{ + SenderID: ev.Sender, + Depth: int64(room.Depth + 1), // depth starts at 1 + Type: ev.Type, + StateKey: ev.StateKey, + RoomID: room.RoomID, + PrevEvents: prevEvents, + AuthEvents: ev.AuthEvents, + Redacts: ev.Redacts, + Version: gomatrixserverlib.MustGetRoomVersion(room.Version), + } + if err := proto.SetContent(ev.Content); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) + } + if err := proto.SetUnsigned(ev.Content); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) + } + if proto.AuthEvents == nil { + var stateNeeded gomatrixserverlib.StateNeeded + // this does the right thing for v12 + stateNeeded, err := gomatrixserverlib.StateNeededForProtoEvent(&proto) + if err != nil { + return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) + } + // we never include the create event if the HS supports MSC4291 + stateNeeded.Create = false + proto.AuthEvents = room.AuthEvents(stateNeeded) + } + return &proto, nil +} + +// Test that the creator can kick an admin created both via +// trusted_private_chat and by explicit promotion, including beyond PL100. +// Also checks the creator isn't in the PL event. +func TestMSC4289PrivilegedRoomCreators(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + + kickBob := func(roomID string) { + t.Helper() + alice.MustDo(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": bob.UserID, + }), + ) + } + + t.Run("PL event is missing creator in users map", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("users", map[string]any{})) + }) + + t.Run("m.room.tombstone needs PL150 in the PL event", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + content := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("events."+client.GjsonEscape("m.room.tombstone"), 150)) + }) + + t.Run("creator cannot set self in PL event", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ + "users": map[string]int{ + alice.UserID: 100, + }, + })) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) + + t.Run("creator can kick admin", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: 100, + }, + }, + }) + kickBob(roomID) + }) + t.Run("creator can kick admin above PL100", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: 949342, + }, + }, + }) + kickBob(roomID) + }) + t.Run("creator can kick admin at JSON max value", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt, + }, + }, + }) + kickBob(roomID) + }) + // technically not a MSC4289 thing but implementations may set the creator PL to be + // above the value expressible in canonical JSON to implement "infinite". + t.Run("power level cannot be set beyond max canonical JSON int", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + resp := alice.Do( + t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, + client.WithJSONBody(t, map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt + 1, + }, + }), + ) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) + t.Run("admin with >PL100 cannot kick creator", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob.UserID: maxCanonicalJSONInt, + }, + }, + }) + resp := bob.Do(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": alice.UserID, + }), + ) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 403, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_FORBIDDEN"), + }, + }) + }) + t.Run("admin with >PL100 sorts after the room creator for state resolution", func(t *testing.T) { + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + plEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": map[string]any{ + bob: 9493420, + }, + }, + }) + room.WaiterForEvent(plEventID).Waitf(t, 5*time.Second, "failed to see PL event giving bob >PL100") + + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Content: map[string]any{ + "join_rule": spec.Invite, + }, + }) + // Bob concurrently sets the join rule to 'knock'. + // State resolution will apply power events (join rules) from highest PL to lowest + // so ensure the end result is Bob's 'knock'. + bobJREvent := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Content: map[string]any{ + "join_rule": spec.Knock, + }, + PrevEvents: []string{plEventID}, + Sender: bob, + }) + room.AddEvent(bobJREvent) + + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{bobJREvent.JSON()}, nil) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, bobJREvent.EventID())) + joinRuleContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomJoinRules, "") + must.MatchGJSON(t, joinRuleContent, match.JSONKeyEqual("join_rule", "knock")) + }) + // Some servers may apply validation to ensure the creator appears in the power_level_content_override, + // which for v12 rooms is wrong. + t.Run("power_level_content_override can be set", func(t *testing.T) { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + "power_level_content_override": map[string]any{ + "users": map[string]int{ + bob.UserID: 100, + }, + }, + }) + plContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, plContent, match.JSONKeyEqual("users", map[string]float64{ + bob.UserID: 100, + })) + }) + t.Run("power_level_content_override cannot set the room creator", func(t *testing.T) { + resp := alice.CreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "invite": []string{bob.UserID}, + "power_level_content_override": map[string]any{ + "users": map[string]int{ + alice.UserID: 100, + }, + }, + }) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + }) +} + +// Check that additional_creators works in the happy case +func TestMSC4289PrivilegedRoomCreators_Additional(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "creation_content": map[string]any{ + "additional_creators": []string{bob.UserID}, + }, + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + // we should not be able to kick bob + res := alice.Do(t, + "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, + client.WithJSONBody(t, map[string]any{ + "user_id": bob.UserID, + }), + ) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: 403, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_FORBIDDEN"), + }, + }) + // Bob should be able to do privileged operations like set the room name + bob.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomName, + StateKey: b.Ptr(""), + Content: map[string]any{ + "name": "Bob's room name", + }, + }) + // Bob should not be able to be inserted into content.users in the PL event + // because they are in additional_creators + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomPowerLevels, ""}, client.WithJSONBody(t, map[string]any{ + "users": map[string]int{ + bob.UserID: 100, + }, + })) + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) +} + +func TestMSC4289PrivilegedRoomCreators_InvitedAreCreators(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "trusted_private_chat", + "is_direct": true, + "invite": []string{bob.UserID}, + }) + createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, match.JSONKeyEqual("additional_creators", []string{bob.UserID})) +} + +// Ensure that trusted_private_chat handling doesn't replace additional_creators +func TestMSC4289PrivilegedRoomCreators_AdditionalCreatorsAndInvited(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "charlie", + }) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "trusted_private_chat", + "is_direct": true, + "invite": []string{bob.UserID}, + "creation_content": map[string]any{ + "additional_creators": []string{charlie.UserID}, + }, + }) + createContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, + match.JSONCheckOff("additional_creators", []interface{}{bob.UserID, charlie.UserID}, match.CheckOffMapper(func(r gjson.Result) interface{} { + return r.Str + })), + ) +} + +// Check that 'additional_creators' is validated correctly. +func TestMSC4289PrivilegedRoomCreators_AdditionalValidation(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + + testCases := []struct { + Name string + AdditionalCreators any + WantSuccess bool + }{ + { + Name: "additional_creators isn't an array", + AdditionalCreators: "not-an-array", + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't strings", + AdditionalCreators: []any{"@foo:example.com", 42}, + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't user ID strings", + AdditionalCreators: []any{"@foo:example.com", "not-a-user-id"}, + WantSuccess: false, + }, + { + Name: "additional_creators elements aren't valid user ID strings (domain)", + AdditionalCreators: []any{"@invalid:dom$ain$.com"}, + WantSuccess: false, + }, + { + Name: "additional_creators are valid", + AdditionalCreators: []any{"@foo:example.com", "@bar:baz.code"}, + WantSuccess: true, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + resp := alice.CreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + "creation_content": map[string]any{ + "additional_creators": tc.AdditionalCreators, + }, + }) + if tc.WantSuccess { + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 200, + }) + } else { + must.MatchResponse(t, resp, match.HTTPResponse{ + StatusCode: 400, + }) + } + }) + } +} + +func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "charlie", + }) + + testCases := []struct { + name string + initialCreator *client.CSAPI + initialAdditionalCreators []string + initialVersion string + initialUserPLs map[string]int + entitiyDoingUpgrade *client.CSAPI + newAdditionalCreators []string + // assertions + wantAdditionalCreators []string + wantNewUsersMap map[string]int64 + }{ + { + name: "non-creator admins can upgrade v11 rooms to v12", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + bob.UserID: 100, + }, + entitiyDoingUpgrade: bob, + wantAdditionalCreators: []string{}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "non-creator admins can upgrade v11 rooms to v12 with additional moderators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + bob.UserID: 100, + charlie.UserID: 100, + }, + entitiyDoingUpgrade: bob, + wantAdditionalCreators: []string{}, + wantNewUsersMap: map[string]int64{ + charlie.UserID: 100, + }, + }, + { + name: "non-creator admins can upgrade v12 rooms to v12 with different creators", + initialCreator: alice, + initialVersion: roomVersion12, + initialUserPLs: map[string]int{ + bob.UserID: 150, // bob has enough permission to upgrade + }, + entitiyDoingUpgrade: bob, + newAdditionalCreators: []string{charlie.UserID}, + wantAdditionalCreators: []string{charlie.UserID}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "non-creator admins can upgrade v12 rooms to v12 with different creators with additional moderators", + initialCreator: alice, + initialVersion: roomVersion12, + initialUserPLs: map[string]int{ + bob.UserID: 150, // bob has enough permission to upgrade + charlie.UserID: 50, // gets removed as he will become an additional creator + }, + entitiyDoingUpgrade: bob, + newAdditionalCreators: []string{charlie.UserID}, + wantAdditionalCreators: []string{charlie.UserID}, + wantNewUsersMap: map[string]int64{}, + }, + { + name: "creator admins can upgrade v11 rooms to v12 with additional_creators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + alice.UserID: 100, + bob.UserID: 100, + }, + entitiyDoingUpgrade: alice, + newAdditionalCreators: []string{bob.UserID}, + wantAdditionalCreators: []string{bob.UserID}, + wantNewUsersMap: map[string]int64{}, // both alice and bob are removed as they are now creators. + }, + { + name: "creator admins can upgrade v11 rooms to v12 with additional_creators and moderators", + initialCreator: alice, + initialVersion: "11", + initialUserPLs: map[string]int{ + alice.UserID: 100, + bob.UserID: 100, + charlie.UserID: 50, + }, + entitiyDoingUpgrade: alice, + newAdditionalCreators: []string{bob.UserID}, + wantAdditionalCreators: []string{bob.UserID}, + wantNewUsersMap: map[string]int64{ + charlie.UserID: 50, + }, + }, + } + + for _, tc := range testCases { + createBody := map[string]interface{}{ + "room_version": tc.initialVersion, + "preset": "public_chat", + } + if tc.initialAdditionalCreators != nil { + must.Equal(t, tc.initialVersion, roomVersion12, "can only set additional_creators on v12") + createBody["additional_creators"] = tc.initialAdditionalCreators + } + roomID := tc.initialCreator.MustCreateRoom(t, createBody) + alice.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + bob.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + charlie.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + tc.initialCreator.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]interface{}{ + "users": tc.initialUserPLs, + }, + }) + upgradeBody := map[string]any{ + "new_version": roomVersion12, + } + if tc.newAdditionalCreators != nil { + upgradeBody["additional_creators"] = tc.newAdditionalCreators + } + res := tc.entitiyDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) + newRoomID := must.ParseJSON(t, res.Body).Get("replacement_room").Str + // New Create event assertions + createContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + createAssertions := []match.JSON{ + match.JSONKeyEqual("room_version", roomVersion12), + } + if tc.wantAdditionalCreators != nil { + if len(tc.wantAdditionalCreators) > 0 { + createAssertions = append(createAssertions, match.JSONKeyEqual("additional_creators", tc.wantAdditionalCreators)) + } else { + createAssertions = append(createAssertions, match.JSONKeyMissing("additional_creators")) + } + } + must.MatchGJSON( + t, createContent, createAssertions..., + ) + // New PL assertions + plContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") + if tc.wantNewUsersMap != nil { + plContent.Get("users").ForEach(func(key, v gjson.Result) bool { + gotVal := v.Int() + wantVal, ok := tc.wantNewUsersMap[key.Str] + if !ok { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v but want it missing", tc.name, key.Str, gotVal) + return true + } + if gotVal != wantVal { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v want %v", tc.name, key.Str, gotVal, wantVal) + } + delete(tc.wantNewUsersMap, key.Str) + return true + }) + if len(tc.wantNewUsersMap) > 0 { + ct.Errorf(t, "%s: upgraded room PL content missed these users %v", tc.name, tc.wantNewUsersMap) + } + } + t.Logf("OK: %v", tc.name) + } +} + +// Test that the room ID is in fact the hash of the create event. +func TestMSC4291RoomIDAsHashOfCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + assertCreateEventIsRoomID(t, alice, roomID) +} + +func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + + createEvent := room.CurrentState(spec.MRoomCreate, "") + if createEvent == nil { + ct.Fatalf(t, "missing create event from /send_join response") + } + t.Logf("Create event is %s", createEvent.EventID()) + createEventID := createEvent.EventID() + must.Equal(t, + roomID, fmt.Sprintf("!%s", createEventID[1:]), // swap $ for ! + "room ID was not the hash of the create event ID", + ) + + for _, event := range room.Timeline { + rawAuthEvents := gjson.GetBytes(event.JSON(), "auth_events") + must.Equal(t, rawAuthEvents.IsArray(), true, "auth_events key is missing / not an array") + var authEventIDs []string + for _, rawAuthEventID := range rawAuthEvents.Array() { + authEventIDs = append(authEventIDs, rawAuthEventID.Str) + } + t.Logf("create=%v authEventIDs=>%v", createEvent.EventID(), authEventIDs) + if slices.Contains(authEventIDs, createEvent.EventID()) { + ct.Fatalf(t, "Event %s (%s) contains the create event in auth_events: %v", event.EventID(), event.Type(), authEventIDs) + } + must.Equal(t, event.RoomID().String(), roomID, fmt.Sprintf("event %s room ID mismatch: got %v want %v", event.EventID(), event.RoomID(), roomID)) + } +} + +// Test that /upgrade also makes a room where the create event ID is the room ID +func TestMSC4291RoomIDAsHashOfCreateEvent_UpgradedRooms(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + + testCases := []struct { + initialVersion string + }{ + { + initialVersion: roomVersion12, + }, + { + initialVersion: "11", + }, + { + initialVersion: "10", + }, + } + for _, tc := range testCases { + oldRoomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": tc.initialVersion, + "preset": "public_chat", + }) + bob.MustJoinRoom(t, oldRoomID, []spec.ServerName{"hs1"}) + res := alice.MustDo(t, "POST", []string{ + "_matrix", "client", "v3", "rooms", oldRoomID, "upgrade", + }, client.WithJSONBody(t, map[string]any{ + "new_version": roomVersion12, + })) + newRoomID := gjson.GetBytes(client.ParseJSON(t, res), "replacement_room").Str + t.Logf("upgraded from %s (%s) to %s (%s)", tc.initialVersion, oldRoomID, roomVersion12, newRoomID) + assertCreateEventIsRoomID(t, alice, newRoomID) + tombstoneContent := alice.MustGetStateEventContent(t, oldRoomID, "m.room.tombstone", "") + must.MatchGJSON(t, tombstoneContent, match.JSONKeyEqual("replacement_room", newRoomID)) + createContent := alice.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + must.MatchGJSON(t, createContent, match.JSONKeyEqual("predecessor.room_id", oldRoomID), match.JSONKeyMissing("predecessor.event_id")) + } +} + +// Ensure that clients cannot send an m.room.create event in an existing room. +func TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + for _, version := range []string{"11", roomVersion12} { + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": version, + }) + resp := alice.Do(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", spec.MRoomCreate, ""}, client.WithJSONBody(t, map[string]any{ + "room_version": version, + // some homeservers may not create a new event if the content exactly matches the prior state, + // so just add some entropy. + "entropy": 100, + })) + must.MatchResponse(t, resp, match.HTTPResponse{StatusCode: 400}) + } +} + +// Test that all CS APIs that return events include the room_id for the create event, +// with the exception of /sync as that always removes room IDs. +func TestMSC4291RoomIDAsHashOfCreateEvent_RoomIDIsOnCreateEvent(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + }) + eventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Hello", + }, + }) + createEventID := "$" + roomID[1:] + + testCases := []struct { + name string + path []string + qps url.Values + extractCreateEvent func(resp gjson.Result) *gjson.Result + }{ + { + name: "/state", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "state"}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/messages", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "messages"}, + qps: url.Values{ + "dir": {"b"}, + "limit": {"100"}, + }, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("chunk").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/event/{eventID}", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "event", createEventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + return &resp + }, + }, + { + name: "/context direct", + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", createEventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + ev := resp.Get("event") + return &ev + }, + }, + { + name: "/context indirect", + qps: url.Values{ + "limit": {"100"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("events_before").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/context state", + qps: url.Values{ + "limit": {"100"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "context", eventID}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + for _, ev := range resp.Get("state").Array() { + if ev.Get("type").Str == spec.MRoomCreate { + return &ev + } + } + return nil + }, + }, + { + name: "/state?format=event", + qps: url.Values{ + "format": {"event"}, + }, + path: []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.create", ""}, + extractCreateEvent: func(resp gjson.Result) *gjson.Result { + return &resp + }, + }, + } + for _, tc := range testCases { + opts := []client.RequestOpt{} + if tc.qps != nil { + opts = append(opts, client.WithQueries(tc.qps)) + } + resp := alice.MustDo(t, "GET", tc.path, opts...) + body := must.ParseJSON(t, resp.Body) + createEvent := tc.extractCreateEvent(body) + if createEvent == nil { + ct.Errorf(t, "%s: failed to find create event", tc.name) + continue + } + must.Equal(t, createEvent.Get("room_id").Str, roomID, fmt.Sprintf("%s: create event is missing room ID", tc.name)) + } +} + +func assertCreateEventIsRoomID(t ct.TestLike, client *client.CSAPI, roomID string) (createEventID string) { + t.Helper() + res := client.MustDo(t, "GET", []string{ + "_matrix", "client", "v3", "rooms", roomID, "state", + }) + stateEvents := must.ParseJSON(t, res.Body) + stateEvents.ForEach(func(_, value gjson.Result) bool { + if value.Get("type").Str == spec.MRoomCreate && value.Get("state_key").Str == "" { + createEventID = value.Get("event_id").Str + return false + } + return true + }) + if createEventID == "" { + ct.Fatalf(t, "failed to find create event ID from /state respone: %v", stateEvents.Raw) + } + must.Equal(t, + roomID, fmt.Sprintf("!%s", createEventID[1:]), + "room ID was not the hash of the create event ID", + ) + return createEventID +} + +// Test that v2.1 has implemented starting from the empty set not the unconflicted set +// This test assumes a few things about the underlying server implementation: +// - It eventually gives up calling /get_missing_events for some n < 250 and hits /state or /state_ids for the historical state. +// - It does not call /event_auth but may call /event/{eventID} +// - On encountering DAG gaps, the current state is the resolution of all the forwards extremities for each section. +// In other words, the server calculates the current state as the merger of (what_we_knew_before, what_we_know_now), +// despite there being no events with >1 prev_events. +// +// The scenario in this test is similar to the one in the MSC but different in two key ways: +// - To force incorrect state, "Charlie changes display name" happens 250 times to force a /state{_ids} request. +// In the MSC this only happened once. +// - "Bob changes display name" does not exist. We rely on the server calculating the current state as the +// merging of the forwards extremitiy before the gap and the forwards extremity after the gap, so +// in other words we apply state resolution to (Alice leave, 250th Charlie display name change). +func TestMSC4297StateResolutionV2_1_starts_from_empty_set(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + charlie := srv.UserID("charlie") + + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + joinRulePublic := room.CurrentState(spec.MRoomJoinRules, "") + aliceJoin := room.CurrentState(spec.MRoomMember, alice.UserID) + synchronisationEventID := bob.SendEventSynced(t, room.RoomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "can you hear me charlie?", + }, + }) + room.WaiterForEvent(synchronisationEventID).Waitf(t, 5*time.Second, "failed to see synchronisation event, is federation working?") + + // Alice makes the room invite-only then leaves + joinRuleInviteOnlyEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomJoinRules, + StateKey: b.Ptr(""), + Sender: alice.UserID, + Content: map[string]interface{}{ + "join_rule": "invite", + }, + }) + room.WaiterForEvent(joinRuleInviteOnlyEventID).Waitf(t, 5*time.Second, "failed to see invite join rule event") + + alice.MustLeaveRoom(t, roomID) + // Wait for Charlie to see it + time.Sleep(time.Second) + aliceLeaveEvent := room.CurrentState(spec.MRoomMember, alice.UserID) + if membership, err := aliceLeaveEvent.Membership(); err != nil || membership != spec.Leave { + ct.Fatalf(t, "failed to see Alice leave the room, alice event is %s", string(aliceLeaveEvent.JSON())) + } + + // Now only Bob (server under test) and Charlie (Complement server) are left in the room. + // Charlie is going to send an event with unknown prev_event, causing /get_missing_events + // until eventually /state_ids is hit. When it is, we'll return incorrect room state, claiming + // that the current join rule is public, not invite. This will cause the join rules to get conflicted + // and replayed. V2 would base the checks off the unconflicted state, and since all servers agree + // that Alice=leave it would start like that, making the join rule transitions invalid and causing the + // room to have no join rule at all. V2.1 fixes this by loading the auth_events of the event being replayed + // which correctly has Alice joined. Alice isn't automatically re-joined to the room though because the + // last step of the algorithm is to apply the unconflicted state on top of the resolved conflicts, without + // any extra checks. + + // We don't know how far back server impls will go, so let's use 250 as a large enough value. + charlieEvents := make([]gomatrixserverlib.PDU, 250) + eventIDToIndex := make(map[string]int) + for i := range charlieEvents { + ev := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &charlie, + Sender: charlie, + Content: map[string]interface{}{ + "membership": spec.Join, + "displayname": fmt.Sprintf("Charlie %d", i), + }, + }) + room.AddEvent(ev) + charlieEvents[i] = ev + eventIDToIndex[ev.EventID()] = i + } + + seenStateIDs := helpers.NewWaiter() + // process requests to walk back through charlie's display name changes + srv.Mux().HandleFunc( + "/_matrix/federation/v1/get_missing_events/{roomID}", + srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, charlieEvents)), + ) + + getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { + t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + // Find the correct state for charlie + // we want to return the state before the requested event hence -1 + stateEvents = append(stateEvents, charlieEvents[eventIDToIndex[atEventID]-1]) + // charlie's auth events are everything prior to this + authChain = append(authChain, charlieEvents[:eventIDToIndex[atEventID]-1]...) + + stateEvents = append(stateEvents, + room.CurrentState(spec.MRoomCreate, ""), + room.CurrentState(spec.MRoomPowerLevels, ""), + room.CurrentState(spec.MRoomMember, bob.UserID), + joinRulePublic, // This is wrong, and should be invite. + aliceLeaveEvent, + ) + authChain = append(authChain, aliceJoin) + return + } + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state_ids/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChainIDs []string `json:"auth_chain_ids"` + PDUIDs []string `json:"pdu_ids"` + }{ + AuthChainIDs: asEventIDs(authChain), + PDUIDs: asEventIDs(stateEvents), + }, + } + }), + ) + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` + PDUs gomatrixserverlib.EventJSONs `json:"pdus"` + }{ + AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), + PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), + }, + } + }), + ) + + // Send the last event to trigger /get_missing_events and /state_ids + finalEvent := charlieEvents[len(charlieEvents)-1] + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ + finalEvent.JSON(), + }, nil) + + seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") + + // wait until bob sees the final event + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) + + // the join rules should be `invite`. + // Servers which do not implement v2.1 will get a HTTP 404 here. + content := bob.MustGetStateEventContent(t, room.RoomID, spec.MRoomJoinRules, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("join_rule", "invite")) +} + +func TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // needs additional fixes + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + creator := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "creator"}) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"}) + charlie := srv.UserID("charlie") + eve := srv.UserID("eve") + zara := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "zara"}) + + // We play every event from Problem B from the MSC except for Eve's events. We separate out + // Alice and Creator roles for combined MSC compatibility. + roomID := creator.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + creator.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + }, + }, + }) + alice.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + firstPowerLevelEvent := room.CurrentState(spec.MRoomPowerLevels, "") + alice.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + bob.UserID: 50, + }, + }, + }) + pl3EventID := bob.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: map[string]any{ + "users": map[string]any{ + alice.UserID: 100, + bob.UserID: 50, + charlie: 50, + }, + }, + }) + zara.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) + + // Now Eve will join from the third PL event + eveJoinEvent := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &eve, + Sender: eve, + Content: map[string]interface{}{ + "membership": spec.Join, + }, + PrevEvents: []string{ + pl3EventID, + }, + }) + room.AddEvent(eveJoinEvent) + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{eveJoinEvent.JSON()}, nil) + aliceSince := alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, eveJoinEvent.EventID())) + + // Now change Eve's display name many times to force partial synchronisation + + // We don't know how far back server impls will go, so let's use 250 as a large enough value. + eveEvents := make([]gomatrixserverlib.PDU, 250) + eventIDToIndex := make(map[string]int) + prevEvents := eveJoinEvent.EventID() + for i := range eveEvents { + ev := srv.MustCreateEvent(t, room, federation.Event{ + Type: spec.MRoomMember, + StateKey: &eve, + Sender: eve, + Content: map[string]interface{}{ + "membership": spec.Join, + "displayname": fmt.Sprintf("Eve %d", i), + }, + PrevEvents: []string{prevEvents}, + }) + room.AddEvent(ev) + eveEvents[i] = ev + eventIDToIndex[ev.EventID()] = i + prevEvents = ev.EventID() + } + + seenStateIDs := helpers.NewWaiter() + + // process requests to walk back through eve's display name changes + srv.Mux().HandleFunc( + "/_matrix/federation/v1/get_missing_events/{roomID}", + srv.ValidFederationRequest(t, getMissingEventsHandler(t, room, eventIDToIndex, eveEvents)), + ) + + getIncorrectState := func(atEventID string) (authChain, stateEvents []gomatrixserverlib.PDU) { + t.Logf("calculating state before event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + // Find the correct state for eve + // we want to return the state before the requested event hence -1 + stateEvents = append(stateEvents, eveEvents[eventIDToIndex[atEventID]-1]) + // charlie's auth events are everything prior to this + authChain = append(authChain, eveEvents[:eventIDToIndex[atEventID]-1]...) + + stateEvents = append(stateEvents, + room.CurrentState(spec.MRoomCreate, ""), + room.CurrentState(spec.MRoomMember, alice.UserID), + firstPowerLevelEvent, // This is wrong, and should be the 3rd PL event. + room.CurrentState(spec.MRoomJoinRules, ""), + room.CurrentState(spec.MRoomMember, bob.UserID), + room.CurrentState(spec.MRoomMember, charlie), + ) + authChain = append(authChain, eveJoinEvent) + return + } + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state_ids/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state_ids for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state_ids at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChainIDs []string `json:"auth_chain_ids"` + PDUIDs []string `json:"pdu_ids"` + }{ + AuthChainIDs: asEventIDs(authChain), + PDUIDs: asEventIDs(stateEvents), + }, + } + }), + ) + srv.Mux().HandleFunc( + "/_matrix/federation/v1/state/{roomID}", + srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /state for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + defer seenStateIDs.Finish() + req, err := fr.HTTPRequest() + must.NotError(t, "failed to get fed http reqest", err) + atEventID := req.URL.Query().Get("event_id") + t.Logf("received /state at event %v (i=%v)", atEventID, eventIDToIndex[atEventID]) + authChain, stateEvents := getIncorrectState(atEventID) + return util.JSONResponse{ + Code: 200, + JSON: struct { + AuthChain gomatrixserverlib.EventJSONs `json:"auth_chain"` + PDUs gomatrixserverlib.EventJSONs `json:"pdus"` + }{ + AuthChain: gomatrixserverlib.NewEventJSONsFromEvents(authChain), + PDUs: gomatrixserverlib.NewEventJSONsFromEvents(stateEvents), + }, + } + }), + ) + + // Send the last event to trigger /get_missing_events and /state_ids + finalEvent := eveEvents[len(eveEvents)-1] + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{ + finalEvent.JSON(), + }, nil) + + seenStateIDs.Waitf(t, 5*time.Second, "failed to see a /state{_ids} request") + + // wait until bob sees the final event + alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, client.SyncTimelineHasEventID(room.RoomID, finalEvent.EventID())) + + // the power levels should be the 3rd one (Alice:PL100, Bob:PL50, Charlie:PL59), not the 1st (Alice: PL100). + // Servers which do not implement v2.1 will see the 1st one. + content := alice.MustGetStateEventContent(t, room.RoomID, spec.MRoomPowerLevels, "") + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(alice.UserID), 100)) + // v2 Servers will fail here as bob does not exist in this map because it reset to an earlier event. + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(bob.UserID), 50)) + must.MatchGJSON(t, content, match.JSONKeyEqual("users."+client.GjsonEscape(charlie), 50)) +} + +func getMissingEventsHandler(t ct.TestLike, room *federation.ServerRoom, eventIDToIndex map[string]int, events []gomatrixserverlib.PDU) func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + return func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != room.RoomID { + t.Errorf("Received /get_missing_events for the wrong room: %s", room.RoomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", + } + } + latestEventID := gjson.ParseBytes(fr.Content()).Get("latest_events").Array()[0].Str + j, ok := eventIDToIndex[latestEventID] + if !ok { + ct.Fatalf(t, "received /get_missing_events with latest=%s which is unknown", latestEventID) + } + t.Logf("received /get_missing_events with latest=%s (i=%d)", latestEventID, j) + // return 10 earlier events. + slice := events[j-10 : j] + for _, ev := range slice { + t.Logf("/get_missing_events returning %v (i=%v)", ev.EventID(), eventIDToIndex[ev.EventID()]) + } + return util.JSONResponse{ + Code: 200, + JSON: struct { + Events gomatrixserverlib.EventJSONs `json:"events"` + }{ + Events: gomatrixserverlib.NewEventJSONsFromEvents(slice), + }, + } + } +} + +func asEventIDs(pdus []gomatrixserverlib.PDU) []string { + eventIDs := make([]string, len(pdus)) + for i := range pdus { + eventIDs[i] = pdus[i].EventID() + } + return eventIDs +} + +func TestMSC4311FullCreateEventOnStrippedState(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet + deployment := complement.Deploy(t, 2) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) + remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + for _, target := range []*client.CSAPI{local, remote} { + t.Logf("checking %s", target.UserID) + alice.MustInviteRoom(t, roomID, target.UserID) + resp, _ := target.MustSync(t, client.SyncReq{}) + inviteState := resp.Get( + fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)), + ) + must.NotEqual(t, len(inviteState.Array()), 0, "no events in invite_state") + // find the create event + found := false + for _, ev := range inviteState.Array() { + if ev.Get("type").Str == spec.MRoomCreate { + found = true + // we should have extra fields + must.MatchGJSON(t, ev, + match.JSONKeyPresent("origin_server_ts"), + ) + } + } + if !found { + ct.Errorf(t, "failed to find create event in invite_state") + } + } + +}