Compare commits
740 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb9e258468 | |||
| 974723ceef | |||
| 9b8dcf53ed | |||
| 324af3ee67 | |||
| ec6c0946d4 | |||
| e5f480b032 | |||
| 6bf4ed8672 | |||
| c18d691ef5 | |||
| 97cf73bc52 | |||
| aa25103665 | |||
| 858db67778 | |||
| e230abee45 | |||
| 8c16d69f3c | |||
| 55b9116c99 | |||
| 3a5d66057e | |||
| 3f7af189e4 | |||
| 16ddcb0ed0 | |||
| 9e35b8dd0a | |||
| bed787b749 | |||
| d260b8be56 | |||
| 97991dad02 | |||
| b8c19c47ab | |||
| 1476ffbd15 | |||
| 62f0a65472 | |||
| 2ef7ae7661 | |||
| 61c0a49971 | |||
| 2172f28888 | |||
| 2e9b34e0c3 | |||
| 5a782b7377 | |||
| 54bc807056 | |||
| 9e07710d80 | |||
| e9ed91d800 | |||
| 88ba4fad71 | |||
| 21b3471453 | |||
| 0ada9803ab | |||
| 1744f0e97b | |||
| fd0c4a7f56 | |||
| 615f7f9e72 | |||
| 77259e81c9 | |||
| 2193cd9d1c | |||
| 6d28154dcd | |||
| 83d447adfe | |||
| 73c9f4e322 | |||
| e6fa4cdb3c | |||
| a04653a72c | |||
| 5f9341f39c | |||
| 906946c419 | |||
| 4397b9d640 | |||
| 90da2cf439 | |||
| 6edd45787b | |||
| 84444ec11e | |||
| 0e95df5dba | |||
| 29b815b678 | |||
| 0cf056958b | |||
| 79d4113a6b | |||
| 8a80886358 | |||
| de7959de6c | |||
| 533c21a515 | |||
| 6b018b6927 | |||
| 38c3abb364 | |||
| a47f319665 | |||
| ecef9fd755 | |||
| 7dffd8ffd3 | |||
| 66492e7ba8 | |||
| 43b2404865 | |||
| fed9910fa1 | |||
| f77662406c | |||
| 8cc0cf1a70 | |||
| dfa2429094 | |||
| 3e2460707c | |||
| 706c084fa7 | |||
| eb7faa6c07 | |||
| d45a0b894a | |||
| 102739e0fb | |||
| 0d7e4a0fa5 | |||
| d4628e78d4 | |||
| 0b193f4665 | |||
| 8ef2e848b9 | |||
| d92936fba5 | |||
| f005984df3 | |||
| 13fec49e74 | |||
| 008294cfc6 | |||
| b05f933d83 | |||
| b186d79dde | |||
| e82b5fe1db | |||
| 9602aa88ea | |||
| 0fb3dc1b13 | |||
| aeede332be | |||
| b052950a19 | |||
| 1cb5fff5a1 | |||
| 01226e41d9 | |||
| e3919fd93b | |||
| 3b88ea19b7 | |||
| dcf26f3e48 | |||
| 3385adf5f6 | |||
| 9db6ce107a | |||
| d5b22e1deb | |||
| a5e606a1e7 | |||
| f2471b6dbd | |||
| dcf71e0c8f | |||
| 77267e393c | |||
| 1fdc0af5b7 | |||
| d2b782a2f5 | |||
| 5df4ebaada | |||
| e68a1471c1 | |||
| e42dd74426 | |||
| 2751e191d3 | |||
| b5b86bf1b5 | |||
| 4990bf5ca0 | |||
| b8fa030d5d | |||
| b606d1e54b | |||
| cd7c519dc4 | |||
| 30dd28960c | |||
| 5b635df08d | |||
| 592c497902 | |||
| 8e3f2f3262 | |||
| 5751df1288 | |||
| 40a71101e2 | |||
| 3f095caf2d | |||
| 12a94bdd94 | |||
| 1c1ac137d3 | |||
| 89cabc4912 | |||
| 5be4548b3d | |||
| 09de76bd43 | |||
| 3a694f4998 | |||
| 3a8a1389f5 | |||
| c271e1533a | |||
| 722debe8f9 | |||
| 5165899e82 | |||
| 1828826661 | |||
| 24cee68fa2 | |||
| e645af1fc5 | |||
| de64779c27 | |||
| acbcb4658a | |||
| 815484b543 | |||
| 5a3d1a2a67 | |||
| 18626169e4 | |||
| e4a9f958a0 | |||
| ff29de743c | |||
| 5a68861418 | |||
| e285932776 | |||
| 2af0706b16 | |||
| 4382d2a425 | |||
| 9de4a057df | |||
| b703d4a2cc | |||
| d1dec4cd08 | |||
| 326a13bcfe | |||
| e8fb47fdca | |||
| bd66e3859d | |||
| 96e484a3fe | |||
| 3e646bdfa0 | |||
| 48c4127035 | |||
| f16a6bc654 | |||
| f884c78579 | |||
| 3c59476cf7 | |||
| c8f6c4dd0d | |||
| e8c89e9977 | |||
| df78d7cf67 | |||
| 80fec814a2 | |||
| 8b9672ba43 | |||
| ca00094e67 | |||
| 9c6d5a6c55 | |||
| b77fe465f7 | |||
| 8d93f49443 | |||
| f03fbed668 | |||
| c84efb57ef | |||
| 49f11578f7 | |||
| 8df4be0939 | |||
| 80cdbe1058 | |||
| 9c62d15447 | |||
| afc70528cc | |||
| f938d10f7b | |||
| 22f0b781ea | |||
| 1bae10c4b2 | |||
| 9b5b533c6f | |||
| c425945353 | |||
| 0545f6df09 | |||
| d14fc426e6 | |||
| 9f30defcd1 | |||
| 9b175a4985 | |||
| 826ea5bc58 | |||
| 7b316613f6 | |||
| 387b3485ae | |||
| 9f6073478f | |||
| b47c87f909 | |||
| c66850e897 | |||
| 2766146c49 | |||
| 61497c9a8f | |||
| 5981feeb44 | |||
| 51218ddc1d | |||
| b907433d38 | |||
| 32c0b81332 | |||
| 634b8ebbb4 | |||
| 4ab8066e1f | |||
| 13dccb3d71 | |||
| ef1f5bf232 | |||
| a03e3dd501 | |||
| 3cfad3cdeb | |||
| 60c715d5df | |||
| c2942ddbc7 | |||
| 2d7fdde7ed | |||
| cf34e90cb4 | |||
| f0fa4d2cc8 | |||
| c52e4b6329 | |||
| 9451b55985 | |||
| f41fa84e72 | |||
| 3c4134537a | |||
| 39714bfe6f | |||
| f5f6100b1e | |||
| ecd700a36e | |||
| ea7042efb9 | |||
| 04a6c4e6c4 | |||
| 0329824cab | |||
| 3351c4f57a | |||
| e70a1a1eff | |||
| 1a5af9d8e3 | |||
| 258f157ebc | |||
| dfb079a76f | |||
| 858155e0ef | |||
| 946a1cef0f | |||
| 6c1fdbb7e9 | |||
| 8cd7c96496 | |||
| cc9d530bcf | |||
| cabf6da6a7 | |||
| 895a82efcd | |||
| d4414341d6 | |||
| f173014fc0 | |||
| 81cb44db7f | |||
| 71f9b25db7 | |||
| 9c5c7ddb17 | |||
| feb424b0a9 | |||
| 648a1a09b1 | |||
| 56c5375bbd | |||
| b29e1e9d21 | |||
| 7ade461a4c | |||
| b5414ea914 | |||
| 711bf4710d | |||
| 48b60bb885 | |||
| 7ed14dc749 | |||
| 26736b6bb1 | |||
| 056aae823d | |||
| d1bfdca0c9 | |||
| c0577c29c4 | |||
| a2d1dee0a1 | |||
| d7bcdff29b | |||
| ed71cdeccd | |||
| 729f924de1 | |||
| 4732098731 | |||
| a7d503a8ac | |||
| 063d69eff1 | |||
| 614f446361 | |||
| b62fac9dad | |||
| ff07cb642f | |||
| fba3cf2756 | |||
| 5973c66726 | |||
| b22fc1f9d9 | |||
| 3f48a954d8 | |||
| b5d544df68 | |||
| cd26ba67d4 | |||
| 488390365a | |||
| b7b1129478 | |||
| 0fa9528a3f | |||
| a7b1dcaf95 | |||
| bb5bccbf78 | |||
| ece3ccb958 | |||
| a769cf88f7 | |||
| 8b0f1a0c59 | |||
| 978e748246 | |||
| 32a5cc4728 | |||
| d580f56c0b | |||
| 789458e0d4 | |||
| 8d14d45272 | |||
| 2ec1fa6605 | |||
| f15d682938 | |||
| 21a10a2d14 | |||
| fc02e550bd | |||
| 78637a0689 | |||
| 4ca882fcd4 | |||
| 13ee0eb7f5 | |||
| cb018dfc80 | |||
| 7574dacdb3 | |||
| 0c417b7c32 | |||
| daf845d7bd | |||
| 52792ec89b | |||
| 6dc4a62e8c | |||
| 1cd8ea61ea | |||
| 0c5eb277e4 | |||
| d459a91af3 | |||
| 18722d0031 | |||
| 5119934268 | |||
| 077da23d08 | |||
| 083b4cb17e | |||
| 4316009401 | |||
| 72f3c360b6 | |||
| fcbc195fbe | |||
| af38021d28 | |||
| 5e8cb9fa18 | |||
| 6ef9f6c55e | |||
| e6a3b0ebc0 | |||
| aaae55736f | |||
| 9e586ab634 | |||
| 7ff44d4a50 | |||
| 63abd00ca7 | |||
| 40f2579158 | |||
| ceb2a57feb | |||
| 90e8336797 | |||
| 73ca9c9ed2 | |||
| cc065c2772 | |||
| 028be0fee2 | |||
| d4500da59a | |||
| b6aef6772e | |||
| 49696cecbd | |||
| 83cb52c89c | |||
| e82bae2c4d | |||
| ee2b0204aa | |||
| 1ec7670f6a | |||
| 8ab2e10471 | |||
| 8ff8685ae5 | |||
| f3772cdf82 | |||
| ee2f1cdfd4 | |||
| 0de73a0b3e | |||
| be742e811c | |||
| ca9263fa64 | |||
| 9f619be08d | |||
| 7792254c12 | |||
| fff41f1f27 | |||
| 3e0f9f582e | |||
| 47ba8cfa24 | |||
| 54bb4c8011 | |||
| 71e763263e | |||
| 2ebcda2a55 | |||
| e10db6f7e9 | |||
| 1e041a2957 | |||
| 1631d6f3c0 | |||
| 3d86821258 | |||
| 261bc81554 | |||
| 8f701f43fb | |||
| 4bdb9111dd | |||
| 93e2135223 | |||
| 56cb05aac0 | |||
| 38d5b202c9 | |||
| cfffa9c518 | |||
| 73dbd709d8 | |||
| eef67e2c03 | |||
| e3498f0668 | |||
| f04c147faa | |||
| 33bbd45f1e | |||
| fd91a534c7 | |||
| c2afc357b9 | |||
| 1d3f67f2ce | |||
| 514e4f07f1 | |||
| fbb1c4b2bd | |||
| 63dea599b1 | |||
| 743ba5f050 | |||
| cb180b4195 | |||
| c805b7e29d | |||
| 8f6814450b | |||
| 37e8391cde | |||
| 90234402a7 | |||
| 8ecf603d73 | |||
| af243581ff | |||
| 01afed9ff9 | |||
| 2687bb37fb | |||
| 65b1a10803 | |||
| 9d230ef0d6 | |||
| a03438f2af | |||
| c622e9260f | |||
| 8bf53d6f90 | |||
| 8c30a3b0df | |||
| c61d53eed0 | |||
| 95f7d1d347 | |||
| 72d70bb929 | |||
| 4f67e59692 | |||
| d40d5c8a39 | |||
| 87398ac555 | |||
| de3d5ead42 | |||
| 1e1b571b28 | |||
| f400a7b1b2 | |||
| a0bcb5777f | |||
| f8a625eddb | |||
| 69a2a15b95 | |||
| b9d0596dd7 | |||
| 72af8c193c | |||
| ed8c326856 | |||
| f5bf6b1be6 | |||
| 0e19f8dc69 | |||
| 6049c0bf37 | |||
| a102253f30 | |||
| f71d86f005 | |||
| 70e34ffb76 | |||
| 91aa7b26e6 | |||
| 7d45947fb3 | |||
| 6e174328e8 | |||
| 5fb97fcce4 | |||
| 3d1a450129 | |||
| a58c5aacdf | |||
| d7e165a279 | |||
| a57ee803f1 | |||
| 170a52b09f | |||
| 2be5889d18 | |||
| ca6b574bee | |||
| 57b0172a2d | |||
| 53260ee25d | |||
| 964281322f | |||
| 7c3f483396 | |||
| 59784aa9fe | |||
| 72a2b6d571 | |||
| 5854af0eae | |||
| acd3d3a804 | |||
| 1b8c04a430 | |||
| 378b73f8b8 | |||
| c482a6ab15 | |||
| 2daa429b77 | |||
| 6ebbc15359 | |||
| 9a840d484c | |||
| e89467c9fb | |||
| 0b396c005c | |||
| d05313f95e | |||
| 3f97853011 | |||
| 2a5c4b1edf | |||
| 65a3c6707c | |||
| 942477f0bf | |||
| 1770b3131a | |||
| 41d3ffdab9 | |||
| 78aa6cb62b | |||
| b56b8040e6 | |||
| d1cf98b177 | |||
| 5fc6b3ed17 | |||
| 6e41533fed | |||
| bc76532bd5 | |||
| 62f1dd79bc | |||
| fd3f53e814 | |||
| 798ac7b94c | |||
| eb0c0f7b93 | |||
| f03293f53d | |||
| 7d062387b7 | |||
| 9acf3b18ca | |||
| b41d067c94 | |||
| c8503b3120 | |||
| da03c3b529 | |||
| d48b19e052 | |||
| 6861c67f56 | |||
| c87048bd9f | |||
| 02269f33b7 | |||
| 7f46ae7b97 | |||
| 2ab3566f95 | |||
| 9a504af18e | |||
| 8b50986906 | |||
| f9a222ecea | |||
| 5b5a3d8b5e | |||
| 037cbdd214 | |||
| 0349411e6d | |||
| d7b75e4b9e | |||
| 254f043ab0 | |||
| 5f3e115545 | |||
| fc55c4c72a | |||
| f795577e14 | |||
| f12cee984a | |||
| c3b4572841 | |||
| 40fe159c10 | |||
| ddecc87947 | |||
| 23837266fc | |||
| 3c9ca8c373 | |||
| 7f2a4c2568 | |||
| 2ad647a73c | |||
| 26663e67fd | |||
| 0cfc67c679 | |||
| 7faba5c2f0 | |||
| 1d9250b277 | |||
| 08054c1d6d | |||
| 333872e878 | |||
| 913cd257f4 | |||
| 69f7789c40 | |||
| e79ef1f33a | |||
| fb8f61a5ec | |||
| 9b1f2a1d11 | |||
| 686216fb75 | |||
| f4b83e1a27 | |||
| 97f21b6635 | |||
| 87641a6803 | |||
| 7e4331172a | |||
| a976080d1b | |||
| bcf3bba44e | |||
| 54ac36d424 | |||
| e84c90dbbc | |||
| 4424438658 | |||
| 13d95c8219 | |||
| e119dc4e89 | |||
| b4cdc5a923 | |||
| a82e22b5de | |||
| c894d09d8c | |||
| 585ce07260 | |||
| 8cbbdaa239 | |||
| cd526a254d | |||
| e782a2afa3 | |||
| 565339b1fd | |||
| 493203050a | |||
| 41782c4593 | |||
| 86256a4e74 | |||
| 933a0c9909 | |||
| c8a4d9b88a | |||
| 437128d11b | |||
| c18d09fd22 | |||
| 2a5e5e6a59 | |||
| 68e354752b | |||
| d80b7499fd | |||
| 9c8093eb3e | |||
| aec1c11037 | |||
| e2e9986059 | |||
| d70ffdbc02 | |||
| 0f1f1db3d2 | |||
| 77b91a45cb | |||
| fe6add9396 | |||
| 21cc9c3d8a | |||
| 2d59c4647d | |||
| 1f0c6a6dc9 | |||
| c9b502fb0e | |||
| 330fbaccfc | |||
| 937f370655 | |||
| a8ad3ed26d | |||
| decac58a18 | |||
| 1a91ba59a6 | |||
| 89df43a975 | |||
| ad98706db4 | |||
| 195d1730bd | |||
| db4bd907f8 | |||
| cdd7dbbb2b | |||
| 108f157324 | |||
| 8da39ec8f4 | |||
| 182534288c | |||
| f81b7e5e6f | |||
| 6dda9e532d | |||
| 5e17626fe0 | |||
| abc9c9dcb0 | |||
| f346fcb056 | |||
| c67325ba07 | |||
| a063ae8ce7 | |||
| f9e5535492 | |||
| 015d9c5c4f | |||
| b6d40078d9 | |||
| b8a8f4850a | |||
| 1cc23d789c | |||
| 5cf0bb46a4 | |||
| 16672b3d0c | |||
| f61db81961 | |||
| e2a694115f | |||
| 71cf812d24 | |||
| 8a3d7d5671 | |||
| 2a363598dd | |||
| 05bf6428bc | |||
| e492a44dde | |||
| 44d2e47f96 | |||
| 31459a5d63 | |||
| 8f5db463e7 | |||
| 4e8affafcc | |||
| 2800681bb1 | |||
| b2a9e6f12f | |||
| fbd2c97f87 | |||
| 6c6304a620 | |||
| 0c1d5f6b25 | |||
| 1c26dc0233 | |||
| a163a202e7 | |||
| e15cf9976f | |||
| bdc3926417 | |||
| 4f918f684e | |||
| c142232f4d | |||
| c9bc20aa4d | |||
| 5c0cb3a536 | |||
| 415576d0a0 | |||
| 4f9fad66e4 | |||
| ebd9854980 | |||
| cb2fab64d8 | |||
| f446b49e49 | |||
| 22f5e41058 | |||
| fee5a006f1 | |||
| ef51ee28fd | |||
| cb61345780 | |||
| a18d4e226e | |||
| b09b33eb4c | |||
| 5fedc06d7c | |||
| d8c9f6db33 | |||
| 0af5fa0328 | |||
| b328b72cd5 | |||
| ce2a9d7036 | |||
| 66ae985af5 | |||
| 1828e2849c | |||
| b4f8b0fe4f | |||
| 70656e954f | |||
| 40a4c8d954 | |||
| 7ed787b86a | |||
| 1f58ee7f2c | |||
| 2e28e9117a | |||
| a58a36e062 | |||
| 6cf6a0c522 | |||
| 02aa3edda4 | |||
| e04ea02c62 | |||
| 64197bf4db | |||
| c309fe6942 | |||
| 8408f36c12 | |||
| dcd8f91e02 | |||
| cabe14d7e2 | |||
| c019f2bb19 | |||
| baeb4acddf | |||
| 4a6e9a0f8f | |||
| ea5ce8d1d8 | |||
| 41854918a5 | |||
| 495642d041 | |||
| c7210b9e9d | |||
| 83563c7a01 | |||
| 2fcc4811dd | |||
| c392bc455d | |||
| b8711f15fd | |||
| 81f3aef960 | |||
| d6b8332567 | |||
| 85b34b46c5 | |||
| 4179f2978d | |||
| 97df6db49c | |||
| 3e693fab23 | |||
| 1ee487a2ff | |||
| b76e7ca782 | |||
| ddce1bcd28 | |||
| a34d06c7c2 | |||
| 7b10fa367d | |||
| 7f5d7091de | |||
| 96f673ae92 | |||
| 79faee7a67 | |||
| 89d2984432 | |||
| bc78784688 | |||
| eb058edb1b | |||
| 4847d78b42 | |||
| 94f1eda830 | |||
| bc2a182ee9 | |||
| 789aec732a | |||
| 3246114772 | |||
| 39cf1863f1 | |||
| aa1e118f18 | |||
| c10152e098 | |||
| 6b7efbcd91 | |||
| d23c3cb8b2 | |||
| 9e37980e2d | |||
| de176dbd66 | |||
| ac10b40f67 | |||
| f35298a326 | |||
| d7bf0f85c0 | |||
| 1d87f5b163 | |||
| 3e97067b3e | |||
| 3ce582d004 | |||
| 3e48c76a77 | |||
| 8e29f8ead0 | |||
| 185ded4ebc | |||
| 3564a3546f | |||
| c6090325b3 | |||
| 999e355136 | |||
| 7de4164444 | |||
| e2ce379b56 | |||
| 424212cd65 | |||
| c7c16256df | |||
| 8a4c95ee72 | |||
| c3d422f5fb | |||
| fdb80ad259 | |||
| 981acf0044 | |||
| f00f70bfb8 | |||
| 12cc7be31c | |||
| 628bcbf33a | |||
| c4ca0b2e07 | |||
| d7442147b9 | |||
| b1566ee540 | |||
| ca98d9ff11 | |||
| bba4a35665 | |||
| 896f6227a0 | |||
| 4d10cf3074 | |||
| bb23df9423 | |||
| d02559cf3c | |||
| ec6272aa3d | |||
| 695b773f8b | |||
| 030abe1563 | |||
| 22f10f71b8 | |||
| 9ca3e7272e | |||
| 64119ef915 | |||
| 9ac7165e99 | |||
| 6168cedf32 | |||
| 7c34deecb6 | |||
| cef5507ab1 | |||
| 9b372d23ca | |||
| e9fef19c8f | |||
| 9ebc91aa5a | |||
| fcb75d547e | |||
| 33c9af952e | |||
| 3b66b28e71 | |||
| 7d37bb1edb | |||
| 0f717a9306 | |||
| ce776b9989 | |||
| ff1b0e51ea | |||
| 21e66a5c34 | |||
| aead401005 | |||
| af9525ed5f | |||
| 1ebcac37cc | |||
| 51a4cc5e18 | |||
| 48baa6315c | |||
| efc87d8084 | |||
| aab873fc58 | |||
| 52ed04c825 | |||
| fdd1428e19 | |||
| ec2405ac99 | |||
| b31712f2ff | |||
| 61e2606bc4 | |||
| 45f6c5b079 | |||
| b83c372848 | |||
| 6d58a54039 | |||
| 3872c5f099 | |||
| 4f86eee250 | |||
| 618242ef3c | |||
| 96ee5b1256 | |||
| 8af0ff111b | |||
| a04800f030 | |||
| 4683fbe848 | |||
| c973b26fa2 | |||
| 7b96c730b8 | |||
| f8bf6083de | |||
| 447319737a | |||
| 39e127b4e3 | |||
| 15ef8fabb7 | |||
| df42014ef5 | |||
| b765b18381 | |||
| b2a10e6db3 | |||
| a0aa5074ed | |||
| 70a033c2fd | |||
| fcf12b49e3 | |||
| 11ac3d9e58 | |||
| 071d5e71e4 | |||
| 9841f92415 | |||
| ed91bd9c11 | |||
| c953fc9fb7 | |||
| bf78a64d82 | |||
| 9a98e8008f | |||
| ad8bb5d2cd | |||
| 9a731cdf4f | |||
| d1ede036e2 | |||
| d3f08fec03 | |||
| 3a1897629a |
+1
-1
@@ -23,4 +23,4 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
+15
-1
@@ -1,6 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
@@ -60,6 +63,17 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"jest/no-disabled-tests": "off",
|
||||
// Also treat "oldBackendOnly" as a test function.
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -38,4 +38,5 @@ cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||
|
||||
# prettier
|
||||
349c2c2587c2885bb69eda4aa078b5383724cf5e
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
- uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# cypress tests (with access to repo secrets)
|
||||
|
||||
name: matrix-react-sdk Cypress End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build downstream artifacts"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
cypress:
|
||||
name: Cypress
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.77.1
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
secrets:
|
||||
# secrets are not automatically shared with called workflows, so share the cypress dashboard key, and the Kiwi login details
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
TCMS_USERNAME: ${{ secrets.TCMS_USERNAME }}
|
||||
TCMS_PASSWORD: ${{ secrets.TCMS_PASSWORD }}
|
||||
with:
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
rust-crypto: true
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v1
|
||||
uses: matrix-org/netlify-pr-preview@v2
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Build downstream artifacts
|
||||
on:
|
||||
# We only want the Rust Crypto Cypress tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume 4 concurrent runners.
|
||||
# Anyone working on Rust Crypto is able to run the tests locally if required.
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
# For now at least, we don't run this or the cypress-tests against pushes
|
||||
# to develop or master.
|
||||
#
|
||||
# Note that if we later choose to do so, we'll need to find a way to stop
|
||||
# the results in Cypress Cloud from clobbering those from the 'develop'
|
||||
# branch of matrix-react-sdk.
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.77.1
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
uses: peter-evans/repository-dispatch@26b39ed245ab8f31526069329e112ab2fb224588 # v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -2,23 +2,20 @@ name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_call:
|
||||
inputs:
|
||||
labels:
|
||||
type: string
|
||||
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
required: false
|
||||
description: "No longer used, uses allchange logic now, will be removed at a later date"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
@@ -42,7 +39,7 @@ jobs:
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v2
|
||||
uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
@@ -20,22 +20,20 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
uses: JS-DevTools/npm-publish@5a85faf05d2ade2d5b6682bfe5359915d5159c6c # v2.2.1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
tag: next
|
||||
ignore-scripts: false
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: github.event.release.prerelease == false
|
||||
run: |
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
if: github.event.release.prerelease == false && steps.npm-publish.outputs.id
|
||||
run: npm dist-tag add "$release" latest
|
||||
env:
|
||||
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
|
||||
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.version }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.id }}
|
||||
|
||||
@@ -11,45 +11,39 @@ jobs:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: _docs
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
- name: 🔨 Install symlinks
|
||||
run: |
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y symlinks
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
- name: 📖 Generate docs
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
VERSION="${tag#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/_docs/ $VERSION
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
yarn tpv purge --yes --out _docs --stale --major 10
|
||||
yarn gendoc
|
||||
symlinks -rc _docs
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
git add . --all
|
||||
git commit -m "Update docs"
|
||||
git push
|
||||
working-directory: _docs
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.3
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.5
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,6 +11,7 @@ jobs:
|
||||
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
|
||||
@@ -19,7 +20,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
@@ -35,6 +36,7 @@ jobs:
|
||||
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
needs: prepare
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
@@ -63,7 +65,14 @@ jobs:
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
run: "yarn run gendoc --treatWarningsAsErrors"
|
||||
|
||||
# Upload artifact duplicates symlink contents so we do this to save 75% space
|
||||
- name: Flatten symlink and write _redirects
|
||||
run: |
|
||||
find _docs -mindepth 1 -maxdepth 1 ! -type f ! -name stable -printf '/%f/* /stable/:splat\n' > _docs/_redirects
|
||||
find _docs -mindepth 1 -maxdepth 1 -type l -delete
|
||||
find _docs -mindepth 1 -maxdepth 1 -type d -execdir mv {} stable \; -quit
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
+50
-13
@@ -1,11 +1,15 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||
jobs:
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})"
|
||||
@@ -14,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [browserify, integ, unit]
|
||||
node: [16, 18, latest]
|
||||
node: [18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@@ -34,28 +38,61 @@ jobs:
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
uses: SimenB/github-actions-cpu-cores@410541432439795d30db6501fb1d8178eb41e502 # v1
|
||||
|
||||
- name: Run tests with coverage and metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
./spec/${{ matrix.specs }}
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- name: Run tests with coverage
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
# Hook for branch protection to skip downstream testing outside of merge queues
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
description: SonarCloud skipped
|
||||
context: SonarCloud Code Analysis
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
@@ -31,8 +31,8 @@ jobs:
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
run: gh pr merge --merge --auto "$PR_NUMBER"
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
|
||||
@@ -19,3 +19,4 @@ out
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -24,3 +24,6 @@ out
|
||||
|
||||
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||
/CHANGELOG.md
|
||||
|
||||
# This file is also autogenerated
|
||||
/spec/test-utils/test-data/index.ts
|
||||
|
||||
+349
-2
@@ -1,8 +1,355 @@
|
||||
Changes in [23.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0-rc.1) (2022-12-14)
|
||||
============================================================================================================
|
||||
Changes in [28.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.0.0) (2023-08-29)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Set minimum supported Matrix 1.1 version (drop legacy r0 versions) ([\#3007](https://github.com/matrix-org/matrix-js-sdk/pull/3007)). Fixes vector-im/element-web#16876.
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* ElementR: Add `CryptoApi.requestVerificationDM` ([\#3643](https://github.com/matrix-org/matrix-js-sdk/pull/3643)). Contributed by @florianduros.
|
||||
* Implement `CryptoApi.checkKeyBackupAndEnable` ([\#3633](https://github.com/matrix-org/matrix-js-sdk/pull/3633)). Fixes vector-im/crypto-internal#111 and vector-im/crypto-internal#112.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* ElementR: Process all verification events, not just requests ([\#3650](https://github.com/matrix-org/matrix-js-sdk/pull/3650)). Contributed by @florianduros.
|
||||
|
||||
Changes in [27.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.2.0) (2023-08-15)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* Allow knocking rooms ([\#3647](https://github.com/matrix-org/matrix-js-sdk/pull/3647)). Contributed by @charlynguyen.
|
||||
* Bump pagination limit to account for threaded events ([\#3638](https://github.com/matrix-org/matrix-js-sdk/pull/3638)).
|
||||
* ElementR: Add `CryptoApi.findVerificationRequestDMInProgress` ([\#3601](https://github.com/matrix-org/matrix-js-sdk/pull/3601)). Contributed by @florianduros.
|
||||
* Export more into the public interface ([\#3614](https://github.com/matrix-org/matrix-js-sdk/pull/3614)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong handling of encrypted rooms when loading them from sync accumulator ([\#3640](https://github.com/matrix-org/matrix-js-sdk/pull/3640)). Fixes vector-im/element-web#25803.
|
||||
* Skip processing thread roots and fetching threads list when support is disabled ([\#3642](https://github.com/matrix-org/matrix-js-sdk/pull/3642)).
|
||||
* Ensure we don't overinflate the total notification count ([\#3634](https://github.com/matrix-org/matrix-js-sdk/pull/3634)). Fixes vector-im/element-web#25803.
|
||||
|
||||
Changes in [27.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.1.0) (2023-08-01)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* ElementR: Add `CryptoApi.getCrossSigningKeyId` ([\#3619](https://github.com/matrix-org/matrix-js-sdk/pull/3619)). Contributed by @florianduros.
|
||||
* ElementR: Stub `CheckOwnCrossSigningTrust`, import cross signing keys and verify local device in `bootstrapCrossSigning` ([\#3608](https://github.com/matrix-org/matrix-js-sdk/pull/3608)). Contributed by @florianduros.
|
||||
* Specify /preview_url requests as low priority ([\#3609](https://github.com/matrix-org/matrix-js-sdk/pull/3609)). Fixes vector-im/element-web#7292.
|
||||
* Element-R: support for displaying QR codes during verification ([\#3588](https://github.com/matrix-org/matrix-js-sdk/pull/3588)). Fixes vector-im/crypto-internal#124.
|
||||
* Add support for scanning QR codes during verification, with Rust crypto ([\#3565](https://github.com/matrix-org/matrix-js-sdk/pull/3565)).
|
||||
* Add methods to influence set_presence on /sync API calls ([\#3578](https://github.com/matrix-org/matrix-js-sdk/pull/3578)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix threads ending up with chunks of their timelines missing ([\#3618](https://github.com/matrix-org/matrix-js-sdk/pull/3618)). Fixes vector-im/element-web#24466.
|
||||
* Ensure we do not clobber a newer RR with an older unthreaded one ([\#3617](https://github.com/matrix-org/matrix-js-sdk/pull/3617)). Fixes vector-im/element-web#25806.
|
||||
* Fix registration check your emails stage regression ([\#3616](https://github.com/matrix-org/matrix-js-sdk/pull/3616)).
|
||||
* Fix how `Room::eventShouldLiveIn` handles replies to unknown parents ([\#3615](https://github.com/matrix-org/matrix-js-sdk/pull/3615)). Fixes vector-im/element-web#22603.
|
||||
* Only send threaded read receipts if threads support is enabled ([\#3612](https://github.com/matrix-org/matrix-js-sdk/pull/3612)).
|
||||
* ElementR: Fix `userId` parameter usage in `CryptoApi#getVerificationRequestsToDeviceInProgress` ([\#3611](https://github.com/matrix-org/matrix-js-sdk/pull/3611)). Contributed by @florianduros.
|
||||
* Fix edge cases around non-thread relations to thread roots and read receipts ([\#3607](https://github.com/matrix-org/matrix-js-sdk/pull/3607)).
|
||||
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
|
||||
* Export typed event emitter key types ([\#3597](https://github.com/matrix-org/matrix-js-sdk/pull/3597)). Fixes #3506.
|
||||
* Element-R: ensure that `userHasCrossSigningKeys` uses up-to-date data ([\#3599](https://github.com/matrix-org/matrix-js-sdk/pull/3599)). Fixes vector-im/element-web#25773.
|
||||
* Fix sending `auth: null` due to broken types around UIA ([\#3594](https://github.com/matrix-org/matrix-js-sdk/pull/3594)).
|
||||
|
||||
Changes in [27.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.0.0) (2023-07-18)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Drop support for Node 16 ([\#3533](https://github.com/matrix-org/matrix-js-sdk/pull/3533)).
|
||||
* Improve types around login, registration, UIA and identity servers ([\#3537](https://github.com/matrix-org/matrix-js-sdk/pull/3537)).
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
* Simplify `MatrixClient::setPowerLevel` API ([\#3570](https://github.com/matrix-org/matrix-js-sdk/pull/3570)). Fixes vector-im/element-web#13900 and #1844.
|
||||
* Deprecate `VerificationRequest.getQRCodeBytes` and replace it with the asynchronous `generateQRCode`. ([\#3562](https://github.com/matrix-org/matrix-js-sdk/pull/3562)).
|
||||
* Deprecate `VerificationRequest.beginKeyVerification()` in favour of `VerificationRequest.startVerification()`. ([\#3528](https://github.com/matrix-org/matrix-js-sdk/pull/3528)).
|
||||
* Deprecate `Crypto.VerificationRequest` application event, replacing it with `Crypto.VerificationRequestReceived`. ([\#3514](https://github.com/matrix-org/matrix-js-sdk/pull/3514)).
|
||||
|
||||
## ✨ Features
|
||||
* Throw saner error when peeking has its room pulled out from under it ([\#3577](https://github.com/matrix-org/matrix-js-sdk/pull/3577)). Fixes vector-im/element-web#18679.
|
||||
* OIDC: Log in ([\#3554](https://github.com/matrix-org/matrix-js-sdk/pull/3554)). Contributed by @kerryarchibald.
|
||||
* Prevent threads code from making identical simultaneous API hits ([\#3541](https://github.com/matrix-org/matrix-js-sdk/pull/3541)). Fixes vector-im/element-web#25395.
|
||||
* Update IUnsigned type to be extensible ([\#3547](https://github.com/matrix-org/matrix-js-sdk/pull/3547)).
|
||||
* add stop() api to BackupManager for clean shutdown ([\#3553](https://github.com/matrix-org/matrix-js-sdk/pull/3553)).
|
||||
* Log the message ID of any undecryptable to-device messages ([\#3543](https://github.com/matrix-org/matrix-js-sdk/pull/3543)).
|
||||
* Ignore thread relations on state events for consistency with edits ([\#3540](https://github.com/matrix-org/matrix-js-sdk/pull/3540)).
|
||||
* OIDC: validate id token ([\#3531](https://github.com/matrix-org/matrix-js-sdk/pull/3531)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
|
||||
* Fix `TypedEventEmitter::removeAllListeners(void)` not working ([\#3561](https://github.com/matrix-org/matrix-js-sdk/pull/3561)).
|
||||
* Don't allow Olm unwedging rate-limiting to race ([\#3549](https://github.com/matrix-org/matrix-js-sdk/pull/3549)). Fixes vector-im/element-web#25716.
|
||||
* Fix an instance of failed to decrypt error when an in flight `/keys/query` fails. ([\#3486](https://github.com/matrix-org/matrix-js-sdk/pull/3486)).
|
||||
* Use the right anchor emoji for SAS verification ([\#3534](https://github.com/matrix-org/matrix-js-sdk/pull/3534)).
|
||||
* fix a bug which caused the wrong emoji to be shown during SAS device verification. ([\#3523](https://github.com/matrix-org/matrix-js-sdk/pull/3523)).
|
||||
|
||||
Changes in [26.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.2.0) (2023-07-04)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. ([\#3189](https://github.com/matrix-org/matrix-js-sdk/issues/3189)).
|
||||
* ElementR: Add `CryptoApi#bootstrapSecretStorage` ([\#3483](https://github.com/matrix-org/matrix-js-sdk/pull/3483)). Contributed by @florianduros.
|
||||
* Deprecate `MatrixClient.findVerificationRequestDMInProgress`, `MatrixClient.getVerificationRequestsToDeviceInProgress`, and `MatrixClient.requestVerification`, in favour of methods in `CryptoApi`. ([\#3474](https://github.com/matrix-org/matrix-js-sdk/pull/3474)).
|
||||
* Introduce a new `Crypto.VerificationRequest` interface, and deprecate direct access to the old `VerificationRequest` class. Also deprecate some related classes that were exported from `src/crypto/verification/request/VerificationRequest` ([\#3449](https://github.com/matrix-org/matrix-js-sdk/pull/3449)).
|
||||
|
||||
## ✨ Features
|
||||
* OIDC: navigate to authorization endpoint ([\#3499](https://github.com/matrix-org/matrix-js-sdk/pull/3499)). Contributed by @kerryarchibald.
|
||||
* Support for interactive device verification in Element-R. ([\#3505](https://github.com/matrix-org/matrix-js-sdk/pull/3505)).
|
||||
* Support for interactive device verification in Element-R. ([\#3508](https://github.com/matrix-org/matrix-js-sdk/pull/3508)).
|
||||
* Support for interactive device verification in Element-R. ([\#3490](https://github.com/matrix-org/matrix-js-sdk/pull/3490)). Fixes vector-im/element-web#25316.
|
||||
* Element-R: Store cross signing keys in secret storage ([\#3498](https://github.com/matrix-org/matrix-js-sdk/pull/3498)). Contributed by @florianduros.
|
||||
* OIDC: add dynamic client registration util function ([\#3481](https://github.com/matrix-org/matrix-js-sdk/pull/3481)). Contributed by @kerryarchibald.
|
||||
* Add getLastUnthreadedReceiptFor utility to Thread delegating to the underlying Room ([\#3493](https://github.com/matrix-org/matrix-js-sdk/pull/3493)).
|
||||
* ElementR: Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation ([\#3472](https://github.com/matrix-org/matrix-js-sdk/pull/3472)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Aggregate relations regardless of whether event fits into the timeline ([\#3496](https://github.com/matrix-org/matrix-js-sdk/pull/3496)). Fixes vector-im/element-web#25596.
|
||||
* Fix bug where switching media caused media in subsequent calls to fail ([\#3489](https://github.com/matrix-org/matrix-js-sdk/pull/3489)).
|
||||
* Fix: remove polls from room state on redaction ([\#3475](https://github.com/matrix-org/matrix-js-sdk/pull/3475)). Fixes vector-im/element-web#25573. Contributed by @kerryarchibald.
|
||||
* Fix export type `GeneratedSecretStorageKey` ([\#3479](https://github.com/matrix-org/matrix-js-sdk/pull/3479)). Contributed by @florianduros.
|
||||
* Close IDB database before deleting it to prevent spurious unexpected close errors ([\#3478](https://github.com/matrix-org/matrix-js-sdk/pull/3478)). Fixes vector-im/element-web#25597.
|
||||
|
||||
Changes in [26.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.1.0) (2023-06-20)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Introduce a new `Crypto.Verifier` interface, and deprecate direct access to `VerificationBase`, `SAS` and `ReciprocateQRCode` ([\#3414](https://github.com/matrix-org/matrix-js-sdk/pull/3414)).
|
||||
|
||||
## ✨ Features
|
||||
* Add `rust-crypto#isCrossSigningReady` implementation ([\#3462](https://github.com/matrix-org/matrix-js-sdk/pull/3462)). Contributed by @florianduros.
|
||||
* OIDC: Validate `m.authentication` configuration ([\#3419](https://github.com/matrix-org/matrix-js-sdk/pull/3419)). Contributed by @kerryarchibald.
|
||||
* ElementR: Add `CryptoApi.getCrossSigningStatus` ([\#3452](https://github.com/matrix-org/matrix-js-sdk/pull/3452)). Contributed by @florianduros.
|
||||
* Extend stats summary with call device and user count based on room state ([\#3424](https://github.com/matrix-org/matrix-js-sdk/pull/3424)). Contributed by @toger5.
|
||||
* Update MSC3912 implementation to use `with_rel_type` instead of `with_relations` ([\#3420](https://github.com/matrix-org/matrix-js-sdk/pull/3420)).
|
||||
* Export thread-related types from SDK ([\#3447](https://github.com/matrix-org/matrix-js-sdk/pull/3447)). Contributed by @stas-demydiuk.
|
||||
* Use correct /v3 prefix for /refresh ([\#3016](https://github.com/matrix-org/matrix-js-sdk/pull/3016)). Contributed by @davidisaaclee.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix thread list being ordered based on all updates ([\#3458](https://github.com/matrix-org/matrix-js-sdk/pull/3458)). Fixes vector-im/element-web#25522.
|
||||
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
|
||||
* use cli.canSupport to determine intentional mentions support ([\#3445](https://github.com/matrix-org/matrix-js-sdk/pull/3445)). Fixes vector-im/element-web#25497. Contributed by @kerryarchibald.
|
||||
* Make sliding sync linearize processing of sync requests ([\#3442](https://github.com/matrix-org/matrix-js-sdk/pull/3442)).
|
||||
* Fix edge cases around 2nd order relations and threads ([\#3437](https://github.com/matrix-org/matrix-js-sdk/pull/3437)).
|
||||
|
||||
Changes in [26.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.1) (2023-06-09)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
|
||||
|
||||
Changes in [26.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.0) (2023-06-06)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Ensure we do not add relations to the wrong timeline ([\#3427](https://github.com/matrix-org/matrix-js-sdk/pull/3427)). Fixes vector-im/element-web#25450 and vector-im/element-web#25494.
|
||||
* Deprecate `QrCodeEvent`, `SasEvent` and `VerificationEvent` ([\#3386](https://github.com/matrix-org/matrix-js-sdk/pull/3386)).
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Move crypto classes into a separate namespace ([\#3385](https://github.com/matrix-org/matrix-js-sdk/pull/3385)).
|
||||
|
||||
## ✨ Features
|
||||
* Mention deno support in the README ([\#3417](https://github.com/matrix-org/matrix-js-sdk/pull/3417)). Contributed by @sigmaSd.
|
||||
* Mark room version 10 as safe ([\#3425](https://github.com/matrix-org/matrix-js-sdk/pull/3425)).
|
||||
* Prioritise entirely supported flows for UIA ([\#3402](https://github.com/matrix-org/matrix-js-sdk/pull/3402)).
|
||||
* Add methods to terminate idb worker ([\#3362](https://github.com/matrix-org/matrix-js-sdk/pull/3362)).
|
||||
* Total summary count ([\#3351](https://github.com/matrix-org/matrix-js-sdk/pull/3351)). Contributed by @toger5.
|
||||
* Audio concealment ([\#3349](https://github.com/matrix-org/matrix-js-sdk/pull/3349)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Correctly accumulate sync summaries. ([\#3366](https://github.com/matrix-org/matrix-js-sdk/pull/3366)). Fixes vector-im/element-web#23345.
|
||||
* Keep measuring a call feed's volume after a stream replacement ([\#3361](https://github.com/matrix-org/matrix-js-sdk/pull/3361)). Fixes vector-im/element-call#1051.
|
||||
* Element-R: Avoid uploading a new fallback key at every `/sync` ([\#3338](https://github.com/matrix-org/matrix-js-sdk/pull/3338)). Fixes vector-im/element-web#25215.
|
||||
* Accumulate receipts for the main thread and unthreaded separately ([\#3339](https://github.com/matrix-org/matrix-js-sdk/pull/3339)). Fixes vector-im/element-web#24629.
|
||||
* Remove spec non-compliant extended glob format ([\#3423](https://github.com/matrix-org/matrix-js-sdk/pull/3423)). Fixes vector-im/element-web#25474.
|
||||
* Fix bug where original event was inserted into timeline instead of the edit event ([\#3398](https://github.com/matrix-org/matrix-js-sdk/pull/3398)). Contributed by @andybalaam.
|
||||
* Only add a local receipt if it's after an existing receipt ([\#3399](https://github.com/matrix-org/matrix-js-sdk/pull/3399)). Contributed by @andybalaam.
|
||||
* Attempt a potential workaround for stuck notifs ([\#3384](https://github.com/matrix-org/matrix-js-sdk/pull/3384)). Fixes vector-im/element-web#25406. Contributed by @andybalaam.
|
||||
* Fix verification bug with `pendingEventOrdering: "chronological"` ([\#3382](https://github.com/matrix-org/matrix-js-sdk/pull/3382)).
|
||||
|
||||
Changes in [25.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.1) (2023-05-16)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Rebuild to fix packaging glitch in 25.1.0. Fixes #3363
|
||||
|
||||
Changes in [25.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.0) (2023-05-09)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate MatrixClient::resolveRoomAlias ([\#3316](https://github.com/matrix-org/matrix-js-sdk/pull/3316)).
|
||||
|
||||
## ✨ Features
|
||||
* add client method to remove pusher ([\#3324](https://github.com/matrix-org/matrix-js-sdk/pull/3324)). Contributed by @kerryarchibald.
|
||||
* Implement MSC 3981 ([\#3248](https://github.com/matrix-org/matrix-js-sdk/pull/3248)). Fixes vector-im/element-web#25021. Contributed by @justjanne.
|
||||
* Added `Room.getLastLiveEvent` and `Room.getLastThread`. Deprecated `Room.lastThread` in favour of `Room.getLastThread`. ([\#3321](https://github.com/matrix-org/matrix-js-sdk/pull/3321)).
|
||||
* Element-R: wire up device lists ([\#3272](https://github.com/matrix-org/matrix-js-sdk/pull/3272)). Contributed by @florianduros.
|
||||
* Node 20 support ([\#3302](https://github.com/matrix-org/matrix-js-sdk/pull/3302)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix racing between one-time-keys processing and sync ([\#3327](https://github.com/matrix-org/matrix-js-sdk/pull/3327)). Fixes vector-im/element-web#25214. Contributed by @florianduros.
|
||||
* Fix lack of media when a user reconnects ([\#3318](https://github.com/matrix-org/matrix-js-sdk/pull/3318)).
|
||||
* Fix TimelineWindow getEvents exploding if no neigbouring timeline ([\#3285](https://github.com/matrix-org/matrix-js-sdk/pull/3285)). Fixes vector-im/element-web#25104.
|
||||
|
||||
Changes in [25.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.0.0) (2023-04-25)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Change `Store.save()` to return a `Promise` ([\#3221](https://github.com/matrix-org/matrix-js-sdk/pull/3221)). Contributed by @texuf.
|
||||
|
||||
## ✨ Features
|
||||
* Add typedoc-plugin-mdn-links ([\#3292](https://github.com/matrix-org/matrix-js-sdk/pull/3292)).
|
||||
* Annotate events with executed push rule ([\#3284](https://github.com/matrix-org/matrix-js-sdk/pull/3284)). Contributed by @kerryarchibald.
|
||||
* Element-R: pass device list change notifications into rust crypto-sdk ([\#3254](https://github.com/matrix-org/matrix-js-sdk/pull/3254)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
|
||||
* Support for MSC3882 revision 1 ([\#3228](https://github.com/matrix-org/matrix-js-sdk/pull/3228)). Contributed by @hughns.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix screen sharing on Firefox 113 ([\#3282](https://github.com/matrix-org/matrix-js-sdk/pull/3282)). Contributed by @tulir.
|
||||
* Retry processing potential poll events after decryption ([\#3246](https://github.com/matrix-org/matrix-js-sdk/pull/3246)). Fixes vector-im/element-web#24568.
|
||||
* Element-R: handle events which arrive before their keys ([\#3230](https://github.com/matrix-org/matrix-js-sdk/pull/3230)). Fixes vector-im/element-web#24489.
|
||||
|
||||
Changes in [24.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.1.0) (2023-04-11)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Allow via_servers property in findPredecessor (update to MSC3946) ([\#3240](https://github.com/matrix-org/matrix-js-sdk/pull/3240)). Contributed by @andybalaam.
|
||||
* Fire `closed` event when IndexedDB closes unexpectedly ([\#3218](https://github.com/matrix-org/matrix-js-sdk/pull/3218)).
|
||||
* Implement MSC3952: intentional mentions ([\#3092](https://github.com/matrix-org/matrix-js-sdk/pull/3092)). Fixes vector-im/element-web#24376.
|
||||
* Send one time key count and unused fallback keys for rust-crypto ([\#3215](https://github.com/matrix-org/matrix-js-sdk/pull/3215)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
|
||||
* Improve `processBeaconEvents` hotpath ([\#3200](https://github.com/matrix-org/matrix-js-sdk/pull/3200)).
|
||||
* Implement MSC3966: a push rule condition to check if an array contains a value ([\#3180](https://github.com/matrix-org/matrix-js-sdk/pull/3180)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* indexddb-local-backend - return the current sync to database promise … ([\#3222](https://github.com/matrix-org/matrix-js-sdk/pull/3222)). Contributed by @texuf.
|
||||
* Revert "Add the call object to Call events" ([\#3236](https://github.com/matrix-org/matrix-js-sdk/pull/3236)).
|
||||
* Handle group call redaction ([\#3231](https://github.com/matrix-org/matrix-js-sdk/pull/3231)). Fixes vector-im/voip-internal#128.
|
||||
* Stop doing O(n^2) work to find event's home (`eventShouldLiveIn`) ([\#3227](https://github.com/matrix-org/matrix-js-sdk/pull/3227)). Contributed by @jryans.
|
||||
* Fix bug where video would not unmute if it started muted ([\#3213](https://github.com/matrix-org/matrix-js-sdk/pull/3213)). Fixes vector-im/element-call#925.
|
||||
* Fixes to event encryption in the Rust Crypto implementation ([\#3202](https://github.com/matrix-org/matrix-js-sdk/pull/3202)).
|
||||
|
||||
Changes in [24.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.0.0) (2023-03-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🔒 Security
|
||||
* Fixes for [CVE-2023-28427](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28427) / GHSA-mwq8-fjpf-c2gr
|
||||
|
||||
Changes in [23.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.5.0) (2023-03-15)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Implement MSC3758: a push rule condition to match event properties exactly ([\#3179](https://github.com/matrix-org/matrix-js-sdk/pull/3179)).
|
||||
* Enable group calls without video and audio track by configuration of MatrixClient ([\#3162](https://github.com/matrix-org/matrix-js-sdk/pull/3162)). Contributed by @EnricoSchw.
|
||||
* Updates to protocol used for Sign in with QR code ([\#3155](https://github.com/matrix-org/matrix-js-sdk/pull/3155)). Contributed by @hughns.
|
||||
* Implement MSC3873 to handle escaped dots in push rule keys ([\#3134](https://github.com/matrix-org/matrix-js-sdk/pull/3134)). Fixes undefined/matrix-js-sdk#1454.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
|
||||
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
|
||||
|
||||
Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0) (2023-02-28)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." ([\#3167](https://github.com/matrix-org/matrix-js-sdk/pull/3167)). Contributed by @florianduros.
|
||||
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
|
||||
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Better type guard parseTopicContent ([\#3165](https://github.com/matrix-org/matrix-js-sdk/pull/3165)). Fixes matrix-org/element-web-rageshakes#20177 and matrix-org/element-web-rageshakes#20178.
|
||||
* Fix a bug where events in encrypted rooms would sometimes erroneously increment the total unread counter after being processed locally. ([\#3130](https://github.com/matrix-org/matrix-js-sdk/pull/3130)). Fixes vector-im/element-web#24448. Contributed by @Half-Shot.
|
||||
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
|
||||
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
|
||||
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
|
||||
|
||||
Changes in [23.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0) (2023-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Element-R: implement encryption of outgoing events ([\#3122](https://github.com/matrix-org/matrix-js-sdk/pull/3122)).
|
||||
* Poll model - page /relations results ([\#3073](https://github.com/matrix-org/matrix-js-sdk/pull/3073)). Contributed by @kerryarchibald.
|
||||
* Poll model - validate end events ([\#3072](https://github.com/matrix-org/matrix-js-sdk/pull/3072)). Contributed by @kerryarchibald.
|
||||
* Handle optional last_known_event_id property in m.predecessor ([\#3119](https://github.com/matrix-org/matrix-js-sdk/pull/3119)). Contributed by @andybalaam.
|
||||
* Add support for stable identifier for fixed MAC in SAS verification ([\#3101](https://github.com/matrix-org/matrix-js-sdk/pull/3101)).
|
||||
* Provide eventId as well as roomId from Room.findPredecessor ([\#3095](https://github.com/matrix-org/matrix-js-sdk/pull/3095)). Contributed by @andybalaam.
|
||||
* MSC3946 Dynamic room predecessors ([\#3042](https://github.com/matrix-org/matrix-js-sdk/pull/3042)). Contributed by @andybalaam.
|
||||
* Poll model ([\#3036](https://github.com/matrix-org/matrix-js-sdk/pull/3036)). Contributed by @kerryarchibald.
|
||||
* Remove video tracks on video mute without renegotiating ([\#3091](https://github.com/matrix-org/matrix-js-sdk/pull/3091)).
|
||||
* Introduces a backwards-compatible API change. `MegolmEncrypter#prepareToEncrypt`'s return type has changed from `void` to `() => void`. ([\#3035](https://github.com/matrix-org/matrix-js-sdk/pull/3035)). Contributed by @clarkf.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
|
||||
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
|
||||
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
|
||||
* Element-R: fix a bug which prevented encryption working after a reload ([\#3126](https://github.com/matrix-org/matrix-js-sdk/pull/3126)).
|
||||
* Element-R: Fix invite processing ([\#3121](https://github.com/matrix-org/matrix-js-sdk/pull/3121)).
|
||||
* Don't throw with no `opponentDeviceInfo` ([\#3107](https://github.com/matrix-org/matrix-js-sdk/pull/3107)).
|
||||
* Remove flaky megolm test ([\#3098](https://github.com/matrix-org/matrix-js-sdk/pull/3098)). Contributed by @clarkf.
|
||||
* Fix "verifyLinks" functionality of getRoomUpgradeHistory ([\#3089](https://github.com/matrix-org/matrix-js-sdk/pull/3089)). Contributed by @andybalaam.
|
||||
|
||||
Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Implement decryption via the rust sdk ([\#3074](https://github.com/matrix-org/matrix-js-sdk/pull/3074)).
|
||||
* Handle edits which are bundled with an event, per MSC3925 ([\#3045](https://github.com/matrix-org/matrix-js-sdk/pull/3045)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Add null check for our own member event ([\#3082](https://github.com/matrix-org/matrix-js-sdk/pull/3082)).
|
||||
* Handle group call getting initialised twice in quick succession ([\#3078](https://github.com/matrix-org/matrix-js-sdk/pull/3078)). Fixes vector-im/element-call#847.
|
||||
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. Contributed by @justjanne.
|
||||
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
|
||||
* Fix spurious "Decryption key withheld" messages ([\#3061](https://github.com/matrix-org/matrix-js-sdk/pull/3061)). Fixes vector-im/element-web#23803.
|
||||
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
|
||||
|
||||
Changes in [23.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.1) (2023-01-20)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix backwards compability for environment not support Array.prototype.at ([\#3080](https://github.com/matrix-org/matrix-js-sdk/pull/3080)).
|
||||
|
||||
Changes in [23.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0) (2023-01-18)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Remove extensible events v1 field population on legacy events ([\#3040](https://github.com/matrix-org/matrix-js-sdk/pull/3040)).
|
||||
|
||||
## ✨ Features
|
||||
* Improve hasUserReadEvent and getUserReadUpTo realibility with threads ([\#3031](https://github.com/matrix-org/matrix-js-sdk/pull/3031)). Fixes vector-im/element-web#24164.
|
||||
* Remove video track when muting video ([\#3028](https://github.com/matrix-org/matrix-js-sdk/pull/3028)). Fixes vector-im/element-call#209.
|
||||
* Make poll start event type available (PSG-962) ([\#3034](https://github.com/matrix-org/matrix-js-sdk/pull/3034)).
|
||||
* Add alt event type matching in Relations model ([\#3018](https://github.com/matrix-org/matrix-js-sdk/pull/3018)).
|
||||
* Remove usage of v1 Identity Server API ([\#3003](https://github.com/matrix-org/matrix-js-sdk/pull/3003)).
|
||||
* Add `device_id` to `/account/whoami` types ([\#3005](https://github.com/matrix-org/matrix-js-sdk/pull/3005)).
|
||||
* Implement MSC3912: Relation-based redactions ([\#2954](https://github.com/matrix-org/matrix-js-sdk/pull/2954)).
|
||||
* Introduce a mechanism for using the rust-crypto-sdk ([\#2969](https://github.com/matrix-org/matrix-js-sdk/pull/2969)).
|
||||
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix threaded cache receipt when event holds multiple receipts ([\#3026](https://github.com/matrix-org/matrix-js-sdk/pull/3026)).
|
||||
* Fix false key requests after verifying new device ([\#3029](https://github.com/matrix-org/matrix-js-sdk/pull/3029)). Fixes vector-im/element-web#24167 and vector-im/element-web#23333.
|
||||
* Avoid triggering decryption errors when decrypting redacted events ([\#3004](https://github.com/matrix-org/matrix-js-sdk/pull/3004)). Fixes vector-im/element-web#24084.
|
||||
* bugfix: upload OTKs in sliding sync mode ([\#3008](https://github.com/matrix-org/matrix-js-sdk/pull/3008)).
|
||||
* Apply edits discovered from sync after thread is initialised ([\#3002](https://github.com/matrix-org/matrix-js-sdk/pull/3002)). Fixes vector-im/element-web#23921.
|
||||
* Sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
|
||||
* Threads are missing from the timeline ([\#2996](https://github.com/matrix-org/matrix-js-sdk/pull/2996)). Fixes vector-im/element-web#24036.
|
||||
* Close all streams when a call ends ([\#2992](https://github.com/matrix-org/matrix-js-sdk/pull/2992)). Fixes vector-im/element-call#742.
|
||||
* Resume to-device message queue after resumed sync ([\#2920](https://github.com/matrix-org/matrix-js-sdk/pull/2920)). Fixes matrix-org/element-web-rageshakes#17170.
|
||||
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
|
||||
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
|
||||
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952.
|
||||
|
||||
Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Process `m.room.encryption` events before emitting `RoomMember` events ([\#2914](https://github.com/matrix-org/matrix-js-sdk/pull/2914)). Fixes vector-im/element-web#23819.
|
||||
* Don't expose `calls` on `GroupCall` ([\#2941](https://github.com/matrix-org/matrix-js-sdk/pull/2941)).
|
||||
|
||||
## ✨ Features
|
||||
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
|
||||
browser or in Node.js.
|
||||
|
||||
#### Minimum Matrix server version: v1.1
|
||||
|
||||
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
|
||||
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
|
||||
is removed in v1.4 then the feature is _eligible_ for removal from the SDK when v1.8 is released. This SDK has no
|
||||
@@ -21,6 +23,8 @@ endpoints from before Matrix 1.1, for example.
|
||||
|
||||
## In a browser
|
||||
|
||||
### Note, the browserify build has been deprecated. Please use a bundler like webpack or vite instead.
|
||||
|
||||
Download the browser version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
`<script>` to your page. There will be a global variable `matrixcs`
|
||||
@@ -46,7 +50,7 @@ if you do not have it already.
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient("https://matrix.org");
|
||||
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
|
||||
client.publicRooms(function (err, data) {
|
||||
console.log("Public Rooms: %s", JSON.stringify(data));
|
||||
});
|
||||
@@ -55,6 +59,8 @@ client.publicRooms(function (err, data) {
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
|
||||
|
||||
To start the client:
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
To try it out, **you must build the SDK first** and then host this folder:
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
$ yarn build
|
||||
$ cd examples/browser
|
||||
$ python -m http.server 8003
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Config } from "jest";
|
||||
import { env } from "process";
|
||||
|
||||
const config: Config = {
|
||||
testEnvironment: "node",
|
||||
testMatch: ["<rootDir>/spec/**/*.spec.{js,ts}"],
|
||||
setupFilesAfterEnv: ["<rootDir>/spec/setupTests.ts"],
|
||||
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
|
||||
coverageReporters: ["text-summary", "lcov"],
|
||||
testResultsProcessor: "@casualbot/jest-sonar-reporter",
|
||||
|
||||
// Always print out a summary if there are any failing tests. Normally
|
||||
// a summary is only printed if there are more than 20 test *suites*.
|
||||
reporters: [["default", { summaryThreshold: 0 }]],
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable the GHA reporter
|
||||
if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
const reporters: Config["reporters"] = [
|
||||
["github-actions", { silent: false }],
|
||||
// as above: always show a summary if there were any failing tests.
|
||||
["summary", { summaryThreshold: 0 }],
|
||||
];
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
reporters.push("<rootDir>/spec/slowReporter.js");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
export default config;
|
||||
+157
-143
@@ -1,147 +1,161 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "23.0.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./lib/browser-index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.ts",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-matrix-org": "^0.9.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "28.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdir dist && BROWSERIFYSWAP_ENV='no-rust-crypto' browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/spec/setupTests.ts"
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.ts",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_browser": "./lib/browser-index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
"release.sh"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^1.2.1",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.5.0",
|
||||
"oidc-client-ts": "^2.2.4",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"browserify": "^17.0.0",
|
||||
"browserify-swap": "^0.2.2",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^46.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^48.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.8",
|
||||
"rimraf": "^5.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.24.0",
|
||||
"typedoc-plugin-coverage": "^2.1.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typedoc-plugin-versions": "^0.2.3",
|
||||
"typedoc-plugin-versions-cli": "^0.1.12",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"browserify-swap",
|
||||
[
|
||||
"babelify",
|
||||
{
|
||||
"sourceMaps": "inline",
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"browserify-swap": {
|
||||
"no-rust-crypto": {
|
||||
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
|
||||
}
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||
for i in main typings
|
||||
for i in main typings browser
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
@@ -21,9 +21,9 @@ if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
+4
-4
@@ -130,7 +130,7 @@ fi
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
@@ -180,11 +180,11 @@ yarn version --no-git-tag-version --new-version "$release"
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
for i in main typings browser
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
yarn install --pure-lockfile
|
||||
yarn install --frozen-lockfile
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
||||
@@ -15,4 +15,4 @@ for line in sys.stdin:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print line
|
||||
print(line)
|
||||
|
||||
+45
-25
@@ -16,26 +16,33 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "./olm-loader";
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
|
||||
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
|
||||
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
||||
import { logger } from "../src/logger";
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { createClient, IStartClientOpts } from "../src/matrix";
|
||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
|
||||
import { IKeyBackupSession } from "../src/crypto/keybackup";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
|
||||
import { ISyncResponder } from "./test-utils/SyncResponder";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
|
||||
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||
*/
|
||||
export class TestClient {
|
||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys?: IDeviceKeys | null;
|
||||
@@ -79,9 +86,12 @@ export class TestClient {
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*/
|
||||
public start(): Promise<void> {
|
||||
public start(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(this + ": starting");
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {});
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {
|
||||
// we have tests that rely on support for lazy-loading members
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
@@ -93,6 +103,8 @@ export class TestClient {
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
|
||||
...opts,
|
||||
});
|
||||
|
||||
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
|
||||
@@ -110,13 +122,12 @@ export class TestClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
* Set up expectations that the client will upload device keys (and possibly one-time keys)
|
||||
*/
|
||||
public expectDeviceKeyUpload() {
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(this + ": received device keys");
|
||||
@@ -124,7 +135,17 @@ export class TestClient {
|
||||
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
|
||||
|
||||
this.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
|
||||
// the first batch of one-time keys may be uploaded at the same time.
|
||||
if (content.one_time_keys) {
|
||||
logger.log(`${this}: received ${Object.keys(content.one_time_keys).length} one-time keys`);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
}
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,21 +212,6 @@ export class TestClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query key backups for a particular session
|
||||
*/
|
||||
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
|
||||
this.httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
encodeUri("/room_keys/keys/$roomId/$sessionId", {
|
||||
$roomId: roomId,
|
||||
$sessionId: sessionId,
|
||||
}),
|
||||
)
|
||||
.respond(status, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
@@ -226,8 +232,22 @@ export class TestClient {
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* Calling this will register a response for `/sync`, and then, in the background, flush a single `/sync` request.
|
||||
* Try calling {@link syncPromise} to wait for the sync to complete.
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
public sendOrQueueSyncResponse(syncResponse: object): void {
|
||||
this.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
this.httpBackend.flush("/sync", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @deprecated: prefer to use {@link #sendOrQueueSyncResponse} followed by {@link syncPromise}.
|
||||
*/
|
||||
public flushSync(): Promise<void> {
|
||||
logger.log(`${this}: flushSync`);
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import {
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
} from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
/**
|
||||
* Integration tests for cross-signing functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
|
||||
// backend. Once we drop support for legacy crypto, it will go away.
|
||||
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
// Encryption key used to encrypt cross signing keys
|
||||
const encryptionKey = new Uint8Array(32);
|
||||
|
||||
/**
|
||||
* Create the {@link CryptoCallbacks}
|
||||
*/
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
return {
|
||||
getSecretStorageKey: (keys, name) => {
|
||||
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
});
|
||||
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
/**
|
||||
* Create cross-signing keys and publish the keys
|
||||
*
|
||||
* @param authDict - The parameters to as the `auth` dict in the key upload request.
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
|
||||
*/
|
||||
async function bootstrapCrossSigning(authDict: AuthDict): Promise<void> {
|
||||
await aliceClient.getCrypto()?.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: (makeRequest) => makeRequest(authDict).then(() => undefined),
|
||||
});
|
||||
}
|
||||
|
||||
describe("bootstrapCrossSigning (before initialsync completes)", () => {
|
||||
it("publishes keys if none were yet published", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// check the cross-signing keys upload
|
||||
expect(fetchMock.called("upload-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
||||
);
|
||||
});
|
||||
|
||||
newBackendOnly("get cross signing keys from secret storage and import them", async () => {
|
||||
// Return public cross signing keys
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
|
||||
const masterKey = await encryptAES(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.master",
|
||||
);
|
||||
const selfSigningKey = await encryptAES(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.self_signing",
|
||||
);
|
||||
const userSigningKey = await encryptAES(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.user_signing",
|
||||
);
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: masterKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: selfSigningKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: userSigningKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.secret_storage.key.key_id",
|
||||
content: {
|
||||
key: "key_id",
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
|
||||
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
|
||||
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
|
||||
);
|
||||
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// Check if the UserTrustStatusChanged event was fired
|
||||
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
|
||||
|
||||
// Expect the signature to be uploaded
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// the device should have a signature with the public self cross signing keys.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCrossSigningStatus()", () => {
|
||||
it("should return correct values without bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
|
||||
// Expect the cross signing keys to be unavailable
|
||||
expect(crossSigningStatus).toStrictEqual({
|
||||
publicKeysOnDevice: false,
|
||||
privateKeysInSecretStorage: false,
|
||||
privateKeysCachedLocally: { masterKey: false, userSigningKey: false, selfSigningKey: false },
|
||||
});
|
||||
});
|
||||
|
||||
it("should return correct values after bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
|
||||
// Expect the cross signing keys to be available
|
||||
expect(crossSigningStatus).toStrictEqual({
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysInSecretStorage: false,
|
||||
privateKeysCachedLocally: { masterKey: true, userSigningKey: true, selfSigningKey: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCrossSigningReady()", () => {
|
||||
it("should return false if cross-signing is not bootstrapped", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true after bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
await bootstrapCrossSigning({ type: "test" });
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCrossSigningKeyId", () => {
|
||||
/**
|
||||
* Intercept /keys/device_signing/upload request and return the cross signing keys
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload
|
||||
*
|
||||
* @returns the cross signing keys
|
||||
*/
|
||||
function awaitCrossSigningKeysUpload() {
|
||||
return new Promise<any>((resolve) => {
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
(url, options) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
resolve(content);
|
||||
return {};
|
||||
},
|
||||
// Override the routes define in `mockSetupCrossSigningRequests`
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("should return the cross signing key id for each cross signing key", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// Intercept cross signing keys upload
|
||||
const crossSigningKeysPromise = awaitCrossSigningKeysUpload();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
// Get the cross signing keys
|
||||
const crossSigningKeys = await crossSigningKeysPromise;
|
||||
|
||||
const getPubKey = (crossSigningKey: any) => Object.values(crossSigningKey!.keys)[0];
|
||||
|
||||
const masterKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId();
|
||||
expect(masterKeyId).toBe(getPubKey(crossSigningKeys.master_key));
|
||||
|
||||
const selfSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.SelfSigning);
|
||||
expect(selfSigningKeyId).toBe(getPubKey(crossSigningKeys.self_signing_key));
|
||||
|
||||
const userSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.UserSigning);
|
||||
expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key));
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,725 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient, TypedEventEmitter } from "../../../src";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */
|
||||
const TEST_HOMESERVER_URL = "https://alice-server.com";
|
||||
|
||||
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
|
||||
|
||||
const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
type: "m.room.encrypted",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: SESSION_ID,
|
||||
ciphertext:
|
||||
"AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
|
||||
"CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
|
||||
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
|
||||
},
|
||||
room_id: "!ROOM:ID",
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
};
|
||||
|
||||
const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
ciphertext:
|
||||
"2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
|
||||
"6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
|
||||
"Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
|
||||
"SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
|
||||
"Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
|
||||
"ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
|
||||
"4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
|
||||
"C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
|
||||
"Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
|
||||
"QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
|
||||
"iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
|
||||
mac: "5lxYBHQU80M",
|
||||
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
|
||||
},
|
||||
};
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
enum MockKeyUploadEvent {
|
||||
KeyUploaded = "KeyUploaded",
|
||||
}
|
||||
|
||||
type MockKeyUploadEventHandlerMap = {
|
||||
[MockKeyUploadEvent.KeyUploaded]: (roomId: string, sessionId: string, backupVersion: string) => void;
|
||||
};
|
||||
|
||||
/*
|
||||
* Test helper. Returns an event emitter that will emit an event every time fetchmock sees a request to backup a key.
|
||||
*/
|
||||
function mockUploadEmitter(
|
||||
expectedVersion: string,
|
||||
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
|
||||
const emitter = new TypedEventEmitter();
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
if (version != expectedVersion) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
current_version: expectedVersion,
|
||||
errcode: "M_WRONG_ROOM_KEYS_VERSION",
|
||||
error: "Wrong backup version.",
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
emitter.emit(MockKeyUploadEvent.KeyUploaded, roomId, sessionId, version);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: count,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
/** an object which intercepts `/sync` requests on the test homeserver */
|
||||
let syncResponder: SyncResponder;
|
||||
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
let e2eKeyReceiver: E2EKeyReceiver;
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
mockInitialApiRequests(TEST_HOMESERVER_URL);
|
||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
|
||||
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
||||
const client = createClient({
|
||||
baseUrl: TEST_HOMESERVER_URL,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
...opts,
|
||||
});
|
||||
await initCrypto(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
oldBackendOnly("Alice checks key backups when receiving a message she can't decrypt", async function () {
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [ENCRYPTED_EVENT],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", CURVE25519_KEY_BACKUP_DATA);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"));
|
||||
|
||||
// start after saving the private key
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
|
||||
// XXX: should we automatically re-check after a device becomes verified?
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.checkKeyBackup();
|
||||
|
||||
// Now, send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.getContent()).toEqual("testytest");
|
||||
});
|
||||
|
||||
describe("backupLoop", () => {
|
||||
it("Alice should upload known keys when backup is enabled", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that signalling is working
|
||||
const remainingZeroPromise = new Promise<void>((resolve, reject) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
|
||||
if (remaining == 0) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
|
||||
const uploadMockEmitter = mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
|
||||
|
||||
const uploadPromises = someRoomKeys.map((data) => {
|
||||
new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (
|
||||
data.room_id == roomId &&
|
||||
data.session_id == sessionId &&
|
||||
version == testData.SIGNED_BACKUP_DATA.version
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
// Wait until all keys are backed up to ensure that when a new key is received the loop is restarted
|
||||
await remainingZeroPromise;
|
||||
|
||||
// A new key import should trigger a new upload.
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const newKeyUploadPromise = new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (
|
||||
newKey.room_id == roomId &&
|
||||
newKey.session_id == sessionId &&
|
||||
version == testData.SIGNED_BACKUP_DATA.version
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
it("Alice should re-upload all keys if a new trusted backup is available", async function () {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that signalling is working
|
||||
const remainingZeroPromise = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
|
||||
if (remaining == 0) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
|
||||
// wait for all keys to be backed up
|
||||
await remainingZeroPromise;
|
||||
|
||||
const newBackupVersion = "2";
|
||||
const uploadMockEmitter = mockUploadEmitter(newBackupVersion);
|
||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
// Let's simulate that a new backup is available by returning error code on key upload
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
// If we import a new key the loop will try to upload to old version, it will
|
||||
// fail then check the current version and switch if trusted
|
||||
const uploadPromises = someRoomKeys.map((data) => {
|
||||
new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (data.room_id == roomId && data.session_id == sessionId && version == newBackupVersion) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const disableOldBackup = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupFailed, (errCode) => {
|
||||
if (errCode == "M_WRONG_ROOM_KEYS_VERSION") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const enableNewBackup = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// A new key import should trigger a new upload.
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const newKeyUploadPromise = new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (newKey.room_id == roomId && newKey.session_id == sessionId && version == newBackupVersion) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await disableOldBackup;
|
||||
await enableNewBackup;
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
it("Backup loop should be resistant to network failures", async function () {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
// on the first key upload attempt, simulate a network failure
|
||||
const failurePromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// kick the import loop off and wait for the failed request
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
jest.runAllTimers();
|
||||
await failurePromise;
|
||||
|
||||
// Fix the endpoint to do successful uploads
|
||||
const successPromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
|
||||
const allKeysUploadedPromise = new Promise((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
|
||||
if (remaining == 0) {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// run the timers, which will make the backup loop redo the request
|
||||
await jest.runAllTimersAsync();
|
||||
await successPromise;
|
||||
await allKeysUploadedPromise;
|
||||
});
|
||||
});
|
||||
|
||||
it("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
// At this point there is no backup
|
||||
let backupStatus: string | null;
|
||||
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
|
||||
expect(backupStatus).toBeNull();
|
||||
|
||||
// Serve a backup with no trusted signature
|
||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const checked = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
||||
expect(checked?.trustInfo?.trusted).toBeFalsy();
|
||||
|
||||
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
|
||||
expect(backupStatus).toBeNull();
|
||||
|
||||
// Add a valid signature to the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
// check that signalling is working
|
||||
const backupPromise = new Promise<void>((resolve, reject) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const validCheck = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(validCheck?.trustInfo?.trusted).toStrictEqual(true);
|
||||
|
||||
await backupPromise;
|
||||
|
||||
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
|
||||
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
describe("isKeyBackupTrusted", () => {
|
||||
it("does not trust a backup signed by an untrusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// download the device list, to match the trusted case
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
|
||||
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
|
||||
});
|
||||
|
||||
it("trusts a backup signed by a trusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
|
||||
expect(result).toEqual({ trusted: true, matchesDecryptionKey: false });
|
||||
});
|
||||
|
||||
it("recognises a backup which matches the decryption key", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
await aliceClient.startClient();
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
);
|
||||
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
|
||||
expect(result).toEqual({ trusted: false, matchesDecryptionKey: true });
|
||||
});
|
||||
|
||||
it("is not fooled by a backup which matches the decryption key but uses a different algorithm", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
await aliceClient.startClient();
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
);
|
||||
|
||||
const backup: KeyBackupInfo = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
backup.algorithm = "m.megolm_backup.v1.aes-hmac-sha2";
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(backup);
|
||||
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkKeyBackupAndEnable", () => {
|
||||
it("enables a backup signed by a trusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.trustInfo).toEqual({ trusted: true, matchesDecryptionKey: false });
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
it("does not enable a backup signed by an untrusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// download the device list, to match the trusted case
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: false });
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
|
||||
it("disables backup when a new untrusted backup is available", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
unsignedBackup.version = "2";
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
|
||||
it("switches backup when a new trusted backup is available", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
const newBackupVersion = "2";
|
||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
|
||||
});
|
||||
|
||||
it("Disables when backup is deleted", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(noResult).toBeNull();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/** make sure that the client knows about the dummy device */
|
||||
async function waitForDeviceList(): Promise<void> {
|
||||
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
|
||||
}
|
||||
});
|
||||
+12
-12
@@ -22,20 +22,20 @@ limitations under the License.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `megolm.spec.js`.
|
||||
* See also `crypto.spec.js`.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "../olm-loader";
|
||||
import "../../olm-loader";
|
||||
|
||||
import type { Session } from "@matrix-org/olm";
|
||||
import { logger } from "../../src/logger";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -472,7 +472,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -483,7 +483,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -545,7 +545,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { createClient } from "../../../src";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
describe("MatrixClient.initRustCrypto", () => {
|
||||
it("should raise if userId or deviceId is unknown", async () => {
|
||||
const unknownUserClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
await expect(() => unknownUserClient.initRustCrypto()).rejects.toThrow("unknown userId");
|
||||
|
||||
const unknownDeviceClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:test",
|
||||
});
|
||||
await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId");
|
||||
});
|
||||
|
||||
it("should create the indexed dbs", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// should have two dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore a second call", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient.clearStores", () => {
|
||||
it("should clear the indexeddbs", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
expect(await indexedDB.databases()).toHaveLength(2);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores();
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function (done) {
|
||||
it("should emit User events", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
@@ -192,10 +192,8 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(function () {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(fired).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit Room events", function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -21,11 +21,13 @@ import {
|
||||
EventStatus,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
@@ -33,6 +35,7 @@ import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/util
|
||||
import { TestClient } from "../TestClient";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -204,7 +207,7 @@ function startClient(httpBackend: HttpBackend, client: MatrixClient) {
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
|
||||
client.startClient();
|
||||
client.startClient({ threadSupport: true });
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
const prom = new Promise<void>((resolve) => {
|
||||
@@ -245,7 +248,7 @@ describe("getEventTimeline support", function () {
|
||||
return startClient(httpBackend, client).then(function () {
|
||||
const room = client.getRoom(roomId)!;
|
||||
const timelineSet = room!.getTimelineSets()[0];
|
||||
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
|
||||
return expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -257,7 +260,18 @@ describe("getEventTimeline support", function () {
|
||||
return startClient(httpBackend, client).then(() => {
|
||||
const room = client.getRoom(roomId)!;
|
||||
const timelineSet = room!.getTimelineSets()[0];
|
||||
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
|
||||
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/context/event`).respond(200, () => ({
|
||||
event: {
|
||||
event_id: "event",
|
||||
},
|
||||
events_after: [],
|
||||
events_before: [],
|
||||
state: [],
|
||||
}));
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, "event")).resolves.toBeTruthy(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -268,7 +282,7 @@ describe("getEventTimeline support", function () {
|
||||
|
||||
return startClient(httpBackend, client).then(function () {
|
||||
const timelineSet = new EventTimelineSet(undefined);
|
||||
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
|
||||
return expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -590,17 +604,11 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
@@ -631,12 +639,6 @@ describe("MatrixClient event timelines", function () {
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
await httpBackend.flushAllExpected();
|
||||
const timelineSet = thread.timelineSet;
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
await flushHttp(emitPromise(thread, ThreadEvent.Update));
|
||||
|
||||
const timeline = await client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
|
||||
@@ -647,7 +649,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -680,7 +682,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -709,7 +711,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -787,7 +789,18 @@ describe("MatrixClient event timelines", function () {
|
||||
return startClient(httpBackend, client).then(() => {
|
||||
const room = client.getRoom(roomId)!;
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(client.getLatestTimeline(timelineSet)).rejects.toBeFalsy();
|
||||
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/context/event`).respond(200, () => ({
|
||||
event: {
|
||||
event_id: "event",
|
||||
},
|
||||
events_after: [],
|
||||
events_before: [],
|
||||
state: [],
|
||||
}));
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, "event")).resolves.toBeTruthy(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1016,6 +1029,263 @@ describe("MatrixClient event timelines", function () {
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create threads for thread roots discovered", function () {
|
||||
const room = client.getRoom(roomId)!;
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id!))
|
||||
.respond(200, function () {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function (req) {
|
||||
const params = req.queryParams!;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual("30");
|
||||
})
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2], THREAD_ROOT],
|
||||
end: "start_token1",
|
||||
};
|
||||
});
|
||||
|
||||
let tl: EventTimeline;
|
||||
return Promise.all([
|
||||
client
|
||||
.getEventTimeline(timelineSet, EVENTS[0].event_id!)
|
||||
.then(function (tl0) {
|
||||
tl = tl0!;
|
||||
return client.paginateEventTimeline(tl, { backwards: true });
|
||||
})
|
||||
.then(function (success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl!.getEvents().length).toEqual(4);
|
||||
expect(tl!.getEvents()[0].event).toEqual(THREAD_ROOT);
|
||||
expect(tl!.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl!.getEvents()[2].event).toEqual(EVENTS[1]);
|
||||
expect(tl!.getEvents()[3].event).toEqual(EVENTS[0]);
|
||||
expect(room.getThreads().map((it) => it.id)).toEqual([THREAD_ROOT.event_id!]);
|
||||
expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("start_token1");
|
||||
expect(tl!.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should ensure thread events are ordered correctly", async () => {
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY2 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 2",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY2.localTimestamp += 1000;
|
||||
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY3 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 3",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY3.localTimestamp += 2000;
|
||||
|
||||
// Test data for the first thread, with the second reply
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
...THREAD_ROOT,
|
||||
unsigned: {
|
||||
...THREAD_ROOT.unsigned,
|
||||
"m.relations": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 3,
|
||||
latest_event: THREAD_REPLY3.event,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
client.fetchRoomEvent = () => Promise.resolve(THREAD_ROOT_UPDATED);
|
||||
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name),
|
||||
)
|
||||
.respond(200, {
|
||||
chunk: [THREAD_REPLY3.event, THREAD_REPLY2.event, THREAD_REPLY],
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD_REPLY.event_id,
|
||||
THREAD_REPLY2.getId(),
|
||||
THREAD_REPLY3.getId(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ensure thread events don't get reordered with recursive relations", async () => {
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY2 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 2",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY2.localTimestamp += 1000;
|
||||
const THREAD_ROOT_REACTION = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.Reaction,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: THREAD_ROOT.event_id!,
|
||||
key: Math.random().toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
THREAD_ROOT_REACTION.localTimestamp += 2000;
|
||||
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY3 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 3",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY3.localTimestamp += 3000;
|
||||
|
||||
// Test data for the first thread, with the second reply
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
...THREAD_ROOT,
|
||||
unsigned: {
|
||||
...THREAD_ROOT.unsigned,
|
||||
"m.relations": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 3,
|
||||
latest_event: THREAD_REPLY3.event,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
client.canSupport.set(Feature.RelationsRecursion, ServerSupport.Stable);
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
client.fetchRoomEvent = () => Promise.resolve(THREAD_ROOT_UPDATED);
|
||||
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({
|
||||
dir: Direction.Backward,
|
||||
limit: 3,
|
||||
recurse: true,
|
||||
}),
|
||||
)
|
||||
.respond(200, {
|
||||
chunk: [THREAD_REPLY3.event, THREAD_ROOT_REACTION, THREAD_REPLY2.event, THREAD_REPLY],
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD_REPLY.event_id,
|
||||
THREAD_REPLY2.getId(),
|
||||
THREAD_REPLY3.getId(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline for thread list timeline", function () {
|
||||
@@ -1062,7 +1332,7 @@ describe("MatrixClient event timelines", function () {
|
||||
request.respond(200, function () {
|
||||
return {
|
||||
original_event: root,
|
||||
chunk: [replies],
|
||||
chunk: replies,
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
@@ -1072,7 +1342,7 @@ describe("MatrixClient event timelines", function () {
|
||||
function respondToContext(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
|
||||
const request = httpBackend.when(
|
||||
"GET",
|
||||
encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", {
|
||||
encodeUri("/_matrix/client/v3/rooms/$roomId/context/$eventId", {
|
||||
$roomId: roomId,
|
||||
$eventId: event.event_id!,
|
||||
}),
|
||||
@@ -1090,7 +1360,7 @@ describe("MatrixClient event timelines", function () {
|
||||
function respondToEvent(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
|
||||
const request = httpBackend.when(
|
||||
"GET",
|
||||
encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", {
|
||||
encodeUri("/_matrix/client/v3/rooms/$roomId/event/$eventId", {
|
||||
$roomId: roomId,
|
||||
$eventId: event.event_id!,
|
||||
}),
|
||||
@@ -1101,7 +1371,7 @@ describe("MatrixClient event timelines", function () {
|
||||
function respondToMessagesRequest(): ExpectedHttpRequest {
|
||||
const request = httpBackend.when(
|
||||
"GET",
|
||||
encodeUri("/_matrix/client/r0/rooms/$roomId/messages", {
|
||||
encodeUri("/_matrix/client/v3/rooms/$roomId/messages", {
|
||||
$roomId: roomId,
|
||||
}),
|
||||
);
|
||||
@@ -1117,7 +1387,7 @@ describe("MatrixClient event timelines", function () {
|
||||
describe("with server compatibility", function () {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1192,7 +1462,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(room.getPendingEvents()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should handle thread updates by reordering the thread list", async () => {
|
||||
it("should handle new thread replies by reordering the thread list", async () => {
|
||||
// Test data for a second thread
|
||||
const THREAD2_ROOT = utils.mkEvent({
|
||||
room: roomId,
|
||||
@@ -1219,7 +1489,7 @@ describe("MatrixClient event timelines", function () {
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply",
|
||||
"body": "thread2 reply",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
@@ -1239,7 +1509,7 @@ describe("MatrixClient event timelines", function () {
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply",
|
||||
"body": "thread reply2",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
@@ -1249,7 +1519,8 @@ describe("MatrixClient event timelines", function () {
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY2.localTimestamp += 1000;
|
||||
// this has to come after THREAD_REPLY which hasn't been instantiated by us
|
||||
THREAD_REPLY2.localTimestamp += 10000000;
|
||||
|
||||
// Test data for the first thread, with the second reply
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
@@ -1275,7 +1546,7 @@ describe("MatrixClient event timelines", function () {
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1309,10 +1580,8 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.NewReply);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
@@ -1322,12 +1591,138 @@ describe("MatrixClient event timelines", function () {
|
||||
THREAD_ROOT.event_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not reorder the thread list on other thread updates", async () => {
|
||||
// Test data for a second thread
|
||||
const THREAD2_ROOT = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "thread root",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"io.element.thread": {
|
||||
//"latest_event": undefined,
|
||||
count: 1,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
event: false,
|
||||
});
|
||||
|
||||
const THREAD2_REPLY = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread2 reply",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: false,
|
||||
});
|
||||
|
||||
// @ts-ignore we know this is a defined path for THREAD ROOT
|
||||
THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY;
|
||||
|
||||
const THREAD_REPLY_REACTION = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.reaction",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: THREAD_REPLY.event_id,
|
||||
key: "🪿",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY_REACTION.localTimestamp += 1000;
|
||||
|
||||
// Modified thread root event containing latest thread reply in its unsigned
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
...THREAD_ROOT,
|
||||
unsigned: {
|
||||
...THREAD_ROOT.unsigned,
|
||||
"m.relations": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 2,
|
||||
latest_event: THREAD_REPLY,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Response with test data for the thread list request
|
||||
const threadsResponse = {
|
||||
chunk: [THREAD2_ROOT, THREAD_ROOT],
|
||||
state: [],
|
||||
next_batch: RANDOM_TOKEN as string | null,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
// Set up room threads
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
respondToThreads(threadsResponse);
|
||||
respondToThreads(threadsResponse);
|
||||
respondToEvent(THREAD_ROOT);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
|
||||
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
|
||||
await flushHttp(room.fetchRoomThreads());
|
||||
const threadIds = room.getThreads().map((thread) => thread.id);
|
||||
expect(threadIds).toContain(THREAD_ROOT.event_id);
|
||||
expect(threadIds).toContain(THREAD2_ROOT.event_id);
|
||||
const [allThreads] = timelineSets!;
|
||||
const timeline = allThreads.getLiveTimeline()!;
|
||||
// Test threads are in chronological order
|
||||
expect(timeline.getEvents().map((it) => it.event.event_id)).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD2_ROOT.event_id,
|
||||
]);
|
||||
|
||||
// Test adding a second event to the first thread
|
||||
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
|
||||
// Test thread order is unchanged
|
||||
expect(timeline!.getEvents().map((it) => it.event.event_id)).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD2_ROOT.event_id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without server compatibility", function () {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
});
|
||||
@@ -1393,7 +1788,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should add lazy loading filter", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
// @ts-ignore
|
||||
@@ -1421,7 +1816,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should correctly pass pagination token", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
|
||||
@@ -1657,73 +2052,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(thread.initialEventsFetched).toBeTruthy();
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward, from: "start_token" }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [],
|
||||
};
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Forward, from: "end_token" }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [THREAD_REPLY],
|
||||
};
|
||||
});
|
||||
|
||||
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
|
||||
const timeline = await client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
@@ -1746,7 +2075,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1756,7 +2085,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in backwards compatible unstable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
|
||||
@@ -1766,7 +2095,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in backwards compatible mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -94,16 +94,16 @@ describe("MatrixClient opts", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should be able to send messages", function (done) {
|
||||
it("should be able to send messages", async () => {
|
||||
const eventId = "$flibble:wibble";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
});
|
||||
|
||||
it("should be able to sync / get new events", async function () {
|
||||
@@ -149,7 +149,7 @@ describe("MatrixClient opts", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function (done) {
|
||||
it("shouldn't retry sending events", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(
|
||||
500,
|
||||
new MatrixError({
|
||||
@@ -157,19 +157,13 @@ describe("MatrixClient opts", function () {
|
||||
error: "Ruh roh",
|
||||
}),
|
||||
);
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(
|
||||
function (res) {
|
||||
expect(false).toBe(true);
|
||||
},
|
||||
function (err) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
done();
|
||||
},
|
||||
);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
|
||||
await expect(
|
||||
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
|
||||
).rejects.toThrow("MatrixError: [500] Unknown message");
|
||||
});
|
||||
|
||||
it("shouldn't queue events", function (done) {
|
||||
it("shouldn't queue events", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "AAA",
|
||||
});
|
||||
@@ -178,30 +172,38 @@ describe("MatrixClient opts", function () {
|
||||
});
|
||||
let sentA = false;
|
||||
let sentB = false;
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
const messageASendPromise = client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
sentA = true;
|
||||
// We expect messageB to be sent before messageA to ensure as we're
|
||||
// testing that there is no queueing that blocks each other
|
||||
expect(sentB).toBe(true);
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
|
||||
const messageBSendPromise = client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
|
||||
sentB = true;
|
||||
// We expect messageB to be sent before messageA to ensure as we're
|
||||
// testing that there is no queueing that blocks each other
|
||||
expect(sentA).toBe(false);
|
||||
});
|
||||
httpBackend.flush("/txn2", 1).then(function () {
|
||||
httpBackend.flush("/txn1", 1).then(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
// Allow messageB to succeed first
|
||||
await httpBackend.flush("/txn2", 1);
|
||||
// Then allow messageA to succeed
|
||||
await httpBackend.flush("/txn1", 1);
|
||||
|
||||
// Now await the message send promises to
|
||||
await messageBSendPromise;
|
||||
await messageASendPromise;
|
||||
});
|
||||
|
||||
it("should be able to send messages", function (done) {
|
||||
it("should be able to send messages", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "foo",
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual("foo");
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
|
||||
expect(res.event_id).toEqual("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,13 +48,13 @@ describe("MatrixClient retrying", function () {
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
it.skip("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
|
||||
xit("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
it.skip("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
it.skip("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
it.skip("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function () {
|
||||
// send a couple of events; the second will be queued
|
||||
@@ -130,7 +130,7 @@ describe("MatrixClient retrying", function () {
|
||||
});
|
||||
|
||||
describe("resending", function () {
|
||||
xit("should be able to resend a NOT_SENT event", function () {});
|
||||
xit("should be able to resend a sent event", function () {});
|
||||
it.skip("should be able to resend a NOT_SENT event", function () {});
|
||||
it.skip("should be able to resend a sent event", function () {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,36 +163,38 @@ describe("MatrixClient room timelines", function () {
|
||||
it(
|
||||
"should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender",
|
||||
function (done) {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
async () => {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
done();
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream",
|
||||
function (done) {
|
||||
async () => {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
@@ -207,28 +209,30 @@ describe("MatrixClient room timelines", function () {
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(function () {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(async () => {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
resolve(null);
|
||||
});
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should be updated correctly when the send request finishes " +
|
||||
"AFTER the event comes down the event stream",
|
||||
function (done) {
|
||||
async () => {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
@@ -243,23 +247,24 @@ describe("MatrixClient room timelines", function () {
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const messageSendPromise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
promise.then(function () {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
await messageSendPromise;
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -279,30 +284,29 @@ describe("MatrixClient room timelines", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" + " of the timeline.", function (done) {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
it("should set Room.oldState.paginationToken to null at the start of the timeline.", async () => {
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function (done) {
|
||||
it("should set the right event.sender values", async () => {
|
||||
// We're aiming for an eventual timeline of:
|
||||
//
|
||||
// 'Old Alice' joined the room
|
||||
@@ -353,15 +357,17 @@ describe("MatrixClient room timelines", function () {
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender?.name).toEqual("Old Alice");
|
||||
@@ -371,17 +377,15 @@ describe("MatrixClient room timelines", function () {
|
||||
expect(newMsg.sender?.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function (done) {
|
||||
it("should add it them to the right place in the timeline", async () => {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
@@ -396,30 +400,30 @@ describe("MatrixClient room timelines", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function (done) {
|
||||
it("should use 'end' as the next pagination token", async () => {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
@@ -429,25 +433,24 @@ describe("MatrixClient room timelines", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client!.scrollback(room, 1).then(function () {
|
||||
await Promise.all([client!.scrollback(room, 1), httpBackend!.flush("/messages", 1)]);
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1).then(function () {
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,11 +36,16 @@ import {
|
||||
NotificationCountType,
|
||||
IEphemeral,
|
||||
Room,
|
||||
IndexedDBStore,
|
||||
RelationType,
|
||||
EventType,
|
||||
} from "../../src";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
|
||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
@@ -81,17 +86,15 @@ describe("MatrixClient syncing", () => {
|
||||
presence: {},
|
||||
};
|
||||
|
||||
it("should /sync after /pushrules and /filter.", (done) => {
|
||||
it("should /sync after /pushrules and /filter.", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => {
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
@@ -102,9 +105,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
@@ -223,8 +224,6 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should honour lazyLoadMembers if user is not a guest", () => {
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
@@ -241,8 +240,6 @@ describe("MatrixClient syncing", () => {
|
||||
it("should not honour lazyLoadMembers if user is a guest", () => {
|
||||
httpBackend!.expectedRequests = [];
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
@@ -724,7 +721,7 @@ describe("MatrixClient syncing", () => {
|
||||
// events that arrive in the incremental sync as if they preceeded the
|
||||
// timeline events, however this breaks peeking, so it's disabled
|
||||
// (see sync.js)
|
||||
xit("should correctly interpret state in incremental sync.", () => {
|
||||
it.skip("should correctly interpret state in incremental sync.", () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
@@ -741,9 +738,9 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
xit("should update power levels for users in a room", () => {});
|
||||
it.skip("should update power levels for users in a room", () => {});
|
||||
|
||||
xit("should update the room topic", () => {});
|
||||
it.skip("should update the room topic", () => {});
|
||||
|
||||
describe("onMarkerStateEvent", () => {
|
||||
const normalMessageEvent = utils.mkMessage({
|
||||
@@ -840,6 +837,7 @@ describe("MatrixClient syncing", () => {
|
||||
roomVersion: "org.matrix.msc2716v3",
|
||||
},
|
||||
].forEach((testMeta) => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(testMeta.label, () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
@@ -1543,30 +1541,135 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("only replays receipts relevant to the current context", async () => {
|
||||
const THREAD_ID = "$unknownthread:localhost";
|
||||
|
||||
const receipt = {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 666, thread_id: THREAD_ID },
|
||||
},
|
||||
},
|
||||
"$otherevent:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 999, thread_id: "$otherthread:localhost" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne].ephemeral.events = [receipt];
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
|
||||
const room = client?.getRoom(roomOne);
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
|
||||
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true);
|
||||
|
||||
const thread = room!.createThread(THREAD_ID, undefined, [], true);
|
||||
|
||||
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false);
|
||||
|
||||
const receipt = thread.getReadReceiptForUserId("@alice:localhost");
|
||||
|
||||
expect(receipt).toStrictEqual({
|
||||
data: {
|
||||
thread_id: "$unknownthread:localhost",
|
||||
ts: 666,
|
||||
},
|
||||
eventId: "$event1:localhost",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
||||
const roomId = "!room123:server";
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [],
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
room: roomId,
|
||||
event: true,
|
||||
skey: "",
|
||||
type: EventType.RoomEncryption,
|
||||
content: {},
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", () => {
|
||||
xit(
|
||||
it.skip(
|
||||
"should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)",
|
||||
() => {},
|
||||
);
|
||||
|
||||
xit("should sync when the user explicitly calls joinRoom", () => {});
|
||||
it.skip("should sync when the user explicitly calls joinRoom", () => {});
|
||||
});
|
||||
|
||||
describe("syncLeftRooms", () => {
|
||||
beforeEach((done) => {
|
||||
beforeEach(async () => {
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
httpBackend!.when("GET", "/sync").respond(200, {});
|
||||
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
await httpBackend!.when("GET", "/sync").respond(200, {});
|
||||
});
|
||||
|
||||
it("should create and use an appropriate filter", () => {
|
||||
@@ -1827,4 +1930,124 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
idbClient.stopClient();
|
||||
idbHttpBackend.stop();
|
||||
});
|
||||
|
||||
it("should query server for which thread a 2nd order relation belongs to and stash in sync accumulator", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
|
||||
async function startClient(client: MatrixClient): Promise<void> {
|
||||
await Promise.all([
|
||||
idbClient.startClient({
|
||||
// Without this all events just go into the main timeline
|
||||
threadSupport: true,
|
||||
}),
|
||||
idbHttpBackend.flushAllExpected(),
|
||||
emitPromise(idbClient, ClientEvent.Room),
|
||||
]);
|
||||
}
|
||||
|
||||
function assertEventsExpected(client: MatrixClient): void {
|
||||
const room = client.getRoom(roomId);
|
||||
const mainTimelineEvents = room!.getLiveTimeline().getEvents();
|
||||
expect(mainTimelineEvents).toHaveLength(1);
|
||||
expect(mainTimelineEvents[0].getContent().body).toEqual("Test");
|
||||
|
||||
const thread = room!.getThread("$someThreadId")!;
|
||||
expect(thread.replayEvents).toHaveLength(1);
|
||||
expect(thread.replayEvents![0].getRelation()!.key).toEqual("🪿");
|
||||
}
|
||||
|
||||
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
let idbHttpBackend = idbTestClient.httpBackend;
|
||||
let idbClient = idbTestClient.client;
|
||||
await idbClient.store.startup();
|
||||
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
const syncRoomSection = {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
prev_batch: "foo",
|
||||
events: [
|
||||
mkMessage({
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
msg: "Test",
|
||||
}),
|
||||
mkEvent({
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: "$someUnknownEvent",
|
||||
key: "🪿",
|
||||
},
|
||||
},
|
||||
type: "m.reaction",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
idbHttpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: syncRoomSection,
|
||||
});
|
||||
idbHttpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/event/%24someUnknownEvent`).respond(
|
||||
200,
|
||||
mkEvent({
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
"body": "Thread response",
|
||||
"m.relates_to": {
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
event_id: "$someThreadId",
|
||||
},
|
||||
},
|
||||
type: "m.room.message",
|
||||
}),
|
||||
);
|
||||
|
||||
await startClient(idbClient);
|
||||
assertEventsExpected(idbClient);
|
||||
|
||||
idbHttpBackend.verifyNoOutstandingExpectation();
|
||||
// Force sync accumulator to persist, reset client, assert it doesn't re-fetch event on next start-up
|
||||
await idbClient.store.save(true);
|
||||
await idbClient.stopClient();
|
||||
await idbClient.store.destroy();
|
||||
await idbHttpBackend.stop();
|
||||
|
||||
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
idbHttpBackend = idbTestClient.httpBackend;
|
||||
idbClient = idbTestClient.client;
|
||||
await idbClient.store.startup();
|
||||
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
idbHttpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await startClient(idbClient);
|
||||
assertEventsExpected(idbClient);
|
||||
|
||||
idbHttpBackend.verifyNoOutstandingExpectation();
|
||||
await idbClient.stopClient();
|
||||
await idbHttpBackend.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import {
|
||||
Category,
|
||||
ClientEvent,
|
||||
EventType,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
NotificationCountType,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { mkThread } from "../test-utils/thread";
|
||||
import { SyncState } from "../../src/sync";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const userA = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
|
||||
const selfUserId = userA;
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTestClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
it("reactions in thread set the correct timeline to unread", async () => {
|
||||
const roomId = "!room:localhost";
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
[Category.Join]: {},
|
||||
[Category.Leave]: {},
|
||||
[Category.Invite]: {},
|
||||
},
|
||||
});
|
||||
client!.startClient({ threadSupport: true });
|
||||
await Promise.all([
|
||||
httpBackend?.flushAllExpected(),
|
||||
new Promise<void>((resolve) => {
|
||||
client!.on(ClientEvent.Sync, (state) => state === SyncState.Syncing && resolve());
|
||||
}),
|
||||
]);
|
||||
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
await room.addLiveEvents([thread.rootEvent]);
|
||||
|
||||
// Initialize read receipt datastructure before testing the reaction
|
||||
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
|
||||
thread.thread.addReceiptToStructure(
|
||||
threadReply.getId()!,
|
||||
ReceiptType.Read,
|
||||
selfUserId,
|
||||
{ thread_id: thread.thread.id, ts: 1 },
|
||||
false,
|
||||
);
|
||||
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
|
||||
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(threadReply.getId());
|
||||
|
||||
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
|
||||
let lastEvent: MatrixEvent | null = null;
|
||||
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
lastEvent = event as MatrixEvent;
|
||||
return { event_id: reactionEventId };
|
||||
});
|
||||
|
||||
await client!.sendEvent(roomId, EventType.Reaction, {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: threadReply.getId(),
|
||||
key: "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastEvent!.getId()).toEqual(reactionEventId);
|
||||
room.handleRemoteEcho(new MatrixEvent(lastEvent!.event), lastEvent!);
|
||||
|
||||
// Our ideal state after this is the following:
|
||||
//
|
||||
// Room: [synthetic: threadroot, actual: threadroot]
|
||||
// Thread: [synthetic: threadreaction, actual: threadreply]
|
||||
//
|
||||
// The reaction and reply are both in the thread, and their receipts should be isolated to the thread.
|
||||
// The reaction has not been acknowledged in a dedicated read receipt message, so only the synthetic receipt
|
||||
// should be updated.
|
||||
|
||||
// Ensure the synthetic receipt for the room has not been updated
|
||||
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
|
||||
expect(room.getEventReadUpTo(selfUserId, false)).toEqual(thread.rootEvent.getId());
|
||||
// Ensure the actual receipt for the room has not been updated
|
||||
expect(room.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(thread.rootEvent.getId());
|
||||
expect(room.getEventReadUpTo(selfUserId, true)).toEqual(thread.rootEvent.getId());
|
||||
// Ensure the synthetic receipt for the thread has been updated
|
||||
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(reactionEventId);
|
||||
expect(thread.thread.getEventReadUpTo(selfUserId, false)).toEqual(reactionEventId);
|
||||
// Ensure the actual receipt for the thread has not been updated
|
||||
expect(thread.thread.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(threadReply.getId());
|
||||
expect(thread.thread.getEventReadUpTo(selfUserId, true)).toEqual(threadReply.getId());
|
||||
});
|
||||
|
||||
describe("Stuck unread notifications integration tests", () => {
|
||||
const ROOM_ID = "!room:localhost";
|
||||
|
||||
const syncData = getSampleStuckNotificationSyncResponse(ROOM_ID);
|
||||
|
||||
it("resets notifications if the last event originates from the logged in user", async () => {
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
expect(req.queryParams!.filter).toEqual("a filter id");
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
const room = client?.getRoom(ROOM_ID);
|
||||
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room?.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
function getSampleStuckNotificationSyncResponse(roomId: string): Partial<ISyncResponse> {
|
||||
return {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
[Category.Join]: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
{
|
||||
content: {
|
||||
creator: userB,
|
||||
room_version: "9",
|
||||
},
|
||||
origin_server_ts: 1,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.create",
|
||||
event_id: "$event1",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userB,
|
||||
membership: "join",
|
||||
},
|
||||
origin_server_ts: 2,
|
||||
sender: userB,
|
||||
state_key: userB,
|
||||
type: "m.room.member",
|
||||
event_id: "$event2",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
ban: 50,
|
||||
events: {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.server_acl": 100,
|
||||
"m.room.tombstone": 100,
|
||||
},
|
||||
events_default: 0,
|
||||
historical: 100,
|
||||
invite: 0,
|
||||
kick: 50,
|
||||
redact: 50,
|
||||
state_default: 50,
|
||||
users: {
|
||||
[userA]: 100,
|
||||
[userB]: 100,
|
||||
},
|
||||
users_default: 0,
|
||||
},
|
||||
origin_server_ts: 3,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.power_levels",
|
||||
event_id: "$event3",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
},
|
||||
origin_server_ts: 4,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.join_rules",
|
||||
event_id: "$event4",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
history_visibility: "shared",
|
||||
},
|
||||
origin_server_ts: 5,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.history_visibility",
|
||||
event_id: "$event5",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
guest_access: "can_join",
|
||||
},
|
||||
origin_server_ts: 6,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.guest_access",
|
||||
unsigned: {
|
||||
age: 1651569,
|
||||
},
|
||||
event_id: "$event6",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
origin_server_ts: 7,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
event_id: "$event7",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
is_direct: true,
|
||||
membership: "invite",
|
||||
},
|
||||
origin_server_ts: 8,
|
||||
sender: userB,
|
||||
state_key: userA,
|
||||
type: "m.room.member",
|
||||
event_id: "$event8",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello",
|
||||
},
|
||||
origin_server_ts: 9,
|
||||
sender: userB,
|
||||
type: "m.room.message",
|
||||
event_id: "$event9",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
membership: "join",
|
||||
},
|
||||
origin_server_ts: 10,
|
||||
sender: userA,
|
||||
state_key: userA,
|
||||
type: "m.room.member",
|
||||
event_id: "$event10",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "world",
|
||||
},
|
||||
origin_server_ts: 11,
|
||||
sender: userA,
|
||||
type: "m.room.message",
|
||||
event_id: "$event11",
|
||||
},
|
||||
],
|
||||
prev_batch: "123",
|
||||
limited: false,
|
||||
},
|
||||
state: {
|
||||
events: [],
|
||||
},
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.fully_read",
|
||||
content: {
|
||||
event_id: "$dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
{
|
||||
type: "m.receipt",
|
||||
content: {
|
||||
$event9: {
|
||||
"m.read": {
|
||||
[userA]: {
|
||||
ts: 100,
|
||||
},
|
||||
},
|
||||
"m.read.private": {
|
||||
[userA]: {
|
||||
ts: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU: {
|
||||
"m.read": {
|
||||
[userB]: {
|
||||
ts: 666,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
unread_notifications: {
|
||||
notification_count: 1,
|
||||
highlight_count: 0,
|
||||
},
|
||||
summary: {
|
||||
"m.joined_member_count": 2,
|
||||
"m.invited_member_count": 0,
|
||||
"m.heroes": [userB],
|
||||
},
|
||||
},
|
||||
},
|
||||
[Category.Leave]: {},
|
||||
[Category.Invite]: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Account } from "@matrix-org/olm";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { decodeRecoveryKey } from "../../src/crypto/recoverykey";
|
||||
import { IKeyBackupInfo, IKeyBackupSession } from "../../src/crypto/keybackup";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { IEvent } from "../../src";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
|
||||
|
||||
const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
type: "m.room.encrypted",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: SESSION_ID,
|
||||
ciphertext:
|
||||
"AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
|
||||
"CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
|
||||
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
|
||||
},
|
||||
room_id: "!ROOM:ID",
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
};
|
||||
|
||||
const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
ciphertext:
|
||||
"2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
|
||||
"6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
|
||||
"Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
|
||||
"SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
|
||||
"Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
|
||||
"ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
|
||||
"4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
|
||||
"C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
|
||||
"Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
|
||||
"QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
|
||||
"iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
|
||||
mac: "5lxYBHQU80M",
|
||||
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
|
||||
},
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO: IKeyBackupInfo = {
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: "1",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/**
|
||||
* start an Olm session with a given recipient
|
||||
*/
|
||||
function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClient): Promise<Olm.Session> {
|
||||
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
});
|
||||
}
|
||||
|
||||
describe("megolm key backups", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn("not running megolm tests: Olm not present");
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
let testOlmAccount: Olm.Account;
|
||||
let aliceTestClient: TestClient;
|
||||
|
||||
const setupTestClient = (): [Account, TestClient] => {
|
||||
const aliceTestClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs");
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount!.create();
|
||||
|
||||
return [testOlmAccount, aliceTestClient];
|
||||
};
|
||||
|
||||
beforeAll(function () {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
[testOlmAccount, aliceTestClient] = setupTestClient();
|
||||
await aliceTestClient!.client.initCrypto();
|
||||
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
return aliceTestClient!.stop();
|
||||
});
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", function () {
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [ENCRYPTED_EVENT],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return aliceTestClient!
|
||||
.start()
|
||||
.then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
})
|
||||
.then(() => {
|
||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||
})
|
||||
.then(() => {
|
||||
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient!.expectKeyBackupQuery(ROOM_ID, SESSION_ID, 200, CURVE25519_KEY_BACKUP_DATA);
|
||||
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||
})
|
||||
.then(function (): Promise<MatrixEvent> {
|
||||
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then((event) => {
|
||||
expect(event.getContent()).toEqual("testytest");
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ import { fail } from "assert";
|
||||
|
||||
import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Extension } from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
|
||||
import { IRoomEvent, IStateEvent } from "../../src";
|
||||
import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
@@ -38,10 +38,11 @@ import {
|
||||
IRoomTimelineData,
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient | undefined;
|
||||
@@ -52,10 +53,9 @@ describe("SlidingSyncSdk", () => {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||
s.getList = jest.fn();
|
||||
s.getListParams = jest.fn();
|
||||
s.getListData = jest.fn();
|
||||
s.getRoomSubscriptions = jest.fn();
|
||||
s.listLength = jest.fn();
|
||||
s.modifyRoomSubscriptionInfo = jest.fn();
|
||||
s.modifyRoomSubscriptions = jest.fn();
|
||||
s.registerExtension = jest.fn();
|
||||
@@ -111,17 +111,18 @@ describe("SlidingSyncSdk", () => {
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
|
||||
testOpts = testOpts || {};
|
||||
const syncOpts: SyncApiOptions = {};
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
testOpts.crypto = client!.crypto;
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
||||
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
@@ -153,11 +154,11 @@ describe("SlidingSyncSdk", () => {
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
expect(mockSlidingSync!.start).toBeCalled();
|
||||
expect(mockSlidingSync!.start).toHaveBeenCalled();
|
||||
});
|
||||
it("can stop()", async () => {
|
||||
sdk!.stop();
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
expect(mockSlidingSync!.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,67 +302,57 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
};
|
||||
|
||||
it("can be created with required_state and timeline", () => {
|
||||
it("can be created with required_state and timeline", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
});
|
||||
|
||||
it("can be created with timeline only", () => {
|
||||
it("can be created with timeline only", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
});
|
||||
|
||||
it("can be created with a highlight_count", () => {
|
||||
it("can be created with a highlight_count", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
|
||||
data[roomC].highlight_count,
|
||||
);
|
||||
});
|
||||
|
||||
it("can be created with a notification_count", () => {
|
||||
it("can be created with a notification_count", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
|
||||
data[roomD].notification_count,
|
||||
);
|
||||
});
|
||||
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
it("can be created with an invited/joined_count", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom!.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
});
|
||||
|
||||
it("can be created with live events", () => {
|
||||
let seenLiveEvent = false;
|
||||
it("can be created with live events", async () => {
|
||||
const seenLiveEventDeferred = defer<boolean>();
|
||||
const listener = (
|
||||
ev: MatrixEvent,
|
||||
room?: Room,
|
||||
@@ -371,43 +362,37 @@ describe("SlidingSyncSdk", () => {
|
||||
) => {
|
||||
if (timelineData?.liveEvent) {
|
||||
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
|
||||
seenLiveEvent = true;
|
||||
seenLiveEventDeferred.resolve(true);
|
||||
}
|
||||
};
|
||||
client!.on(RoomEvent.Timeline, listener);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
client!.off(RoomEvent.Timeline, listener);
|
||||
const gotRoom = client!.getRoom(roomH);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
// check the entire timeline is correct
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
expect(seenLiveEvent).toBe(true);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
it("can be created with invite_state", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
|
||||
it("uses the 'name' field to caluclate the room name", () => {
|
||||
it("uses the 'name' field to caluclate the room name", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomF].name);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomF].name);
|
||||
});
|
||||
|
||||
describe("updating", () => {
|
||||
@@ -419,33 +404,33 @@ describe("SlidingSyncSdk", () => {
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
const newTimeline = data[roomA].timeline;
|
||||
newTimeline.push(newEvent);
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
});
|
||||
|
||||
it("can update with a new required_state event", async () => {
|
||||
let gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
@@ -456,11 +441,11 @@ describe("SlidingSyncSdk", () => {
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
@@ -471,11 +456,11 @@ describe("SlidingSyncSdk", () => {
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
@@ -486,11 +471,11 @@ describe("SlidingSyncSdk", () => {
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
expect(gotRoom!.getJoinedMemberCount()).toEqual(1);
|
||||
});
|
||||
|
||||
// Regression test for a bug which caused the timeline entries to be out-of-order
|
||||
@@ -512,7 +497,7 @@ describe("SlidingSyncSdk", () => {
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
@@ -530,7 +515,7 @@ describe("SlidingSyncSdk", () => {
|
||||
);
|
||||
|
||||
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), oldTimeline);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -548,7 +533,7 @@ describe("SlidingSyncSdk", () => {
|
||||
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "h",
|
||||
lists: [],
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
@@ -576,7 +561,7 @@ describe("SlidingSyncSdk", () => {
|
||||
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "i",
|
||||
lists: [],
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
@@ -584,7 +569,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
expect(mockSlidingSync!.stop).not.toHaveBeenCalled();
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
@@ -595,7 +580,7 @@ describe("SlidingSyncSdk", () => {
|
||||
}),
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
expect(mockSlidingSync!.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -626,9 +611,9 @@ describe("SlidingSyncSdk", () => {
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
await emitPromise(client!, RoomMemberEvent.Name);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
const inviteeMember = room.getMember(invitee)!;
|
||||
expect(inviteeMember).toBeDefined();
|
||||
expect(inviteeMember).toBeTruthy();
|
||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||
});
|
||||
@@ -662,41 +647,30 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
client!.crypto!.processDeviceLists = jest.fn();
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
},
|
||||
});
|
||||
// TODO: more assertions?
|
||||
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
});
|
||||
});
|
||||
|
||||
it("can update OTK counts", () => {
|
||||
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||
it("can update OTK counts and unused fallback keys", () => {
|
||||
client!.crypto!.processKeyCounts = jest.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
});
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
not_signed_curve25519: 42,
|
||||
// missing field -> default to 0
|
||||
},
|
||||
});
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("can update fallback keys", () => {
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["not_signed_curve25519"],
|
||||
});
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
"signed_curve25519",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -734,7 +708,7 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
});
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData).toBeTruthy();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
@@ -755,6 +729,7 @@ describe("SlidingSyncSdk", () => {
|
||||
foo: "bar",
|
||||
};
|
||||
const roomType = "test";
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: [
|
||||
@@ -766,9 +741,9 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeDefined();
|
||||
expect(event).toBeTruthy();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
@@ -891,11 +866,9 @@ describe("SlidingSyncSdk", () => {
|
||||
const evType = ev.getType();
|
||||
expect(seen[evType]).toBeFalsy();
|
||||
seen[evType] = true;
|
||||
if (evType === "m.key.verification.start" || evType === "m.key.verification.request") {
|
||||
expect(ev.isCancelled()).toEqual(true);
|
||||
} else {
|
||||
expect(ev.isCancelled()).toEqual(false);
|
||||
}
|
||||
expect(ev.isCancelled()).toEqual(
|
||||
evType === "m.key.verification.start" || evType === "m.key.verification.request",
|
||||
);
|
||||
});
|
||||
ext.onResponse({
|
||||
next_batch: "45678",
|
||||
@@ -956,8 +929,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
@@ -997,7 +971,7 @@ describe("SlidingSyncSdk", () => {
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
@@ -1090,12 +1064,13 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
|
||||
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
|
||||
const receipt = room.getReadReceiptForUserId(alice);
|
||||
expect(receipt).toBeDefined();
|
||||
expect(receipt).toBeTruthy();
|
||||
expect(receipt?.eventId).toEqual(lastEvent.event_id);
|
||||
expect(receipt?.data.ts).toEqual(1234567);
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
|
||||
+254
-153
@@ -64,10 +64,10 @@ describe("SlidingSync", () => {
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
it("should start the sync loop upon calling start()", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
const fakeResp = {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
};
|
||||
@@ -90,7 +90,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should reset the connection on HTTP 400 and send everything again", async () => {
|
||||
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
const roomId = "!sub:localhost";
|
||||
const subInfo = {
|
||||
timeline_limit: 42,
|
||||
@@ -114,7 +114,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
slidingSync.modifyRoomSubscriptionInfo(subInfo);
|
||||
slidingSync.setList(0, listInfo);
|
||||
slidingSync.setList("a", listInfo);
|
||||
slidingSync.registerExtension(ext);
|
||||
slidingSync.start();
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("SlidingSync", () => {
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.lists["a"]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
@@ -137,7 +137,7 @@ describe("SlidingSync", () => {
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "11",
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
txn_id: txnId,
|
||||
};
|
||||
@@ -151,7 +151,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -161,7 +161,7 @@ describe("SlidingSync", () => {
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "12",
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -185,7 +185,7 @@ describe("SlidingSync", () => {
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.lists["a"]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
@@ -193,7 +193,7 @@ describe("SlidingSync", () => {
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "1",
|
||||
lists: [{ count: 6 }],
|
||||
lists: { a: { count: 6 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -221,7 +221,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should be able to subscribe to a room", async () => {
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -233,7 +233,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: wantRoomData,
|
||||
@@ -266,7 +266,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: wantRoomData,
|
||||
@@ -313,7 +313,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[anotherRoomID]: anotherRoomData,
|
||||
@@ -344,7 +344,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
});
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
@@ -402,19 +402,19 @@ describe("SlidingSync", () => {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map([["a", listReq]]), {}, client!, 1);
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual(listReq);
|
||||
expect(body.lists["a"]).toEqual(listReq);
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "a",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -424,7 +424,7 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
rooms: rooms,
|
||||
});
|
||||
const listenerData: Record<string, MSC3575RoomData> = {};
|
||||
@@ -444,15 +444,14 @@ describe("SlidingSync", () => {
|
||||
expect(listenerData[roomB]).toEqual(rooms[roomB]);
|
||||
expect(listenerData[roomC]).toEqual(rooms[roomC]);
|
||||
|
||||
expect(slidingSync.listLength()).toEqual(1);
|
||||
slidingSync.off(SlidingSyncEvent.RoomData, dataListener);
|
||||
});
|
||||
|
||||
it("should be possible to retrieve list data", () => {
|
||||
expect(slidingSync.getList(0)).toBeDefined();
|
||||
expect(slidingSync.getList(5)).toBeNull();
|
||||
expect(slidingSync.getListData(5)).toBeNull();
|
||||
const syncData = slidingSync.getListData(0)!;
|
||||
expect(slidingSync.getListParams("a")).toBeDefined();
|
||||
expect(slidingSync.getListParams("b")).toBeNull();
|
||||
expect(slidingSync.getListData("b")).toBeNull();
|
||||
const syncData = slidingSync.getListData("a")!;
|
||||
expect(syncData.joinedCount).toEqual(500); // from previous test
|
||||
expect(syncData.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -467,17 +466,17 @@ describe("SlidingSync", () => {
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("next ranges", body.lists[0].ranges);
|
||||
logger.log("next ranges", body.lists["a"].ranges);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -487,15 +486,17 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.RequestFinished;
|
||||
});
|
||||
slidingSync.setListRanges(0, newRanges);
|
||||
slidingSync.setListRanges("a", newRanges);
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
// setListRanges for an invalid list key returns an error
|
||||
await expect(slidingSync.setListRanges("idontexist", newRanges)).rejects.toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be possible to add an extra list", async () => {
|
||||
@@ -513,19 +514,19 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.log("extra list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
expect(body.lists[1]).toEqual(extraListReq);
|
||||
expect(body.lists["b"]).toEqual(extraListReq);
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "c",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
ops: [
|
||||
{
|
||||
@@ -535,10 +536,10 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(1);
|
||||
listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("b");
|
||||
expect(joinedCount).toEqual(50);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -550,7 +551,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.setList(1, extraListReq);
|
||||
slidingSync.setList("b", extraListReq);
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
@@ -559,8 +560,8 @@ describe("SlidingSync", () => {
|
||||
// move C (2) to A (0)
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -574,16 +575,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -603,8 +604,8 @@ describe("SlidingSync", () => {
|
||||
// move C (0) back to A (2)
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -618,13 +619,13 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -644,8 +645,8 @@ describe("SlidingSync", () => {
|
||||
it("should ignore invalid list indexes", async () => {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -654,16 +655,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -684,8 +685,8 @@ describe("SlidingSync", () => {
|
||||
it("should be possible to update a list", async () => {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 42,
|
||||
ops: [
|
||||
{
|
||||
@@ -699,13 +700,13 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
// update the list with a new filter
|
||||
slidingSync.setList(0, {
|
||||
slidingSync.setList("a", {
|
||||
filters: {
|
||||
is_encrypted: true,
|
||||
},
|
||||
@@ -714,8 +715,8 @@ describe("SlidingSync", () => {
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(42);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
@@ -738,12 +739,12 @@ describe("SlidingSync", () => {
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
};
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -761,16 +762,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
return true;
|
||||
@@ -785,14 +786,14 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should handle deletions correctly", async () => {
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 499,
|
||||
ops: [
|
||||
{
|
||||
@@ -801,16 +802,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(499);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -827,13 +828,13 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should handle insertions correctly", async () => {
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -843,16 +844,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -870,8 +871,8 @@ describe("SlidingSync", () => {
|
||||
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 501,
|
||||
ops: [
|
||||
{
|
||||
@@ -881,13 +882,13 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(501);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -910,11 +911,14 @@ describe("SlidingSync", () => {
|
||||
it("should handle insertions with a spurious DELETE correctly", async () => {
|
||||
slidingSync = new SlidingSync(
|
||||
proxyBaseUrl,
|
||||
[
|
||||
{
|
||||
ranges: [[0, 20]],
|
||||
},
|
||||
],
|
||||
new Map([
|
||||
[
|
||||
"a",
|
||||
{
|
||||
ranges: [[0, 20]],
|
||||
},
|
||||
],
|
||||
]),
|
||||
{},
|
||||
client!,
|
||||
1,
|
||||
@@ -922,22 +926,22 @@ describe("SlidingSync", () => {
|
||||
// initially start with nothing
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "a",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 0,
|
||||
ops: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({});
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({});
|
||||
|
||||
// insert a room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "b",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
@@ -951,18 +955,18 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
});
|
||||
|
||||
// insert another room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
@@ -976,10 +980,10 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomA,
|
||||
});
|
||||
@@ -987,8 +991,8 @@ describe("SlidingSync", () => {
|
||||
// insert a final room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
@@ -1002,10 +1006,10 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomB,
|
||||
2: roomA,
|
||||
@@ -1028,7 +1032,7 @@ describe("SlidingSync", () => {
|
||||
required_state: [["m.room.name", ""]],
|
||||
};
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
|
||||
// modification before SlidingSync.start()
|
||||
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
let txnId: string | undefined;
|
||||
@@ -1046,7 +1050,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "aaa",
|
||||
txn_id: txnId,
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
@@ -1065,7 +1069,7 @@ describe("SlidingSync", () => {
|
||||
const newList = {
|
||||
ranges: [[0, 20]],
|
||||
};
|
||||
const promise = slidingSync.setList(0, newList);
|
||||
const promise = slidingSync.setList("a", newList);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -1073,7 +1077,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual(newList);
|
||||
expect(body.lists["a"]).toEqual(newList);
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
txnId = body.txn_id;
|
||||
})
|
||||
@@ -1081,7 +1085,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "bbb",
|
||||
txn_id: txnId,
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -1090,7 +1094,7 @@ describe("SlidingSync", () => {
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should resolve setListRanges during a connection", async () => {
|
||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const promise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -1098,7 +1102,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[20, 40]],
|
||||
});
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
@@ -1108,7 +1112,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "ccc",
|
||||
txn_id: txnId,
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -1150,19 +1154,14 @@ describe("SlidingSync", () => {
|
||||
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
};
|
||||
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const failPromise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
const failPromise2 = slidingSync.setListRanges("a", [[60, 70]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
expect(failPromise).rejects.toEqual(gotTxnIds[0]);
|
||||
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
|
||||
|
||||
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
|
||||
const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -1176,8 +1175,12 @@ describe("SlidingSync", () => {
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await okPromise;
|
||||
await Promise.all([
|
||||
expect(failPromise).rejects.toEqual(gotTxnIds[0]),
|
||||
expect(failPromise2).rejects.toEqual(gotTxnIds[1]),
|
||||
httpBackend!.flushAllExpected(),
|
||||
okPromise,
|
||||
]);
|
||||
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
@@ -1187,18 +1190,17 @@ describe("SlidingSync", () => {
|
||||
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
|
||||
gotTxnIds.push(req.data?.txn_id);
|
||||
};
|
||||
const A = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const A = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend!.flushAllExpected();
|
||||
const B = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
const B = slidingSync.setListRanges("a", [[60, 70]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
expect(A).rejects.toEqual(gotTxnIds[0]);
|
||||
|
||||
const C = slidingSync.setListRanges(0, [[0, 20]]);
|
||||
const C = slidingSync.setListRanges("a", [[0, 20]]);
|
||||
let pendingC = true;
|
||||
C.finally(() => {
|
||||
pendingC = false;
|
||||
@@ -1213,13 +1215,16 @@ describe("SlidingSync", () => {
|
||||
txn_id: gotTxnIds[1],
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
// A is rejected, see above
|
||||
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
|
||||
await Promise.all([
|
||||
expect(A).rejects.toEqual(gotTxnIds[0]),
|
||||
httpBackend!.flushAllExpected(),
|
||||
// A is rejected, see above
|
||||
expect(B).resolves.toEqual(gotTxnIds[1]), // B is resolved
|
||||
]);
|
||||
expect(pendingC).toBe(true); // C is pending still
|
||||
});
|
||||
it("should do nothing for unknown txn_ids", async () => {
|
||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const promise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
let pending = true;
|
||||
promise.finally(() => {
|
||||
pending = false;
|
||||
@@ -1231,7 +1236,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[20, 40]],
|
||||
});
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
@@ -1241,7 +1246,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "ccc",
|
||||
txn_id: "bogus transaction id",
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -1279,7 +1284,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
|
||||
it("should be possible to use custom subscriptions on startup", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
@@ -1302,7 +1307,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1312,7 +1317,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should be possible to use custom subscriptions mid-connection", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
@@ -1326,7 +1331,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1344,7 +1349,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1363,7 +1368,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1383,7 +1388,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1395,7 +1400,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("uses the default subscription for unknown subscription names", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.useCustomSubscription(roomA, "unknown name");
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
@@ -1410,7 +1415,103 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("should not be possible to add/modify an already added custom subscription", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub2);
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("should change the custom subscription if they are different", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// using the same subscription doesn't unsub nor changes subscriptions
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeUndefined();
|
||||
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// Changing the subscription works
|
||||
slidingSync.useCustomSubscription(roomA, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub2);
|
||||
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1463,7 +1564,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
|
||||
it("should be able to register an extension", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
|
||||
const callbackOrder: string[] = [];
|
||||
@@ -1588,7 +1689,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("is not possible to register the same extension name twice", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
expect(() => {
|
||||
slidingSync.registerExtension(extPre);
|
||||
@@ -1598,7 +1699,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
function timeout(delayMs: number, reason: string): { promise: Promise<never>; cancel: () => void } {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return {
|
||||
promise: new Promise((resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
|
||||
+1
-1
@@ -23,5 +23,5 @@ try {
|
||||
global.Olm = require("@matrix-org/olm");
|
||||
logger.log("loaded libolm");
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
logger.warn("unable to run crypto tests: libolm not available", e);
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import DOMException from "domexception";
|
||||
|
||||
global.DOMException = DOMException;
|
||||
global.DOMException = DOMException as typeof global.DOMException;
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
|
||||
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
|
||||
*
|
||||
* Common interface implemented by {@link TestClient} and {@link E2EKeyReceiver}
|
||||
*/
|
||||
export interface IE2EKeyReceiver {
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
getSigningKey(): string;
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
getDeviceKey(): string;
|
||||
|
||||
/**
|
||||
* Wait for one-time-keys to be uploaded, then return them.
|
||||
*
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>>;
|
||||
}
|
||||
|
||||
/** E2EKeyReceiver: An object which intercepts `/keys/uploads` fetches via fetch-mock.
|
||||
*
|
||||
* It stashes the uploaded keys for use elsewhere in the tests.
|
||||
*/
|
||||
export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
private readonly debug: Debugger;
|
||||
|
||||
private deviceKeys: IDeviceKeys | null = null;
|
||||
private oneTimeKeys: Record<string, IOneTimeKey> = {};
|
||||
private readonly oneTimeKeysPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* Construct a new E2EKeyReceiver.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
|
||||
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
|
||||
|
||||
// set up a listener for /keys/upload.
|
||||
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
||||
const listener = (url: string, options: RequestInit) =>
|
||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
||||
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
||||
});
|
||||
}
|
||||
|
||||
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
|
||||
const content = JSON.parse(options.body as string);
|
||||
|
||||
// device keys may only be uploaded once
|
||||
if (content.device_keys && Object.keys(content.device_keys).length > 0) {
|
||||
if (this.deviceKeys) {
|
||||
throw new Error("Application attempted to upload E2E device keys multiple times");
|
||||
}
|
||||
this.debug(`received device keys`);
|
||||
this.deviceKeys = content.device_keys;
|
||||
}
|
||||
|
||||
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
|
||||
// this is a one-time-key upload
|
||||
|
||||
// if we already have a batch of one-time keys, then slow-roll the response,
|
||||
// otherwise the client ends up tight-looping one-time-key-uploads and filling the logs with junk.
|
||||
if (Object.keys(this.oneTimeKeys).length > 0) {
|
||||
this.debug(`received second batch of one-time keys: blocking response`);
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
this.debug(`received ${Object.keys(content.one_time_keys).length} one-time keys`);
|
||||
Object.assign(this.oneTimeKeys, content.one_time_keys);
|
||||
onOnTimeKeysUploaded();
|
||||
}
|
||||
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Get the uploaded Ed25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
if (!this.deviceKeys) {
|
||||
throw new Error("Device keys not yet uploaded");
|
||||
}
|
||||
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("ed25519:"));
|
||||
if (keyIds.length != 1) {
|
||||
throw new Error(`Expected exactly 1 ed25519 key uploaded, got ${keyIds}`);
|
||||
}
|
||||
return this.deviceKeys.keys[keyIds[0]];
|
||||
}
|
||||
|
||||
/** Get the uploaded Curve25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
if (!this.deviceKeys) {
|
||||
throw new Error("Device keys not yet uploaded");
|
||||
}
|
||||
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("curve25519:"));
|
||||
if (keyIds.length != 1) {
|
||||
throw new Error(`Expected exactly 1 curve25519 key uploaded, got ${keyIds}`);
|
||||
}
|
||||
return this.deviceKeys.keys[keyIds[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* If the device keys have already been uploaded, return them. Else return null.
|
||||
*/
|
||||
public getUploadedDeviceKeys(): IDeviceKeys | null {
|
||||
return this.deviceKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
public async awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
await this.oneTimeKeysPromise;
|
||||
return this.oneTimeKeys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { IDownloadKeyResult } from "../../src";
|
||||
import { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
|
||||
/**
|
||||
* An object which intercepts `/keys/query` fetches via fetch-mock.
|
||||
*/
|
||||
export class E2EKeyResponder {
|
||||
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
|
||||
private e2eKeyReceiversByUser = new Map<string, E2EKeyReceiver>();
|
||||
private masterKeysByUser: Record<string, any> = {};
|
||||
private selfSigningKeysByUser: Record<string, any> = {};
|
||||
private userSigningKeysByUser: Record<string, any> = {};
|
||||
|
||||
/**
|
||||
* Construct a new E2EKeyResponder.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/query` requests for the given homeserverUrl.
|
||||
* Only /query requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
// set up a listener for /keys/query.
|
||||
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
|
||||
}
|
||||
|
||||
private onKeyQueryRequest(options: RequestInit) {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const usersToReturn = Object.keys(content["device_keys"]);
|
||||
const response = {
|
||||
device_keys: {} as { [userId: string]: any },
|
||||
master_keys: {} as { [userId: string]: any },
|
||||
self_signing_keys: {} as { [userId: string]: any },
|
||||
user_signing_keys: {} as { [userId: string]: any },
|
||||
failures: {} as { [serverName: string]: any },
|
||||
};
|
||||
for (const user of usersToReturn) {
|
||||
const userKeys = this.deviceKeysByUserByDevice.get(user);
|
||||
if (userKeys !== undefined) {
|
||||
response.device_keys[user] = Object.fromEntries(userKeys.entries());
|
||||
}
|
||||
|
||||
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
|
||||
if (e2eKeyReceiver !== undefined) {
|
||||
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
|
||||
if (deviceKeys !== null) {
|
||||
response.device_keys[user] ??= {};
|
||||
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.masterKeysByUser.hasOwnProperty(user)) {
|
||||
response.master_keys[user] = this.masterKeysByUser[user];
|
||||
}
|
||||
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
|
||||
}
|
||||
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of device keys for return by a future `/keys/query`, as if they had been `/upload`ed
|
||||
*
|
||||
* @param keys - device keys for this device.
|
||||
*/
|
||||
public addDeviceKeys(keys: IDeviceKeys) {
|
||||
this.deviceKeysByUserByDevice.getOrCreate(keys.user_id).set(keys.device_id, keys);
|
||||
}
|
||||
|
||||
/** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed
|
||||
*
|
||||
* @param data cross-signing data
|
||||
*/
|
||||
public addCrossSigningData(
|
||||
data: Pick<IDownloadKeyResult, "master_keys" | "self_signing_keys" | "user_signing_keys">,
|
||||
) {
|
||||
Object.assign(this.masterKeysByUser, data.master_keys);
|
||||
Object.assign(this.selfSigningKeysByUser, data.self_signing_keys);
|
||||
Object.assign(this.userSigningKeysByUser, data.user_signing_keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an E2EKeyReceiver to poll for uploaded keys
|
||||
*
|
||||
* Any keys which have been uploaded to the given `E2EKeyReceiver` at the time of the `/keys/query` request will
|
||||
* be added to the response.
|
||||
*
|
||||
* @param e2eKeyReceiver
|
||||
*/
|
||||
public addKeyReceiver(userId: string, e2eKeyReceiver: E2EKeyReceiver) {
|
||||
this.e2eKeyReceiversByUser.set(userId, e2eKeyReceiver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
* Common interface implemented by {@link TestClient} and {@link SyncResponder}
|
||||
*/
|
||||
export interface ISyncResponder {
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
sendOrQueueSyncResponse(response: object): void;
|
||||
}
|
||||
|
||||
enum SyncResponderState {
|
||||
IDLE,
|
||||
WAITING_FOR_REQUEST,
|
||||
WAITING_FOR_RESPONSE,
|
||||
}
|
||||
|
||||
/** SyncResponder: An object which intercepts `/sync` fetches via fetch-mock.
|
||||
*
|
||||
* Two modes are possible:
|
||||
* * A response can be queued up; the next call to `/sync` will return it.
|
||||
* * If a call to `/sync` arrives before a response is queued, it will block until a call to {@link #sendOrQueueSyncResponse}.
|
||||
*/
|
||||
export class SyncResponder implements ISyncResponder {
|
||||
private readonly debug: Debugger;
|
||||
private state: SyncResponderState = SyncResponderState.IDLE;
|
||||
|
||||
/*
|
||||
* properties that are only valid in WAITING_FOR_REQUEST
|
||||
*/
|
||||
|
||||
/** the response to be sent when the request is made */
|
||||
private pendingResponse: object | null = null;
|
||||
|
||||
/*
|
||||
* properties that are only valid in WAITING_FOR_RESPONSE
|
||||
*/
|
||||
|
||||
/** a callback to be called with a response once one is registered.
|
||||
*
|
||||
* It will release the /sync request and update the state.
|
||||
*/
|
||||
private onResponseReceived: ((response: object) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Construct a new SyncResponder.
|
||||
*
|
||||
* It will immediately register an intercept of `/sync` requests for the given homeserverUrl.
|
||||
* Only /sync requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have overlapping /sync requests.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
this.onSyncRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<MockResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
const res = await new Promise<object>((resolve) => {
|
||||
this.onResponseReceived = resolve;
|
||||
this.state = SyncResponderState.WAITING_FOR_RESPONSE;
|
||||
});
|
||||
this.debug("Responding to /sync");
|
||||
this.state = SyncResponderState.IDLE;
|
||||
this.onResponseReceived = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
case SyncResponderState.WAITING_FOR_REQUEST: {
|
||||
this.debug("Got /sync request: responding immediately with queued response");
|
||||
const res = this.pendingResponse!;
|
||||
this.state = SyncResponderState.IDLE;
|
||||
this.pendingResponse = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
default:
|
||||
// we must already be in WAITING_FOR_RESPONSE, ie we already have a /sync request in progress
|
||||
throw new Error(`Got unexpected /sync request in state ${this.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
public sendOrQueueSyncResponse(response: object): void {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE:
|
||||
this.pendingResponse = response;
|
||||
this.state = SyncResponderState.WAITING_FOR_REQUEST;
|
||||
break;
|
||||
|
||||
case SyncResponderState.WAITING_FOR_RESPONSE:
|
||||
this.onResponseReceived!(response);
|
||||
break;
|
||||
|
||||
default:
|
||||
// we already have a response queued
|
||||
throw new Error(`Cannot queue more than one /sync response`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export const getMockClientWithEventEmitter = (
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
@@ -85,7 +86,6 @@ export const mockClientMethodsEvents = () => ({
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
doesServerSupportSeparateAddAndBind: jest.fn(),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCapabilities: jest.fn().mockReturnValue({}),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
/**
|
||||
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
|
||||
*
|
||||
* @param homeserverUrl - the homeserver url for the client under test
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||
filter_id: "fid",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the requests needed to set up cross signing
|
||||
*
|
||||
* Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request
|
||||
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
|
||||
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
|
||||
*/
|
||||
export function mockSetupCrossSigningRequests(): void {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/v3/user/:userId/account_data/:type", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { OidcClientConfig } from "../../src";
|
||||
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
|
||||
|
||||
/**
|
||||
* Makes a valid OidcClientConfig with minimum valid values
|
||||
* @param issuer used as the base for all other urls
|
||||
* @returns OidcClientConfig
|
||||
*/
|
||||
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
|
||||
const metadata = mockOpenIdConfiguration(issuer);
|
||||
|
||||
return {
|
||||
issuer,
|
||||
account: issuer + "account",
|
||||
registrationEndpoint: metadata.registration_endpoint,
|
||||
authorizationEndpoint: metadata.authorization_endpoint,
|
||||
tokenEndpoint: metadata.token_endpoint,
|
||||
metadata,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Useful for mocking <issuer>/.well-known/openid-configuration
|
||||
* @param issuer used as the base for all other urls
|
||||
* @returns ValidatedIssuerMetadata
|
||||
*/
|
||||
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
|
||||
issuer,
|
||||
revocation_endpoint: issuer + "revoke",
|
||||
token_endpoint: issuer + "token",
|
||||
authorization_endpoint: issuer + "auth",
|
||||
registration_endpoint: issuer + "registration",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
/env
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
#!/bin/env python
|
||||
#
|
||||
# Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
This file is a Python script to generate test data for crypto tests.
|
||||
|
||||
To run it:
|
||||
|
||||
python -m venv env
|
||||
./env/bin/pip install cryptography canonicaljson
|
||||
./env/bin/python generate-test-data.py > index.ts
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
from canonicaljson import encode_canonical_json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from random import randbytes, seed
|
||||
|
||||
ALICE_DATA = {
|
||||
"TEST_USER_ID": "@alice:localhost",
|
||||
"TEST_DEVICE_ID": "test_device",
|
||||
"TEST_ROOM_ID": "!room:id",
|
||||
# any 32-byte string can be an ed25519 private key.
|
||||
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"deadbeefdeadbeefdeadbeefdeadbeef",
|
||||
|
||||
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"doyouspeakwhaaaaaaaaaaaaaaaaaale",
|
||||
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"useruseruseruseruseruseruseruser",
|
||||
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"selfselfselfselfselfselfselfself",
|
||||
|
||||
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
|
||||
"B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
|
||||
|
||||
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
|
||||
}
|
||||
|
||||
BOB_DATA = {
|
||||
"TEST_USER_ID": "@bob:xyz",
|
||||
"TEST_DEVICE_ID": "bob_device",
|
||||
"TEST_ROOM_ID": "!room:id",
|
||||
# any 32-byte string can be an ed25519 private key.
|
||||
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"Deadbeefdeadbeefdeadbeefdeadbeef",
|
||||
|
||||
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
|
||||
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
|
||||
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
|
||||
|
||||
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
|
||||
"B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
|
||||
|
||||
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
|
||||
}
|
||||
|
||||
def main() -> None:
|
||||
print(
|
||||
f"""\
|
||||
/* Test data for cryptography tests
|
||||
*
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult }} from "../../../src";
|
||||
import {{ KeyBackupInfo }} from "../../../src/crypto-api";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
// Alice data
|
||||
|
||||
{build_test_data(ALICE_DATA)}
|
||||
// Bob data
|
||||
|
||||
{build_test_data(BOB_DATA, "BOB_")}
|
||||
""",
|
||||
end="",
|
||||
)
|
||||
|
||||
# Use static seed to have stable random test data upon new generation
|
||||
seed(10)
|
||||
|
||||
def build_test_data(user_data, prefix = "") -> str:
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["TEST_DEVICE_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_public_key = encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
device_data = {
|
||||
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
"device_id": user_data["TEST_DEVICE_ID"],
|
||||
"keys": {
|
||||
f"curve25519:{user_data['TEST_DEVICE_ID']}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
f"ed25519:{user_data['TEST_DEVICE_ID']}": b64_public_key,
|
||||
},
|
||||
"signatures": {user_data['TEST_USER_ID']: {}},
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
}
|
||||
|
||||
device_data["signatures"][user_data["TEST_USER_ID"]][f"ed25519:{user_data['TEST_DEVICE_ID']}"] = sign_json(
|
||||
device_data, private_key
|
||||
)
|
||||
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
b64_master_private_key = encode_base64(user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
|
||||
|
||||
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_self_signing_public_key = encode_base64(
|
||||
self_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
b64_self_signing_private_key = encode_base64( user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
|
||||
|
||||
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_user_signing_public_key = encode_base64(
|
||||
user_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
b64_user_signing_private_key = encode_base64(user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
|
||||
|
||||
backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes(
|
||||
base64.b64decode(user_data["B64_BACKUP_DECRYPTION_KEY"])
|
||||
)
|
||||
b64_backup_public_key = encode_base64(
|
||||
backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
backup_data = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": b64_backup_public_key,
|
||||
},
|
||||
}
|
||||
# sign with our device key
|
||||
sig = sign_json(backup_data["auth_data"], private_key)
|
||||
backup_data["auth_data"]["signatures"] = {
|
||||
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig}
|
||||
}
|
||||
|
||||
set_of_exported_room_keys = [build_exported_megolm_key(), build_exported_megolm_key()]
|
||||
|
||||
additional_exported_room_key = build_exported_megolm_key()
|
||||
|
||||
otk_to_sign = {
|
||||
"key": user_data['OTK']
|
||||
}
|
||||
# sign our public otk key with our device key
|
||||
otk = sign_json(otk_to_sign, private_key)
|
||||
otks = {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
user_data['TEST_DEVICE_ID']: {
|
||||
"signed_curve25519:AAAAHQ": {
|
||||
"key": user_data["OTK"],
|
||||
"signatures": {
|
||||
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": otk}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return f"""\
|
||||
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
|
||||
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
|
||||
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const {prefix}TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
|
||||
|
||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||
export const {prefix}SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
|
||||
|
||||
/** base64-encoded public master cross-signing key */
|
||||
export const {prefix}MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
|
||||
|
||||
/** base64-encoded private master cross-signing key */
|
||||
export const {prefix}MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
|
||||
|
||||
/** base64-encoded public self cross-signing key */
|
||||
export const {prefix}SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
|
||||
|
||||
/** base64-encoded private self signing cross-signing key */
|
||||
export const {prefix}SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
|
||||
|
||||
/** base64-encoded public user cross-signing key */
|
||||
export const {prefix}USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
|
||||
|
||||
/** base64-encoded private user signing cross-signing key */
|
||||
export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
||||
json.dumps(set_of_exported_room_keys, indent=4)
|
||||
};
|
||||
|
||||
/** An exported megolm session */
|
||||
export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
json.dumps(additional_exported_room_key, indent=4)
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
"""
|
||||
|
||||
|
||||
def build_cross_signing_keys_data(user_data) -> dict:
|
||||
"""Build the signed cross-signing-keys data for return from /keys/query"""
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_self_signing_public_key = encode_base64(
|
||||
self_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_user_signing_public_key = encode_base64(
|
||||
user_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
# create without signatures initially
|
||||
cross_signing_keys_data = {
|
||||
"master_keys": {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_master_public_key}": b64_master_public_key,
|
||||
},
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
"usage": ["master"],
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
|
||||
},
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
"usage": ["self_signing"],
|
||||
},
|
||||
},
|
||||
"user_signing_keys": {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
|
||||
},
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
"usage": ["user_signing"],
|
||||
},
|
||||
},
|
||||
}
|
||||
# sign the sub-keys with the master
|
||||
for k in ["self_signing_keys", "user_signing_keys"]:
|
||||
to_sign = cross_signing_keys_data[k][user_data["TEST_USER_ID"]]
|
||||
sig = sign_json(to_sign, master_private_key)
|
||||
to_sign["signatures"] = {
|
||||
user_data["TEST_USER_ID"]: {f"ed25519:{b64_master_public_key}": sig}
|
||||
}
|
||||
|
||||
return cross_signing_keys_data
|
||||
|
||||
|
||||
def encode_base64(input_bytes: bytes) -> str:
|
||||
"""Encode with unpadded base64"""
|
||||
output_bytes = base64.b64encode(input_bytes)
|
||||
output_string = output_bytes.decode("ascii")
|
||||
return output_string.rstrip("=")
|
||||
|
||||
|
||||
def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||
"""
|
||||
Sign the given json object
|
||||
|
||||
Returns the base64-encoded signature of signing `input` following the Matrix
|
||||
JSON signature algorithm [1]
|
||||
|
||||
[1]: https://spec.matrix.org/v1.7/appendices/#signing-details
|
||||
"""
|
||||
signatures = json_object.pop("signatures", {})
|
||||
unsigned = json_object.pop("unsigned", None)
|
||||
|
||||
signature = private_key.sign(encode_canonical_json(json_object))
|
||||
signature_base64 = encode_base64(signature)
|
||||
|
||||
json_object["signatures"] = signatures
|
||||
if unsigned is not None:
|
||||
json_object["unsigned"] = unsigned
|
||||
|
||||
return signature_base64
|
||||
|
||||
def build_exported_megolm_key() -> dict:
|
||||
"""
|
||||
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
|
||||
that can be imported via importRoomKeys API.
|
||||
"""
|
||||
index = 0
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32))
|
||||
# Just use radom bytes for the ratchet parts
|
||||
ratchet = randbytes(32 * 4)
|
||||
# exported key, start with version byte
|
||||
exported_key = bytearray(b'\x01')
|
||||
exported_key += index.to_bytes(4, 'big')
|
||||
exported_key += ratchet
|
||||
# KPub
|
||||
exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
|
||||
|
||||
megolm_export = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
),
|
||||
"session_key": encode_base64(exported_key),
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||
},
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
}
|
||||
|
||||
return megolm_export
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,341 @@
|
||||
/* Test data for cryptography tests
|
||||
*
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { IDownloadKeyResult } from "../../../src";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
// Alice data
|
||||
|
||||
export const TEST_USER_ID = "@alice:localhost";
|
||||
export const TEST_DEVICE_ID = "test_device";
|
||||
export const TEST_ROOM_ID = "!room:id";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU";
|
||||
|
||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "test_device",
|
||||
"keys": {
|
||||
"curve25519:test_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
"ed25519:test_device": "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "LmQC/yAUZJmkxZ+3L0nEwvtVWOzjqQqADWBhk+C47SPaFYHeV+E291mgXaSCJVeGltX+HC49Aw7nb6ga7sw0Aw"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded public master cross-signing key */
|
||||
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
|
||||
|
||||
/** base64-encoded private master cross-signing key */
|
||||
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "ZG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
|
||||
|
||||
/** base64-encoded public self cross-signing key */
|
||||
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY";
|
||||
|
||||
/** base64-encoded private self signing cross-signing key */
|
||||
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "c2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
|
||||
|
||||
/** base64-encoded public user cross-signing key */
|
||||
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY";
|
||||
|
||||
/** base64-encoded private user signing cross-signing key */
|
||||
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "dXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
"@alice:localhost": {
|
||||
"keys": {
|
||||
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"usage": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@alice:localhost": {
|
||||
"keys": {
|
||||
"ed25519:aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY": "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "XfhYEhZmOs8BJdb3viatILBZ/bElsHXEW28V4tIaY5CxrBR0YOym3yZHWmRmypXessHZAKOhZn3yBMXzdajyCw"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@alice:localhost": {
|
||||
"keys": {
|
||||
"ed25519:g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY": "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "6AkD1XM2H0/ebgP9oBdMKNeft7uxsrb0XN1CsjjHgeZCvCTMmv3BHlLiT/Hzy4fe8H+S1tr484dcXN/PIdnfDA"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "FYOoKQSwe4d9jhTZ/LQCZFJINjPEqZ7Or4Z08reP92M",
|
||||
"session_key": "AQAAAABZ0jXQOprFfXe41tIFmAtHxflJp4O2hM/vzQQpOazOCFeWSoW5P3Z9Q+voU3eXehMwyP8/hm/Q8xLP6/PmJdy+71se/17kdFwcDGgLxBWfa4ODM9zlI4EjKbNqmiii5loJ7rBhA/XXaw80m0hfU6zTDX/KrO55J0Pt4vJ0LDa3LBWDqCkEsHuHfY4U2fy0AmRSSDYzxKmezq+GdPK3j/dj",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "mPYSGA2l1tOQiipEDEVYhDSdTSFh2lDW1qpGKYZRxTc",
|
||||
"session_key": "AQAAAAAHwgkB49BTPAEGTCK6degxUIbl8GPG2ugPRYhNtOpNic63u11+baXFfjDw5fmVfD1gJXpQQjGsqrIYioxrB1xzl7mfb942UHhYdaMQZowpp1fSpJVsxR5TddUU2EWifYD9EQsoz8mY1zqoazm4vUP4v9yxaTcUBj2c6HMJCY0gCJj2EhgNpdbTkIoqRAxFWIQ0nU0hYdpQ1taqRimGUcU3",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
}
|
||||
];
|
||||
|
||||
/** An exported megolm session */
|
||||
export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
|
||||
"session_key": "AQAAAABXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIXBlK8tAfJo3cJnlh7F4ltEOAqrdME6dU0zXTkqXmURqYqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const ONE_TIME_KEYS = {
|
||||
"@alice:localhost": {
|
||||
"test_device": {
|
||||
"signed_curve25519:AAAAHQ": {
|
||||
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Bob data
|
||||
|
||||
export const BOB_TEST_USER_ID = "@bob:xyz";
|
||||
export const BOB_TEST_DEVICE_ID = "bob_device";
|
||||
export const BOB_TEST_ROOM_ID = "!room:id";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const BOB_TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50";
|
||||
|
||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||
export const BOB_SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "bob_device",
|
||||
"keys": {
|
||||
"curve25519:bob_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
"ed25519:bob_device": "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "4ApBs9jaeGyfdYaWRUdBvQAkDyXjACJ9KJ0xLHMgiFT/1yo6VqPTx2iziKGnrBiGhbtKNxEhDPOvZZkBU73cDQ"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded public master cross-signing key */
|
||||
export const BOB_MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA";
|
||||
|
||||
/** base64-encoded private master cross-signing key */
|
||||
export const BOB_MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "RG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
|
||||
|
||||
/** base64-encoded public self cross-signing key */
|
||||
export const BOB_SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A";
|
||||
|
||||
/** base64-encoded private self signing cross-signing key */
|
||||
export const BOB_SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "U2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
|
||||
|
||||
/** base64-encoded public user cross-signing key */
|
||||
export const BOB_USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw";
|
||||
|
||||
/** base64-encoded private user signing cross-signing key */
|
||||
export const BOB_USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "VXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "RxM8iJU6ZkyzQSVtNnXIJMPyEahVsN+fQQTBNKAs+kqySFyXBgchx+8czZaAhJCpXh9gD1nskT4yyFd2eyUXBw"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "jF8fvnPZulrPyh/4E8dNDVBP3iHHl9bRc+rRArVyGzoom+uVrokOck7BN2YmPyCRFZJJx7fgRA1Bveyu+mTVAg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "X9SK5JceUdvUr9gZkmdfS78qmKztoL60oORJ9JA5XD8",
|
||||
"session_key": "AQAAAADbfOdUj/ec5bK4xVEhhRw+jfd1FD4uA0MLy7NyVHugZxyXNZUx5YEof4H9YyjBretviZreMSXqflbgYKz257rkKu7MPeKFf7zmln2GxX0F/p++GOnvpY1FqOglhfRQi3tqiyOa7SL4f7TuERDTOpMqlWhIfTKQnqy0AyF2vpDi5V/UiuSXHlHb1K/YGZJnX0u/Kpis7aC+tKDkSfSQOVw/",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "ZG6lrfATe+958wN1xaGf3dKG/CThEfkmNdp1jcu4zok"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA",
|
||||
"session_key": "AQAAAACv0khqPrQ91MmWCgm0RTzfpn65AGCrRnAKLxGJdfSfECNZ8gyj34FZLwi+F+xC6ibFddcbLXW0mzR6PnTnHF3VHM4g/h+2rcxtlix8fySpIwFzaXViba7cOSy/b+dHTMZB40iA7F4y7AdTdHLv4N1XUj3puU/KVUIKf9/lEDLqyReD+39WdEY24mTIB5NcQQhtyguPzYPT5sSyeIUNd7Bw",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "HxUKnGfeUu0fF3cLyCFSDXYtVCQHy/+33q9RkzKfsiU"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
}
|
||||
];
|
||||
|
||||
/** An exported megolm session */
|
||||
export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM",
|
||||
"session_key": "AQAAAACvcoGk7mOY59fOqZaxFUiTCBRV1Ia94KBjAZx6kgdgBtkkvs50z8od8/Nc9ncK2UsEiXNvCTTp2dlN3du+Rx0/m7vet2ZOEEp2oYDjHMLLFmwd1gtlGuWYPdXA6Y1+9Yyph0/EDVfS+zd3XvbL0QgbyL43+yQnFNHKlxVJX1eiKTrGTHQtYEOZz6/i/bbk+sV7GhSZFT5IMT9hXsRxdf0D",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "dV0TIhhkToXpL+gZLo+zXDHJfw7MWYxpg80cynIQDv0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const BOB_ONE_TIME_KEYS = {
|
||||
"@bob:xyz": {
|
||||
"bob_device": {
|
||||
"signed_curve25519:AAAAHQ": {
|
||||
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,9 +6,19 @@ import "../olm-loader";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventType,
|
||||
IJoinedRoom,
|
||||
IPusher,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
MsgType,
|
||||
RelationType,
|
||||
} from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { TEST_ROOM_ID } from "./test-data";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -39,6 +49,61 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
|
||||
* @param roomMembers
|
||||
* @param roomId
|
||||
*
|
||||
* @returns the sync response
|
||||
*/
|
||||
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse {
|
||||
const roomResponse: IJoinedRoom = {
|
||||
summary: {
|
||||
"m.heroes": [],
|
||||
"m.joined_member_count": roomMembers.length,
|
||||
"m.invited_member_count": roomMembers.length,
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
mkEventCustom({
|
||||
sender: roomMembers[0],
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
prev_batch: "",
|
||||
},
|
||||
ephemeral: { events: [] },
|
||||
account_data: { events: [] },
|
||||
unread_notifications: {},
|
||||
};
|
||||
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
mkMembershipCustom({
|
||||
membership: "join",
|
||||
sender: roomMembers[i],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
next_batch: "1",
|
||||
rooms: {
|
||||
join: { [roomId]: roomResponse },
|
||||
invite: {},
|
||||
leave: {},
|
||||
},
|
||||
account_data: { events: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
* @param constr - The class constructor (used with 'new')
|
||||
@@ -258,6 +323,9 @@ export interface IMessageOpts {
|
||||
* @param opts.user - The user ID for the event.
|
||||
* @param opts.msg - Optional. The content.body for the event.
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param opts.relatesTo - An IEventRelation relating this to another event.
|
||||
* @param opts.ts - The timestamp of the event.
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @returns The event
|
||||
*/
|
||||
@@ -297,6 +365,7 @@ interface IReplyMessageOpts extends IMessageOpts {
|
||||
* @param opts.room - The room ID for the event.
|
||||
* @param opts.user - The user ID for the event.
|
||||
* @param opts.msg - Optional. The content.body for the event.
|
||||
* @param opts.ts - The timestamp of the event.
|
||||
* @param opts.replyToMessage - The replied message
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
|
||||
@@ -330,6 +399,73 @@ export function mkReplyMessage(
|
||||
return mkEvent(eventOpts, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reaction event.
|
||||
*
|
||||
* @param target - the event we are reacting to.
|
||||
* @param client - the MatrixClient
|
||||
* @param userId - the userId of the sender
|
||||
* @param roomId - the id of the room we are in
|
||||
* @param ts - The timestamp of the event.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkReaction(
|
||||
target: MatrixEvent,
|
||||
client: MatrixClient,
|
||||
userId: string,
|
||||
roomId: string,
|
||||
ts?: number,
|
||||
): MatrixEvent {
|
||||
return mkEvent(
|
||||
{
|
||||
event: true,
|
||||
type: EventType.Reaction,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: target.getId()!,
|
||||
key: Math.random().toString(),
|
||||
},
|
||||
},
|
||||
ts,
|
||||
},
|
||||
client,
|
||||
);
|
||||
}
|
||||
|
||||
export function mkEdit(
|
||||
target: MatrixEvent,
|
||||
client: MatrixClient,
|
||||
userId: string,
|
||||
roomId: string,
|
||||
msg?: string,
|
||||
ts?: number,
|
||||
) {
|
||||
msg = msg ?? `Edit of ${target.getId()}`;
|
||||
return mkEvent(
|
||||
{
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
content: {
|
||||
"body": `* ${msg}`,
|
||||
"m.new_content": {
|
||||
body: msg,
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: target.getId()!,
|
||||
},
|
||||
},
|
||||
ts,
|
||||
},
|
||||
client,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*/
|
||||
@@ -375,17 +511,17 @@ export async function awaitDecryption(
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
|
||||
logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`);
|
||||
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
|
||||
} else {
|
||||
return event;
|
||||
}
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`);
|
||||
logger.log(`${Date.now()}: event ${event.getId()} is not yet decrypted; waiting`);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
@@ -403,3 +539,15 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
pushkey: "pushpush",
|
||||
...extra,
|
||||
});
|
||||
|
||||
/**
|
||||
* a list of the supported crypto implementations, each with a callback to initialise that implementation
|
||||
* for the given client
|
||||
*/
|
||||
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
|
||||
export type InitCrypto = (_: MatrixClient) => Promise<void>;
|
||||
|
||||
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
|
||||
if (global.Olm) {
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { RelationType } from "../../src/@types/event";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { mkMessage } from "./test-utils";
|
||||
|
||||
export const makeThreadEvent = ({
|
||||
@@ -34,7 +34,7 @@ export const makeThreadEvent = ({
|
||||
...props,
|
||||
relatesTo: {
|
||||
event_id: rootEventId,
|
||||
rel_type: "m.thread",
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
["m.in_reply_to"]: {
|
||||
event_id: replyToEventId,
|
||||
},
|
||||
@@ -115,6 +115,26 @@ type MakeThreadProps = {
|
||||
ts?: number;
|
||||
};
|
||||
|
||||
type MakeThreadResult = {
|
||||
/**
|
||||
* Thread model
|
||||
*/
|
||||
thread: Thread;
|
||||
/**
|
||||
* Thread root event
|
||||
*/
|
||||
rootEvent: MatrixEvent;
|
||||
/**
|
||||
* Events added to the thread
|
||||
*/
|
||||
events: MatrixEvent[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts a new thread in a room by creating a message as thread root.
|
||||
* Also creates a Thread model and adds it to the room.
|
||||
* Does not insert the messages into a timeline.
|
||||
*/
|
||||
export const mkThread = ({
|
||||
room,
|
||||
client,
|
||||
@@ -122,7 +142,7 @@ export const mkThread = ({
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => {
|
||||
}: MakeThreadProps): MakeThreadResult => {
|
||||
const { rootEvent, events } = makeThreadEvents({
|
||||
roomId: room.roomId,
|
||||
authorId,
|
||||
@@ -137,7 +157,7 @@ export const mkThread = ({
|
||||
room?.reEmitter.reEmit(evt, [MatrixEventEvent.BeforeRedaction]);
|
||||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, [rootEvent, ...events], true);
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
|
||||
+228
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -122,6 +123,7 @@ export class MockRTCPeerConnection {
|
||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||
public iceConnectionStateChangeListener?: () => void;
|
||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||
public onDataChannelListener?: (ev: RTCDataChannelEvent) => void;
|
||||
public needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate?: () => void;
|
||||
@@ -167,6 +169,8 @@ export class MockRTCPeerConnection {
|
||||
this.iceConnectionStateChangeListener = listener;
|
||||
} else if (type == "track") {
|
||||
this.onTrackListener = listener;
|
||||
} else if (type == "datachannel") {
|
||||
this.onDataChannelListener = listener;
|
||||
}
|
||||
}
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) {
|
||||
@@ -231,6 +235,12 @@ export class MockRTCPeerConnection {
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
|
||||
public triggerIncomingDataChannel(): void {
|
||||
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
|
||||
}
|
||||
|
||||
public restartIce(): void {}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
@@ -443,13 +453,13 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
>();
|
||||
public sendToDevice = jest.fn<
|
||||
Promise<{}>,
|
||||
[
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]
|
||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
||||
>();
|
||||
|
||||
public isInitialSyncComplete(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getMediaHandler(): MediaHandler {
|
||||
return this.mediaHandler.typed();
|
||||
}
|
||||
@@ -476,7 +486,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
public supportsThreads(): boolean {
|
||||
return true;
|
||||
}
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
@@ -498,24 +508,25 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
public state = CallState.Ringing;
|
||||
public opponentUserId = FAKE_USER_ID_1;
|
||||
public opponentDeviceId = FAKE_DEVICE_ID_1;
|
||||
public opponentSessionId = FAKE_SESSION_ID_1;
|
||||
public opponentMember = { userId: this.opponentUserId };
|
||||
public callId = "1";
|
||||
public localUsermediaFeed = {
|
||||
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
|
||||
isAudioMuted: jest.fn().mockReturnValue(false),
|
||||
isVideoMuted: jest.fn().mockReturnValue(false),
|
||||
stream: new MockMediaStream("stream"),
|
||||
};
|
||||
} as unknown as CallFeed;
|
||||
public remoteUsermediaFeed?: CallFeed;
|
||||
public remoteScreensharingFeed?: CallFeed;
|
||||
|
||||
public reject = jest.fn<void, []>();
|
||||
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
|
||||
public hangup = jest.fn<void, []>();
|
||||
public initStats = jest.fn<void, []>();
|
||||
|
||||
public sendMetadataUpdate = jest.fn<void, []>();
|
||||
|
||||
public on = jest.fn();
|
||||
public removeListener = jest.fn();
|
||||
|
||||
public getOpponentMember(): Partial<RoomMember> {
|
||||
return this.opponentMember;
|
||||
}
|
||||
@@ -524,6 +535,14 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
return this.opponentDeviceId;
|
||||
}
|
||||
|
||||
public getOpponentSessionId(): string | undefined {
|
||||
return this.opponentSessionId;
|
||||
}
|
||||
|
||||
public getLocalFeeds(): CallFeed[] {
|
||||
return [this.localUsermediaFeed];
|
||||
}
|
||||
|
||||
public typed(): MatrixCall {
|
||||
return this as unknown as MatrixCall;
|
||||
}
|
||||
@@ -584,6 +603,7 @@ export function makeMockGroupCallStateEvent(
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
},
|
||||
redacted?: boolean,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
@@ -591,6 +611,7 @@ export function makeMockGroupCallStateEvent(
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
@@ -603,3 +624,199 @@ export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: s
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export const REMOTE_SFU_DESCRIPTION =
|
||||
"v=0\n" +
|
||||
"o=- 3242942315779688438 1678878001 IN IP4 0.0.0.0\n" +
|
||||
"s=-\n" +
|
||||
"t=0 0\n" +
|
||||
"a=fingerprint:sha-256 EA:30:B2:7F:49:B5:46:D6:40:72:BF:79:95:C1:65:08:6E:35:09:FB:90:89:DA:EF:6B:82:D1:38:8C:25:39:B2\n" +
|
||||
"a=group:BUNDLE 0 1 2\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:0\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
|
||||
"a=rtcp-mux\n" +
|
||||
"a=rtcp-rsize\n" +
|
||||
"a=rtpmap:111 opus/48000/2\n" +
|
||||
"a=fmtp:111 minptime=10;usedtx=1;useinbandfec=1\n" +
|
||||
"a=rtcp-fb:111 transport-cc \n" +
|
||||
"a=rtpmap:9 G722/8000\n" +
|
||||
"a=rtpmap:0 PCMU/8000\n" +
|
||||
"a=rtpmap:8 PCMA/8000\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
|
||||
"a=ssrc:2963372119 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:2963372119 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=ssrc:2963372119 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:2963372119 label:4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=sendrecv\n" +
|
||||
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 41385 typ host\n" +
|
||||
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 41385 typ host\n" +
|
||||
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 40026 typ host\n" +
|
||||
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 40026 typ host\n" +
|
||||
"a=end-of-candidates\n" +
|
||||
"m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 106 108 109 98 99 112 116\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:1\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
|
||||
"a=rtcp-mux\n" +
|
||||
"a=rtcp-rsize\n" +
|
||||
"a=rtpmap:96 VP8/90000\n" +
|
||||
"a=rtcp-fb:96 goog-remb \n" +
|
||||
"a=rtcp-fb:96 transport-cc \n" +
|
||||
"a=rtcp-fb:96 ccm fir\n" +
|
||||
"a=rtcp-fb:96 nack \n" +
|
||||
"a=rtcp-fb:96 nack pli\n" +
|
||||
"a=rtpmap:97 rtx/90000\n" +
|
||||
"a=fmtp:97 apt=96\n" +
|
||||
"a=rtpmap:102 H264/90000\n" +
|
||||
"a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\n" +
|
||||
"a=rtcp-fb:102 goog-remb \n" +
|
||||
"a=rtcp-fb:102 transport-cc \n" +
|
||||
"a=rtcp-fb:102 ccm fir\n" +
|
||||
"a=rtcp-fb:102 nack \n" +
|
||||
"a=rtcp-fb:102 nack pli\n" +
|
||||
"a=rtpmap:103 rtx/90000\n" +
|
||||
"a=fmtp:103 apt=102\n" +
|
||||
"a=rtpmap:104 H264/90000\n" +
|
||||
"a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\n" +
|
||||
"a=rtcp-fb:104 goog-remb \n" +
|
||||
"a=rtcp-fb:104 transport-cc \n" +
|
||||
"a=rtcp-fb:104 ccm fir\n" +
|
||||
"a=rtcp-fb:104 nack \n" +
|
||||
"a=rtcp-fb:104 nack pli\n" +
|
||||
"a=rtpmap:106 H264/90000\n" +
|
||||
"a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\n" +
|
||||
"a=rtcp-fb:106 goog-remb \n" +
|
||||
"a=rtcp-fb:106 transport-cc \n" +
|
||||
"a=rtcp-fb:106 ccm fir\n" +
|
||||
"a=rtcp-fb:106 nack \n" +
|
||||
"a=rtcp-fb:106 nack pli\n" +
|
||||
"a=rtpmap:108 H264/90000\n" +
|
||||
"a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\n" +
|
||||
"a=rtcp-fb:108 goog-remb \n" +
|
||||
"a=rtcp-fb:108 transport-cc \n" +
|
||||
"a=rtcp-fb:108 ccm fir\n" +
|
||||
"a=rtcp-fb:108 nack \n" +
|
||||
"a=rtcp-fb:108 nack pli\n" +
|
||||
"a=rtpmap:109 rtx/90000\n" +
|
||||
"a=fmtp:109 apt=108\n" +
|
||||
"a=rtpmap:98 VP9/90000\n" +
|
||||
"a=fmtp:98 profile-id=0\n" +
|
||||
"a=rtcp-fb:98 goog-remb \n" +
|
||||
"a=rtcp-fb:98 transport-cc \n" +
|
||||
"a=rtcp-fb:98 ccm fir\n" +
|
||||
"a=rtcp-fb:98 nack \n" +
|
||||
"a=rtcp-fb:98 nack pli\n" +
|
||||
"a=rtpmap:99 rtx/90000\n" +
|
||||
"a=fmtp:99 apt=98\n" +
|
||||
"a=rtpmap:112 H264/90000\n" +
|
||||
"a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\n" +
|
||||
"a=rtcp-fb:112 goog-remb \n" +
|
||||
"a=rtcp-fb:112 transport-cc \n" +
|
||||
"a=rtcp-fb:112 ccm fir\n" +
|
||||
"a=rtcp-fb:112 nack \n" +
|
||||
"a=rtcp-fb:112 nack pli\n" +
|
||||
"a=rtpmap:116 ulpfec/90000\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\n" +
|
||||
"a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\n" +
|
||||
"a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\n" +
|
||||
"a=rid:f recv\n" +
|
||||
"a=rid:h recv\n" +
|
||||
"a=rid:q recv\n" +
|
||||
"a=simulcast:recv f;h;q\n" +
|
||||
"a=ssrc:1212931603 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:1212931603 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=ssrc:1212931603 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:1212931603 label:12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=sendrecv\n" +
|
||||
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:2\n" +
|
||||
"a=sendrecv\n" +
|
||||
"a=sctp-port:5000\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs";
|
||||
|
||||
export const groupCallParticipantsFourOtherDevices = new Map([
|
||||
[
|
||||
new RoomMember("roomId0", "user1"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId0",
|
||||
{
|
||||
sessionId: "0",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"deviceId1",
|
||||
{
|
||||
sessionId: "1",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"deviceId2",
|
||||
{
|
||||
sessionId: "2",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
new RoomMember("roomId0", "user2"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId3",
|
||||
{
|
||||
sessionId: "0",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"deviceId4",
|
||||
{
|
||||
sessionId: "1",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
export const groupCallParticipantsOneOtherDevice = new Map([
|
||||
[
|
||||
new RoomMember("roomId1", "thisMember"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId0",
|
||||
{
|
||||
sessionId: "0",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
new RoomMember("roomId1", "opponentMember"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId1",
|
||||
{
|
||||
sessionId: "1",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,13 +46,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
it("should not permit falsey values for both parts", () => {
|
||||
try {
|
||||
new UnstableValue(null!, null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
expect(() => new UnstableValue(null!, null!)).toThrow("One of stable or unstable values must be supplied");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,12 +66,6 @@ describe("UnstableValue", () => {
|
||||
});
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
try {
|
||||
new UnstableValue("stable", null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
expect(() => new UnstableValue("stable", null!)).toThrow("Unstable value must be supplied");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ConnectionError } from "../../src/http-api/errors";
|
||||
import { ClientEvent, MatrixClient, Store } from "../../src/client";
|
||||
import { ToDeviceMessageQueue } from "../../src/ToDeviceMessageQueue";
|
||||
import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
let shouldFailSendToDevice: Boolean;
|
||||
let onSendToDeviceFailure: () => void;
|
||||
let onSendToDeviceSuccess: () => void;
|
||||
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
|
||||
|
||||
let store: Store;
|
||||
let mockClient: MatrixClient;
|
||||
let queue: ToDeviceMessageQueue;
|
||||
|
||||
beforeEach(() => {
|
||||
batch = {
|
||||
id: 0,
|
||||
txnId: "123",
|
||||
eventType: "m.dummy",
|
||||
batch: [],
|
||||
};
|
||||
|
||||
shouldFailSendToDevice = true;
|
||||
onSendToDeviceFailure = () => {};
|
||||
onSendToDeviceSuccess = () => {};
|
||||
resumeSync = (newState, oldState) => {
|
||||
shouldFailSendToDevice = false;
|
||||
mockClient.emit(ClientEvent.Sync, newState, oldState);
|
||||
};
|
||||
|
||||
store = new StubStore();
|
||||
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
return batch;
|
||||
});
|
||||
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
batch = null;
|
||||
});
|
||||
|
||||
mockClient = getMockClientWithEventEmitter({});
|
||||
mockClient.store = store;
|
||||
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
|
||||
if (shouldFailSendToDevice) {
|
||||
await Promise.reject(new ConnectionError("")).finally(() => {
|
||||
setTimeout(onSendToDeviceFailure, 0);
|
||||
});
|
||||
} else {
|
||||
await Promise.resolve({}).finally(() => {
|
||||
setTimeout(onSendToDeviceSuccess, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
queue = new ToDeviceMessageQueue(mockClient);
|
||||
});
|
||||
|
||||
it("resends queue after connectivity restored", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
resumeSync(SyncState.Syncing, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(2);
|
||||
};
|
||||
|
||||
onSendToDeviceSuccess = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
|
||||
expect(store.removeToDeviceBatch).toHaveBeenCalled();
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
it("does not resend queue if client sync still catching up", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
resumeSync(SyncState.Catchup, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
it("does not resend queue if connectivity restored after queue stopped", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
queue.stop();
|
||||
|
||||
resumeSync(SyncState.Syncing, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
});
|
||||
+192
-18
@@ -15,9 +15,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
import { OidcError } from "../../src/oidc/error";
|
||||
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
|
||||
|
||||
// keep to reset the fetch function after using MockHttpBackend
|
||||
// @ts-ignore private property
|
||||
const realAutoDiscoveryFetch: typeof global.fetch = AutoDiscovery.fetchFn;
|
||||
|
||||
describe("AutoDiscovery", function () {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
@@ -26,6 +34,10 @@ describe("AutoDiscovery", function () {
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
afterAll(() => {
|
||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
||||
});
|
||||
|
||||
it("should throw an error when no domain is specified", function () {
|
||||
getHttpBackend();
|
||||
return Promise.all([
|
||||
@@ -339,7 +351,7 @@ describe("AutoDiscovery", function () {
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["r0.0.1"],
|
||||
not_matrix_versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -368,7 +380,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
);
|
||||
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " + "m.homeserver", function () {
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for m.homeserver", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
@@ -376,7 +388,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -397,6 +409,10 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -412,7 +428,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -434,6 +450,54 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS with authentication error when authentication config is invalid", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.authentication": {
|
||||
invalid: true,
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "FAIL_ERROR",
|
||||
error: OidcError.Misconfigured,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -451,7 +515,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -496,7 +560,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -542,9 +606,9 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
@@ -589,9 +653,9 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
@@ -625,7 +689,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
);
|
||||
|
||||
it("should return SUCCESS when the identity server configuration is " + "verifiably accurate", function () {
|
||||
it("should return SUCCESS when the identity server configuration is verifiably accurate", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
@@ -633,12 +697,12 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/api/v1")
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -664,6 +728,10 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -671,7 +739,7 @@ describe("AutoDiscovery", function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS and preserve non-standard keys from the " + ".well-known response", function () {
|
||||
it("should return SUCCESS and preserve non-standard keys from the .well-known response", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
@@ -679,12 +747,12 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/api/v1")
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -716,6 +784,10 @@ describe("AutoDiscovery", function () {
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -794,4 +866,106 @@ describe("AutoDiscovery", function () {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should FAIL_ERROR for unsupported Matrix version", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.6.0"],
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("m.authentication", () => {
|
||||
const homeserverName = "example.org";
|
||||
const homeserverUrl = "https://chat.example.org/";
|
||||
const issuer = "https://auth.org/";
|
||||
|
||||
beforeAll(() => {
|
||||
// make these tests independent from fetch mocking above
|
||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.resetBehavior();
|
||||
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.1"] });
|
||||
|
||||
fetchMock.get("https://example.org/.well-known/matrix/client", {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.authentication": {
|
||||
issuer,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid authentication configuration", async () => {
|
||||
const config = makeDelegatedAuthConfig(issuer);
|
||||
|
||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
||||
|
||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
...config,
|
||||
signingKeys: [],
|
||||
account: undefined,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set state to error for invalid authentication configuration", async () => {
|
||||
const config = makeDelegatedAuthConfig(issuer);
|
||||
// authorization_code is required
|
||||
config.metadata.grant_types_supported = ["openid"];
|
||||
|
||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
||||
|
||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: OidcError.OpSupport,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
|
||||
import { M_TOPIC } from "../../src/@types/topic";
|
||||
import {
|
||||
@@ -25,6 +23,7 @@ import {
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
import { REFERENCE_RELATION } from "../../src/@types/extensible_events";
|
||||
|
||||
describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("ContentRepo", function () {
|
||||
it("should return a download URL if no width/height/resize are specified", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -44,21 +44,21 @@ describe("ContentRepo", function () {
|
||||
it("should return a thumbnail URL if a width/height/resize is specified", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
|
||||
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32#automade",
|
||||
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+78
-58
@@ -167,6 +167,38 @@ describe("Crypto", function () {
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
|
||||
const client = new TestClient("@alice:example.com", "deviceid").client;
|
||||
await client.initCrypto();
|
||||
|
||||
const event = new MatrixEvent({
|
||||
content: {},
|
||||
event_id: "$event_id",
|
||||
room_id: "!room_id",
|
||||
sender: "@bob:example.com",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {
|
||||
redacted_because: {
|
||||
content: {},
|
||||
event_id: "$redaction_event_id",
|
||||
redacts: "$event_id",
|
||||
room_id: "!room_id",
|
||||
origin_server_ts: 1234567890,
|
||||
sender: "@bob:example.com",
|
||||
type: "m.room.redaction",
|
||||
unsigned: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await event.attemptDecryption(client.crypto!);
|
||||
expect(event.isDecryptionFailure()).toBeFalsy();
|
||||
// since the redaction event isn't encrypted, the redacted_because
|
||||
// should be the same as in the original event
|
||||
expect(event.getRedactionEvent()).toEqual(event.getUnsigned().redacted_because);
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Session management", function () {
|
||||
@@ -222,6 +254,7 @@ describe("Crypto", function () {
|
||||
sendToDevice: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
isGuest: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
mockRoomList = {} as unknown as RoomList;
|
||||
|
||||
@@ -271,20 +304,24 @@ describe("Crypto", function () {
|
||||
|
||||
describe("Key requests", function () {
|
||||
let aliceClient: MatrixClient;
|
||||
let secondAliceClient: MatrixClient;
|
||||
let bobClient: MatrixClient;
|
||||
let claraClient: MatrixClient;
|
||||
|
||||
beforeEach(async function () {
|
||||
aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
|
||||
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
claraClient = new TestClient("@clara:example.com", "claradevice").client;
|
||||
await aliceClient.initCrypto();
|
||||
await secondAliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
await claraClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
aliceClient.stopClient();
|
||||
secondAliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
claraClient.stopClient();
|
||||
});
|
||||
@@ -344,12 +381,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -368,7 +400,7 @@ describe("Crypto", function () {
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
@@ -517,7 +549,7 @@ describe("Crypto", function () {
|
||||
aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceSendToDevice).toBeCalledTimes(1);
|
||||
expect(aliceSendToDevice).toHaveBeenCalledTimes(1);
|
||||
const txnId = aliceSendToDevice.mock.calls[0][2];
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
@@ -531,21 +563,21 @@ describe("Crypto", function () {
|
||||
// cancelAndResend will call sendToDevice twice:
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
// the second call to sendToDevice will be the key request
|
||||
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||
expect(aliceSendToDevice).toHaveBeenCalledTimes(3);
|
||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
});
|
||||
|
||||
it("should accept forwarded keys which it requested", async function () {
|
||||
it("should accept forwarded keys it requested from one of its own user's other devices", async function () {
|
||||
const encryptionCfg = {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
const bobRoom = new Room(roomId, secondAliceClient, "@alice:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
secondAliceClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await secondAliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
@@ -580,20 +612,16 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(secondAliceClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||
secondAliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
secondAliceClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const cryptoStore = secondAliceClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
@@ -609,7 +637,7 @@ describe("Crypto", function () {
|
||||
state: RoomKeyRequestState.Sent,
|
||||
});
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
|
||||
const bobDecryptor = secondAliceClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
|
||||
|
||||
const decryptEventsPromise = Promise.all(
|
||||
events.map((ev) => {
|
||||
@@ -618,7 +646,7 @@ describe("Crypto", function () {
|
||||
);
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await secondAliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
@@ -687,12 +715,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -722,7 +745,7 @@ describe("Crypto", function () {
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("should accept forwarded keys from one of its own user's other devices", async function () {
|
||||
it("should not accept requested forwarded keys from other users", async function () {
|
||||
const encryptionCfg = {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
@@ -767,40 +790,43 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
|
||||
expect(outgoingReq).toBeDefined();
|
||||
await cryptoStore.updateOutgoingRoomKeyRequest(outgoingReq!.requestId, RoomKeyRequestState.Unsent, {
|
||||
state: RoomKeyRequestState.Sent,
|
||||
});
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
|
||||
|
||||
const decryptEventsPromise = Promise.all(
|
||||
events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}),
|
||||
);
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = bobClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
|
||||
ksEvent.event.sender = aliceClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, aliceClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(key).toBeNull();
|
||||
});
|
||||
|
||||
it("should not accept unexpected forwarded keys for a room it's in", async function () {
|
||||
@@ -851,12 +877,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -965,7 +986,6 @@ describe("Crypto", function () {
|
||||
jest.setTimeout(10000);
|
||||
const client = new TestClient("@a:example.com", "dev").client;
|
||||
await client.initCrypto();
|
||||
client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.isCrossSigningReady = async () => false;
|
||||
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
@@ -993,7 +1013,7 @@ describe("Crypto", function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
|
||||
ensureOlmSessionsForDevices.mockResolvedValue({});
|
||||
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
|
||||
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
|
||||
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
|
||||
result.plaintext = { type: 0, body: JSON.stringify(payload) };
|
||||
|
||||
@@ -102,9 +102,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
expect(getCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(getCrossSigningKeyCache).toHaveBeenLastCalledWith(type, expect.any(String));
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -115,10 +116,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenLastCalledWith(type, testKey);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("DeviceList", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should have an outdated devicelist on an invalidation while an " + "update is in progress", function () {
|
||||
it("should have an outdated devicelist on an invalidation while an update is in progress", async function () {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList("@test1:sw1v.org");
|
||||
@@ -148,7 +148,8 @@ describe("DeviceList", function () {
|
||||
dl.invalidateUserDeviceList("@test1:sw1v.org");
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
dl.saveIfDirty()
|
||||
await dl
|
||||
.saveIfDirty()
|
||||
.then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
@@ -159,12 +160,13 @@ describe("DeviceList", function () {
|
||||
});
|
||||
return prom1;
|
||||
})
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.mockReset();
|
||||
const dl2 = createTestDeviceList();
|
||||
await dl2.load();
|
||||
const queryDefer3 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer3.promise);
|
||||
|
||||
@@ -196,7 +198,7 @@ describe("DeviceList", function () {
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
|
||||
import type { DeviceInfoMap } from "../../../../src/crypto/DeviceList";
|
||||
import "../../../olm-loader";
|
||||
import type { OutboundGroupSession } from "@matrix-org/olm";
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
@@ -33,6 +34,8 @@ import { ClientEvent, MatrixClient, RoomMember } from "../../../../src";
|
||||
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
|
||||
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
|
||||
import { recursiveMapToObject } from "../../../../src/utils";
|
||||
import { sleep } from "../../../../src/utils";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
@@ -58,6 +61,12 @@ describe("MegolmDecryption", function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
mockCrypto = testUtils.mock(Crypto, "Crypto") as MockedObject<Crypto>;
|
||||
|
||||
// @ts-ignore assigning to readonly prop
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
|
||||
mockBaseApis = {
|
||||
claimOneTimeKeys: jest.fn(),
|
||||
sendToDevice: jest.fn(),
|
||||
@@ -175,14 +184,22 @@ describe("MegolmDecryption", function () {
|
||||
const deviceInfo = {} as DeviceInfo;
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
"@alice:foo": {
|
||||
alidevice: {
|
||||
sessionId: "alisession",
|
||||
device: new DeviceInfo("alidevice"),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
"@alice:foo",
|
||||
new Map([
|
||||
[
|
||||
"alidevice",
|
||||
{
|
||||
sessionId: "alisession",
|
||||
device: new DeviceInfo("alidevice"),
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
@@ -203,7 +220,7 @@ describe("MegolmDecryption", function () {
|
||||
.then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
||||
expect(mockOlmLib.encryptMessageForDevice).toHaveBeenCalledTimes(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
||||
const payload = call[6];
|
||||
@@ -314,10 +331,6 @@ describe("MegolmDecryption", function () {
|
||||
let olmDevice: OlmDevice;
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-ignore assigning to readonly prop
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
@@ -353,11 +366,7 @@ describe("MegolmDecryption", function () {
|
||||
} as unknown as DeviceInfo;
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(
|
||||
Promise.resolve({
|
||||
"@alice:home.server": {
|
||||
aliceDevice: aliceDeviceInfo,
|
||||
},
|
||||
}),
|
||||
Promise.resolve(new Map([["@alice:home.server", new Map([["aliceDevice", aliceDeviceInfo]])]])),
|
||||
);
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
@@ -387,8 +396,10 @@ describe("MegolmDecryption", function () {
|
||||
mockCrypto.baseApis = mockBaseApis;
|
||||
|
||||
mockRoom = {
|
||||
roomId: ROOM_ID,
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue([{ userId: "@alice:home.server" }]),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(false),
|
||||
} as unknown as Room;
|
||||
});
|
||||
|
||||
@@ -513,6 +524,87 @@ describe("MegolmDecryption", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareToEncrypt", () => {
|
||||
let megolm: MegolmEncryptionClass;
|
||||
let room: jest.Mocked<Room>;
|
||||
|
||||
const deviceMap: DeviceInfoMap = new Map([
|
||||
[
|
||||
"user-a",
|
||||
new Map([
|
||||
["device-a", new DeviceInfo("device-a")],
|
||||
["device-b", new DeviceInfo("device-b")],
|
||||
["device-c", new DeviceInfo("device-c")],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"user-b",
|
||||
new Map([
|
||||
["device-d", new DeviceInfo("device-d")],
|
||||
["device-e", new DeviceInfo("device-e")],
|
||||
["device-f", new DeviceInfo("device-f")],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"user-c",
|
||||
new Map([
|
||||
["device-g", new DeviceInfo("device-g")],
|
||||
["device-h", new DeviceInfo("device-h")],
|
||||
["device-i", new DeviceInfo("device-i")],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
room = testUtils.mock(Room, "Room") as jest.Mocked<Room>;
|
||||
room.getEncryptionTargetMembers.mockImplementation(async () => [
|
||||
new RoomMember(room.roomId, "@user:example.org"),
|
||||
]);
|
||||
room.getBlacklistUnverifiedDevices.mockReturnValue(false);
|
||||
|
||||
mockCrypto.downloadKeys.mockImplementation(async () => deviceMap);
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockImplementation(() => new DeviceTrustLevel(true, true, true, true));
|
||||
|
||||
const olmDevice = new OlmDevice(new MemoryCryptoStore());
|
||||
megolm = new MegolmEncryptionClass({
|
||||
userId: "@user:id",
|
||||
deviceId: "12345",
|
||||
crypto: mockCrypto,
|
||||
olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: room.roomId,
|
||||
config: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_ms: 9_999_999,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("checks each device", async () => {
|
||||
megolm.prepareToEncrypt(room);
|
||||
//@ts-ignore private member access, gross
|
||||
await megolm.encryptionPreparation?.promise;
|
||||
|
||||
for (const [userId, devices] of deviceMap) {
|
||||
for (const deviceId of devices.keys()) {
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("is cancellable", async () => {
|
||||
const stop = megolm.prepareToEncrypt(room);
|
||||
|
||||
const before = mockCrypto.checkDeviceTrust.mock.calls.length;
|
||||
stop();
|
||||
|
||||
// Ensure that no more devices were checked after cancellation.
|
||||
await sleep(10);
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledTimes(before);
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies devices that have been blocked", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
|
||||
@@ -580,20 +672,20 @@ describe("MegolmDecryption", function () {
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"];
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"];
|
||||
expect(contentMap).toStrictEqual({
|
||||
"@bob:example.com": {
|
||||
bobdevice1: {
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["session_id"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["org.matrix.msgid"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["session_id"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["org.matrix.msgid"];
|
||||
expect(recursiveMapToObject(contentMap)).toStrictEqual({
|
||||
["@bob:example.com"]: {
|
||||
["bobdevice1"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: "m.unverified",
|
||||
reason: "The sender has disabled encrypting to unverified devices.",
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
bobdevice2: {
|
||||
["bobdevice2"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: "m.blacklisted",
|
||||
@@ -761,10 +853,10 @@ describe("MegolmDecryption", function () {
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"];
|
||||
expect(contentMap).toStrictEqual({
|
||||
"@bob:example.com": {
|
||||
bobdevice: {
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice")?.["org.matrix.msgid"];
|
||||
expect(recursiveMapToObject(contentMap)).toStrictEqual({
|
||||
["@bob:example.com"]: {
|
||||
["bobdevice"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
code: "m.no_olm",
|
||||
reason: "Unable to establish a secure channel.",
|
||||
|
||||
@@ -146,18 +146,21 @@ describe("OlmDevice", function () {
|
||||
});
|
||||
},
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
const devicesByUser = new Map([
|
||||
[
|
||||
"@bob:example.com",
|
||||
[
|
||||
DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
},
|
||||
},
|
||||
},
|
||||
"ABCDEFG",
|
||||
),
|
||||
"ABCDEFG",
|
||||
),
|
||||
],
|
||||
],
|
||||
};
|
||||
]);
|
||||
|
||||
// start two tasks that try to ensure that there's an olm session
|
||||
const promises = Promise.all([
|
||||
@@ -218,12 +221,8 @@ describe("OlmDevice", function () {
|
||||
// There's no required ordering of devices per user, so here we
|
||||
// create two different orderings so that each task reserves a
|
||||
// device the other task needs before continuing.
|
||||
const devicesByUserAB = {
|
||||
"@bob:example.com": [deviceBobA, deviceBobB],
|
||||
};
|
||||
const devicesByUserBA = {
|
||||
"@bob:example.com": [deviceBobB, deviceBobA],
|
||||
};
|
||||
const devicesByUserAB = new Map([["@bob:example.com", [deviceBobA, deviceBobB]]]);
|
||||
const devicesByUserBA = new Map([["@bob:example.com", [deviceBobB, deviceBobA]]]);
|
||||
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
|
||||
|
||||
|
||||
@@ -456,6 +456,7 @@ describe("MegolmBackup", function () {
|
||||
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (numCalls === 1) {
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
@@ -482,6 +483,7 @@ describe("MegolmBackup", function () {
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
|
||||
@@ -24,10 +24,11 @@ import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
|
||||
import { CryptoEvent, IBootstrapCrossSigningOpts } from "../../../src/crypto";
|
||||
import { CryptoEvent } from "../../../src/crypto";
|
||||
import { IDevice } from "../../../src/crypto/deviceinfo";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BootstrapCrossSigningOpts } from "../../../src/crypto-api";
|
||||
|
||||
const PUSH_RULES_RESPONSE: Response = {
|
||||
method: "GET",
|
||||
@@ -146,7 +147,7 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends { [k: string]: any }>(): Promise<T | null> => ({} as T);
|
||||
const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
const authUploadDeviceSigningKeys: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
await func({});
|
||||
};
|
||||
|
||||
@@ -1148,6 +1149,6 @@ describe("userHasCrossSigningKeys", function () {
|
||||
|
||||
it("throws an error if crypto is disabled", () => {
|
||||
aliceClient["cryptoBackend"] = undefined;
|
||||
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
|
||||
expect(() => aliceClient.userHasCrossSigningKeys()).toThrow("encryption disabled");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { DeviceVerification } from "../../../src";
|
||||
import { deviceInfoToDevice } from "../../../src/crypto/device-converter";
|
||||
|
||||
describe("device-converter", () => {
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId = "xcvf";
|
||||
|
||||
// All parameters for DeviceInfo initialization
|
||||
const keys = {
|
||||
[`ed25519:${deviceId}`]: "key1",
|
||||
[`curve25519:${deviceId}`]: "key2",
|
||||
};
|
||||
const algorithms = ["algo1", "algo2"];
|
||||
const verified = DeviceVerification.Verified;
|
||||
const signatures = { [userId]: { [deviceId]: "sign1" } };
|
||||
const displayName = "display name";
|
||||
const unsigned = {
|
||||
device_display_name: displayName,
|
||||
};
|
||||
|
||||
describe("deviceInfoToDevice", () => {
|
||||
it("should convert a DeviceInfo to a Device", () => {
|
||||
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified, signatures, unsigned }, deviceId);
|
||||
const device = deviceInfoToDevice(deviceInfo, userId);
|
||||
|
||||
expect(device.deviceId).toBe(deviceId);
|
||||
expect(device.userId).toBe(userId);
|
||||
expect(device.verified).toBe(verified);
|
||||
expect(device.getIdentityKey()).toBe(keys[`curve25519:${deviceId}`]);
|
||||
expect(device.getFingerprint()).toBe(keys[`ed25519:${deviceId}`]);
|
||||
expect(device.displayName).toBe(displayName);
|
||||
});
|
||||
|
||||
it("should add empty signatures", () => {
|
||||
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified }, deviceId);
|
||||
const device = deviceInfoToDevice(deviceInfo, userId);
|
||||
|
||||
expect(device.signatures.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
import "../../olm-loader";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { IObject } from "../../../src/crypto/olmlib";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { makeTestClients } from "./verification/util";
|
||||
@@ -25,10 +24,10 @@ import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
|
||||
import { ISecretStorageKeyInfo } from "../../../src/crypto/api";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { ISignatures } from "../../../src/@types/signed";
|
||||
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
|
||||
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string; deviceId: string },
|
||||
@@ -45,7 +44,7 @@ async function makeTestClient(
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue({});
|
||||
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -149,22 +148,14 @@ describe("Secrets", function () {
|
||||
it("should throw if given a key that doesn't exist", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
|
||||
// should be able to use expect(...).toThrow() but mocha still fails
|
||||
// the test even when it throws for reasons I have no inclination to debug
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {}
|
||||
await expect(alice.storeSecret("foo", "bar", ["this secret does not exist"])).rejects.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", []);
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {}
|
||||
await expect(alice.storeSecret("foo", "bar", [])).rejects.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
@@ -215,10 +206,7 @@ describe("Secrets", function () {
|
||||
it("should refuse to encrypt if no keys given and no default key", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar");
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {}
|
||||
await expect(alice.storeSecret("foo", "bar")).rejects.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
@@ -274,7 +262,7 @@ describe("Secrets", function () {
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
@@ -541,7 +529,9 @@ describe("Secrets", function () {
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
|
||||
const keyInfo = alice
|
||||
.getAccountData("m.secret_storage.key.key_id")!
|
||||
.getContent<SecretStorageKeyDescription>();
|
||||
expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
algorithm: "m.pbkdf2",
|
||||
|
||||
@@ -121,12 +121,12 @@ describe("SAS verification", function () {
|
||||
|
||||
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
@@ -144,7 +144,7 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
@@ -169,13 +169,14 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([alice.stop(), bob.stop()]);
|
||||
|
||||
@@ -186,10 +187,14 @@ describe("SAS verification", function () {
|
||||
let macMethod;
|
||||
let keyAgreement;
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function (type, map) {
|
||||
bob.client.sendToDevice = async (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.message_authentication_code;
|
||||
keyAgreement = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
};
|
||||
@@ -215,7 +220,7 @@ describe("SAS verification", function () {
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256.v2");
|
||||
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
@@ -237,7 +242,7 @@ describe("SAS verification", function () {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
|
||||
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
|
||||
"hkdf-hmac-sha256",
|
||||
];
|
||||
}
|
||||
@@ -246,7 +251,9 @@ describe("SAS verification", function () {
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)!
|
||||
.get(alice.client.deviceId!)!.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
@@ -291,14 +298,18 @@ describe("SAS verification", function () {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
|
||||
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
|
||||
"hmac-sha256",
|
||||
];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)!
|
||||
.get(alice.client.deviceId!)!.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
@@ -363,6 +374,12 @@ describe("SAS verification", function () {
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
const bobDeviceVerificationStatus = (await alice.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@bob:example.com", "Dynabook"))!;
|
||||
expect(bobDeviceVerificationStatus.localVerified).toBe(true);
|
||||
expect(bobDeviceVerificationStatus.crossSigningVerified).toBe(false);
|
||||
|
||||
const aliceTrust = bob.client.checkUserTrust("@alice:example.com");
|
||||
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(aliceTrust.isTofu()).toBeTruthy();
|
||||
@@ -370,6 +387,17 @@ describe("SAS verification", function () {
|
||||
const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
const aliceDeviceVerificationStatus = (await bob.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@alice:example.com", "Osborne2"))!;
|
||||
expect(aliceDeviceVerificationStatus.localVerified).toBe(true);
|
||||
expect(aliceDeviceVerificationStatus.crossSigningVerified).toBe(false);
|
||||
|
||||
const unknownDeviceVerificationStatus = await bob.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@alice:example.com", "xyz");
|
||||
expect(unknownDeviceVerificationStatus).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,7 +482,7 @@ describe("SAS verification", function () {
|
||||
);
|
||||
};
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
bob.client.crypto!.setDeviceVerification = jest.fn();
|
||||
@@ -472,7 +500,7 @@ describe("SAS verification", function () {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
@@ -491,7 +519,7 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
@@ -515,7 +543,7 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "../../../olm-loader";
|
||||
|
||||
import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("crypto.setDeviceVerification", () => {
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId1 = "device1";
|
||||
let client: MatrixClient;
|
||||
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient(userId, deviceId1).client;
|
||||
await client.initCrypto();
|
||||
});
|
||||
|
||||
it("client should provide crypto", () => {
|
||||
expect(client.crypto).not.toBeUndefined();
|
||||
});
|
||||
|
||||
describe("when setting an own device as verified", () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests");
|
||||
await client.crypto!.setDeviceVerification(userId, deviceId1, true);
|
||||
});
|
||||
|
||||
it("cancelAndResendAllOutgoingKeyRequests should be called", () => {
|
||||
expect(client.crypto!.cancelAndResendAllOutgoingKeyRequests).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
|
||||
import { Room, RoomEvent } from "../../../../src/models/room";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
|
||||
import { MatrixClient, ClientEvent, ICreateClientOpts, SendToDeviceContentMap } from "../../../../src/client";
|
||||
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
@@ -36,16 +36,16 @@ export async function makeTestClients(
|
||||
const clientMap: Record<string, Record<string, MatrixClient>> = {};
|
||||
const makeSendToDevice =
|
||||
(matrixClient: MatrixClient): MatrixClient["sendToDevice"] =>
|
||||
async (type, map) => {
|
||||
async (type: string, contentMap: SendToDeviceContentMap) => {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
for (const [userId, deviceMessages] of contentMap) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
for (const [deviceId, message] of deviceMessages) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: msg,
|
||||
content: message,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted()
|
||||
|
||||
@@ -25,6 +25,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
import { MapWithDefault } from "../../../../src/utils";
|
||||
|
||||
type MockClient = MatrixClient & {
|
||||
popEvents: () => MatrixEvent[];
|
||||
@@ -33,7 +34,9 @@ type MockClient = MatrixClient & {
|
||||
function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
let counter = 1;
|
||||
let events: MatrixEvent[] = [];
|
||||
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
|
||||
const deviceEvents: MapWithDefault<string, MapWithDefault<string, MatrixEvent[]>> = new MapWithDefault(
|
||||
() => new MapWithDefault(() => []),
|
||||
);
|
||||
return {
|
||||
getUserId() {
|
||||
return userId;
|
||||
@@ -58,15 +61,11 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
},
|
||||
|
||||
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
|
||||
for (const userId of Object.keys(msgMap)) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
sendToDevice(type: string, msgMap: Map<string, Map<string, IContent>>) {
|
||||
for (const [userId, deviceMessages] of msgMap) {
|
||||
for (const [deviceId, content] of deviceMessages) {
|
||||
const event = new MatrixEvent({ content, type });
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
deviceEvents.getOrCreate(userId).getOrCreate(deviceId).push(event);
|
||||
}
|
||||
}
|
||||
return Promise.resolve({});
|
||||
@@ -79,14 +78,9 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
return e;
|
||||
},
|
||||
|
||||
// @ts-ignore special testing fn
|
||||
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
|
||||
const forDevice = deviceEvents[userId];
|
||||
const events = forDevice && forDevice[deviceId];
|
||||
const result = events || [];
|
||||
if (events) {
|
||||
delete forDevice[deviceId];
|
||||
}
|
||||
const result = deviceEvents.get(userId)?.get(deviceId) || [];
|
||||
deviceEvents?.get(userId)?.delete(deviceId);
|
||||
return result;
|
||||
},
|
||||
} as unknown as MockClient;
|
||||
|
||||
@@ -204,9 +204,14 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = {
|
||||
"@alice:example.org": { "*": { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
|
||||
]);
|
||||
|
||||
const expectedRequestData = {
|
||||
["@alice:example.org"]: { ["*"]: { hello: "alice!" } },
|
||||
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
@@ -214,7 +219,7 @@ describe("RoomWidgetClient", () => {
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await client.sendToDevice("org.example.foo", unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends unencrypted (queueToDevice)", async () => {
|
||||
@@ -229,7 +234,7 @@ describe("RoomWidgetClient", () => {
|
||||
],
|
||||
};
|
||||
await client.queueToDevice(batch);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends encrypted (encryptAndSendToDevices)", async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
DuplicateStrategy,
|
||||
@@ -79,6 +81,7 @@ describe("EventTimelineSet", () => {
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
room = new Room(roomId, client, userA);
|
||||
eventTimelineSet = new EventTimelineSet(room);
|
||||
eventTimeline = new EventTimeline(eventTimelineSet);
|
||||
@@ -141,13 +144,6 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
describe("addEventToTimeline", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("Adds event to timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -167,6 +163,42 @@ describe("EventTimelineSet", () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
|
||||
// If threads are disabled all events go into the main timeline
|
||||
mocked(client.supportsThreads).mockReturnValue(true);
|
||||
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
|
||||
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents()).toHaveLength(1);
|
||||
const [event] = liveTimeline.getEvents();
|
||||
const reactions = eventTimelineSet.relations!.getChildEventsForEvent(
|
||||
event.getId()!,
|
||||
"m.annotation",
|
||||
"m.reaction",
|
||||
)!;
|
||||
const relations = reactions.getRelations();
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0].getId()).toBe(reactionEvent.getId());
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEventToTimeline (thread timeline)", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
|
||||
const eventTimelineSet2 = new EventTimelineSet(room);
|
||||
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
|
||||
@@ -176,7 +208,7 @@ describe("EventTimelineSet", () => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
}).toThrowError();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should not add a threaded reply to the main room timeline", () => {
|
||||
@@ -196,7 +228,14 @@ describe("EventTimelineSet", () => {
|
||||
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
const normalMessage = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: "Hello!",
|
||||
event: true,
|
||||
});
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -308,13 +347,13 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("should throw if timeline set has no room", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(undefined, {}, client);
|
||||
expect(() => eventTimelineSet.canContain(messageEvent)).toThrowError();
|
||||
expect(() => eventTimelineSet.canContain(messageEvent)).toThrow();
|
||||
});
|
||||
|
||||
it("should return false if timeline set is for thread but event is not threaded", () => {
|
||||
@@ -335,7 +374,9 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
it("should return true if the timeline set is not for a thread and the event is a thread root", () => {
|
||||
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client);
|
||||
messageEvent.setThread(thread);
|
||||
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExtensibleEventType, IPartialEvent } from "../../../src/@types/extensible_events";
|
||||
import { ExtensibleEvent } from "../../../src/extensible_events_v1/ExtensibleEvent";
|
||||
|
||||
class MockEvent extends ExtensibleEvent<any> {
|
||||
public constructor(wireEvent: IPartialEvent<any>) {
|
||||
super(wireEvent);
|
||||
}
|
||||
|
||||
public serialize(): IPartialEvent<object> {
|
||||
throw new Error("Not implemented for tests");
|
||||
}
|
||||
|
||||
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
|
||||
throw new Error("Not implemented for tests");
|
||||
}
|
||||
}
|
||||
|
||||
describe("ExtensibleEvent", () => {
|
||||
it("should expose the wire event directly", () => {
|
||||
const input: IPartialEvent<any> = { type: "org.example.custom", content: { hello: "world" } };
|
||||
const event = new MockEvent(input);
|
||||
expect(event.wireFormat).toBe(input);
|
||||
expect(event.wireContent).toBe(input.content);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExtensibleAnyMessageEventContent,
|
||||
IPartialEvent,
|
||||
M_HTML,
|
||||
M_MESSAGE,
|
||||
M_TEXT,
|
||||
} from "../../../src/@types/extensible_events";
|
||||
import { MessageEvent } from "../../../src/extensible_events_v1/MessageEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("MessageEvent", () => {
|
||||
it("should parse m.text", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_TEXT.name]: "Text here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBeFalsy();
|
||||
expect(message.renderings.length).toBe(1);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should parse m.html", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_TEXT.name]: "Text here",
|
||||
[M_HTML.name]: "HTML here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(2);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should parse m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: [
|
||||
{ body: "Text here", mimetype: "text/plain" },
|
||||
{ body: "HTML here", mimetype: "text/html" },
|
||||
{ body: "MD here", mimetype: "text/markdown" },
|
||||
],
|
||||
|
||||
// These should be ignored
|
||||
[M_TEXT.name]: "WRONG Text here",
|
||||
[M_HTML.name]: "WRONG HTML here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(3);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to parse missing text", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
hello: "world",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(
|
||||
new InvalidEventError("Missing textual representation for event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse missing plain text in m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: [{ body: "HTML here", mimetype: "text/html" }],
|
||||
},
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(
|
||||
new InvalidEventError("m.message is missing a plain text representation"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse non-array m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: "invalid",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array"));
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a legacy fallback", () => {
|
||||
const message = MessageEvent.from("Text here", "HTML here");
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(2);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
|
||||
const serialized = message.serialize();
|
||||
expect(serialized.type).toBe("m.room.message");
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_MESSAGE.name]: [
|
||||
{ body: "Text here", mimetype: "text/plain" },
|
||||
{ body: "HTML here", mimetype: "text/html" },
|
||||
],
|
||||
body: "Text here",
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "HTML here",
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize non-html content to a legacy fallback", () => {
|
||||
const message = MessageEvent.from("Text here");
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.renderings.length).toBe(1);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
|
||||
const serialized = message.serialize();
|
||||
expect(serialized.type).toBe("m.room.message");
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Text here",
|
||||
body: "Text here",
|
||||
msgtype: "m.text",
|
||||
format: undefined,
|
||||
formatted_body: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { PollEndEventContent, M_POLL_END } from "../../../src/@types/polls";
|
||||
import { IPartialEvent, REFERENCE_RELATION, M_TEXT } from "../../../src/@types/extensible_events";
|
||||
import { PollEndEvent } from "../../../src/extensible_events_v1/PollEndEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("PollEndEvent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse a poll closure", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
},
|
||||
};
|
||||
const event = new PollEndEvent(input);
|
||||
expect(event.pollEventId).toBe("$poll");
|
||||
expect(event.closingMessage.text).toBe("Poll closed");
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship event ID", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse an improper relationship", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "org.example.not-relationship",
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll end event", () => {
|
||||
const event = PollEndEvent.from("$poll", "Poll closed");
|
||||
expect(event.pollEventId).toBe("$poll");
|
||||
expect(event.closingMessage.text).toBe("Poll closed");
|
||||
|
||||
const serialized = event.serialize();
|
||||
expect(M_POLL_END.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { M_TEXT, IPartialEvent, REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import {
|
||||
M_POLL_START,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
PollResponseEventContent,
|
||||
M_POLL_RESPONSE,
|
||||
} from "../../../src/@types/polls";
|
||||
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
import { PollResponseEvent } from "../../../src/extensible_events_v1/PollResponseEvent";
|
||||
|
||||
const SAMPLE_POLL = new PollStartEvent({
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("PollResponseEvent", () => {
|
||||
it("should parse a poll response", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["one"]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship event ID", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse an improper relationship", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "org.example.not-relationship",
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
describe("validateAgainst", () => {
|
||||
it("should spoil the vote when no answers", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are empty", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are not strings", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [1, 2, 3],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
describe("consumer usage", () => {
|
||||
it("should spoil the vote when invalid answers are given", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["A", "B", "C"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false); // it won't know better
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should truncate answers to the poll max selections", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one", "two", "thr"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false); // it won't know better
|
||||
expect(response.answerIds).toMatchObject(["one", "two", "thr"]);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["one", "two"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll response event", () => {
|
||||
const response = PollResponseEvent.from(["A", "B", "C"], "$poll");
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["A", "B", "C"]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
|
||||
const serialized = response.serialize();
|
||||
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["A", "B", "C"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize a spoiled vote", () => {
|
||||
const response = PollResponseEvent.from([], "$poll");
|
||||
expect(response.spoiled).toBe(true);
|
||||
expect(response.answerIds).toMatchObject([]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
|
||||
const serialized = response.serialize();
|
||||
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { M_TEXT, IPartialEvent } from "../../../src/@types/extensible_events";
|
||||
import {
|
||||
M_POLL_START,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
PollAnswer,
|
||||
PollStartEventContent,
|
||||
M_POLL_KIND_UNDISCLOSED,
|
||||
} from "../../../src/@types/polls";
|
||||
import { PollStartEvent, PollAnswerSubevent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("PollAnswerSubevent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse an answer representation", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
id: "one",
|
||||
[M_TEXT.name]: "ONE",
|
||||
},
|
||||
};
|
||||
const answer = new PollAnswerSubevent(input);
|
||||
expect(answer.id).toBe("one");
|
||||
expect(answer.text).toBe("ONE");
|
||||
});
|
||||
|
||||
it("should fail to parse answers without an ID", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
[M_TEXT.name]: "ONE",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollAnswerSubevent(input)).toThrow(
|
||||
new InvalidEventError("Answer ID must be a non-empty string"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse answers without text", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
id: "one",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollAnswerSubevent(input)).toThrow(); // we don't check message - that'll be MessageEvent's problem
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a placeholder representation", () => {
|
||||
const answer = PollAnswerSubevent.from("one", "ONE");
|
||||
expect(answer.id).toBe("one");
|
||||
expect(answer.text).toBe("ONE");
|
||||
|
||||
const serialized = answer.serialize();
|
||||
expect(serialized.type).toBe("org.matrix.sdk.poll.answer");
|
||||
expect(serialized.content).toMatchObject({
|
||||
id: "one",
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("PollStartEvent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse a poll", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.question).toBeDefined();
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
|
||||
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.id === "one" && a.text === "ONE")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.id === "two" && a.text === "TWO")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.id === "thr" && a.text === "THR")).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing question", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("A question is required"));
|
||||
});
|
||||
|
||||
it("should fail to parse non-array answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: "one",
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("Poll answers must be an array"));
|
||||
});
|
||||
|
||||
it("should fail to parse invalid answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [{ id: "one" }, { [M_TEXT.name]: "TWO" }],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(); // error tested by PollAnswerSubevent tests
|
||||
});
|
||||
|
||||
it("should fail to parse lack of answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("No answers available"));
|
||||
});
|
||||
|
||||
it("should truncate answers at 20", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
{ id: "04", [M_TEXT.name]: "D" },
|
||||
{ id: "05", [M_TEXT.name]: "E" },
|
||||
{ id: "06", [M_TEXT.name]: "F" },
|
||||
{ id: "07", [M_TEXT.name]: "G" },
|
||||
{ id: "08", [M_TEXT.name]: "H" },
|
||||
{ id: "09", [M_TEXT.name]: "I" },
|
||||
{ id: "10", [M_TEXT.name]: "J" },
|
||||
{ id: "11", [M_TEXT.name]: "K" },
|
||||
{ id: "12", [M_TEXT.name]: "L" },
|
||||
{ id: "13", [M_TEXT.name]: "M" },
|
||||
{ id: "14", [M_TEXT.name]: "N" },
|
||||
{ id: "15", [M_TEXT.name]: "O" },
|
||||
{ id: "16", [M_TEXT.name]: "P" },
|
||||
{ id: "17", [M_TEXT.name]: "Q" },
|
||||
{ id: "18", [M_TEXT.name]: "R" },
|
||||
{ id: "19", [M_TEXT.name]: "S" },
|
||||
{ id: "20", [M_TEXT.name]: "T" },
|
||||
{ id: "FAIL", [M_TEXT.name]: "U" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.answers.length).toBe(20);
|
||||
expect(poll.answers.some((a) => a.id === "FAIL")).toBe(false);
|
||||
});
|
||||
|
||||
it("should infer a kind from unknown kinds", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: "org.example.custom.poll.kind",
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBe("org.example.custom.poll.kind");
|
||||
});
|
||||
|
||||
it("should infer a kind from missing kinds", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll start event", () => {
|
||||
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], M_POLL_KIND_DISCLOSED, 2);
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
|
||||
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
|
||||
|
||||
// Ids are non-empty and unique
|
||||
expect(poll.answers[0].id).toHaveLength(16);
|
||||
expect(poll.answers[1].id).toHaveLength(16);
|
||||
expect(poll.answers[2].id).toHaveLength(16);
|
||||
expect(poll.answers[0].id).not.toEqual(poll.answers[1].id);
|
||||
expect(poll.answers[0].id).not.toEqual(poll.answers[2].id);
|
||||
expect(poll.answers[1].id).not.toEqual(poll.answers[2].id);
|
||||
|
||||
const serialized = poll.serialize();
|
||||
expect(M_POLL_START.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
|
||||
[M_POLL_START.name]: {
|
||||
question: {
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
},
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
// M_TEXT tested by MessageEvent tests
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize to a custom kind poll start event", () => {
|
||||
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], "org.example.poll.kind", 2);
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBe("org.example.poll.kind");
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
|
||||
|
||||
const serialized = poll.serialize();
|
||||
expect(M_POLL_START.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
|
||||
[M_POLL_START.name]: {
|
||||
question: {
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
},
|
||||
kind: "org.example.poll.kind",
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
// M_MESSAGE tested by MessageEvent tests
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { NamespacedValue } from "matrix-events-sdk";
|
||||
|
||||
import { isEventTypeSame } from "../../../src/@types/extensible_events";
|
||||
|
||||
describe("isEventTypeSame", () => {
|
||||
it("should match string and string", () => {
|
||||
const a = "org.example.message-like";
|
||||
const b = "org.example.different";
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match string and namespace", () => {
|
||||
const a = "org.example.message-like";
|
||||
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match namespace and namespace", () => {
|
||||
const a = new NamespacedValue<string, string>("org.example.stable1", "org.example.unstable1");
|
||||
const b = new NamespacedValue<string, string>("org.example.stable2", "org.example.unstable2");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.name, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.altName, a)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.name)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.altName)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match namespaces of different pointers", () => {
|
||||
const a = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, a)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.name, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.altName, a)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.name)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.altName)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,6 @@ exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`]
|
||||
"params": {
|
||||
"access_token": "token",
|
||||
},
|
||||
"path": "/_matrix/media/r0/upload",
|
||||
"path": "/_matrix/media/v3/upload",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { defer, QueryDict } from "../../../src/utils";
|
||||
import { logger } from "../../../src/logger";
|
||||
|
||||
describe("FetchHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
@@ -235,4 +239,86 @@ describe("FetchHttpApi", () => {
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUrl()", () => {
|
||||
const localBaseUrl = "http://baseurl";
|
||||
const baseUrlWithTrailingSlash = "http://baseurl/";
|
||||
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn });
|
||||
};
|
||||
|
||||
type TestParams = {
|
||||
path: string;
|
||||
queryParams?: QueryDict;
|
||||
prefix?: string;
|
||||
baseUrl?: string;
|
||||
};
|
||||
type TestCase = [TestParams, string];
|
||||
const queryParams: QueryDict = {
|
||||
test1: 99,
|
||||
test2: ["a", "b"],
|
||||
};
|
||||
const testPrefix = "/just/testing";
|
||||
const testUrl = "http://justtesting.com";
|
||||
const testUrlWithTrailingSlash = "http://justtesting.com/";
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
[{ path: "/terms" }, `${localBaseUrl}${prefix}/terms`],
|
||||
[{ path: "/terms", queryParams }, `${localBaseUrl}${prefix}/terms?test1=99&test2=a&test2=b`],
|
||||
[{ path: "/terms", prefix: testPrefix }, `${localBaseUrl}${testPrefix}/terms`],
|
||||
[{ path: "/terms", baseUrl: testUrl }, `${testUrl}${prefix}/terms`],
|
||||
[{ path: "/terms", baseUrl: testUrlWithTrailingSlash }, `${testUrl}${prefix}/terms`],
|
||||
[
|
||||
{ path: "/terms", queryParams, prefix: testPrefix, baseUrl: testUrl },
|
||||
`${testUrl}${testPrefix}/terms?test1=99&test2=a&test2=b`,
|
||||
],
|
||||
];
|
||||
const runTests = (fetchBaseUrl: string) => {
|
||||
it.each<TestCase>(testCases)(
|
||||
"creates url with params %s => %s",
|
||||
({ path, queryParams, prefix, baseUrl }, expected) => {
|
||||
const api = makeApi(fetchBaseUrl);
|
||||
|
||||
const result = api.getUrl(path, queryParams, prefix, baseUrl);
|
||||
// we only check the stringified URL, to avoid having the test depend on the internals of URL.
|
||||
expect(result.toString()).toEqual(expected);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe("when fetch.opts.baseUrl does not have a trailing slash", () => {
|
||||
runTests(localBaseUrl);
|
||||
});
|
||||
describe("when fetch.opts.baseUrl does have a trailing slash", () => {
|
||||
runTests(baseUrlWithTrailingSlash);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not log query parameters", async () => {
|
||||
jest.useFakeTimers();
|
||||
const deferred = defer<Response>();
|
||||
const fetchFn = jest.fn().mockReturnValue(deferred.promise);
|
||||
jest.spyOn(logger, "debug").mockImplementation(() => {});
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
|
||||
jest.advanceTimersByTime(1234);
|
||||
deferred.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
|
||||
await prom;
|
||||
expect(logger.debug).not.toHaveBeenCalledWith("fragment");
|
||||
expect(logger.debug).not.toHaveBeenCalledWith("query");
|
||||
expect(logger.debug).not.toHaveBeenCalledWith("param");
|
||||
expect(logger.debug).toHaveBeenCalledTimes(2);
|
||||
expect(mocked(logger.debug).mock.calls[0]).toMatchInlineSnapshot(`
|
||||
[
|
||||
"FetchHttpApi: --> GET https://server:8448/some/path?query=xxx",
|
||||
]
|
||||
`);
|
||||
expect(mocked(logger.debug).mock.calls[1]).toMatchInlineSnapshot(`
|
||||
[
|
||||
"FetchHttpApi: <-- GET https://server:8448/some/path?query=xxx [1234ms 200]",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import DOMException from "domexception";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
|
||||
@@ -33,8 +32,6 @@ describe("MatrixHttpApi", () => {
|
||||
|
||||
const DONE = 0;
|
||||
|
||||
global.DOMException = DOMException;
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = {
|
||||
upload: {} as XMLHttpRequestUpload,
|
||||
@@ -87,7 +84,7 @@ describe("MatrixHttpApi", () => {
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token",
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
|
||||
);
|
||||
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
});
|
||||
@@ -99,7 +96,7 @@ describe("MatrixHttpApi", () => {
|
||||
accessToken: "token",
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
});
|
||||
|
||||
@@ -108,14 +105,14 @@ describe("MatrixHttpApi", () => {
|
||||
upload = api.uploadContent({} as File, { name: "name" });
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name",
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow not sending the filename", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
});
|
||||
|
||||
it("should abort xhr when the upload is aborted", () => {
|
||||
|
||||
@@ -78,11 +78,11 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle auth errcode presence ", async () => {
|
||||
it("should handle auth errcode presence", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
@@ -94,7 +94,6 @@ describe("InteractiveAuth", () => {
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
errcode: "MockError0",
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
@@ -128,8 +127,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle set emailSid for email flow", async () => {
|
||||
@@ -180,9 +179,9 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(requestEmailToken).toBeCalledTimes(0);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
expect(requestEmailToken).toHaveBeenCalledTimes(0);
|
||||
expect(ia.getEmailSid()).toBe("myEmailSid");
|
||||
});
|
||||
|
||||
@@ -244,8 +243,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(2);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should make a request if authdata is null", async () => {
|
||||
@@ -306,8 +305,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(2);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", async () => {
|
||||
@@ -376,7 +375,7 @@ describe("InteractiveAuth", () => {
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
|
||||
});
|
||||
|
||||
it("should handle unexpected error types without data propery set", async () => {
|
||||
it("should handle unexpected error types without data property set", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
@@ -430,8 +429,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(0);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe("requestEmailToken", () => {
|
||||
@@ -464,35 +463,6 @@ describe("InteractiveAuth", () => {
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("passes errors through", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
@@ -508,7 +478,7 @@ describe("InteractiveAuth", () => {
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
|
||||
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrow("unspecific network error");
|
||||
});
|
||||
|
||||
it("only starts one request at a time", async () => {
|
||||
@@ -546,4 +516,83 @@ describe("InteractiveAuth", () => {
|
||||
expect(ia.getEmailSid()).toEqual(sid);
|
||||
});
|
||||
});
|
||||
|
||||
it("should prioritise shorter flows", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
ia.chooseStage();
|
||||
expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]);
|
||||
});
|
||||
|
||||
it("should prioritise flows with entirely supported stages", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
|
||||
params: {},
|
||||
},
|
||||
supportedStages: [AuthType.Password],
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
ia.chooseStage();
|
||||
expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]);
|
||||
});
|
||||
|
||||
it("should fire stateUpdated callback if with error when encountered", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.mockImplementation((stage) => {
|
||||
expect(stage).toEqual(AuthType.Password);
|
||||
ia.submitAuthDict({
|
||||
type: AuthType.Password,
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
doRequest.mockRejectedValue(new MatrixError({ errcode: "M_UNKNOWN", error: "This is an error" }));
|
||||
|
||||
await Promise.allSettled([ia.attemptAuth()]);
|
||||
expect(stateUpdated).toHaveBeenCalledWith("m.login.password", {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "This is an error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
M_TIMESTAMP,
|
||||
LocationEventWireContent,
|
||||
} from "../../src/@types/location";
|
||||
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
|
||||
import { M_TEXT } from "../../src/@types/extensible_events";
|
||||
import { MsgType } from "../../src/@types/event";
|
||||
|
||||
describe("Location", function () {
|
||||
@@ -32,7 +32,7 @@ describe("Location", function () {
|
||||
geo_uri: "geo:-36.24484561954707,175.46884959563613;u=10",
|
||||
[M_LOCATION.name]: { uri: "geo:-36.24484561954707,175.46884959563613;u=10", description: null },
|
||||
[M_ASSET.name]: { type: "m.self" },
|
||||
[TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
|
||||
[M_TEXT.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
|
||||
[M_TIMESTAMP.name]: 1646823712443,
|
||||
} as any;
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("Location", function () {
|
||||
description: undefined,
|
||||
});
|
||||
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
|
||||
expect(M_TEXT.findIn(loc)).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
|
||||
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("Location", function () {
|
||||
description: "desc",
|
||||
});
|
||||
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
|
||||
expect(M_TEXT.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
|
||||
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { ClientPrefix, MatrixClient } from "../../src";
|
||||
import { SSOAction } from "../../src/@types/auth";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
function createExampleMatrixClient(): MatrixClient {
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://example.com",
|
||||
});
|
||||
}
|
||||
|
||||
describe("Login request", function () {
|
||||
let client: TestClient;
|
||||
|
||||
@@ -57,3 +66,84 @@ describe("SSO login URL", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshToken", () => {
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
const response = {
|
||||
access_token: "access_token",
|
||||
refresh_token: "refresh_token",
|
||||
expires_in_ms: 30000,
|
||||
};
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), response);
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), () => {
|
||||
throw new Error("/v1/refresh unexpectedly called");
|
||||
});
|
||||
|
||||
const refreshResult = await client.refreshToken("initial_refresh_token");
|
||||
expect(refreshResult).toEqual(response);
|
||||
});
|
||||
|
||||
it("falls back to /v1 when server does not recognized /v3 refresh", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
const response = {
|
||||
access_token: "access_token",
|
||||
refresh_token: "refresh_token",
|
||||
expires_in_ms: 30000,
|
||||
};
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), response);
|
||||
|
||||
const refreshResult = await client.refreshToken("initial_refresh_token");
|
||||
expect(refreshResult).toEqual(response);
|
||||
});
|
||||
|
||||
it("re-raises M_UNRECOGNIZED exceptions from /v1", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
|
||||
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ errcode: "M_UNRECOGNIZED" });
|
||||
});
|
||||
|
||||
it("re-raises non-M_UNRECOGNIZED exceptions from /v3", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), 429);
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), () => {
|
||||
throw new Error("/v1/refresh unexpectedly called");
|
||||
});
|
||||
|
||||
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
|
||||
});
|
||||
|
||||
it("re-raises non-M_UNRECOGNIZED exceptions from /v1", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), 429);
|
||||
|
||||
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
|
||||
});
|
||||
});
|
||||
|
||||
+1268
-94
File diff suppressed because it is too large
Load Diff
@@ -265,11 +265,13 @@ describe("MSC3089Branch", () => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
|
||||
if (stateKey === fileEventId) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: false,
|
||||
});
|
||||
} else if (stateKey === fileEventId2) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
version: 2,
|
||||
|
||||
@@ -130,14 +130,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
|
||||
});
|
||||
client.invite = fn;
|
||||
try {
|
||||
await tree.invite(target, false, false);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
|
||||
}
|
||||
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -357,13 +351,18 @@ describe("MSC3089TreeSpace", () => {
|
||||
.fn()
|
||||
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect([tree.roomId, subspaceId]).toContain(roomId);
|
||||
|
||||
let expectedType: string;
|
||||
let expectedStateKey: string;
|
||||
if (roomId === subspaceId) {
|
||||
expect(eventType).toEqual(EventType.SpaceParent);
|
||||
expect(stateKey).toEqual(tree.roomId);
|
||||
expectedType = EventType.SpaceParent;
|
||||
expectedStateKey = tree.roomId;
|
||||
} else {
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(stateKey).toEqual(subspaceId);
|
||||
expectedType = EventType.SpaceChild;
|
||||
expectedStateKey = subspaceId;
|
||||
}
|
||||
expect(eventType).toEqual(expectedType);
|
||||
expect(stateKey).toEqual(expectedStateKey);
|
||||
expect(content).toMatchObject({ via: [domain] });
|
||||
|
||||
// return value not used
|
||||
@@ -629,15 +628,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
|
||||
it("should throw when setting an order at the top level space", async () => {
|
||||
try {
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await tree.setOrder(2);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
|
||||
}
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await expect(tree.setOrder(2)).rejects.toThrow("Cannot set order of top level spaces currently");
|
||||
});
|
||||
|
||||
it("should return a stable order for unordered children", () => {
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import { MatrixEvent } from "../../../src";
|
||||
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
||||
import { isTimestampInDuration, Beacon, BeaconEvent } from "../../../src/models/beacon";
|
||||
|
||||
+182
-18
@@ -16,8 +16,8 @@ limitations under the License.
|
||||
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { EventType } from "../../../src";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
|
||||
import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
it("should create copies of itself", () => {
|
||||
@@ -88,22 +88,6 @@ describe("MatrixEvent", () => {
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: {
|
||||
body: "Test",
|
||||
},
|
||||
event_id: "$event1:server",
|
||||
});
|
||||
await ev.attemptDecryption({
|
||||
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
|
||||
} as unknown as Crypto);
|
||||
expect(ev.isEncrypted()).toBeTruthy();
|
||||
expect(ev.isBeingDecrypted()).toBeFalsy();
|
||||
expect(ev.isDecryptionFailure()).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("applyVisibilityEvent", () => {
|
||||
it("should emit VisibilityChange if a change was made", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
@@ -134,6 +118,40 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should report decryption errors", async () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: Error: test error **",
|
||||
});
|
||||
});
|
||||
|
||||
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
.fn()
|
||||
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeTruthy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: The sender has disabled encrypting to unverified devices. **",
|
||||
});
|
||||
});
|
||||
|
||||
it("should retry decryption if a retry is queued", async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
|
||||
|
||||
@@ -165,4 +183,150 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.getType()).toEqual("m.room.message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyEventId", () => {
|
||||
it("should ignore 'm.relates_to' from encrypted content even if cleartext lacks one", async () => {
|
||||
const eventId = "test_encrypted_event";
|
||||
const encryptedEvent = new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: "m.room.encrypted",
|
||||
content: {
|
||||
ciphertext: "secrets",
|
||||
},
|
||||
});
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockImplementationOnce(() => {
|
||||
return Promise.resolve<IEventDecryptionResult>({
|
||||
clearEvent: {
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "!anotherEvent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.getType()).toEqual("m.room.message");
|
||||
expect(encryptedEvent.replyEventId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("push details", () => {
|
||||
const pushRule = {
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
|
||||
pattern: "banana",
|
||||
rule_id: "banana",
|
||||
kind: "override",
|
||||
default: false,
|
||||
enabled: true,
|
||||
} as IAnnotatedPushRule;
|
||||
describe("setPushActions()", () => {
|
||||
it("sets actions on event", () => {
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushActions(actions);
|
||||
|
||||
expect(event.getPushActions()).toBe(actions);
|
||||
});
|
||||
|
||||
it("sets actions to undefined", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushActions(null);
|
||||
|
||||
// undefined is set on state
|
||||
expect(event.getPushDetails().actions).toBe(undefined);
|
||||
// but pushActions getter returns null when falsy
|
||||
expect(event.getPushActions()).toBe(null);
|
||||
});
|
||||
|
||||
it("clears existing push rule", () => {
|
||||
const prevActions = { notify: true, tweaks: { highlight: true } };
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushDetails(prevActions, pushRule);
|
||||
|
||||
event.setPushActions(actions);
|
||||
|
||||
// rule is not in event push cache
|
||||
expect(event.getPushDetails()).toEqual({ actions });
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPushDetails()", () => {
|
||||
it("sets actions and rule on event", () => {
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushDetails(actions, pushRule);
|
||||
|
||||
expect(event.getPushDetails()).toEqual({
|
||||
actions,
|
||||
rule: pushRule,
|
||||
});
|
||||
});
|
||||
it("clears existing push rule", () => {
|
||||
const prevActions = { notify: true, tweaks: { highlight: true } };
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushDetails(prevActions, pushRule);
|
||||
|
||||
event.setPushActions(actions);
|
||||
|
||||
// rule is not in event push cache
|
||||
expect(event.getPushDetails()).toEqual({ actions });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore thread relation on state events", async () => {
|
||||
const stateEvent = new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: "some_state_event",
|
||||
content: {
|
||||
"foo": "bar",
|
||||
"m.relates_to": {
|
||||
"event_id": "$thread_id",
|
||||
"m.in_reply_to": {
|
||||
event_id: "$thread_id",
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
state_key: "",
|
||||
});
|
||||
|
||||
expect(stateEvent.isState()).toBeTruthy();
|
||||
expect(stateEvent.threadRootId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { M_POLL_START } from "matrix-events-sdk";
|
||||
|
||||
import { EventType, IEvent, MatrixEvent, PollEvent, Room } from "../../../src";
|
||||
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls";
|
||||
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { isPollEvent, Poll } from "../../../src/models/poll";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("Poll", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
|
||||
relations: jest.fn(),
|
||||
});
|
||||
const roomId = "!room:server";
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent");
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
|
||||
const basePollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
});
|
||||
basePollStartEvent.event.event_id = "$12345";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
|
||||
|
||||
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
|
||||
});
|
||||
|
||||
let eventId = 1;
|
||||
const makeRelatedEvent = (eventProps: Partial<IEvent>, timestamp = now): MatrixEvent => {
|
||||
const event = new MatrixEvent({
|
||||
...eventProps,
|
||||
content: {
|
||||
...(eventProps.content || {}),
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: basePollStartEvent.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
event.event.origin_server_ts = timestamp;
|
||||
event.event.event_id = `${eventId++}`;
|
||||
return event;
|
||||
};
|
||||
|
||||
it("initialises with root event", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
expect(poll.roomId).toEqual(roomId);
|
||||
expect(poll.pollId).toEqual(basePollStartEvent.getId());
|
||||
expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent);
|
||||
expect(poll.isEnded).toBe(false);
|
||||
expect(poll.endEventId).toBe(undefined);
|
||||
});
|
||||
|
||||
it("throws when poll start has no room id", () => {
|
||||
const pollStartEvent = new MatrixEvent(
|
||||
PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
);
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
it("throws when poll start has no event id", () => {
|
||||
const pollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
});
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
describe("fetching responses", () => {
|
||||
it("calls relations api and emits", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const emitSpy = jest.spyOn(poll, "emit");
|
||||
const fetchResponsePromise = poll.getResponses();
|
||||
expect(poll.isFetchingResponses).toBe(true);
|
||||
const responses = await fetchResponsePromise;
|
||||
expect(poll.isFetchingResponses).toBe(false);
|
||||
expect(mockClient.relations).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
basePollStartEvent.getId(),
|
||||
"m.reference",
|
||||
undefined,
|
||||
{ from: undefined },
|
||||
);
|
||||
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
});
|
||||
|
||||
it("returns existing responses object after initial fetch", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
const responses2 = await poll.getResponses();
|
||||
// only fetched relations once
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
// strictly equal
|
||||
expect(responses).toBe(responses2);
|
||||
});
|
||||
|
||||
it("waits for existing relations request to finish when getting responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const firstResponsePromise = poll.getResponses();
|
||||
const secondResponsePromise = poll.getResponses();
|
||||
await firstResponsePromise;
|
||||
expect(firstResponsePromise).toEqual(secondResponsePromise);
|
||||
await secondResponsePromise;
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("filters relations for relevent response events", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, unstableResponseEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
|
||||
});
|
||||
|
||||
describe("with multiple pages of relations", () => {
|
||||
const makeResponses = (count = 1, timestamp = now): MatrixEvent[] =>
|
||||
new Array(count)
|
||||
.fill("x")
|
||||
.map((_x, index) =>
|
||||
makeRelatedEvent(
|
||||
{ type: M_POLL_RESPONSE.stable!, sender: "@bob@server.org" },
|
||||
timestamp + index,
|
||||
),
|
||||
);
|
||||
|
||||
it("page relations responses", async () => {
|
||||
const responseEvents = makeResponses(6);
|
||||
mockClient.relations
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(0, 2),
|
||||
nextBatch: "test-next-1",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(2, 4),
|
||||
nextBatch: "test-next-2",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(4),
|
||||
});
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.relations.mock.calls).toEqual([
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: undefined }],
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-1" }],
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-2" }],
|
||||
]);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledTimes(3);
|
||||
expect(poll.isFetchingResponses).toBeFalsy();
|
||||
expect(responses.getRelations().length).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("undecryptable relations", () => {
|
||||
it("counts undecryptable relation events when getting responses", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1);
|
||||
});
|
||||
|
||||
it("adds to undercryptable event count when new relation is undecryptable", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
const undecryptableEvent2 = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
jest.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
|
||||
await poll.onNewRelation(undecryptableEvent2);
|
||||
|
||||
expect(poll.undecryptableRelationsCount).toBe(2);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with poll end event", () => {
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" });
|
||||
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" });
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, stablePollEndEvent],
|
||||
});
|
||||
});
|
||||
|
||||
it("sets poll end event with stable event type", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.endEventId).toBe(stablePollEndEvent.getId()!);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => {
|
||||
const pollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
sender: "@bob:domain.org",
|
||||
});
|
||||
pollStartEvent.event.event_id = "$6789";
|
||||
const poll = new Poll(pollStartEvent, mockClient, room);
|
||||
const pollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob:domain.org" });
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [pollEndEvent],
|
||||
});
|
||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("sets poll end event with unstable event type", async () => {
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [unstablePollEndEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("filters out responses that were sent after poll end", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
// just response type events
|
||||
// and response with ts after poll end event is excluded
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onNewRelation()", () => {
|
||||
it("discards response if poll responses have not been initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets poll end event when responses are not initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("does not set poll end event when sent by invalid user", async () => {
|
||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" });
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
// didn't end, didn't refilter responses
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBeFalsy();
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
|
||||
});
|
||||
|
||||
it("replaces poll end event and refilters when an older end event already exists", async () => {
|
||||
const earlierPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now,
|
||||
);
|
||||
const laterPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now + 2000,
|
||||
);
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, laterPollEndEvent],
|
||||
});
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
// all responses have a timestamp < laterPollEndEvent
|
||||
expect(responses.getRelations().length).toEqual(3);
|
||||
// first end event set correctly
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
|
||||
// add a valid end event with earlier timestamp
|
||||
poll.onNewRelation(earlierPollEndEvent);
|
||||
|
||||
// emitted new end event
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
// filtered responses and emitted
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
|
||||
it("does not set poll end event when an earlier end event already exists", async () => {
|
||||
const earlierPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now,
|
||||
);
|
||||
const laterPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now + 2000,
|
||||
);
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
await poll.getResponses();
|
||||
|
||||
poll.onNewRelation(earlierPollEndEvent);
|
||||
|
||||
// first end event set correctly
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
|
||||
poll.onNewRelation(laterPollEndEvent);
|
||||
// didn't set new end event, didn't refilter responses
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sets poll end event and refilters responses based on timestamp", async () => {
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
|
||||
expect(responses.getRelations().length).toEqual(3);
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations().length).toEqual(2);
|
||||
// after end timestamp event is removed
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
|
||||
it("filters out irrelevant relations", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
const replyEvent = new MatrixEvent({ type: "m.room.message" });
|
||||
|
||||
poll.onNewRelation(replyEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(responses.getRelations().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("adds poll response relations to responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations()).toEqual([responseEvent]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPollEvent", () => {
|
||||
it("should return »false« for a non-poll event", () => {
|
||||
const messageEvent = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
content: {},
|
||||
user: mockClient.getSafeUserId(),
|
||||
room: room.roomId,
|
||||
});
|
||||
expect(isPollEvent(messageEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([[M_POLL_START.name], [M_POLL_RESPONSE.name], [M_POLL_END.name]])(
|
||||
"should return »true« for a »%s« event",
|
||||
(type: string) => {
|
||||
const pollEvent = mkEvent({
|
||||
event: true,
|
||||
type,
|
||||
content: {},
|
||||
user: mockClient.getSafeUserId(),
|
||||
room: room.roomId,
|
||||
});
|
||||
expect(isPollEvent(pollEvent)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+627
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,13 +14,20 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
|
||||
import { mkThread } from "../../test-utils/thread";
|
||||
import { Room, RoomEvent } from "../../../src/models/room";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
|
||||
import { makeThreadEvent, mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { emitPromise, mkMessage } from "../../test-utils/test-utils";
|
||||
import { EventStatus } from "../../../src";
|
||||
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
|
||||
import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src";
|
||||
import { ReceiptType } from "../../../src/@types/read_receipts";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { ReEmitter } from "../../../src/ReEmitter";
|
||||
import { Feature, ServerSupport } from "../../../src/feature";
|
||||
import { eventMapperFor } from "../../../src/event-mapper";
|
||||
|
||||
describe("Thread", () => {
|
||||
describe("constructor", () => {
|
||||
@@ -36,6 +43,7 @@ describe("Thread", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { timelineSupport: false });
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -71,17 +79,55 @@ describe("Thread", () => {
|
||||
});
|
||||
|
||||
describe("hasUserReadEvent", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
let myUserId: string;
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
client = testClient.client;
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
Object.keys(Feature).forEach((feature) => {
|
||||
client.canSupport.set(feature as Feature, ServerSupport.Stable);
|
||||
});
|
||||
|
||||
myUserId = client.getUserId()!;
|
||||
|
||||
room = new Room("123", client, myUserId);
|
||||
|
||||
const receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
// first threaded receipt
|
||||
"$event0:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[client.getUserId()!]: { ts: 100, thread_id: "$threadId:localhost" },
|
||||
},
|
||||
},
|
||||
// last unthreaded receipt
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[client.getUserId()!]: { ts: 200 },
|
||||
["@alice:example.org"]: { ts: 200 },
|
||||
},
|
||||
},
|
||||
// last threaded receipt
|
||||
"$event2:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[client.getUserId()!]: { ts: 300, thread_id: "$threadId" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
@@ -98,6 +144,7 @@ describe("Thread", () => {
|
||||
length: 2,
|
||||
});
|
||||
|
||||
// The event is automatically considered read as the current user is the sender
|
||||
expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -106,11 +153,579 @@ describe("Thread", () => {
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
participantUserIds: [myUserId],
|
||||
length: 25,
|
||||
ts: 190,
|
||||
});
|
||||
|
||||
// Before alice's last unthreaded receipt
|
||||
expect(thread.hasUserReadEvent("@alice:example.org", events.at(1)!.getId() ?? "")).toBeTruthy();
|
||||
|
||||
// After alice's last unthreaded receipt
|
||||
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("considers event as read if there's a more recent unthreaded receipt", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
ts: 150, // before the latest unthreaded receipt
|
||||
});
|
||||
expect(thread.hasUserReadEvent(client.getUserId()!, events.at(-1)!.getId() ?? "")).toBe(true);
|
||||
});
|
||||
|
||||
it("considers event as unread if there's no more recent unthreaded receipt", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
ts: 1000,
|
||||
});
|
||||
expect(thread.hasUserReadEvent(client.getUserId()!, events.at(-1)!.getId() ?? "")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEventReadUpTo", () => {
|
||||
let myUserId: string;
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
Object.keys(Feature).forEach((feature) => {
|
||||
client.canSupport.set(feature as Feature, ServerSupport.Stable);
|
||||
});
|
||||
|
||||
myUserId = client.getUserId()!;
|
||||
|
||||
room = new Room("123", client, myUserId);
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("uses unthreaded receipt to figure out read up to", () => {
|
||||
const receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
// last unthreaded receipt
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
["@alice:example.org"]: { ts: 200 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: [myUserId],
|
||||
length: 25,
|
||||
ts: 190,
|
||||
});
|
||||
|
||||
// The 10th event has been read, as alice's last unthreaded receipt is at ts 200
|
||||
// and `mkThread` increment every thread response by 1ms.
|
||||
expect(thread.getEventReadUpTo("@alice:example.org")).toBe(events.at(9)!.getId());
|
||||
});
|
||||
|
||||
it("considers thread created before the first threaded receipt to be read", () => {
|
||||
const receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
// last unthreaded receipt
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[myUserId]: { ts: 200, thread_id: "$threadId" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
ts: 10,
|
||||
});
|
||||
|
||||
// This is marked as read as it is before alice's first threaded receipt...
|
||||
expect(thread.getEventReadUpTo(myUserId)).toBe(events.at(-1)!.getId());
|
||||
|
||||
const { thread: thread2 } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
ts: 1000,
|
||||
});
|
||||
|
||||
// Nothing has been read, this thread is after the first threaded receipt...
|
||||
expect(thread2.getEventReadUpTo(myUserId)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetLiveTimeline", () => {
|
||||
// ResetLiveTimeline is used when we have missing messages between the current live timeline's end and newly
|
||||
// received messages. In that case, we want to replace the existing live timeline. To ensure pagination
|
||||
// continues working correctly, new pagination tokens need to be set on both the old live timeline (which is
|
||||
// now a regular timeline) and the new live timeline.
|
||||
it("replaces the live timeline and correctly sets pagination tokens", async () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 3,
|
||||
});
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
|
||||
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
Promise.resolve({
|
||||
chunk: [],
|
||||
start: `${token}-new`,
|
||||
end: `${token}-new`,
|
||||
}),
|
||||
);
|
||||
|
||||
function timelines(): [string | null, string | null][] {
|
||||
return thread.timelineSet
|
||||
.getTimelines()
|
||||
.map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]);
|
||||
}
|
||||
|
||||
expect(timelines()).toEqual([[null, null]]);
|
||||
const promise = thread.resetLiveTimeline("b1", "f1");
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f1"],
|
||||
["b1", null],
|
||||
]);
|
||||
await promise;
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f1-new"],
|
||||
["b1-new", null],
|
||||
]);
|
||||
});
|
||||
|
||||
// As the pagination tokens cannot be used right now, resetLiveTimeline needs to replace them before they can
|
||||
// be used. But if in the future the bug in synapse is fixed, and they can actually be used, we can get into a
|
||||
// state where the client has paginated (and changed the tokens) while resetLiveTimeline tries to set the
|
||||
// corrected tokens. To prevent such a race condition, we make sure that resetLiveTimeline respects any
|
||||
// changes done to the pagination tokens.
|
||||
it("replaces the live timeline but does not replace changed pagination tokens", async () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 3,
|
||||
});
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
|
||||
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
Promise.resolve({
|
||||
chunk: [],
|
||||
start: `${token}-new`,
|
||||
end: `${token}-new`,
|
||||
}),
|
||||
);
|
||||
|
||||
function timelines(): [string | null, string | null][] {
|
||||
return thread.timelineSet
|
||||
.getTimelines()
|
||||
.map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]);
|
||||
}
|
||||
|
||||
expect(timelines()).toEqual([[null, null]]);
|
||||
const promise = thread.resetLiveTimeline("b1", "f1");
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f1"],
|
||||
["b1", null],
|
||||
]);
|
||||
thread.timelineSet.getTimelines()[0].setPaginationToken("f2", Direction.Forward);
|
||||
thread.timelineSet.getTimelines()[1].setPaginationToken("b2", Direction.Backward);
|
||||
await promise;
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f2"],
|
||||
["b2", null],
|
||||
]);
|
||||
});
|
||||
|
||||
it("is correctly called by the room", async () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 3,
|
||||
});
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
const mock = jest.spyOn(thread, "resetLiveTimeline");
|
||||
mock.mockReturnValue(Promise.resolve());
|
||||
|
||||
room.resetLiveTimeline("b1", "f1");
|
||||
expect(mock).toHaveBeenCalledWith("b1", "f1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("insertEventIntoTimeline", () => {
|
||||
it("Inserts a reaction in timestamp order", () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
const client = createClientWithEventMapper();
|
||||
const userId = "user1";
|
||||
const room = new Room("room1", client, userId);
|
||||
|
||||
// Given a thread with a root plus 5 messages
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: userId,
|
||||
participantUserIds: ["@bob:hs", "@chia:hs", "@dv:hs"],
|
||||
length: 6,
|
||||
ts: 100, // Events will be at ts 100, 101, 102, 103, 104 and 105
|
||||
});
|
||||
|
||||
// When we insert a reaction to the second thread message
|
||||
const replyEvent = mkReaction(events[2], client, userId, room.roomId, 104);
|
||||
thread.insertEventIntoTimeline(replyEvent);
|
||||
|
||||
// Then the reaction is inserted based on its timestamp
|
||||
expect(thread.events.map((ev) => ev.getId())).toEqual([
|
||||
events[0].getId(),
|
||||
events[1].getId(),
|
||||
events[2].getId(),
|
||||
events[3].getId(),
|
||||
events[4].getId(),
|
||||
replyEvent.getId(),
|
||||
events[5].getId(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("Without relations recursion support", () => {
|
||||
it("Creates a local echo receipt for new events", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client without relations recursion support
|
||||
const client = createClientWithEventMapper();
|
||||
|
||||
// And a thread with an added event (with later timestamp)
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
|
||||
|
||||
// Then a receipt was added to the thread
|
||||
const receipt = thread.getReadReceiptForUserId(userId);
|
||||
expect(receipt).toBeTruthy();
|
||||
expect(receipt?.eventId).toEqual(message.getId());
|
||||
expect(receipt?.data.ts).toEqual(100);
|
||||
expect(receipt?.data.thread_id).toEqual(thread.id);
|
||||
|
||||
// (And the receipt was synthetic)
|
||||
expect(thread.getReadReceiptForUserId(userId, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("Doesn't create a local echo receipt for events before an existing receipt", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client without relations recursion support
|
||||
const client = createClientWithEventMapper();
|
||||
|
||||
// And a thread with an added event with a lower timestamp than its other events
|
||||
const userId = "user1";
|
||||
const { thread } = await createThreadAndEvent(client, 200, 100, userId);
|
||||
|
||||
// Then no receipt was added to the thread (the receipt is still
|
||||
// for the thread root). This happens because since we have no
|
||||
// recursive relations support, we know that sometimes events
|
||||
// appear out of order, so we have to check their timestamps as
|
||||
// a guess of the correct order.
|
||||
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
|
||||
});
|
||||
});
|
||||
|
||||
describe("With relations recursion support", () => {
|
||||
it("Creates a local echo receipt for new events", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client WITH relations recursion support
|
||||
const client = createClientWithEventMapper(
|
||||
new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]),
|
||||
);
|
||||
|
||||
// And a thread with an added event (with later timestamp)
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
|
||||
|
||||
// Then a receipt was added to the thread
|
||||
const receipt = thread.getReadReceiptForUserId(userId);
|
||||
expect(receipt?.eventId).toEqual(message.getId());
|
||||
});
|
||||
|
||||
it("Creates a local echo receipt even for events BEFORE an existing receipt", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client WITH relations recursion support
|
||||
const client = createClientWithEventMapper(
|
||||
new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]),
|
||||
);
|
||||
|
||||
// And a thread with an added event with a lower timestamp than its other events
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 200, 100, userId);
|
||||
|
||||
// Then a receipt was added to the thread, because relations
|
||||
// recursion is available, so we trust the server to have
|
||||
// provided us with events in the right order.
|
||||
const receipt = thread.getReadReceiptForUserId(userId);
|
||||
expect(receipt?.eventId).toEqual(message.getId());
|
||||
});
|
||||
});
|
||||
|
||||
async function createThreadAndEvent(
|
||||
client: MatrixClient,
|
||||
rootTs: number,
|
||||
eventTs: number,
|
||||
userId: string,
|
||||
): Promise<{ thread: Thread; message: MatrixEvent }> {
|
||||
const room = new Room("room1", client, userId);
|
||||
|
||||
// Given a thread
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: userId,
|
||||
participantUserIds: [],
|
||||
ts: rootTs,
|
||||
});
|
||||
// Sanity: the current receipt is for the thread root
|
||||
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
|
||||
|
||||
const awaitTimelineEvent = new Promise<void>((res) => thread.on(RoomEvent.Timeline, () => res()));
|
||||
|
||||
// When we add a message that is before the latest receipt
|
||||
const message = makeThreadEvent({
|
||||
event: true,
|
||||
rootEventId: thread.id,
|
||||
replyToEventId: thread.id,
|
||||
user: userId,
|
||||
room: room.roomId,
|
||||
ts: eventTs,
|
||||
});
|
||||
await thread.addEvent(message, false, true);
|
||||
await awaitTimelineEvent;
|
||||
|
||||
return { thread, message };
|
||||
}
|
||||
|
||||
function createClientWithEventMapper(canSupport: Map<Feature, ServerSupport> = new Map()): MatrixClient {
|
||||
const client = mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = canSupport;
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
mocked(client.supportsThreads).mockReturnValue(true);
|
||||
return client;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Editing events", () => {
|
||||
describe("Given server support for threads", () => {
|
||||
let previousThreadHasServerSideSupport: FeatureSupport;
|
||||
|
||||
beforeAll(() => {
|
||||
previousThreadHasServerSideSupport = Thread.hasServerSideSupport;
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Thread.hasServerSideSupport = previousThreadHasServerSideSupport;
|
||||
});
|
||||
|
||||
it("Adds edits from sync to the thread timeline and applies them", async () => {
|
||||
// Given a thread
|
||||
const client = createClient();
|
||||
const user = "@alice:matrix.org";
|
||||
const room = "!room:z";
|
||||
const thread = await createThread(client, user, room);
|
||||
|
||||
// When a message and an edit are added to the thread
|
||||
const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply");
|
||||
const editEvent = mkEdit(messageToEdit, client, user, room, "edit");
|
||||
await thread.addEvent(messageToEdit, false);
|
||||
await thread.addEvent(editEvent, false);
|
||||
|
||||
// Then both events end up in the timeline
|
||||
const lastEvent = thread.timeline.at(-1)!;
|
||||
const secondLastEvent = thread.timeline.at(-2)!;
|
||||
expect(lastEvent).toBe(editEvent);
|
||||
expect(secondLastEvent).toBe(messageToEdit);
|
||||
|
||||
// And the first message has been edited
|
||||
expect(secondLastEvent.getContent().body).toEqual("edit");
|
||||
});
|
||||
|
||||
it("Adds edits fetched on demand to the thread timeline and applies them", async () => {
|
||||
// Given we don't support recursive relations
|
||||
const client = createClient(new Map([[Feature.RelationsRecursion, ServerSupport.Unsupported]]));
|
||||
// And we have a thread
|
||||
const user = "@alice:matrix.org";
|
||||
const room = "!room:z";
|
||||
const thread = await createThread(client, user, room);
|
||||
|
||||
// When a message is added to the thread, and an edit to it is provided on demand
|
||||
const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply");
|
||||
// (fetchEditsWhereNeeded only applies to encrypted messages for some reason)
|
||||
messageToEdit.event.type = EventType.RoomMessageEncrypted;
|
||||
const editEvent = mkEdit(messageToEdit, client, user, room, "edit");
|
||||
mocked(client.relations).mockImplementation(async (_roomId, eventId) => {
|
||||
if (eventId === messageToEdit.getId()) {
|
||||
return { events: [editEvent] };
|
||||
} else {
|
||||
return { events: [] };
|
||||
}
|
||||
});
|
||||
await thread.addEvent(messageToEdit, false);
|
||||
|
||||
// Then both events end up in the timeline
|
||||
const lastEvent = thread.timeline.at(-1)!;
|
||||
const secondLastEvent = thread.timeline.at(-2)!;
|
||||
expect(lastEvent).toBe(editEvent);
|
||||
expect(secondLastEvent).toBe(messageToEdit);
|
||||
|
||||
// And the first message has been edited
|
||||
expect(secondLastEvent.getContent().body).toEqual("edit");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a message event that lives in a thread
|
||||
*/
|
||||
function createThreadMessage(threadId: string, user: string, room: string, msg: string): MatrixEvent {
|
||||
return makeThreadEvent({
|
||||
event: true,
|
||||
user,
|
||||
room,
|
||||
msg,
|
||||
rootEventId: threadId,
|
||||
replyToEventId: threadId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thread and wait for it to be properly initialised (so you can safely
|
||||
* add events to it and expect them to appear in the timeline.
|
||||
*/
|
||||
async function createThread(client: MatrixClient, user: string, roomId: string): Promise<Thread> {
|
||||
const root = mkMessage({ event: true, user, room: roomId, msg: "Thread root" });
|
||||
const room = new Room(roomId, client, "@roomcreator:x");
|
||||
|
||||
// Ensure the root is in the room timeline
|
||||
root.setThreadId(root.getId());
|
||||
await room.addLiveEvents([root]);
|
||||
|
||||
// Create the thread and wait for it to be initialised
|
||||
const thread = room.createThread(root.getId()!, root, [], false);
|
||||
await new Promise<void>((res) => thread.once(RoomEvent.TimelineReset, () => res()));
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MatrixClient that supports threads and has all the methods used when
|
||||
* creating a thread that call out to HTTP endpoints mocked out.
|
||||
*/
|
||||
function createClient(canSupport = new Map()): MatrixClient {
|
||||
const client = mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = canSupport;
|
||||
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
|
||||
// Mock methods that call out to HTTP endpoints
|
||||
jest.spyOn(client, "paginateEventTimeline").mockResolvedValue(true);
|
||||
jest.spyOn(client, "relations").mockResolvedValue({ events: [] });
|
||||
jest.spyOn(client, "fetchRoomEvent").mockResolvedValue({});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
import {
|
||||
EventType,
|
||||
@@ -52,10 +53,11 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
mockClient.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
mockClient.canSupport = new Map();
|
||||
@@ -64,6 +66,30 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
});
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "");
|
||||
|
||||
const receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event0:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[mockClient.getUserId()!]: { ts: 123 },
|
||||
},
|
||||
},
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[mockClient.getUserId()!]: { ts: 666, thread_id: THREAD_ID },
|
||||
},
|
||||
},
|
||||
"$otherevent:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[mockClient.getUserId()!]: { ts: 999, thread_id: "$otherthread:localhost" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
@@ -75,6 +101,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
body: "Hello world!",
|
||||
},
|
||||
event: true,
|
||||
ts: 1234,
|
||||
},
|
||||
mockClient,
|
||||
);
|
||||
@@ -90,6 +117,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Thread reply",
|
||||
},
|
||||
ts: 5678,
|
||||
event: true,
|
||||
});
|
||||
room.createThread(THREAD_ID, event, [threadEvent], false);
|
||||
@@ -107,7 +135,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(3);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
@@ -127,11 +155,11 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, threadEvent);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("does not change the room count when there's no unread count", () => {
|
||||
it("does not change the thread count when there's no unread count", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
@@ -155,6 +183,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Thread reply",
|
||||
},
|
||||
ts: 8901,
|
||||
event: true,
|
||||
});
|
||||
|
||||
@@ -164,6 +193,31 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not change the total room count when an event is marked as non-notifying", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not change the total room count when a threaded event is marked as non-notifying", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("emits events", () => {
|
||||
const cb = jest.fn();
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user