Compare commits
951 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25e5d79cf6 | |||
| 6c8e3d0707 | |||
| 53a72df01b | |||
| 75e710d93e | |||
| 1457ab0cf4 | |||
| 14aafb7977 | |||
| 90d00b863f | |||
| 5f0ada9578 | |||
| f01037fe0d | |||
| 2cda6655d7 | |||
| 68317ac836 | |||
| 5c45c980e9 | |||
| 66251e0855 | |||
| ff53557957 | |||
| 126352afd5 | |||
| f33da83d90 | |||
| 74193ad057 | |||
| c672cad1a1 | |||
| d59bb240fa | |||
| 65d988734e | |||
| 4a402f0bd7 | |||
| a491508543 | |||
| 0abba3e626 | |||
| 9fed45e47c | |||
| fe67a68c95 | |||
| 4d3d4028a0 | |||
| 8f901590ff | |||
| d29b8520f7 | |||
| 37e1fd9af5 | |||
| 76dbc7500f | |||
| 3664f8c3c2 | |||
| d0a10497bb | |||
| 6385c9c0da | |||
| cecac3152e | |||
| f6c99b1d25 | |||
| 407ec4d67a | |||
| 4947a0cb64 | |||
| f134d6db01 | |||
| fde6cebc20 | |||
| 425cf6b91e | |||
| a3e273d6f1 | |||
| b1a3b264e5 | |||
| 053643a8ba | |||
| d2ea149012 | |||
| 23d244520c | |||
| 267b52099b | |||
| 430fd5660a | |||
| d669ddfab2 | |||
| 9caa38d386 | |||
| 1c16b5cae6 | |||
| cb375e1351 | |||
| 5e542b3869 | |||
| c9435af637 | |||
| 40168d4419 | |||
| 6d118008b6 | |||
| 1503acb30a | |||
| 1b8507c060 | |||
| d95b5ab27a | |||
| 658e7b1be3 | |||
| 95110eb889 | |||
| 9fbcef556e | |||
| b68ad00394 | |||
| 6836720e1e | |||
| 6f517478df | |||
| 35ba4074de | |||
| c7827d971c | |||
| f963ca5562 | |||
| 8c30b0d12c | |||
| 5d4334ba4c | |||
| 7e691bf700 | |||
| 0700e86f58 | |||
| 6c307d4c63 | |||
| 88ec0e3e17 | |||
| 015e9a5be7 | |||
| 2918d686ae | |||
| 327c18ddc1 | |||
| 8cdd8e882b | |||
| 76e0d5a896 | |||
| 836238c3ba | |||
| 014b29b303 | |||
| 74160806c0 | |||
| 8e0ef98bcc | |||
| d7831f9e5b | |||
| 989c5a3dda | |||
| 0778c4e01e | |||
| c65e329101 | |||
| 5ddd453699 | |||
| 42d982dd69 | |||
| f406ffd3dd | |||
| dec4650d3d | |||
| 4c00b41046 | |||
| a1845ba0ff | |||
| fb9e258468 | |||
| 974723ceef | |||
| 5788d9744b | |||
| 65cbbaaf01 | |||
| c5245a887b | |||
| 321679fd63 | |||
| 15c679b29e | |||
| 85ba069117 | |||
| 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 | |||
| 6cc8e4436c | |||
| 7f1fe46c7c | |||
| b2a10e6db3 | |||
| a0aa5074ed | |||
| 70a033c2fd | |||
| fcf12b49e3 | |||
| 284a475dfb | |||
| 193c38523c | |||
| 11ac3d9e58 | |||
| 071d5e71e4 | |||
| acd220b8a9 | |||
| 74b30246d0 | |||
| 9c17eb6c14 | |||
| 8293011ee2 | |||
| 4c5f416b32 | |||
| 66c678e9fb | |||
| 8a892ede23 | |||
| 2dd06e368e | |||
| fc501de081 | |||
| ada401f4c0 | |||
| 41d762171e | |||
| 5b6bebc1d7 | |||
| 7b5e137ec0 | |||
| 6e0901258c | |||
| 559fbdda26 | |||
| 72dac9a107 | |||
| 349c2c2587 | |||
| 08a9073bd5 | |||
| 9841f92415 | |||
| ed91bd9c11 | |||
| c953fc9fb7 | |||
| bf78a64d82 | |||
| ae849fdd46 | |||
| 39cf212628 | |||
| 224e592701 | |||
| 16d791b038 | |||
| c4006d752a | |||
| a9e7a46c56 | |||
| 4a7365f32f | |||
| a071a82a03 | |||
| 8d018f9c2d | |||
| 6f81371e61 | |||
| ccab6985ad | |||
| 569adc7b0c | |||
| b6369cc2bd | |||
| e6e079f487 | |||
| 683e7fba4a | |||
| 2c8eece5ca | |||
| 4a4d493856 | |||
| 11d8f562c5 | |||
| 7799804762 | |||
| 83a1e07380 | |||
| 7f3123ed65 | |||
| 5a88a6c62a | |||
| 8a7fd270e4 | |||
| 12cecbdcf1 | |||
| 53a45a34df | |||
| fa2eeac5b8 | |||
| 720248466f | |||
| 43bfa0c020 | |||
| c17deb0806 | |||
| 79ccd7c330 | |||
| 9de9ff76b5 | |||
| c0090852ad | |||
| 3870e3395d | |||
| a0f3e5d3bf | |||
| 720ea0e12e | |||
| 9a98e8008f | |||
| ad8bb5d2cd | |||
| 9a731cdf4f | |||
| d1ede036e2 | |||
| d3f08fec03 | |||
| d692a5dbe2 | |||
| 4362297edc | |||
| 1606274c36 | |||
| 8d6d262e5f | |||
| 3577aa98b5 | |||
| e0df53c2ed | |||
| 6611cfa253 | |||
| 25296bb486 | |||
| 31c4f6c16b | |||
| 22271d22f8 | |||
| 3a1897629a | |||
| 9d3ac66cf8 | |||
| a4ad4ed2cf | |||
| 7fd55a61bf | |||
| 847766c114 | |||
| c8c39052a7 | |||
| 6592b2c205 | |||
| fc91153be4 | |||
| 5511a6ef8c | |||
| 19e02e894f | |||
| c54d61e158 | |||
| 44da9040f4 | |||
| 995f5bf7d7 | |||
| ad16b26247 | |||
| aaf3702c66 | |||
| 74147b9943 | |||
| 815370c5f9 | |||
| a01d8e3174 | |||
| 007b7dd242 | |||
| 77d6def1cc | |||
| b318a77ece | |||
| 1a90259326 | |||
| f46ecf970c | |||
| dd98d7eb2c | |||
| f3dc1c4ca2 | |||
| 305b83f8ea | |||
| acc488da64 | |||
| 7217f83db9 | |||
| 37ea905faa | |||
| 78de55b835 | |||
| cb410f463a | |||
| 72f9d5e6f9 | |||
| c389de98f3 | |||
| 20745dc9ac | |||
| 9410902049 | |||
| a6badbb7fa | |||
| 0b65b199e3 | |||
| c1138bc085 | |||
| c0f7df8c3b | |||
| e085609572 | |||
| 0a4f86a79e | |||
| 5d6ff6c7f9 | |||
| ffcdfe166e | |||
| e1aa7d335b | |||
| 29643e745c | |||
| 54622ce424 | |||
| ca2ae24d46 |
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}],
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
|
||||
+1
-1
@@ -23,4 +23,4 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
+93
-53
@@ -1,12 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
"import",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
@@ -27,63 +24,106 @@ module.exports = {
|
||||
"padded-blocks": ["error"],
|
||||
"no-extend-native": ["error"],
|
||||
"camelcase": ["error"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"no-multi-spaces": ["error", { ignoreEOLComments: true }],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
anonymous: "never",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
},
|
||||
],
|
||||
"arrow-parens": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", {
|
||||
name: "events",
|
||||
message: "Please use TypedEventEmitter instead"
|
||||
}],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
name: "events",
|
||||
message: "Please use TypedEventEmitter instead",
|
||||
},
|
||||
],
|
||||
|
||||
"import/no-restricted-paths": ["error", {
|
||||
"zones": [{
|
||||
"target": "./src/",
|
||||
"from": "./src/index.ts",
|
||||
"message": "The package index is dynamic between src and lib depending on " +
|
||||
"whether release or development, target the specific module or matrix.ts instead",
|
||||
}],
|
||||
}],
|
||||
"import/no-restricted-paths": [
|
||||
"error",
|
||||
{
|
||||
zones: [
|
||||
{
|
||||
target: "./src/",
|
||||
from: "./src/index.ts",
|
||||
message:
|
||||
"The package index is dynamic between src and lib depending on " +
|
||||
"whether release or development, target the specific module or matrix.ts instead",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
// 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: [{
|
||||
files: [
|
||||
"**/*.ts",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/typescript",
|
||||
],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
plugins: ["eslint-plugin-tsdoc"],
|
||||
extends: ["plugin:matrix-org/typescript"],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
},
|
||||
}],
|
||||
{
|
||||
// We don't need amazing docs in our spec files
|
||||
files: ["src/**/*.ts"],
|
||||
rules: {
|
||||
"tsdoc/syntax": "error",
|
||||
// We use some select jsdoc rules as the tsdoc linter has only one rule
|
||||
"jsdoc/no-types": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
"jsdoc/check-values": "error",
|
||||
// These need a bit more work before we can enable
|
||||
// "jsdoc/check-param-names": "error",
|
||||
// "jsdoc/check-indentation": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["spec/**/*.ts"],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -38,4 +38,5 @@ cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||
|
||||
# prettier
|
||||
349c2c2587c2885bb69eda4aa078b5383724cf5e
|
||||
|
||||
+8
-4
@@ -1,4 +1,8 @@
|
||||
* @matrix-org/element-web
|
||||
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
* @matrix-org/element-web
|
||||
/.github/workflows/** @matrix-org/element-web-app-team
|
||||
/package.json @matrix-org/element-web-app-team
|
||||
/yarn.lock @matrix-org/element-web-app-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] Tests written for new code (and old code if feasible)
|
||||
* [ ] Linter and other CI checks pass
|
||||
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
- [ ] Tests written for new code (and old code if feasible)
|
||||
- [ ] Linter and other CI checks pass
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
|
||||
<!--
|
||||
If you would like to specify text for the changelog entry other than your PR title, add the following:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>matrix-org/renovate-config-element-web"
|
||||
]
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>matrix-org/renovate-config-element-web"]
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@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
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- 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
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 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
|
||||
|
||||
# We only want to run the cypress tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume 4 concurrent runners.
|
||||
if: github.event.workflow_run.event == 'merge_group'
|
||||
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@66854039a33ed6cfe1fc635ff2daa8bb261c0b56
|
||||
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
|
||||
|
||||
# We want to make the cypress tests a required check for the merge queue.
|
||||
#
|
||||
# Unfortunately, github doesn't distinguish between "checks needed for branch
|
||||
# protection" (ie, the things that must pass before the PR will even be added
|
||||
# to the merge queue) and "checks needed in the merge queue". We just have to add
|
||||
# the check to the branch protection list.
|
||||
#
|
||||
# Ergo, if we know we're not going to run the cypress tests, we need to add a
|
||||
# passing status check manually.
|
||||
mark_skipped:
|
||||
if: github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Sibz/github-status-action@650dd1a882a76dbbbc4576fb5974b8d22f29847f # v1.1.6
|
||||
with:
|
||||
authToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
state: success
|
||||
description: Cypress skipped
|
||||
context: "${{ github.workflow }} / cypress"
|
||||
sha: "${{ github.event.workflow_run.head_sha }}"
|
||||
@@ -1,34 +1,34 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Static Analysis" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Static Analysis"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 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@b12b127cf24433d14b4f93cee62f5465076ba82a # v2.24.1
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 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@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v1
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Documentation preview
|
||||
deployment_env: PR Documentation Preview
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v2
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Documentation preview
|
||||
deployment_env: PR Documentation Preview
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Build downstream artifacts
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
pull_request: {}
|
||||
|
||||
# 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.80.1
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
@@ -1,27 +1,27 @@
|
||||
name: Notify Downstream Projects
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
push:
|
||||
branches: [develop]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-js-sdk'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-js-sdk'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
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
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
event-type: ${{ matrix.event }}
|
||||
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@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
event-type: ${{ matrix.event }}
|
||||
|
||||
@@ -1,91 +1,88 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
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 }}
|
||||
pull_request_target:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
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
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
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
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v6
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
core.setFailed("Preventing merge whilst PR is marked blocked!");
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v6
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
core.setFailed("Preventing merge whilst PR is marked blocked!");
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v1
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
organization: matrix-org
|
||||
team: Core Team
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
organization: matrix-org
|
||||
team: Core Team
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Z-Community-PR']
|
||||
});
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Z-Community-PR']
|
||||
});
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
|
||||
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
|
||||
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
|
||||
});
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
|
||||
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
|
||||
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
|
||||
});
|
||||
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
# Must only be called from `release#published` triggers
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
tag: next
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@fe72237be0920f7a0cafd6a966c9b929c9466e9b # v2.2.2
|
||||
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
|
||||
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 }}
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: github.event.release.prerelease == false && steps.npm-publish.outputs.id
|
||||
run: npm dist-tag add "$release" latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.id }}
|
||||
|
||||
@@ -1,58 +1,52 @@
|
||||
name: Release Process
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
release:
|
||||
types: [published]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
jsdoc:
|
||||
name: Publish Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
jsdoc:
|
||||
name: Publish Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: _docs
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
- name: 🔨 Install symlinks
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y symlinks
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
- name: 📖 Generate docs
|
||||
run: |
|
||||
yarn tpv purge --yes --out _docs --stale --major 10
|
||||
yarn gendoc
|
||||
symlinks -rc _docs
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
VERSION="${tag#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/docs/ $VERSION
|
||||
- name: 🚀 Deploy
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -1,50 +1,52 @@
|
||||
# Must only be called from a workflow_run in the context of the upstream repo
|
||||
name: SonarCloud
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
extra_args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
extra_args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
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
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
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@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.3
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: 'cat package.json | jq -r .version'
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.6
|
||||
# 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:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
|
||||
prepare:
|
||||
name: Prepare
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
|
||||
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
|
||||
steps:
|
||||
# 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
|
||||
# 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 }}
|
||||
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
|
||||
steps:
|
||||
# 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@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
|
||||
|
||||
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:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
|
||||
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
needs: prepare
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
|
||||
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
|
||||
|
||||
@@ -1,74 +1,83 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
- name: Generate Docs
|
||||
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
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
+86
-44
@@ -1,52 +1,94 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
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 }})'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [browserify, integ, unit]
|
||||
node: [16, 18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [integ, unit]
|
||||
node: [18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
./spec/${{ matrix.specs }}
|
||||
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:
|
||||
cache: 'yarn'
|
||||
node-version: ${{ matrix.node }}
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
# 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 }}
|
||||
|
||||
- name: Build
|
||||
if: matrix.specs == 'browserify'
|
||||
run: "yarn build"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage
|
||||
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
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
name: Upgrade Dependencies
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
workflow_dispatch: {}
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
upgrade:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
upgrade:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Upgrade
|
||||
run: yarn upgrade && yarn install
|
||||
- name: Upgrade
|
||||
run: yarn upgrade && yarn install
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
delete-branch: true
|
||||
title: Upgrade dependencies
|
||||
labels: |
|
||||
Dependencies
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
delete-branch: true
|
||||
title: Upgrade dependencies
|
||||
labels: |
|
||||
Dependencies
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
run: gh pr merge --merge --auto "$PR_NUMBER"
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
|
||||
@@ -19,3 +19,4 @@ out
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/_docs
|
||||
.DS_Store
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
/dist
|
||||
/lib
|
||||
/examples/browser/lib
|
||||
/examples/crypto-browser/lib
|
||||
/examples/voip/lib
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
|
||||
+453
@@ -1,3 +1,456 @@
|
||||
Changes in [29.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v29.0.0) (2023-10-10)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Remove browserify builds ([\#3759](https://github.com/matrix-org/matrix-js-sdk/pull/3759)).
|
||||
|
||||
## ✨ Features
|
||||
* Export AutoDiscoveryError and fix type of ALL_ERRORS ([\#3768](https://github.com/matrix-org/matrix-js-sdk/pull/3768)).
|
||||
* Support for stable MSC3882 get_login_token ([\#3416](https://github.com/matrix-org/matrix-js-sdk/pull/3416)). Contributed by @hughns.
|
||||
* Remove IsUserMention and IsRoomMention from DEFAULT_OVERRIDE_RULES ([\#3752](https://github.com/matrix-org/matrix-js-sdk/pull/3752)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix a case where joinRoom creates a duplicate Room object ([\#3747](https://github.com/matrix-org/matrix-js-sdk/pull/3747)).
|
||||
* Add membershipID to call memberships ([\#3745](https://github.com/matrix-org/matrix-js-sdk/pull/3745)).
|
||||
* Fix the warning for messages from unsigned devices ([\#3743](https://github.com/matrix-org/matrix-js-sdk/pull/3743)).
|
||||
* Stop keep alive, when sync was stoped ([\#3720](https://github.com/matrix-org/matrix-js-sdk/pull/3720)). Contributed by @finsterwalder.
|
||||
|
||||
Changes in [28.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.2.0) (2023-09-26)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo` ([\#3693](https://github.com/matrix-org/matrix-js-sdk/pull/3693)).
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* Delete knocked room when knock membership changes ([\#3729](https://github.com/matrix-org/matrix-js-sdk/pull/3729)). Contributed by @maheichyk.
|
||||
* Introduce MatrixRTCSession lower level group call primitive ([\#3663](https://github.com/matrix-org/matrix-js-sdk/pull/3663)).
|
||||
* Sync knock rooms ([\#3703](https://github.com/matrix-org/matrix-js-sdk/pull/3703)). Contributed by @maheichyk.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Dont access indexed db when undefined ([\#3707](https://github.com/matrix-org/matrix-js-sdk/pull/3707)). Contributed by @finsterwalder.
|
||||
* Don't reset unread count when adding a synthetic receipt ([\#3706](https://github.com/matrix-org/matrix-js-sdk/pull/3706)). Fixes #3684. Contributed by @andybalaam.
|
||||
|
||||
Changes in [28.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.1.0) (2023-09-12)
|
||||
============================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate `MatrixClient.checkUserTrust` ([\#3691](https://github.com/matrix-org/matrix-js-sdk/pull/3691)).
|
||||
* Deprecate `MatrixClient.{prepare,create}KeyBackupVersion` in favour of new `CryptoApi.resetKeyBackup` API ([\#3689](https://github.com/matrix-org/matrix-js-sdk/pull/3689)).
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* Allow calls without ICE/TURN/STUN servers ([\#3695](https://github.com/matrix-org/matrix-js-sdk/pull/3695)).
|
||||
* Emit summary update event ([\#3687](https://github.com/matrix-org/matrix-js-sdk/pull/3687)). Fixes vector-im/element-web#26033.
|
||||
* ElementR: Update `CryptoApi.userHasCrossSigningKeys` ([\#3646](https://github.com/matrix-org/matrix-js-sdk/pull/3646)). Contributed by @florianduros.
|
||||
* Add `join_rule` field to /publicRooms response ([\#3673](https://github.com/matrix-org/matrix-js-sdk/pull/3673)). Contributed by @charlynguyen.
|
||||
* Use sender instead of content.creator field on m.room.create events ([\#3675](https://github.com/matrix-org/matrix-js-sdk/pull/3675)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Provide better error for ICE Server SyntaxError ([\#3694](https://github.com/matrix-org/matrix-js-sdk/pull/3694)). Fixes vector-im/element-web#21804.
|
||||
* Legacy crypto: re-check key backup after `bootstrapSecretStorage` ([\#3692](https://github.com/matrix-org/matrix-js-sdk/pull/3692)). Fixes vector-im/element-web#26115.
|
||||
|
||||
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)).
|
||||
* Add a message ID on each to-device message ([\#2938](https://github.com/matrix-org/matrix-js-sdk/pull/2938)).
|
||||
* Enable multiple users' power levels to be set at once ([\#2892](https://github.com/matrix-org/matrix-js-sdk/pull/2892)). Contributed by @GoodGuyMarco.
|
||||
* Include pending events in thread summary and count again ([\#2922](https://github.com/matrix-org/matrix-js-sdk/pull/2922)). Fixes vector-im/element-web#23642.
|
||||
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
|
||||
* Add method to get outgoing room key requests for a given event ([\#2930](https://github.com/matrix-org/matrix-js-sdk/pull/2930)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix messages loaded during initial fetch ending up out of order ([\#2971](https://github.com/matrix-org/matrix-js-sdk/pull/2971)). Fixes vector-im/element-web#23972.
|
||||
* Fix #23919: Root message for new thread loaded from network ([\#2965](https://github.com/matrix-org/matrix-js-sdk/pull/2965)). Fixes vector-im/element-web#23919.
|
||||
* Fix #23916: Prevent edits of the last message in a thread getting lost ([\#2951](https://github.com/matrix-org/matrix-js-sdk/pull/2951)). Fixes vector-im/element-web#23916 and vector-im/element-web#23942.
|
||||
* Fix infinite loop when restoring cached read receipts ([\#2963](https://github.com/matrix-org/matrix-js-sdk/pull/2963)). Fixes vector-im/element-web#23951.
|
||||
* Don't swallow errors coming from the shareSession call ([\#2962](https://github.com/matrix-org/matrix-js-sdk/pull/2962)). Fixes vector-im/element-web#23792.
|
||||
* Make sure that MegolmEncryption.setupPromise always resolves ([\#2960](https://github.com/matrix-org/matrix-js-sdk/pull/2960)).
|
||||
* Do not calculate highlight notifs for threads unknown to the room ([\#2957](https://github.com/matrix-org/matrix-js-sdk/pull/2957)).
|
||||
* Cache read receipts for unknown threads ([\#2953](https://github.com/matrix-org/matrix-js-sdk/pull/2953)).
|
||||
* bugfix: sliding sync initial room timelines shouldn't notify ([\#2933](https://github.com/matrix-org/matrix-js-sdk/pull/2933)).
|
||||
* Redo key sharing after own device verification ([\#2921](https://github.com/matrix-org/matrix-js-sdk/pull/2921)). Fixes vector-im/element-web#23333.
|
||||
* Move updated threads to the end of the thread list ([\#2923](https://github.com/matrix-org/matrix-js-sdk/pull/2923)). Fixes vector-im/element-web#23876.
|
||||
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
|
||||
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
|
||||
|
||||
Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)).
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)).
|
||||
|
||||
## ✨ Features
|
||||
* sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)).
|
||||
* Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)).
|
||||
* Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)).
|
||||
* sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)).
|
||||
* Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)).
|
||||
* Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823.
|
||||
* Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)).
|
||||
* Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)).
|
||||
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
|
||||
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
|
||||
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
|
||||
|
||||
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
# Contributing code to matrix-js-sdk
|
||||
|
||||
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
||||
|
||||
@@ -6,31 +6,26 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
# Matrix JavaScript SDK
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
|
||||
JavaScript. This SDK can be run in a browser or in Node.js.
|
||||
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.
|
||||
|
||||
Quickstart
|
||||
==========
|
||||
#### Minimum Matrix server version: v1.1
|
||||
|
||||
In a browser
|
||||
------------
|
||||
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``
|
||||
attached to ``window`` through which you can access the SDK. See below for how to
|
||||
include libolm to enable end-to-end-encryption.
|
||||
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
|
||||
guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call
|
||||
endpoints from before Matrix 1.1, for example.
|
||||
|
||||
The browser bundle supports recent versions of browsers. Typically this is ES2015
|
||||
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
[browserlists](https://github.com/browserslist/browserslist).
|
||||
# Quickstart
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
## In a browser
|
||||
|
||||
In Node.js
|
||||
----------
|
||||
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
|
||||
|
||||
## In Node.js
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
|
||||
@@ -39,30 +34,32 @@ If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn`
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
`yarn add matrix-js-sdk`
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient("https://matrix.org");
|
||||
client.publicRooms(function(err, data) {
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
|
||||
client.publicRooms(function (err, data) {
|
||||
console.log("Public Rooms: %s", JSON.stringify(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
|
||||
await client.startClient({initialSyncLimit: 10});
|
||||
await client.startClient({ initialSyncLimit: 10 });
|
||||
```
|
||||
|
||||
You can perform a call to `/sync` to get the current state of the client:
|
||||
|
||||
```javascript
|
||||
client.once('sync', function(state, prevState, res) {
|
||||
if(state === 'PREPARED') {
|
||||
client.once("sync", function (state, prevState, res) {
|
||||
if (state === "PREPARED") {
|
||||
console.log("prepared");
|
||||
} else {
|
||||
console.log(state);
|
||||
@@ -75,8 +72,8 @@ To send a message:
|
||||
|
||||
```javascript
|
||||
const content = {
|
||||
"body": "message text",
|
||||
"msgtype": "m.text"
|
||||
body: "message text",
|
||||
msgtype: "m.text",
|
||||
};
|
||||
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
console.log(err);
|
||||
@@ -86,11 +83,11 @@ client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
To listen for message events:
|
||||
|
||||
```javascript
|
||||
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
client.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
});
|
||||
```
|
||||
|
||||
@@ -98,73 +95,70 @@ By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as
|
||||
|
||||
```javascript
|
||||
Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
client.getRoom(roomId).timeline.forEach(t => {
|
||||
console.log(t.event);
|
||||
});
|
||||
client.getRoom(roomId).timeline.forEach((t) => {
|
||||
console.log(t.event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
What does this SDK do?
|
||||
----------------------
|
||||
## What does this SDK do?
|
||||
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
|
||||
- Handles syncing (via `/initialSync` and `/events`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
- Handles syncing (via `/initialSync` and `/events`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
Later versions of the SDK will:
|
||||
- Expose a `RoomSummary` which would be suitable for a recents page.
|
||||
- Provide different pluggable storage layers (e.g. local storage, database-backed)
|
||||
|
||||
Usage
|
||||
=====
|
||||
- Expose a `RoomSummary` which would be suitable for a recents page.
|
||||
- Provide different pluggable storage layers (e.g. local storage, database-backed)
|
||||
|
||||
# Usage
|
||||
|
||||
Conventions
|
||||
-----------
|
||||
## Conventions
|
||||
|
||||
### Emitted events
|
||||
|
||||
The SDK will emit events using an ``EventEmitter``. It also
|
||||
emits object models (e.g. ``Rooms``, ``RoomMembers``) when they
|
||||
The SDK will emit events using an `EventEmitter`. It also
|
||||
emits object models (e.g. `Rooms`, `RoomMembers`) when they
|
||||
are updated.
|
||||
|
||||
```javascript
|
||||
// Listen for low-level MatrixEvents
|
||||
client.on("event", function(event) {
|
||||
// Listen for low-level MatrixEvents
|
||||
client.on("event", function (event) {
|
||||
console.log(event.getType());
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function (event, member) {
|
||||
if (member.typing) {
|
||||
console.log(member.name + " is typing...");
|
||||
console.log(member.name + " is typing...");
|
||||
} else {
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
else {
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
### Promises and Callbacks
|
||||
@@ -181,11 +175,11 @@ The typical usage is something like:
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
|
||||
Alternatively, if you have a Node.js-style `callback(err, result)` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
@@ -193,61 +187,65 @@ promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
|
||||
via an optional ``callback`` argument. The callback support is now deprecated:
|
||||
new methods do not include a ``callback`` argument, and in the future it may be
|
||||
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
|
||||
via an optional `callback` argument. The callback support is now deprecated:
|
||||
new methods do not include a `callback` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
|
||||
Examples
|
||||
--------
|
||||
## Examples
|
||||
|
||||
This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const myUserId = "@example:localhost";
|
||||
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId
|
||||
});
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const myUserId = "@example:localhost";
|
||||
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
```
|
||||
|
||||
### Automatically join rooms when invited
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomMember.membership", function(event, member) {
|
||||
if (member.membership === "invite" && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).then(function() {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
matrixClient.on("RoomMember.membership", function (event, member) {
|
||||
if (member.membership === "invite" && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).then(function () {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
### Print out messages for all rooms
|
||||
|
||||
```javascript
|
||||
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only print messages
|
||||
}
|
||||
console.log(
|
||||
// the room name will update with m.room.name events automatically
|
||||
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
|
||||
);
|
||||
});
|
||||
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only print messages
|
||||
}
|
||||
console.log(
|
||||
// the room name will update with m.room.name events automatically
|
||||
"(%s) %s :: %s",
|
||||
room.name,
|
||||
event.getSender(),
|
||||
event.getContent().body,
|
||||
);
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
(My Room) @megan:localhost :: Hello world
|
||||
(My Room) @megan:localhost :: how are you?
|
||||
@@ -259,27 +257,24 @@ Output:
|
||||
### Print out membership lists whenever they are changed
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomState.members", function(event, state, member) {
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
console.log(
|
||||
"(%s) %s",
|
||||
memberList[i].membership,
|
||||
memberList[i].name
|
||||
);
|
||||
}
|
||||
});
|
||||
matrixClient.on("RoomState.members", function (event, state, member) {
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
console.log("(%s) %s", memberList[i].membership, memberList[i].name);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
My Room
|
||||
=======
|
||||
@@ -289,8 +284,7 @@ Output:
|
||||
(invite) @charlie:localhost
|
||||
```
|
||||
|
||||
API Reference
|
||||
=============
|
||||
# API Reference
|
||||
|
||||
A hosted reference can be found at
|
||||
http://matrix-org.github.io/matrix-js-sdk/index.html
|
||||
@@ -304,21 +298,20 @@ host the API reference from the source files like this:
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
Then visit `http://localhost:8005` to see the API docs.
|
||||
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
# End-to-end encryption support
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
application to make libolm available, via the `Olm` global.
|
||||
|
||||
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
|
||||
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
|
||||
initialise the crypto layer.
|
||||
|
||||
If the ``Olm`` global is not available, the SDK will show a warning, as shown
|
||||
below; ``initCrypto()`` will also fail.
|
||||
If the `Olm` global is not available, the SDK will show a warning, as shown
|
||||
below; `initCrypto()` will also fail.
|
||||
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
@@ -330,46 +323,48 @@ specification.
|
||||
|
||||
To provide the Olm library in a browser application:
|
||||
|
||||
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
|
||||
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://packages.matrix.org/npm/olm/)
|
||||
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
|
||||
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
|
||||
application also works without e2e crypto enabled, add ``--optional`` to mark it
|
||||
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
|
||||
application also works without e2e crypto enabled, add `--optional` to mark it
|
||||
as an optional dependency.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributing
|
||||
============
|
||||
*This section is for people who want to modify the SDK. If you just
|
||||
want to use this SDK, skip this section.*
|
||||
_This section is for people who want to modify the SDK. If you just
|
||||
want to use this SDK, skip this section._
|
||||
|
||||
First, you need to pull in the right build tools:
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
Building
|
||||
--------
|
||||
## Building
|
||||
|
||||
To build a browser version from scratch when developing::
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
To run tests (Jasmine)::
|
||||
To run tests (Jest):
|
||||
|
||||
```
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
To run linting:
|
||||
|
||||
```
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
+27
-27
@@ -20,19 +20,19 @@ blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
* Chrome
|
||||
* Log database may fail to start with AbortError
|
||||
* IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
* When near the quota, QuotaExceededError is used more consistently
|
||||
* Firefox
|
||||
* The first error will be QuotaExceededError
|
||||
* Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
* Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
* Can revive access if you reopen the DB
|
||||
- Chrome
|
||||
- Log database may fail to start with AbortError
|
||||
- IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
- When near the quota, QuotaExceededError is used more consistently
|
||||
- Firefox
|
||||
- The first error will be QuotaExceededError
|
||||
- Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
- Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
- Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
* Chrome, Firefox
|
||||
* IndexedDB for origin deleted
|
||||
* Local Storage remains in place
|
||||
- Chrome, Firefox
|
||||
- IndexedDB for origin deleted
|
||||
- Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
* Chrome
|
||||
* Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
* Firefox
|
||||
* Firefox 67 shows a prompt to grant
|
||||
* Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
- Chrome
|
||||
- Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
- Firefox
|
||||
- Firefox 67 shows a prompt to grant
|
||||
- Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
* Chrome, Firefox
|
||||
* Can run this at any time to request an estimate of space remaining
|
||||
* Firefox
|
||||
* Returns `0` for `usage` if a site is persisted
|
||||
- Chrome, Firefox
|
||||
- Can run this at any time to request an estimate of space remaining
|
||||
- Firefox
|
||||
- Returns `0` for `usage` if a site is persisted
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
To try it out, **you must build the SDK first** and then host this folder:
|
||||
|
||||
```
|
||||
$ npm run build
|
||||
$ cd examples/browser
|
||||
$ python -m SimpleHTTPServer 8003
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8003``.
|
||||
@@ -1,13 +0,0 @@
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("https://matrix.org");
|
||||
client.publicRooms(function (err, data) {
|
||||
if (err) {
|
||||
console.error("err %s", JSON.stringify(err));
|
||||
return;
|
||||
}
|
||||
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
|
||||
console.log("Congratulations! The SDK is working on the browser!");
|
||||
var result = document.getElementById("result");
|
||||
result.innerHTML = "<p>The SDK appears to be working correctly.</p>";
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="data:,">
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Sanity Testing (check the console) : This example is here to make sure that
|
||||
the SDK works inside a browser. It simply does a GET /publicRooms on
|
||||
matrix.org
|
||||
<br/>
|
||||
You should see a message confirming that the SDK works below:
|
||||
<br/>
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -1,2 +0,0 @@
|
||||
olm.js
|
||||
olm.wasm
|
||||
@@ -1 +0,0 @@
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Test Crypto in Browser</title>
|
||||
<script src="lib/olm.js"></script>
|
||||
<script src="lib/matrix.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing export/import of Olm devices in the browser</h1>
|
||||
<ul>
|
||||
<li>
|
||||
Make sure you built the current version of the Matrix JS SDK
|
||||
(<code>yarn build</code>)
|
||||
</li>
|
||||
<li>
|
||||
copy <code>olm.js</code> and <code>olm.wasm</code>
|
||||
from a recent release of Olm (was tested with version 3.1.4)
|
||||
in directory <code>lib/</code>
|
||||
</li>
|
||||
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
|
||||
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
|
||||
<li>
|
||||
in the JS console, do:
|
||||
<pre>
|
||||
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
|
||||
await aliceMatrixClient.exportDevice();
|
||||
await aliceMatrixClient.getAccessToken();
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
|
||||
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
|
||||
</li>
|
||||
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
|
||||
<li>
|
||||
Do the following, replacing <code>ALICE_ID</code>
|
||||
with the user ID of Alice (you can find it in the exported data)
|
||||
<pre>
|
||||
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
|
||||
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
|
||||
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
|
||||
</pre>
|
||||
</li>
|
||||
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
|
||||
<li>
|
||||
Now do the following, using the exported data and the access token you saved previously:
|
||||
<pre>
|
||||
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
|
||||
</pre>
|
||||
</li>
|
||||
<li>You should see the message sent by Bob printed in the console.</li>
|
||||
</ul>
|
||||
|
||||
<script src="olm-device-export-import.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,122 +0,0 @@
|
||||
if (!Olm) {
|
||||
console.error(
|
||||
"global.Olm does not seem to be present."
|
||||
+ " Did you forget to add olm in the lib/ directory?"
|
||||
);
|
||||
}
|
||||
|
||||
const BASE_URL = 'http://localhost:8008';
|
||||
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
|
||||
const PASSWORD = 'password';
|
||||
|
||||
// useful to create new usernames
|
||||
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
|
||||
|
||||
window.newMatrixClient = async function (username) {
|
||||
const registrationClient = matrixcs.createClient(BASE_URL);
|
||||
|
||||
const userRegisterResult = await registrationClient.register(
|
||||
username,
|
||||
PASSWORD,
|
||||
null,
|
||||
{ type: 'm.login.dummy' }
|
||||
);
|
||||
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
userId: userRegisterResult.user_id,
|
||||
accessToken: userRegisterResult.access_token,
|
||||
deviceId: userRegisterResult.device_id,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
window.importMatrixClient = async function (exportedDevice, accessToken) {
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
deviceToImport: exportedDevice,
|
||||
accessToken,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
function extendMatrixClient(matrixClient) {
|
||||
// automatic join
|
||||
matrixClient.on('RoomMember.membership', async (event, member) => {
|
||||
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
|
||||
await matrixClient.joinRoom(member.roomId);
|
||||
// setting up of room encryption seems to be triggered automatically
|
||||
// but if we don't wait for it the first messages we send are unencrypted
|
||||
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.onDecryptedMessage = message => {
|
||||
console.log('Got encrypted message: ', message);
|
||||
}
|
||||
|
||||
matrixClient.on('Event.decrypted', (event) => {
|
||||
if (event.getType() === 'm.room.message'){
|
||||
matrixClient.onDecryptedMessage(event.getContent().body);
|
||||
} else {
|
||||
console.log('decrypted an event of type', event.getType());
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.createEncryptedRoom = async function(usersToInvite) {
|
||||
const {
|
||||
room_id: roomId,
|
||||
} = await this.createRoom({
|
||||
visibility: 'private',
|
||||
invite: usersToInvite,
|
||||
});
|
||||
|
||||
// matrixClient.setRoomEncryption() only updates local state
|
||||
// but does not send anything to the server
|
||||
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
|
||||
// so we do it ourselves with 'sendStateEvent'
|
||||
await this.sendStateEvent(
|
||||
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
await this.setRoomEncryption(
|
||||
roomId, ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
|
||||
// Marking all devices as verified
|
||||
let room = this.getRoom(roomId);
|
||||
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
|
||||
let memberkeys = await this.downloadKeys(members);
|
||||
for (const userId in memberkeys) {
|
||||
for (const deviceId in memberkeys[userId]) {
|
||||
await this.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return roomId;
|
||||
}
|
||||
|
||||
matrixClient.sendTextMessage = async function(message, roomId) {
|
||||
return matrixClient.sendMessage(
|
||||
roomId,
|
||||
{
|
||||
body: message,
|
||||
msgtype: 'm.text',
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists.
|
||||
|
||||
|
||||
To try it out, you will need to edit `app.js` to configure it for your `homeserver`, `access_token` and `user_id`. Then run:
|
||||
|
||||
```
|
||||
@@ -24,7 +23,7 @@ Room list index commands:
|
||||
Room commands:
|
||||
'/exit' Return to the room list index.
|
||||
'/members' Show the room member list.
|
||||
|
||||
|
||||
$ /enter 2
|
||||
|
||||
[2015-06-12 15:14:54] Megan2 <<< herro
|
||||
|
||||
+140
-152
@@ -5,7 +5,7 @@ var clc = require("cli-color");
|
||||
var matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// Data structures
|
||||
@@ -14,15 +14,15 @@ var viewingRoom = null;
|
||||
var numMessagesToShow = 20;
|
||||
|
||||
// Reading from stdin
|
||||
var CLEAR_CONSOLE = '\x1B[2J';
|
||||
var CLEAR_CONSOLE = "\x1B[2J";
|
||||
var readline = require("readline");
|
||||
var rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
completer: completer
|
||||
completer: completer,
|
||||
});
|
||||
rl.setPrompt("$ ");
|
||||
rl.on('line', function(line) {
|
||||
rl.on("line", function (line) {
|
||||
if (line.trim().length === 0) {
|
||||
rl.prompt();
|
||||
return;
|
||||
@@ -37,14 +37,11 @@ rl.on('line', function(line) {
|
||||
if (line === "/exit") {
|
||||
viewingRoom = null;
|
||||
printRoomList();
|
||||
}
|
||||
else if (line === "/members") {
|
||||
} else if (line === "/members") {
|
||||
printMemberList(viewingRoom);
|
||||
}
|
||||
else if (line === "/roominfo") {
|
||||
} else if (line === "/roominfo") {
|
||||
printRoomInfo(viewingRoom);
|
||||
}
|
||||
else if (line === "/resend") {
|
||||
} else if (line === "/resend") {
|
||||
// get the oldest not sent event.
|
||||
var notSentEvent;
|
||||
for (var i = 0; i < viewingRoom.timeline.length; i++) {
|
||||
@@ -54,76 +51,84 @@ rl.on('line', function(line) {
|
||||
}
|
||||
}
|
||||
if (notSentEvent) {
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).then(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
printMessages();
|
||||
print("/resend Error: %s", err);
|
||||
rl.prompt();
|
||||
});
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).then(
|
||||
function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
printMessages();
|
||||
print("/resend Error: %s", err);
|
||||
rl.prompt();
|
||||
},
|
||||
);
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}
|
||||
}
|
||||
else if (line.indexOf("/more ") === 0) {
|
||||
} else if (line.indexOf("/more ") === 0) {
|
||||
var amount = parseInt(line.split(" ")[1]) || 20;
|
||||
matrixClient.scrollback(viewingRoom, amount).then(function(room) {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/more Error: %s", err);
|
||||
});
|
||||
}
|
||||
else if (line.indexOf("/invite ") === 0) {
|
||||
matrixClient.scrollback(viewingRoom, amount).then(
|
||||
function (room) {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/more Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else if (line.indexOf("/invite ") === 0) {
|
||||
var userId = line.split(" ")[1].trim();
|
||||
matrixClient.invite(viewingRoom.roomId, userId).then(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/invite Error: %s", err);
|
||||
});
|
||||
}
|
||||
else if (line.indexOf("/file ") === 0) {
|
||||
matrixClient.invite(viewingRoom.roomId, userId).then(
|
||||
function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/invite Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else if (line.indexOf("/file ") === 0) {
|
||||
var filename = line.split(" ")[1].trim();
|
||||
var stream = fs.createReadStream(filename);
|
||||
matrixClient.uploadContent({
|
||||
stream: stream,
|
||||
name: filename
|
||||
}).then(function(url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
body: filename,
|
||||
url: JSON.parse(url).content_uri
|
||||
};
|
||||
matrixClient.sendMessage(viewingRoom.roomId, content);
|
||||
});
|
||||
}
|
||||
else {
|
||||
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() {
|
||||
matrixClient
|
||||
.uploadContent({
|
||||
stream: stream,
|
||||
name: filename,
|
||||
})
|
||||
.then(function (url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
body: filename,
|
||||
url: JSON.parse(url).content_uri,
|
||||
};
|
||||
matrixClient.sendMessage(viewingRoom.roomId, content);
|
||||
});
|
||||
} else {
|
||||
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
});
|
||||
// print local echo immediately
|
||||
printMessages();
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (line.indexOf("/join ") === 0) {
|
||||
var roomIndex = line.split(" ")[1];
|
||||
viewingRoom = roomList[roomIndex];
|
||||
if (viewingRoom.getMember(myUserId).membership === "invite") {
|
||||
// join the room first
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(function(room) {
|
||||
setRoomList();
|
||||
viewingRoom = room;
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/join Error: %s", err);
|
||||
});
|
||||
}
|
||||
else {
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(
|
||||
function (room) {
|
||||
setRoomList();
|
||||
viewingRoom = room;
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/join Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
printMessages();
|
||||
}
|
||||
}
|
||||
@@ -133,18 +138,18 @@ rl.on('line', function(line) {
|
||||
// ==== END User input
|
||||
|
||||
// show the room list after syncing.
|
||||
matrixClient.on("sync", function(state, prevState, data) {
|
||||
matrixClient.on("sync", function (state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on("Room", function() {
|
||||
matrixClient.on("Room", function () {
|
||||
setRoomList();
|
||||
if (!viewingRoom) {
|
||||
printRoomList();
|
||||
@@ -153,7 +158,7 @@ matrixClient.on("Room", function() {
|
||||
});
|
||||
|
||||
// print incoming messages.
|
||||
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
@@ -165,20 +170,19 @@ matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
|
||||
function setRoomList() {
|
||||
roomList = matrixClient.getRooms();
|
||||
roomList.sort(function(a,b) {
|
||||
roomList.sort(function (a, b) {
|
||||
// < 0 = a comes first (lower index) - we want high indexes = newer
|
||||
var aMsg = a.timeline[a.timeline.length-1];
|
||||
var aMsg = a.timeline[a.timeline.length - 1];
|
||||
if (!aMsg) {
|
||||
return -1;
|
||||
}
|
||||
var bMsg = b.timeline[b.timeline.length-1];
|
||||
var bMsg = b.timeline[b.timeline.length - 1];
|
||||
if (!bMsg) {
|
||||
return 1;
|
||||
}
|
||||
if (aMsg.getTs() > bMsg.getTs()) {
|
||||
return 1;
|
||||
}
|
||||
else if (aMsg.getTs() < bMsg.getTs()) {
|
||||
} else if (aMsg.getTs() < bMsg.getTs()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -189,16 +193,15 @@ function printRoomList() {
|
||||
print(CLEAR_CONSOLE);
|
||||
print("Room List:");
|
||||
var fmts = {
|
||||
"invite": clc.cyanBright,
|
||||
"leave": clc.blackBright
|
||||
invite: clc.cyanBright,
|
||||
leave: clc.blackBright,
|
||||
};
|
||||
for (var i = 0; i < roomList.length; i++) {
|
||||
var msg = roomList[i].timeline[roomList[i].timeline.length-1];
|
||||
var msg = roomList[i].timeline[roomList[i].timeline.length - 1];
|
||||
var dateStr = "---";
|
||||
var fmt;
|
||||
if (msg) {
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(
|
||||
/T/, ' ').replace(/\..+/, '');
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
|
||||
}
|
||||
var myMembership = roomList[i].getMyMembership();
|
||||
if (myMembership) {
|
||||
@@ -207,9 +210,10 @@ function printRoomList() {
|
||||
var roomName = fixWidth(roomList[i].name, 25);
|
||||
print(
|
||||
"[%s] %s (%s members) %s",
|
||||
i, fmt ? fmt(roomName) : roomName,
|
||||
i,
|
||||
fmt ? fmt(roomName) : roomName,
|
||||
roomList[i].getJoinedMembers().length,
|
||||
dateStr
|
||||
dateStr,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -230,12 +234,12 @@ function printHelp() {
|
||||
}
|
||||
|
||||
function completer(line) {
|
||||
var completions = [
|
||||
"/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"
|
||||
];
|
||||
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 });
|
||||
var completions = ["/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"];
|
||||
var hits = completions.filter(function (c) {
|
||||
return c.indexOf(line) == 0;
|
||||
});
|
||||
// show all completions if none found
|
||||
return [hits.length ? hits : completions, line]
|
||||
return [hits.length ? hits : completions, line];
|
||||
}
|
||||
|
||||
function printMessages() {
|
||||
@@ -252,14 +256,14 @@ function printMessages() {
|
||||
|
||||
function printMemberList(room) {
|
||||
var fmts = {
|
||||
"join": clc.green,
|
||||
"ban": clc.red,
|
||||
"invite": clc.blue,
|
||||
"leave": clc.blackBright
|
||||
join: clc.green,
|
||||
ban: clc.red,
|
||||
invite: clc.blue,
|
||||
leave: clc.blackBright,
|
||||
};
|
||||
var members = room.currentState.getMembers();
|
||||
// sorted based on name.
|
||||
members.sort(function(a, b) {
|
||||
members.sort(function (a, b) {
|
||||
if (a.name > b.name) {
|
||||
return -1;
|
||||
}
|
||||
@@ -268,21 +272,24 @@ function printMemberList(room) {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
print("Membership list for room \"%s\"", room.name);
|
||||
print('Membership list for room "%s"', room.name);
|
||||
print(new Array(room.name.length + 28).join("-"));
|
||||
room.currentState.getMembers().forEach(function(member) {
|
||||
room.currentState.getMembers().forEach(function (member) {
|
||||
if (!member.membership) {
|
||||
return;
|
||||
}
|
||||
var fmt = fmts[member.membership] || function(a){return a;};
|
||||
var membershipWithPadding = (
|
||||
member.membership + new Array(10 - member.membership.length).join(" ")
|
||||
);
|
||||
var fmt =
|
||||
fmts[member.membership] ||
|
||||
function (a) {
|
||||
return a;
|
||||
};
|
||||
var membershipWithPadding = member.membership + new Array(10 - member.membership.length).join(" ");
|
||||
print(
|
||||
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
|
||||
membershipWithPadding, member.name,
|
||||
(member.userId === myUserId ? "Me" : member.userId),
|
||||
fmt
|
||||
"%s" + fmt(" :: ") + "%s" + fmt(" (") + "%s" + fmt(")"),
|
||||
membershipWithPadding,
|
||||
member.name,
|
||||
member.userId === myUserId ? "Me" : member.userId,
|
||||
fmt,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -292,38 +299,31 @@ function printRoomInfo(room) {
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
var restCount = (
|
||||
100 - "Content".length - " | ".length - " | ".length -
|
||||
eTypeHeader.length - sendHeader.length
|
||||
);
|
||||
var padSide = new Array(Math.floor(restCount/2)).join(" ");
|
||||
var restCount = 100 - "Content".length - " | ".length - " | ".length - eTypeHeader.length - sendHeader.length;
|
||||
var padSide = new Array(Math.floor(restCount / 2)).join(" ");
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(eTypeHeader + sendHeader + contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
eventMap.keys().forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
eventMap.keys().forEach(function (eventType) {
|
||||
if (eventType === "m.room.member") {
|
||||
return;
|
||||
} // use /members instead.
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
eventEventMap.keys().forEach(function (stateKey) {
|
||||
var typeAndKey = eventType + (stateKey.length > 0 ? "(" + stateKey + ")" : "");
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
);
|
||||
print(typeStr+" | "+sendStr+" | "+contentStr);
|
||||
var contentStr = fixWidth(JSON.stringify(event.getContent()), contentHeader.length);
|
||||
print(typeStr + " | " + sendStr + " | " + contentStr);
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function printLine(event) {
|
||||
var fmt;
|
||||
var name = event.sender ? event.sender.name : event.getSender();
|
||||
var time = new Date(
|
||||
event.getTs()
|
||||
).toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||
var time = new Date(event.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
|
||||
var separator = "<<<";
|
||||
if (event.getSender() === myUserId) {
|
||||
name = "Me";
|
||||
@@ -331,8 +331,7 @@ function printLine(event) {
|
||||
if (event.status === sdk.EventStatus.SENDING) {
|
||||
separator = "...";
|
||||
fmt = clc.xterm(8);
|
||||
}
|
||||
else if (event.status === sdk.EventStatus.NOT_SENT) {
|
||||
} else if (event.status === sdk.EventStatus.NOT_SENT) {
|
||||
separator = " x ";
|
||||
fmt = clc.redBright;
|
||||
}
|
||||
@@ -341,69 +340,58 @@ function printLine(event) {
|
||||
|
||||
var maxNameWidth = 15;
|
||||
if (name.length > maxNameWidth) {
|
||||
name = name.slice(0, maxNameWidth-1) + "\u2026";
|
||||
name = name.slice(0, maxNameWidth - 1) + "\u2026";
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.message") {
|
||||
body = event.getContent().body;
|
||||
}
|
||||
else if (event.isState()) {
|
||||
} else if (event.isState()) {
|
||||
var stateName = event.getType();
|
||||
if (event.getStateKey().length > 0) {
|
||||
stateName += " ("+event.getStateKey()+")";
|
||||
stateName += " (" + event.getStateKey() + ")";
|
||||
}
|
||||
body = (
|
||||
"[State: "+stateName+" updated to: "+JSON.stringify(event.getContent())+"]"
|
||||
);
|
||||
body = "[State: " + stateName + " updated to: " + JSON.stringify(event.getContent()) + "]";
|
||||
separator = "---";
|
||||
fmt = clc.xterm(249).italic;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// random message event
|
||||
body = (
|
||||
"[Message: "+event.getType()+" Content: "+JSON.stringify(event.getContent())+"]"
|
||||
);
|
||||
body = "[Message: " + event.getType() + " Content: " + JSON.stringify(event.getContent()) + "]";
|
||||
separator = "---";
|
||||
fmt = clc.xterm(249).italic;
|
||||
}
|
||||
if (fmt) {
|
||||
print(
|
||||
"[%s] %s %s %s", time, name, separator, body, fmt
|
||||
);
|
||||
}
|
||||
else {
|
||||
print("[%s] %s %s %s", time, name, separator, body, fmt);
|
||||
} else {
|
||||
print("[%s] %s %s %s", time, name, separator, body);
|
||||
}
|
||||
}
|
||||
|
||||
function print(str, formatter) {
|
||||
if (typeof arguments[arguments.length-1] === "function") {
|
||||
if (typeof arguments[arguments.length - 1] === "function") {
|
||||
// last arg is the formatter so get rid of it and use it on each
|
||||
// param passed in but not the template string.
|
||||
var newArgs = [];
|
||||
var i = 0;
|
||||
for (i=0; i<arguments.length-1; i++) {
|
||||
for (i = 0; i < arguments.length - 1; i++) {
|
||||
newArgs.push(arguments[i]);
|
||||
}
|
||||
var fmt = arguments[arguments.length-1];
|
||||
for (i=0; i<newArgs.length; i++) {
|
||||
var fmt = arguments[arguments.length - 1];
|
||||
for (i = 0; i < newArgs.length; i++) {
|
||||
newArgs[i] = fmt(newArgs[i]);
|
||||
}
|
||||
console.log.apply(console.log, newArgs);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.log.apply(console.log, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function fixWidth(str, len) {
|
||||
if (str.length > len) {
|
||||
return str.substring(0, len-2) + "\u2026";
|
||||
}
|
||||
else if (str.length < len) {
|
||||
return str.substring(0, len - 2) + "\u2026";
|
||||
} else if (str.length < len) {
|
||||
return str + new Array(len - str.length).join(" ");
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
matrixClient.startClient(numMessagesToShow); // messages for each room.
|
||||
matrixClient.startClient(numMessagesToShow); // messages for each room.
|
||||
|
||||
+12
-12
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"preinstall": "npm install ../.."
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0"
|
||||
}
|
||||
"name": "example-app",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"preinstall": "npm install ../.."
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ To try it out, **you must build the SDK first** and then host this folder:
|
||||
$ python -m SimpleHTTPServer 8003
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8003``.
|
||||
Then visit `http://localhost:8003`.
|
||||
|
||||
@@ -9,7 +9,7 @@ const client = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
accessToken: TOKEN,
|
||||
userId: USER_ID,
|
||||
deviceId: DEVICE_ID
|
||||
deviceId: DEVICE_ID,
|
||||
});
|
||||
let call;
|
||||
|
||||
@@ -21,18 +21,16 @@ function disableButtons(place, answer, hangup) {
|
||||
|
||||
function addListeners(call) {
|
||||
let lastError = "";
|
||||
call.on("hangup", function() {
|
||||
call.on("hangup", function () {
|
||||
disableButtons(false, true, true);
|
||||
document.getElementById("result").innerHTML = (
|
||||
"<p>Call ended. Last error: "+lastError+"</p>"
|
||||
);
|
||||
document.getElementById("result").innerHTML = "<p>Call ended. Last error: " + lastError + "</p>";
|
||||
});
|
||||
call.on("error", function(err) {
|
||||
call.on("error", function (err) {
|
||||
lastError = err.message;
|
||||
call.hangup();
|
||||
disableButtons(false, true, true);
|
||||
});
|
||||
call.on("feeds_changed", function(feeds) {
|
||||
call.on("feeds_changed", function (feeds) {
|
||||
const localFeed = feeds.find((feed) => feed.isLocal());
|
||||
const remoteFeed = feeds.find((feed) => !feed.isLocal());
|
||||
|
||||
@@ -51,33 +49,38 @@ function addListeners(call) {
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
window.onload = function () {
|
||||
document.getElementById("result").innerHTML = "<p>Please wait. Syncing...</p>";
|
||||
document.getElementById("config").innerHTML = "<p>" +
|
||||
"Homeserver: <code>"+BASE_URL+"</code><br/>"+
|
||||
"Room: <code>"+ROOM_ID+"</code><br/>"+
|
||||
"User: <code>"+USER_ID+"</code><br/>"+
|
||||
document.getElementById("config").innerHTML =
|
||||
"<p>" +
|
||||
"Homeserver: <code>" +
|
||||
BASE_URL +
|
||||
"</code><br/>" +
|
||||
"Room: <code>" +
|
||||
ROOM_ID +
|
||||
"</code><br/>" +
|
||||
"User: <code>" +
|
||||
USER_ID +
|
||||
"</code><br/>" +
|
||||
"</p>";
|
||||
disableButtons(true, true, true);
|
||||
};
|
||||
|
||||
client.on("sync", function(state, prevState, data) {
|
||||
client.on("sync", function (state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
|
||||
disableButtons(false, true, true);
|
||||
|
||||
document.getElementById("call").onclick = function() {
|
||||
document.getElementById("call").onclick = function () {
|
||||
console.log("Placing call...");
|
||||
call = matrixcs.createNewMatrixCall(
|
||||
client, ROOM_ID
|
||||
);
|
||||
call = matrixcs.createNewMatrixCall(client, ROOM_ID);
|
||||
console.log("Call => %s", call);
|
||||
addListeners(call);
|
||||
call.placeVideoCall();
|
||||
@@ -85,14 +88,14 @@ function syncComplete() {
|
||||
disableButtons(true, true, false);
|
||||
};
|
||||
|
||||
document.getElementById("hangup").onclick = function() {
|
||||
document.getElementById("hangup").onclick = function () {
|
||||
console.log("Hanging up call...");
|
||||
console.log("Call => %s", call);
|
||||
call.hangup();
|
||||
document.getElementById("result").innerHTML = "<p>Hungup call.</p>";
|
||||
};
|
||||
|
||||
document.getElementById("answer").onclick = function() {
|
||||
document.getElementById("answer").onclick = function () {
|
||||
console.log("Answering call...");
|
||||
console.log("Call => %s", call);
|
||||
call.answer();
|
||||
@@ -100,7 +103,7 @@ function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Answered call.</p>";
|
||||
};
|
||||
|
||||
client.on("Call.incoming", function(c) {
|
||||
client.on("Call.incoming", function (c) {
|
||||
console.log("Call ringing");
|
||||
disableButtons(true, false, false);
|
||||
document.getElementById("result").innerHTML = "<p>Incoming call...</p>";
|
||||
|
||||
+19
-21
@@ -1,25 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
constants in <code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the constants in
|
||||
<code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
@@ -31,4 +29,4 @@
|
||||
.video-element {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
+131
-137
@@ -1,141 +1,135 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "21.2.0",
|
||||
"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.js -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",
|
||||
"lint:js-fix": "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.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
"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",
|
||||
"@types/sdp-transform": "^2.4.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"
|
||||
},
|
||||
"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.13.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.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": "^1.2.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^0.7.0",
|
||||
"eslint-plugin-unicorn": "^44.0.2",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.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": "29.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\"",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn build:dev",
|
||||
"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",
|
||||
"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": [
|
||||
"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.3-alpha.0",
|
||||
"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.6.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",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.49.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
|
||||
},
|
||||
"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)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require('fs/promises');
|
||||
const fsProm = require("fs/promises");
|
||||
|
||||
const PKGJSON = 'package.json';
|
||||
const PKGJSON = "package.json";
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
|
||||
for (const field of ['main', 'typings']) {
|
||||
if (pkgJson["matrix_lib_"+field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_"+field];
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
|
||||
for (const field of ["main", "typings"]) {
|
||||
if (pkgJson["matrix_lib_" + field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_" + field];
|
||||
}
|
||||
}
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
|
||||
+86
-62
@@ -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 "./olm-loader";
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
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 { 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;
|
||||
@@ -73,15 +80,18 @@ export class TestClient {
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'TestClient[' + this.userId + ']';
|
||||
return "TestClient[" + this.userId + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*/
|
||||
public start(): Promise<void> {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {});
|
||||
public start(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(this + ": starting");
|
||||
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,19 +103,18 @@ 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(() => {
|
||||
logger.log(this + ': started');
|
||||
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
|
||||
logger.log(this + ": started");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
|
||||
* @returns Promise which resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.client.stopClient();
|
||||
@@ -113,20 +122,30 @@ 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")
|
||||
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');
|
||||
logger.log(this + ": received device keys");
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,7 +154,7 @@ export class TestClient {
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns {Promise} for the one-time keys
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
if (Object.keys(this.oneTimeKeys!).length != 0) {
|
||||
@@ -143,30 +162,35 @@ export class TestClient {
|
||||
return Promise.resolve(this.oneTimeKeys!);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
} };
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
logger.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys!).length);
|
||||
logger.log("%s: received %i one-time keys", this, Object.keys(content.one_time_keys!).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
} };
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
|
||||
return this.httpBackend.flush("/keys/upload", 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys!;
|
||||
});
|
||||
@@ -177,57 +201,57 @@ export class TestClient {
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} response response to the query.
|
||||
* @param response - response to the query.
|
||||
*/
|
||||
public expectKeyQuery(response: IDownloadKeyResult) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
|
||||
200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys![userId]).toEqual([]);
|
||||
});
|
||||
return response;
|
||||
this.httpBackend.when("POST", "/keys/query").respond<IDownloadKeyResult>(200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
* @returns base64 device key
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
const keyId = "curve25519:" + this.deviceId;
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
* @returns base64 device key
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
const keyId = "ed25519:" + this.deviceId;
|
||||
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`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
return Promise.all([this.httpBackend.flush("/sync", 1), syncPromise(this.client)]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 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 "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import type { MatrixClient, ClientEvent } from "../../src";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
|
||||
afterAll(() => {
|
||||
// clean up XMLHttpRequest mock
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
||||
|
||||
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
|
||||
global.matrixcs = {
|
||||
...global.matrixcs,
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
import "./setupTests";// uses browser-matrix instead of the src
|
||||
import type { MatrixClient } from "../../src";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
|
||||
beforeEach(() => {
|
||||
httpBackend = new HttpBackend();
|
||||
client = new global.matrixcs.MatrixClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: USER_ID,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
deviceId: DEVICE_ID,
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
client.http.abort();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
await httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", async () => {
|
||||
const event = {
|
||||
type: "m.room.member",
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
membership: "join",
|
||||
name: "Displayname",
|
||||
},
|
||||
event_id: "$foobar",
|
||||
};
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
|
||||
const unexpectedErrorFn = jest.fn();
|
||||
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
|
||||
|
||||
client.startClient();
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await syncPromise;
|
||||
expect(unexpectedErrorFn).not.toHaveBeenCalled();
|
||||
}, 20000); // additional timeout as this test can take quite a while
|
||||
});
|
||||
@@ -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,804 @@
|
||||
/*
|
||||
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 { createClient, CryptoEvent, ICreateClientOpts, 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 = testData.TEST_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 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;
|
||||
// const newBackendOnly = backend === "libolm" ? 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;
|
||||
}
|
||||
|
||||
it("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: [testData.ENCRYPTED_EVENT],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
testData.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.getCrypto()!.checkKeyBackupAndEnable();
|
||||
|
||||
// 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(testData.CLEAR_EVENT.content);
|
||||
});
|
||||
|
||||
describe("recover from backup", () => {
|
||||
it("can restore from backup (Curve25519 version)", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
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);
|
||||
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
let onKeyCached: () => void;
|
||||
const awaitKeyCached = new Promise<void>((resolve) => {
|
||||
onKeyCached = resolve;
|
||||
});
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
cacheCompleteCallback: () => onKeyCached(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
|
||||
await awaitKeyCached;
|
||||
|
||||
// The key should be now cached
|
||||
const afterCache = await aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!);
|
||||
|
||||
expect(afterCache.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("recover specific session from backup", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
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(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
ROOM_ID,
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
check!.backupInfo!,
|
||||
);
|
||||
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("Fails on bad recovery key", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
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);
|
||||
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
await expect(
|
||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
+194
-184
@@ -22,18 +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 { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from '../../src/crypto/deviceinfo';
|
||||
import type { Session } from "@matrix-org/olm";
|
||||
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";
|
||||
@@ -47,39 +49,31 @@ const bobAccessToken = "fewgfkuesa";
|
||||
let aliMessages: IContent[];
|
||||
let bobMessages: IContent[];
|
||||
|
||||
// IMessage isn't exported by src/crypto/algorithms/olm.ts
|
||||
interface OlmPayload {
|
||||
type: number;
|
||||
body: string;
|
||||
}
|
||||
type OlmPayload = ReturnType<Session["encrypt"]>;
|
||||
|
||||
async function bobUploadsDeviceKeys(): Promise<void> {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
await Promise.all([
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that querier will query uploader's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
* @returns resolves once the http request has completed.
|
||||
*/
|
||||
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(uploader.deviceKeys).toBeTruthy();
|
||||
|
||||
const uploaderKeys = {};
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
|
||||
querier.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result = {};
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
const uploaderKeys: Record<string, IDeviceKeys> = {};
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
|
||||
querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result: Record<string, Record<string, IDeviceKeys>> = {};
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
|
||||
@@ -88,16 +82,14 @@ const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
* @returns resolves once the http request has completed.
|
||||
*/
|
||||
async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) {
|
||||
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = '';
|
||||
let keyId = "";
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
@@ -105,7 +97,7 @@ async function expectAliClaimKeys(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {};
|
||||
const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
@@ -138,8 +130,7 @@ async function aliDownloadsKeys(): Promise<void> {
|
||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data!.devices[bobUserId]!;
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,15 +147,13 @@ const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client)
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsFirstMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
@@ -173,14 +162,11 @@ async function aliSendsFirstMessage(): Promise<OlmPayload> {
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]);
|
||||
const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
@@ -188,14 +174,13 @@ async function aliSendsMessage(): Promise<OlmPayload> {
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||
* @returns which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
async function bobSendsReplyMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
expectBobQueryKeys().then(expectBobSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
@@ -203,7 +188,7 @@ async function bobSendsReplyMessage(): Promise<OlmPayload> {
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
|
||||
@@ -217,7 +202,7 @@ async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||
@@ -231,15 +216,13 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
}
|
||||
|
||||
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
return client.sendMessage(
|
||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||
);
|
||||
return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
|
||||
}
|
||||
|
||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise<IContent>((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||
httpBackend.when("PUT", path).respond(200, function (_path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
@@ -254,16 +237,12 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift()!;
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift()!;
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message);
|
||||
}
|
||||
|
||||
async function recvMessage(
|
||||
@@ -276,32 +255,30 @@ async function recvMessage(
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function(event: MatrixEvent) {
|
||||
const onEvent = function (event: MatrixEvent) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
logger.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
logger.log(client.credentials.userId + " received event", event);
|
||||
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
resolve(event);
|
||||
@@ -327,32 +304,32 @@ async function recvMessage(
|
||||
* Send an initial sync response to the client (which just includes the member
|
||||
* list for our test room).
|
||||
*
|
||||
* @param {TestClient} testClient
|
||||
* @returns {Promise} which resolves when the sync has been flushed.
|
||||
* @returns which resolves when the sync has been flushed.
|
||||
*/
|
||||
function firstSync(testClient: TestClient): Promise<void> {
|
||||
// send a sync response including our test room.
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: { },
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
join: {
|
||||
[roomId]: {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -385,6 +362,13 @@ describe("MatrixClient crypto", () => {
|
||||
|
||||
it("Bob uploads device keys", bobUploadsDeviceKeys);
|
||||
|
||||
it("handles failures to upload device keys", async () => {
|
||||
// since device keys are uploaded asynchronously, there's not really much to do here other than fail the
|
||||
// upload.
|
||||
bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh"));
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs device keys", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
await aliDownloadsKeys();
|
||||
@@ -396,10 +380,7 @@ describe("MatrixClient crypto", () => {
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys!;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
expectAliQueryKeys(),
|
||||
]);
|
||||
await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
@@ -409,26 +390,24 @@ describe("MatrixClient crypto", () => {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bvcxz',
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "bvcxz",
|
||||
keys: {
|
||||
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
||||
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
||||
"ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q",
|
||||
"curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ",
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
user_id: "@eve:localhost",
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
"@eve:localhost": {
|
||||
"ed25519:bvcxz":
|
||||
"CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
@@ -445,26 +424,24 @@ describe("MatrixClient crypto", () => {
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", async () => {
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "bad_device",
|
||||
keys: {
|
||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||
"ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0",
|
||||
"curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc",
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
user_id: "@bob:localhost",
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
"@bob:localhost": {
|
||||
"ed25519:bad_device":
|
||||
"fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
@@ -495,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();
|
||||
@@ -506,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();
|
||||
@@ -515,26 +492,25 @@ describe("MatrixClient crypto", () => {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function(event: MatrixEvent) {
|
||||
const onEvent = function (event: MatrixEvent) {
|
||||
logger.log(bobUserId + " received event", event);
|
||||
resolve(event);
|
||||
};
|
||||
@@ -558,11 +534,10 @@ describe("MatrixClient crypto", () => {
|
||||
await aliDownloadsKeys();
|
||||
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||
const p1 = sendMessage(aliTestClient.client);
|
||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
|
||||
.then(function(sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend).then(function (sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
await Promise.all([p1, p2]);
|
||||
});
|
||||
|
||||
@@ -570,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();
|
||||
@@ -588,9 +563,7 @@ describe("MatrixClient crypto", () => {
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {},
|
||||
);
|
||||
bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
@@ -605,28 +578,28 @@ describe("MatrixClient crypto", () => {
|
||||
await aliTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
const syncData = {
|
||||
next_batch: '2',
|
||||
next_batch: "2",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
join: {
|
||||
[roomId]: {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encryption",
|
||||
skey: "",
|
||||
content: {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, syncData);
|
||||
await aliTestClient.httpBackend.flush('/sync', 1);
|
||||
aliTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await aliTestClient.httpBackend.flush("/sync", 1);
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[bobUserId]: {},
|
||||
@@ -649,7 +622,7 @@ describe("MatrixClient crypto", () => {
|
||||
// enqueue expectations:
|
||||
// * Sync with empty one_time_keys => upload keys
|
||||
|
||||
logger.log(aliTestClient + ': starting');
|
||||
logger.log(aliTestClient + ": starting");
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
@@ -659,24 +632,61 @@ describe("MatrixClient crypto", () => {
|
||||
// it will upload one-time keys.
|
||||
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.startClient({}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
logger.log(aliTestClient + ': started');
|
||||
httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await Promise.all([aliTestClient.client.startClient({}), httpBackend.flushAllExpected()]);
|
||||
logger.log(aliTestClient + ": started");
|
||||
httpBackend.when("POST", "/keys/upload").respond(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Checks for outgoing room key requests for a given event's session", async () => {
|
||||
const eventA0 = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const eventA1 = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const eventB = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "othersessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const nonEncryptedEvent = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {},
|
||||
});
|
||||
|
||||
aliTestClient.client.crypto?.onSyncCompleted({});
|
||||
await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0);
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull();
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull();
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
Copyright 2016-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 Olm from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import { IContent, IDeviceKeys, IEvent, MatrixClient } from "../../../src";
|
||||
import { IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { ISyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* A set of utilities for creating Olm accounts and sessions, and encrypting/decrypting with Olm/Megolm.
|
||||
*/
|
||||
|
||||
/** Create an Olm Account object */
|
||||
export async function createOlmAccount(): Promise<Olm.Account> {
|
||||
await Olm.init();
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
return testOlmAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device keys for the test Olm Account
|
||||
*
|
||||
* @param olmAccount - Test olm account
|
||||
* @param userId - The user ID to present the keys as belonging to
|
||||
*/
|
||||
export function getTestOlmAccountKeys(olmAccount: Olm.Account, userId: string, deviceId: string): IDeviceKeys {
|
||||
const testE2eKeys = JSON.parse(olmAccount.identity_keys());
|
||||
const testDeviceKeys: IDeviceKeys = {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: deviceId,
|
||||
keys: {
|
||||
[`curve25519:${deviceId}`]: testE2eKeys.curve25519,
|
||||
[`ed25519:${deviceId}`]: testE2eKeys.ed25519,
|
||||
},
|
||||
user_id: userId,
|
||||
};
|
||||
|
||||
const j = anotherjson.stringify(testDeviceKeys);
|
||||
const sig = olmAccount.sign(j);
|
||||
testDeviceKeys.signatures = { [userId]: { [`ed25519:${deviceId}`]: sig } };
|
||||
return testDeviceKeys;
|
||||
}
|
||||
|
||||
/** start an Olm session with a given recipient */
|
||||
export async function createOlmSession(
|
||||
olmAccount: Olm.Account,
|
||||
recipientTestClient: IE2EKeyReceiver,
|
||||
): Promise<Olm.Session> {
|
||||
const keys = await recipientTestClient.awaitOneTimeKeyUpload();
|
||||
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;
|
||||
}
|
||||
|
||||
// IToDeviceEvent isn't exported by src/sync-accumulator.ts
|
||||
export interface ToDeviceEvent {
|
||||
content: IContent;
|
||||
sender: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/** encrypt an event with an existing olm session */
|
||||
export function encryptOlmEvent(opts: {
|
||||
/** the sender's user id */
|
||||
sender?: string;
|
||||
/** the sender's curve25519 key */
|
||||
senderKey: string;
|
||||
/** the sender's ed25519 key */
|
||||
senderSigningKey: string;
|
||||
/** the olm session to use for encryption */
|
||||
p2pSession: Olm.Session;
|
||||
/** the recipient's user id */
|
||||
recipient: string;
|
||||
/** the recipient's curve25519 key */
|
||||
recipientCurve25519Key: string;
|
||||
/** the recipient's ed25519 key */
|
||||
recipientEd25519Key: string;
|
||||
/** the payload of the message */
|
||||
plaincontent?: object;
|
||||
/** the event type of the payload */
|
||||
plaintype?: string;
|
||||
}): ToDeviceEvent {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.p2pSession).toBeTruthy();
|
||||
expect(opts.recipient).toBeTruthy();
|
||||
|
||||
const plaintext = {
|
||||
content: opts.plaincontent || {},
|
||||
recipient: opts.recipient,
|
||||
recipient_keys: {
|
||||
ed25519: opts.recipientEd25519Key,
|
||||
},
|
||||
keys: {
|
||||
ed25519: opts.senderSigningKey,
|
||||
},
|
||||
sender: opts.sender || "@bob:xyz",
|
||||
type: opts.plaintype || "m.test",
|
||||
};
|
||||
|
||||
return {
|
||||
content: {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
ciphertext: {
|
||||
[opts.recipientCurve25519Key]: opts.p2pSession.encrypt(JSON.stringify(plaintext)),
|
||||
},
|
||||
sender_key: opts.senderKey,
|
||||
},
|
||||
sender: opts.sender || "@bob:xyz",
|
||||
type: "m.room.encrypted",
|
||||
};
|
||||
}
|
||||
|
||||
// encrypt an event with megolm
|
||||
export function encryptMegolmEvent(opts: {
|
||||
senderKey: string;
|
||||
groupSession: Olm.OutboundGroupSession;
|
||||
plaintext?: Partial<IEvent>;
|
||||
room_id?: string;
|
||||
}): IEvent {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.groupSession).toBeTruthy();
|
||||
|
||||
const plaintext = opts.plaintext || {};
|
||||
if (!plaintext.content) {
|
||||
plaintext.content = {
|
||||
body: "42",
|
||||
msgtype: "m.text",
|
||||
};
|
||||
}
|
||||
if (!plaintext.type) {
|
||||
plaintext.type = "m.room.message";
|
||||
}
|
||||
if (!plaintext.room_id) {
|
||||
expect(opts.room_id).toBeTruthy();
|
||||
plaintext.room_id = opts.room_id;
|
||||
}
|
||||
return encryptMegolmEventRawPlainText({
|
||||
senderKey: opts.senderKey,
|
||||
groupSession: opts.groupSession,
|
||||
plaintext,
|
||||
});
|
||||
}
|
||||
|
||||
export function encryptMegolmEventRawPlainText(opts: {
|
||||
senderKey: string;
|
||||
groupSession: Olm.OutboundGroupSession;
|
||||
plaintext: Partial<IEvent>;
|
||||
origin_server_ts?: number;
|
||||
}): IEvent {
|
||||
return {
|
||||
event_id: "$test_megolm_event_" + Math.random(),
|
||||
sender: opts.plaintext.sender ?? "@not_the_real_sender:example.com",
|
||||
origin_server_ts: opts.plaintext.origin_server_ts ?? 1672944778000,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: opts.groupSession.encrypt(JSON.stringify(opts.plaintext)),
|
||||
device_id: "testDevice",
|
||||
sender_key: opts.senderKey,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an encrypted room_key event to share a group session, using an existing olm session */
|
||||
export function encryptGroupSessionKey(opts: {
|
||||
/** recipient's user id */
|
||||
recipient: string;
|
||||
/** the recipient's curve25519 key */
|
||||
recipientCurve25519Key: string;
|
||||
/** the recipient's ed25519 key */
|
||||
recipientEd25519Key: string;
|
||||
/** sender's olm account */
|
||||
olmAccount: Olm.Account;
|
||||
/** sender's olm session with the recipient */
|
||||
p2pSession: Olm.Session;
|
||||
groupSession: Olm.OutboundGroupSession;
|
||||
room_id?: string;
|
||||
}): ToDeviceEvent {
|
||||
const senderKeys = JSON.parse(opts.olmAccount.identity_keys());
|
||||
return encryptOlmEvent({
|
||||
senderKey: senderKeys.curve25519,
|
||||
senderSigningKey: senderKeys.ed25519,
|
||||
recipient: opts.recipient,
|
||||
recipientCurve25519Key: opts.recipientCurve25519Key,
|
||||
recipientEd25519Key: opts.recipientEd25519Key,
|
||||
p2pSession: opts.p2pSession,
|
||||
plaincontent: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: opts.room_id,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
session_key: opts.groupSession.session_key(),
|
||||
},
|
||||
plaintype: "m.room_key",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish an Olm Session with the test user
|
||||
*
|
||||
* Waits for the test user to upload their keys, then sends a /sync response with a to-device message which will
|
||||
* establish an Olm session.
|
||||
*
|
||||
* @param testClient - the MatrixClient under test, which we expect to upload account keys, and to make a
|
||||
* /sync request which we will respond to.
|
||||
* @param keyReceiver - an IE2EKeyReceiver which will intercept the /keys/upload request from the client under test
|
||||
* @param syncResponder - an ISyncResponder which will intercept /sync requests from the client under test
|
||||
* @param peerOlmAccount: an OlmAccount which will be used to initiate the Olm session.
|
||||
*/
|
||||
export async function establishOlmSession(
|
||||
testClient: MatrixClient,
|
||||
keyReceiver: IE2EKeyReceiver,
|
||||
syncResponder: ISyncResponder,
|
||||
peerOlmAccount: Olm.Account,
|
||||
): Promise<Olm.Session> {
|
||||
const peerE2EKeys = JSON.parse(peerOlmAccount.identity_keys());
|
||||
const p2pSession = await createOlmSession(peerOlmAccount, keyReceiver);
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: peerE2EKeys.curve25519,
|
||||
senderSigningKey: peerE2EKeys.ed25519,
|
||||
recipient: testClient.getUserId()!,
|
||||
recipientCurve25519Key: keyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: keyReceiver.getSigningKey(),
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
to_device: { events: [olmEvent] },
|
||||
});
|
||||
await syncPromise(testClient);
|
||||
return p2pSession;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
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 db", 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 an indexed db now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
|
||||
});
|
||||
|
||||
it("should create the meta db if given a pickleKey", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
pickleKey: "testKey",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// should have two indexed 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",
|
||||
pickleKey: "testKey",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
expect(await indexedDB.databases()).toHaveLength(2);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores();
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not fail in environments without indexedDB", async () => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = undefined!;
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores();
|
||||
// No error thrown in clearStores
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+264
-259
@@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../TestClient';
|
||||
import * as testUtils from '../test-utils/test-utils';
|
||||
import { logger } from '../../src/logger';
|
||||
import { TestClient } from "../TestClient";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -26,26 +26,24 @@ const ROOM_ID = "!room:id";
|
||||
* get a /sync response which contains a single e2e room (ROOM_ID), with the
|
||||
* members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} sync response
|
||||
* @returns sync response
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
function getSyncResponse(roomMembers: string[]) {
|
||||
const stateEvents = [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
type: "m.room.encryption",
|
||||
skey: "",
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
Array.prototype.push.apply(
|
||||
stateEvents,
|
||||
roomMembers.map(
|
||||
(m) => testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
roomMembers.map((m) =>
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
@@ -67,24 +65,22 @@ function getSyncResponse(roomMembers) {
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
describe("DeviceList management:", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
logger.warn("not running deviceList tests: Olm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionStoreBackend;
|
||||
let aliceTestClient;
|
||||
let aliceTestClient: TestClient;
|
||||
let sessionStoreBackend: Storage;
|
||||
|
||||
async function createTestClient() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
|
||||
);
|
||||
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
|
||||
await testClient.client.initCrypto();
|
||||
return testClient;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
@@ -92,305 +88,314 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient = await createTestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function () {
|
||||
aliceTestClient.expectKeyQuery({
|
||||
device_keys: { "@alice:localhost": {} },
|
||||
failures: {},
|
||||
});
|
||||
return aliceTestClient
|
||||
.start()
|
||||
.then(function () {
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
})
|
||||
.then(function () {
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(["@bob:xyz"]),
|
||||
aliceTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
})
|
||||
.then(function () {
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, {
|
||||
event_id: "$event_id",
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("We should not get confused by out-of-order device query responses", () => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
aliceTestClient.expectKeyQuery({
|
||||
device_keys: { "@alice:localhost": {} },
|
||||
failures: {},
|
||||
});
|
||||
return aliceTestClient
|
||||
.start()
|
||||
.then(() => {
|
||||
aliceTestClient.httpBackend
|
||||
.when("GET", "/sync")
|
||||
.respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"]));
|
||||
return aliceTestClient.flushSync();
|
||||
})
|
||||
.then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
"@chris:abc": {},
|
||||
},
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
|
||||
aliceTestClient.httpBackend
|
||||
.flush("/keys/query", 1)
|
||||
.then(() => aliceTestClient.httpBackend.flush("/send/", 1)),
|
||||
aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
|
||||
]);
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data!.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "2",
|
||||
device_lists: {
|
||||
changed: ["@bob:xyz"],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "3",
|
||||
device_lists: {
|
||||
changed: ["@chris:abc"],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend
|
||||
.when("POST", "/keys/query", {
|
||||
device_keys: {
|
||||
"@chris:abc": {},
|
||||
},
|
||||
token: "3",
|
||||
})
|
||||
.respond(200, {
|
||||
device_keys: { "@chris:abc": {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error("Unexpected status for bob: wanted 1 or 2, got " + bobStat);
|
||||
}
|
||||
const chrisStat = data!.trackingStatus["@chris:abc"];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + chrisStat);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend
|
||||
.when("POST", "/keys/query", {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
token: "2",
|
||||
})
|
||||
.respond(200, {
|
||||
device_keys: { "@bob:xyz": {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(["@bob:xyz"]);
|
||||
})
|
||||
.then(() => {
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data!.trackingStatus["@chris:abc"];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + bobStat);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(["@chris:abc"]);
|
||||
})
|
||||
.then(() => {
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
const chrisStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data!.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz']));
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
});
|
||||
await aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should be tracking bob's device list
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
it("when Bob leaves", async function () {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ["@bob:xyz"],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Alice leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
it("when Alice leaves", async function () {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ["@bob:xyz"],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves whilst Alice is offline", async function() {
|
||||
it("when Bob leaves whilst Alice is offline", async function () {
|
||||
aliceTestClient.stop();
|
||||
|
||||
const anotherTestClient = await createTestClient();
|
||||
|
||||
try {
|
||||
await anotherTestClient.start();
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
|
||||
|
||||
// @ts-ignore accessing private property
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus['@bob:xyz'];
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
} finally {
|
||||
anotherTestClient.stop();
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
describe("MatrixClient events", function () {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
let client: MatrixClient | undefined;
|
||||
@@ -46,23 +46,25 @@ describe("MatrixClient events", function() {
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
[client!, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
httpBackend?.verifyNoOutstandingExpectation();
|
||||
client?.stopClient();
|
||||
return httpBackend?.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
describe("emissions", function () {
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online",
|
||||
user: "@foo:bar",
|
||||
name: "Foo Bar",
|
||||
presence: "online",
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -72,7 +74,9 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: "hmmm",
|
||||
}),
|
||||
],
|
||||
prev_batch: "s",
|
||||
@@ -80,14 +84,15 @@ describe("MatrixClient events", function() {
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar",
|
||||
room: "!erufh:bar",
|
||||
mship: "join",
|
||||
user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: "!erufh:bar",
|
||||
type: "m.room.create",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar",
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -103,18 +108,23 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: "ello ello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: ":D",
|
||||
}),
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: "!erufh:bar", content: {
|
||||
type: "m.typing",
|
||||
room: "!erufh:bar",
|
||||
content: {
|
||||
user_ids: ["@foo:bar"],
|
||||
},
|
||||
}),
|
||||
@@ -125,50 +135,49 @@ describe("MatrixClient events", function() {
|
||||
},
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
it("should emit events from both the first and subsequent /sync calls", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client!.on(ClientEvent.Event, function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
client!.on(ClientEvent.Event, function (event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
}
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
client!.on(UserEvent.Presence, function(event, user) {
|
||||
client!.on(UserEvent.Presence, function (event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
@@ -177,59 +186,50 @@ describe("MatrixClient events", function() {
|
||||
}
|
||||
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0]?.content?.presence,
|
||||
);
|
||||
expect(user.presence).toEqual(SYNC_DATA.presence.events[0]?.content?.presence);
|
||||
});
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(function() {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(fired).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit Room events", function() {
|
||||
it("should emit Room events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client!.on(ClientEvent.Room, function(room) {
|
||||
client!.on(ClientEvent.Room, function (room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client!.on(RoomEvent.Timeline, function(event, room) {
|
||||
client!.on(RoomEvent.Timeline, function (event, room) {
|
||||
timelineFireCount++;
|
||||
expect(room?.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client!.on(RoomEvent.Name, function(room) {
|
||||
client!.on(RoomEvent.Name, function (room) {
|
||||
roomNameInvokeCount++;
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(roomInvokeCount).toEqual(1);
|
||||
expect(roomNameInvokeCount).toEqual(1);
|
||||
expect(timelineFireCount).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function() {
|
||||
it("should emit RoomState events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
];
|
||||
const roomStateEventTypes = ["m.room.member", "m.room.create"];
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client!.on(RoomStateEvent.Events, function(event, state) {
|
||||
client!.on(RoomStateEvent.Events, function (event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(-1);
|
||||
@@ -237,13 +237,13 @@ describe("MatrixClient events", function() {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
client!.on(RoomStateEvent.Members, function(event, state, member) {
|
||||
client!.on(RoomStateEvent.Members, function (event, state, member) {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
@@ -252,17 +252,14 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(membersInvokeCount).toEqual(1);
|
||||
expect(newMemberInvokeCount).toEqual(1);
|
||||
expect(eventsInvokeCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function() {
|
||||
it("should emit RoomMember events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
@@ -270,27 +267,24 @@ describe("MatrixClient events", function() {
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client!.on(RoomMemberEvent.Name, function(event, member) {
|
||||
client!.on(RoomMemberEvent.Name, function (event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
client!.on(RoomMemberEvent.Typing, function(event, member) {
|
||||
client!.on(RoomMemberEvent.Typing, function (event, member) {
|
||||
typingInvokeCount++;
|
||||
expect(member.typing).toBe(true);
|
||||
});
|
||||
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
|
||||
client!.on(RoomMemberEvent.PowerLevel, function (event, member) {
|
||||
powerLevelInvokeCount++;
|
||||
});
|
||||
client!.on(RoomMemberEvent.Membership, function(event, member) {
|
||||
client!.on(RoomMemberEvent.Membership, function (event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(typingInvokeCount).toEqual(1);
|
||||
expect(powerLevelInvokeCount).toEqual(0);
|
||||
expect(nameInvokeCount).toEqual(0);
|
||||
@@ -298,36 +292,36 @@ describe("MatrixClient events", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function () {
|
||||
const error = { errcode: "M_UNKNOWN_TOKEN" };
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
return httpBackend!.flushAllExpected().then(function () {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function () {
|
||||
const error = { errcode: "M_UNKNOWN_TOKEN", soft_logout: true };
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
return httpBackend!.flushAllExpected().then(function () {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { ClientEvent, MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { IStore } from "../../src/store";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
describe("MatrixClient opts", function () {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let httpBackend = new HttpBackend();
|
||||
const userId = "@alice:localhost";
|
||||
@@ -19,11 +19,14 @@ describe("MatrixClient opts", function() {
|
||||
presence: {},
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
"!foo:bar": {
|
||||
// roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "hello",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
@@ -31,22 +34,30 @@ describe("MatrixClient opts", function() {
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
type: "m.room.name",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userB, name: "Bob",
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: userB,
|
||||
name: "Bob",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: "Alice",
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: userId,
|
||||
name: "Alice",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: userId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -55,18 +66,18 @@ describe("MatrixClient opts", function() {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
httpBackend = new HttpBackend();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("without opts.store", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
describe("without opts.store", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: undefined,
|
||||
@@ -77,34 +88,34 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(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() {
|
||||
const expectedEventTypes = [ // from /initialSync
|
||||
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
|
||||
it("should be able to sync / get new events", async function () {
|
||||
const expectedEventTypes = [
|
||||
// from /initialSync
|
||||
"m.room.message",
|
||||
"m.room.name",
|
||||
"m.room.member",
|
||||
"m.room.member",
|
||||
"m.room.create",
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1,
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
);
|
||||
client.on(ClientEvent.Event, function (event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(-1);
|
||||
expectedEventTypes.splice(expectedEventTypes.indexOf(event.getType()), 1);
|
||||
});
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
@@ -114,19 +125,14 @@ describe("MatrixClient opts", function() {
|
||||
await httpBackend.flush("/versions", 1);
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0,
|
||||
);
|
||||
await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
|
||||
expect(expectedEventTypes.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without opts.scheduler", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
describe("without opts.scheduler", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
@@ -137,25 +143,25 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
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);
|
||||
it("shouldn't retry sending events", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(
|
||||
500,
|
||||
new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
error: "Ruh roh",
|
||||
}),
|
||||
);
|
||||
|
||||
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",
|
||||
});
|
||||
@@ -164,30 +170,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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,13 +29,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
|
||||
@@ -52,76 +46,71 @@ describe("MatrixClient relations", () => {
|
||||
});
|
||||
|
||||
it("should read related events with the default options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null);
|
||||
const response = client!.relations(roomId, "$event-0", null, null);
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
|
||||
const response = client!.relations(roomId, "$event-0", "m.reference", null);
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type and event type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
|
||||
const response = client!.relations(roomId, "$event-0", "m.reference", "m.room.message");
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it("should read related events with custom options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null, {
|
||||
const response = client!.relations(roomId, "$event-0", null, null, {
|
||||
dir: Direction.Forward,
|
||||
from: 'FROM',
|
||||
from: "FROM",
|
||||
limit: 10,
|
||||
to: 'TO',
|
||||
to: "TO",
|
||||
});
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||
const response = client!.fetchRelations(roomId, '$event-0', null, null);
|
||||
it("should use default direction in the fetchRelations endpoint", async () => {
|
||||
const response = client!.fetchRelations(roomId, "$event-0", null, null);
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
|
||||
expect(await response).toEqual({ chunk: [], next_batch: "NEXT" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
describe("MatrixClient retrying", function () {
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
@@ -30,13 +30,7 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
const room = new Room(roomId, client, userId);
|
||||
@@ -45,49 +39,46 @@ describe("MatrixClient retrying", function() {
|
||||
return [client, httpBackend, room];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
[client, httpBackend, room] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function() {
|
||||
it.skip("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
|
||||
});
|
||||
it.skip("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
|
||||
xit("should queue according to MatrixScheduler.queueFn", function() {
|
||||
it.skip("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
|
||||
});
|
||||
it.skip("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.NOT_SENT when giving up", function() {
|
||||
|
||||
});
|
||||
|
||||
xit("should mark events as EventStatus.QUEUED when queued", function() {
|
||||
|
||||
});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function () {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m1",
|
||||
}).then(function() {
|
||||
// we expect the first message to fail
|
||||
throw new Error('Message 1 unexpectedly sent successfully');
|
||||
}, () => {
|
||||
// this is expected
|
||||
});
|
||||
const p1 = client!
|
||||
.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
body: "m1",
|
||||
})
|
||||
.then(
|
||||
function () {
|
||||
// we expect the first message to fail
|
||||
throw new Error("Message 1 unexpectedly sent successfully");
|
||||
},
|
||||
() => {
|
||||
// this is expected
|
||||
},
|
||||
);
|
||||
|
||||
// XXX: it turns out that the promise returned by this message
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
msgtype: "m.text",
|
||||
body: "m2",
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
@@ -100,20 +91,23 @@ describe("MatrixClient retrying", function() {
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// the first message should get sent, and the second should get queued
|
||||
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
httpBackend!
|
||||
.when("PUT", "/send/m.room.message/")
|
||||
.check(function () {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client!.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client!.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() {
|
||||
client!.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
}).respond(400); // fail the first message
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function () {
|
||||
client!.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
})
|
||||
.respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
@@ -122,7 +116,7 @@ describe("MatrixClient retrying", function() {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}).then(function() {
|
||||
}).then(function () {
|
||||
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
@@ -132,19 +126,11 @@ describe("MatrixClient retrying", function() {
|
||||
expect(tl.length).toEqual(0);
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
p1,
|
||||
p3,
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
return Promise.all([p1, p3, httpBackend!.flushAllExpected()]);
|
||||
});
|
||||
|
||||
describe("resending", function() {
|
||||
xit("should be able to resend a NOT_SENT event", function() {
|
||||
|
||||
});
|
||||
xit("should be able to resend a sent event", function() {
|
||||
|
||||
});
|
||||
describe("resending", function () {
|
||||
it.skip("should be able to resend a NOT_SENT event", function () {});
|
||||
it.skip("should be able to resend a sent event", function () {});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
Copyright 2023 Holi Moli GmbH
|
||||
|
||||
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 fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
|
||||
|
||||
const makeQueryablePromise = <T = void>(promise: Promise<T>) => {
|
||||
let resolved = false;
|
||||
let rejected = false;
|
||||
|
||||
// Observe the promise, saving the fulfillment in a closure scope.
|
||||
const newPromise = promise.then(
|
||||
(value) => {
|
||||
resolved = true;
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
rejected = true;
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
const isFulfilled = () => {
|
||||
return resolved || rejected;
|
||||
};
|
||||
const isResolved = () => {
|
||||
return resolved;
|
||||
};
|
||||
const isRejected = () => {
|
||||
return rejected;
|
||||
};
|
||||
return { promise: newPromise, isFulfilled, isResolved, isRejected };
|
||||
};
|
||||
|
||||
const queryablePromise = <T = void>() => {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: any) => void;
|
||||
|
||||
const promise = makeQueryablePromise<T>(
|
||||
new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
}),
|
||||
);
|
||||
|
||||
return { resolve, reject, ...promise };
|
||||
};
|
||||
|
||||
describe("MatrixClient syncing errors", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const unknownTokenErrorData = {
|
||||
status: 401,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
error: "Invalid access token passed.",
|
||||
soft_logout: false,
|
||||
},
|
||||
};
|
||||
let client: MatrixClient | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createClient({
|
||||
baseUrl: "http://tocal.test.server",
|
||||
userId: selfUserId,
|
||||
accessToken: selfAccessToken,
|
||||
deviceId: "myDevice",
|
||||
});
|
||||
});
|
||||
|
||||
it("should retry, until errors are solved.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
fetchMock
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
|
||||
.get("end:versions", {}) // further version checks succeed
|
||||
.getOnce("end:pushrules/", 429) // first pushrules check fails starting retry
|
||||
.get("end:pushrules/", {}) // further pushrules check succeed
|
||||
.catch({}); // all other calls succeed
|
||||
|
||||
const syncEvents = Array.from({ length: 5 }, queryablePromise<SyncState>);
|
||||
|
||||
client!.on(ClientEvent.Sync, (state: SyncState, lastState: SyncState | null) => {
|
||||
let i = 0;
|
||||
for (; i < syncEvents.length && syncEvents[i].isFulfilled(); i++) {
|
||||
// find index of first unfulfilled promise
|
||||
}
|
||||
syncEvents[i].resolve(state);
|
||||
});
|
||||
|
||||
await client!.startClient();
|
||||
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[1].promise).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("should stop sync keep alive when client is stopped.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
fetchMock
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
|
||||
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
|
||||
.post("end:logout", unknownTokenErrorData) // just to keep up a consistent scenario. Does not have a real effect for this testcase
|
||||
.post("end:filter", 401); // just to keep up a consistent scenario. Does not have a real effect for this testcase
|
||||
|
||||
const firstSyncEvent = queryablePromise<SyncState>();
|
||||
const secondSyncEvent = queryablePromise<SyncState>();
|
||||
client!.on(ClientEvent.Sync, (state: SyncState, lastState: SyncState | null) => {
|
||||
if (firstSyncEvent.isFulfilled()) secondSyncEvent.resolve(state);
|
||||
firstSyncEvent.resolve(state);
|
||||
});
|
||||
|
||||
await client!.startClient();
|
||||
const logoutDone = queryablePromise();
|
||||
client!
|
||||
.logout(true)
|
||||
.then(() => {
|
||||
logoutDone.resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
logoutDone.resolve();
|
||||
});
|
||||
|
||||
const syntState = await firstSyncEvent.promise;
|
||||
expect(syntState).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
|
||||
|
||||
jest.useRealTimers(); // we need real timer for the setTimout below to work
|
||||
|
||||
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
|
||||
|
||||
await Promise.race([secondSyncEvent.promise, timeoutPromise.promise]);
|
||||
// when syncing stopped, then the secondSyncEvent will never happen and the promise will not be resolved,
|
||||
/// so the timeoutPromise will be resolved instead
|
||||
expect(timeoutPromise.isFulfilled()).toBe(true);
|
||||
expect(secondSyncEvent.isFulfilled()).toBe(false);
|
||||
|
||||
await logoutDone.promise; // wait for the logout to finish to prevent processing and logging after the test is done.
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: {
|
||||
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]: {},
|
||||
[Category.Knock]: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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: {},
|
||||
},
|
||||
};
|
||||
syncResponse.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
+436
-161
@@ -20,16 +20,29 @@ 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, NotificationCountType, JoinRule, MatrixError,
|
||||
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
NotificationCountType,
|
||||
JoinRule,
|
||||
MatrixError,
|
||||
EventType,
|
||||
IPushRules,
|
||||
PushRuleKind,
|
||||
TweakName,
|
||||
ClientEvent,
|
||||
RoomMemberEvent,
|
||||
RoomEvent,
|
||||
Room,
|
||||
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;
|
||||
@@ -40,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();
|
||||
@@ -67,7 +79,7 @@ describe("SlidingSyncSdk", () => {
|
||||
event_id: "$" + eventIdCounter,
|
||||
};
|
||||
};
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey = ""): IStateEvent => {
|
||||
eventIdCounter++;
|
||||
return {
|
||||
type: evType,
|
||||
@@ -97,19 +109,20 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => {
|
||||
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
|
||||
@@ -119,13 +132,13 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension => {
|
||||
const findExtension = (name: string): Extension<any, any> => {
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension;
|
||||
if (calledExtension && calledExtension.name() === name) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
|
||||
if (calledExtension?.name() === name) {
|
||||
return calledExtension;
|
||||
}
|
||||
}
|
||||
@@ -141,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,11 +183,12 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomE = "!e_with_invite:localhost";
|
||||
const roomF = "!f_calc_room_name:localhost";
|
||||
const roomG = "!g_join_invite_counts:localhost";
|
||||
const roomH = "!g_num_live:localhost";
|
||||
const data: Record<string, MSC3575RoomData> = {
|
||||
[roomA]: {
|
||||
name: "A",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
|
||||
@@ -189,12 +203,11 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "B",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
|
||||
|
||||
],
|
||||
initial: true,
|
||||
},
|
||||
@@ -202,7 +215,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "C",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
|
||||
@@ -215,7 +228,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "D",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
|
||||
@@ -251,7 +264,7 @@ describe("SlidingSyncSdk", () => {
|
||||
[roomF]: {
|
||||
name: "#foo:localhost",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
|
||||
@@ -267,7 +280,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "G",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
],
|
||||
@@ -275,74 +288,111 @@ describe("SlidingSyncSdk", () => {
|
||||
invited_count: 2,
|
||||
initial: true,
|
||||
},
|
||||
[roomH]: {
|
||||
name: "H",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
|
||||
],
|
||||
initial: true,
|
||||
num_live: 1,
|
||||
},
|
||||
};
|
||||
|
||||
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(data[roomC].highlight_count);
|
||||
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(data[roomD].notification_count);
|
||||
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 invite_state", () => {
|
||||
it("can be created with live events", async () => {
|
||||
const seenLiveEventDeferred = defer<boolean>();
|
||||
const listener = (
|
||||
ev: MatrixEvent,
|
||||
room?: Room,
|
||||
toStartOfTimeline?: boolean,
|
||||
deleted?: boolean,
|
||||
timelineData?: IRoomTimelineData,
|
||||
) => {
|
||||
if (timelineData?.liveEvent) {
|
||||
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
|
||||
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).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);
|
||||
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
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", () => {
|
||||
@@ -354,29 +404,33 @@ describe("SlidingSyncSdk", () => {
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
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();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||
],
|
||||
required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
@@ -387,11 +441,11 @@ describe("SlidingSyncSdk", () => {
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(1);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
@@ -402,11 +456,11 @@ describe("SlidingSyncSdk", () => {
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(1);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
@@ -417,9 +471,11 @@ describe("SlidingSyncSdk", () => {
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom!.getJoinedMemberCount()).toEqual(1);
|
||||
});
|
||||
|
||||
// Regression test for a bug which caused the timeline entries to be out-of-order
|
||||
@@ -441,16 +497,25 @@ describe("SlidingSyncSdk", () => {
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
|
||||
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
|
||||
(e) => (e.getType() + " : " + e.getContent().body)),
|
||||
logger.log(
|
||||
"want:",
|
||||
oldTimeline.map((e) => e.type + " : " + (e.content || {}).body),
|
||||
);
|
||||
logger.log(
|
||||
"got:",
|
||||
gotRoom
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.map((e) => e.getType() + " : " + e.getContent().body),
|
||||
);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -466,42 +531,56 @@ describe("SlidingSyncSdk", () => {
|
||||
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
|
||||
|
||||
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
|
||||
{ pos: "h", lists: [], rooms: {}, extensions: {} },
|
||||
);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "h",
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
null,
|
||||
new Error("generic"),
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
|
||||
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
null,
|
||||
new Error("generic"),
|
||||
);
|
||||
}
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
});
|
||||
|
||||
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.Complete,
|
||||
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
||||
);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "i",
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
message: "Oh no your access token is no longer valid",
|
||||
}));
|
||||
expect(mockSlidingSync!.stop).not.toHaveBeenCalled();
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
null,
|
||||
new MatrixError({
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
message: "Oh no your access token is no longer valid",
|
||||
}),
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
expect(mockSlidingSync!.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -523,7 +602,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
@@ -532,16 +611,17 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionE2EE", () => {
|
||||
let ext: Extension;
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient({
|
||||
withCrypto: true,
|
||||
@@ -551,56 +631,52 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionAccountData", () => {
|
||||
let ext: Extension;
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
@@ -608,12 +684,14 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("account_data");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes global account data", async () => {
|
||||
const globalType = "global_test";
|
||||
const globalContent = {
|
||||
@@ -630,20 +708,20 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
});
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData).toBeTruthy();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
it("processes rooms account data", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with account data",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
@@ -651,6 +729,7 @@ describe("SlidingSyncSdk", () => {
|
||||
foo: "bar",
|
||||
};
|
||||
const roomType = "test";
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: [
|
||||
@@ -662,11 +741,12 @@ 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);
|
||||
});
|
||||
|
||||
it("doesn't crash for unknown room account data", async () => {
|
||||
const unknownRoomId = "!unknown:id";
|
||||
const roomType = "tester";
|
||||
@@ -686,22 +766,25 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(room).toBeNull();
|
||||
expect(client!.getAccountData(roomType)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can update push rules via account data", async () => {
|
||||
const roomId = "!foo:bar";
|
||||
const pushRulesContent: IPushRules = {
|
||||
global: {
|
||||
[PushRuleKind.RoomSpecific]: [{
|
||||
enabled: true,
|
||||
default: true,
|
||||
pattern: "monkey",
|
||||
actions: [
|
||||
{
|
||||
set_tweak: TweakName.Sound,
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
rule_id: roomId,
|
||||
}],
|
||||
[PushRuleKind.RoomSpecific]: [
|
||||
{
|
||||
enabled: true,
|
||||
default: true,
|
||||
pattern: "monkey",
|
||||
actions: [
|
||||
{
|
||||
set_tweak: TweakName.Sound,
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
rule_id: roomId,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let pushRule = client!.getRoomPushRule("global", roomId);
|
||||
@@ -718,8 +801,10 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionToDevice", () => {
|
||||
let ext: Extension;
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
@@ -727,12 +812,14 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("to_device");
|
||||
});
|
||||
|
||||
it("gets enabled with a limit on the initial request only", () => {
|
||||
const reqJson: any = ext.onRequest(true);
|
||||
expect(reqJson.enabled).toEqual(true);
|
||||
expect(reqJson.limit).toBeGreaterThan(0);
|
||||
expect(reqJson.since).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates the since value", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "12345",
|
||||
@@ -742,12 +829,14 @@ describe("SlidingSyncSdk", () => {
|
||||
since: "12345",
|
||||
});
|
||||
});
|
||||
|
||||
it("can handle missing fields", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "23456",
|
||||
// no events array
|
||||
});
|
||||
});
|
||||
|
||||
it("emits to-device events on the client", async () => {
|
||||
const toDeviceType = "custom_test";
|
||||
const toDeviceContent = {
|
||||
@@ -770,17 +859,16 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("can cancel key verification requests", async () => {
|
||||
const seen: Record<string, boolean> = {};
|
||||
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
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",
|
||||
@@ -809,4 +897,191 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionTyping", () => {
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("typing");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes typing notifications", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [selfUserId],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(true);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
});
|
||||
|
||||
it("gracefully handles missing rooms and members when typing", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: ["@someone:else"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
"!something:else": {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [selfUserId],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionReceipts", () => {
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
const generateReceiptResponse = (
|
||||
userId: string,
|
||||
roomId: string,
|
||||
eventId: string,
|
||||
recType: string,
|
||||
ts: number,
|
||||
) => {
|
||||
return {
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Receipt,
|
||||
content: {
|
||||
[eventId]: {
|
||||
[recType]: {
|
||||
[userId]: {
|
||||
ts: ts,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("receipts");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
const lastEvent = mkOwnEvent(EventType.RoomMessage, { body: "hello" });
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with receipts",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
state_key: alice,
|
||||
content: { membership: "join" },
|
||||
sender: alice,
|
||||
origin_server_ts: Date.now(),
|
||||
event_id: "$alice",
|
||||
},
|
||||
lastEvent,
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
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).toBeTruthy();
|
||||
expect(receipt?.eventId).toEqual(lastEvent.event_id);
|
||||
expect(receipt?.data.ts).toEqual(1234567);
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
});
|
||||
|
||||
it("gracefully handles missing rooms when receiving receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
const eventId = "$something";
|
||||
ext.onResponse(generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567));
|
||||
// we expect it not to crash
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+941
-660
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../src/logger';
|
||||
import { logger } from "../src/logger";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
globalThis.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,100 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
class JestSlowTestReporter {
|
||||
constructor(globalConfig, options) {
|
||||
this._globalConfig = globalConfig;
|
||||
this._options = options;
|
||||
this._slowTests = [];
|
||||
this._slowTestSuites = [];
|
||||
}
|
||||
|
||||
onRunComplete() {
|
||||
const displayResult = (result, isTestSuite) => {
|
||||
if (!isTestSuite) console.log();
|
||||
|
||||
result.sort((a, b) => b.duration - a.duration);
|
||||
const rootPathRegex = new RegExp(`^${process.cwd()}`);
|
||||
const slowestTests = result.slice(0, this._options.numTests || 10);
|
||||
const slowTestTime = this._slowTestTime(slowestTests);
|
||||
const allTestTime = this._allTestTime(result);
|
||||
const percentTime = (slowTestTime / allTestTime) * 100;
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
const duration = slowestTests[i].duration;
|
||||
const filePath = slowestTests[i].filePath.replace(rootPathRegex, ".");
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
} else {
|
||||
const fullName = slowestTests[i].fullName;
|
||||
console.log(` ${fullName}`);
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
};
|
||||
|
||||
displayResult(this._slowTests);
|
||||
displayResult(this._slowTestSuites, true);
|
||||
}
|
||||
|
||||
onTestResult(test, testResult) {
|
||||
this._slowTestSuites.push({
|
||||
duration: testResult.perfStats.runtime,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
for (let i = 0; i < testResult.testResults.length; i++) {
|
||||
this._slowTests.push({
|
||||
duration: testResult.testResults[i].duration,
|
||||
fullName: testResult.testResults[i].fullName,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_slowTestTime(slowestTests) {
|
||||
let slowTestTime = 0;
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
slowTestTime += slowestTests[i].duration;
|
||||
}
|
||||
return slowTestTime;
|
||||
}
|
||||
|
||||
_allTestTime(result) {
|
||||
let allTestTime = 0;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
allTestTime += result[i].duration;
|
||||
}
|
||||
return allTestTime;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JestSlowTestReporter;
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-21
@@ -17,10 +17,7 @@ limitations under the License.
|
||||
import { MatrixEvent } from "../../src";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { LocationAssetType } from "../../src/@types/location";
|
||||
import {
|
||||
makeBeaconContent,
|
||||
makeBeaconInfoContent,
|
||||
} from "../../src/content-helpers";
|
||||
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
|
||||
type InfoContentProps = {
|
||||
timeout: number;
|
||||
@@ -44,13 +41,7 @@ export const makeBeaconInfoEvent = (
|
||||
contentProps: Partial<InfoContentProps> = {},
|
||||
eventId?: string,
|
||||
): MatrixEvent => {
|
||||
const {
|
||||
timeout,
|
||||
isLive,
|
||||
description,
|
||||
assetType,
|
||||
timestamp,
|
||||
} = {
|
||||
const { timeout, isLive, description, assetType, timestamp } = {
|
||||
...DEFAULT_INFO_CONTENT_PROPS,
|
||||
...contentProps,
|
||||
};
|
||||
@@ -77,9 +68,9 @@ type ContentProps = {
|
||||
description?: string;
|
||||
};
|
||||
const DEFAULT_CONTENT_PROPS: ContentProps = {
|
||||
uri: 'geo:-36.24484561954707,175.46884959563613;u=10',
|
||||
uri: "geo:-36.24484561954707,175.46884959563613;u=10",
|
||||
timestamp: 123,
|
||||
beaconInfoId: '$123',
|
||||
beaconInfoId: "$123",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,10 +78,7 @@ const DEFAULT_CONTENT_PROPS: ContentProps = {
|
||||
* all required properties are mocked
|
||||
* override with contentProps
|
||||
*/
|
||||
export const makeBeaconEvent = (
|
||||
sender: string,
|
||||
contentProps: Partial<ContentProps> = {},
|
||||
): MatrixEvent => {
|
||||
export const makeBeaconEvent = (sender: string, contentProps: Partial<ContentProps> = {}): MatrixEvent => {
|
||||
const { uri, timestamp, beaconInfoId, description } = {
|
||||
...DEFAULT_CONTENT_PROPS,
|
||||
...contentProps,
|
||||
@@ -107,10 +95,13 @@ export const makeBeaconEvent = (
|
||||
* Create a mock geolocation position
|
||||
* defaults all required properties
|
||||
*/
|
||||
export const makeGeolocationPosition = (
|
||||
{ timestamp, coords }:
|
||||
{ timestamp?: number, coords: Partial<GeolocationCoordinates> },
|
||||
): GeolocationPosition => ({
|
||||
export const makeGeolocationPosition = ({
|
||||
timestamp,
|
||||
coords,
|
||||
}: {
|
||||
timestamp?: number;
|
||||
coords: Partial<GeolocationCoordinates>;
|
||||
}): GeolocationPosition => ({
|
||||
timestamp: timestamp ?? 1647256791840,
|
||||
coords: {
|
||||
accuracy: 1,
|
||||
|
||||
@@ -58,11 +58,12 @@ export const getMockClientWithEventEmitter = (
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
|
||||
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'),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
credentials: { userId },
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: jest.fn(),
|
||||
@@ -85,10 +86,8 @@ 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({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise(r => {
|
||||
return new Promise((r) => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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 { KeyBackupInfo } from "../../src/crypto-api";
|
||||
|
||||
/**
|
||||
* 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",
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock out requests to `/room_keys/version`.
|
||||
*
|
||||
* Returns `404 M_NOT_FOUND` for GET requests until `POST room_keys/version` is called.
|
||||
* Once the POST is done, `GET /room_keys/version` will return the posted backup
|
||||
* instead of 404.
|
||||
*
|
||||
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
|
||||
*/
|
||||
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
+654
@@ -0,0 +1,654 @@
|
||||
#!/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
|
||||
import base58
|
||||
|
||||
from canonicaljson import encode_canonical_json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from cryptography.hazmat.primitives import hashes, padding, hmac
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
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",
|
||||
# any 32-byte string can be an curve25519 private key.
|
||||
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"deadmuledeadmuledeadmuledeadmule",
|
||||
|
||||
"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",
|
||||
# any 32-byte string can be an curve25519 private key.
|
||||
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
|
||||
|
||||
"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, IEvent }} from "../../../src";
|
||||
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* 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"]
|
||||
)
|
||||
|
||||
device_curve_key = x25519.X25519PrivateKey.from_private_bytes(
|
||||
user_data["TEST_DEVICE_CURVE_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(device_curve_key)[0], build_exported_megolm_key(device_curve_key)[0]]
|
||||
|
||||
additional_exported_room_key, additional_exported_ed_key = build_exported_megolm_key(device_curve_key)
|
||||
ratcheted_exported_room_key = symetric_ratchet_step_of_megolm_key(additional_exported_room_key, additional_exported_ed_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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
backed_up_room_key = encrypt_megolm_key_for_backup(additional_exported_room_key, backup_decryption_key.public_key())
|
||||
|
||||
clear_event, encrypted_event = generate_encrypted_event_content(additional_exported_room_key, additional_exported_ed_key, device_curve_key)
|
||||
|
||||
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_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)
|
||||
};
|
||||
|
||||
/** A ratcheted version of {prefix}MEGOLM_SESSION_DATA */
|
||||
export const {prefix}RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
json.dumps(ratcheted_exported_room_key, indent=4)
|
||||
};
|
||||
|
||||
/** The key from {prefix}MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
|
||||
export const {prefix}CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {json.dumps(backed_up_room_key, indent=4)};
|
||||
|
||||
/** A test clear event */
|
||||
export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, indent=4)};
|
||||
|
||||
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, 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(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
|
||||
"""
|
||||
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.
|
||||
Returns the exported key, the matching privat edKey (needed to encrypt)
|
||||
"""
|
||||
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": "!room:id",
|
||||
"sender_key": encode_base64(
|
||||
device_curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
),
|
||||
"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, private_key
|
||||
|
||||
def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed25519.Ed25519PrivateKey) -> dict:
|
||||
|
||||
"""
|
||||
Very simple ratchet step from 0 to 1
|
||||
Used to generate a ratcheted key to test unknown message index.
|
||||
"""
|
||||
session_key: str = previous["session_key"]
|
||||
|
||||
# Get the megolm R0 from the export format
|
||||
decoded = base64.b64decode(session_key.encode("ascii"))
|
||||
ri = decoded[5:133]
|
||||
|
||||
ri0 = ri[0:32]
|
||||
ri1 = ri[32:64]
|
||||
ri2 = ri[64:96]
|
||||
ri3 = ri[96:128]
|
||||
|
||||
h = hmac.HMAC(ri3, hashes.SHA256())
|
||||
h.update(b'x\03')
|
||||
ri1_3 = h.finalize()
|
||||
|
||||
index = 1
|
||||
private_key = megolm_private_key
|
||||
|
||||
# exported key, start with version byte
|
||||
exported_key = bytearray(b'\x01')
|
||||
exported_key += index.to_bytes(4, 'big')
|
||||
exported_key += ri0
|
||||
exported_key += ri1
|
||||
exported_key += ri2
|
||||
exported_key += ri1_3
|
||||
# KPub
|
||||
exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
|
||||
|
||||
megolm_export = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!room:id",
|
||||
"sender_key": previous["sender_key"],
|
||||
"session_id": previous["session_id"],
|
||||
"session_key": encode_base64(exported_key),
|
||||
"sender_claimed_keys": previous["sender_claimed_keys"],
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
}
|
||||
|
||||
return megolm_export
|
||||
|
||||
def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.X25519PublicKey) -> dict:
|
||||
|
||||
"""
|
||||
Encrypts an exported megolm key for key backup, using the m.megolm_backup.v1.curve25519-aes-sha2 algorithm.
|
||||
"""
|
||||
data = encode_canonical_json(session_data)
|
||||
|
||||
# Generate an ephemeral curve25519 key, and perform an ECDH with the ephemeral key
|
||||
# and the backup’s public key to generate a shared secret.
|
||||
# The public half of the ephemeral key, encoded using unpadded base64,
|
||||
# becomes the ephemeral property of the session_data.
|
||||
ephemeral_keypair = x25519.X25519PrivateKey.from_private_bytes(randbytes(32))
|
||||
shared_secret = ephemeral_keypair.exchange(backup_public_key)
|
||||
ephemeral = encode_base64(ephemeral_keypair.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw))
|
||||
|
||||
# Using the shared secret, generate 80 bytes by performing an HKDF using SHA-256 as the hash,
|
||||
# with a salt of 32 bytes of 0, and with the empty string as the info.
|
||||
# The first 32 bytes are used as the AES key, the next 32 bytes are used as the MAC key,
|
||||
# and the last 16 bytes are used as the AES initialization vector.
|
||||
salt = bytes(32)
|
||||
info = b""
|
||||
|
||||
hkdf = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=80,
|
||||
salt=salt,
|
||||
info=info,
|
||||
)
|
||||
|
||||
raw_key = hkdf.derive(shared_secret)
|
||||
aes_key = raw_key[:32]
|
||||
mac = raw_key[32:64]
|
||||
iv = raw_key[64:80]
|
||||
|
||||
# Stringify the JSON object, and encrypt it using AES-CBC-256 with PKCS#7 padding.
|
||||
# This encrypted data, encoded using unpadded base64, becomes the ciphertext property of the session_data.
|
||||
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
|
||||
encryptor = cipher.encryptor()
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(data) + padder.finalize()
|
||||
ct = encryptor.update(padded_data) + encryptor.finalize()
|
||||
cipher_text = encode_base64(ct)
|
||||
|
||||
# Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
|
||||
# The first 8 bytes of the resulting MAC are base64-encoded, and become the mac property of the session_data.
|
||||
h = hmac.HMAC(mac, hashes.SHA256())
|
||||
# h.update(ct)
|
||||
signature = h.finalize()
|
||||
mac = encode_base64(signature[:8])
|
||||
|
||||
encrypted_key = {
|
||||
"first_message_index": 1,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": False,
|
||||
"session_data": {
|
||||
"ciphertext": cipher_text,
|
||||
"ephemeral": ephemeral,
|
||||
"mac": mac
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return encrypted_key
|
||||
|
||||
def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519PrivateKey, curve_key: x25519.X25519PrivateKey) -> tuple[dict, dict]:
|
||||
"""
|
||||
Encrypts an event using the given key in session export format.
|
||||
Will not do any ratcheting, just encrypt at index 0.
|
||||
"""
|
||||
|
||||
clear_event = {
|
||||
"type": "m.room.message",
|
||||
"room_id": "!room:id",
|
||||
"sender": "@alice:localhost",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "Hello world"
|
||||
}
|
||||
}
|
||||
|
||||
session_key: str = exported_key["session_key"]
|
||||
|
||||
# Get the megolm R0 from the export format
|
||||
decoded = base64.b64decode(session_key.encode("ascii"))
|
||||
r0 = decoded[5:133]
|
||||
|
||||
hkdf = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=80,
|
||||
salt=bytes(32),
|
||||
info=b"MEGOLM_KEYS",
|
||||
)
|
||||
|
||||
raw_key = hkdf.derive(r0)
|
||||
aes_key = raw_key[:32]
|
||||
mac = raw_key[32:64]
|
||||
aes_iv = raw_key[64:80]
|
||||
|
||||
payload_json = {
|
||||
"room_id": clear_event["room_id"],
|
||||
"type": clear_event["type"],
|
||||
"content": clear_event["content"]
|
||||
}
|
||||
|
||||
payload_string = encode_canonical_json(payload_json)
|
||||
|
||||
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv))
|
||||
encryptor = cipher.encryptor()
|
||||
padder = padding.PKCS7(128).padder()
|
||||
|
||||
padded_data = padder.update(payload_string)
|
||||
padded_data += padder.finalize()
|
||||
|
||||
ct = encryptor.update(padded_data) + encryptor.finalize()
|
||||
|
||||
# The ratchet index i, and the cipher-text, are then packed
|
||||
# into a message as described in Message format. Then the entire message
|
||||
# (including the version bytes and all payload bytes) are passed through
|
||||
# HMAC-SHA-256. The first 8 bytes of the MAC are appended to the message.
|
||||
message = bytearray()
|
||||
message += b'\x03'
|
||||
# int tag for index
|
||||
message += b'\x08'
|
||||
# index is 0
|
||||
message += b'\x00'
|
||||
message += b'\x12'
|
||||
# probably works only for short messages
|
||||
message += len(ct).to_bytes(1, 'big')
|
||||
# encrypted data
|
||||
message += ct
|
||||
|
||||
h = hmac.HMAC(mac, hashes.SHA256())
|
||||
h.update(message)
|
||||
signature = h.finalize()
|
||||
mac = signature[:8]
|
||||
|
||||
message += mac
|
||||
|
||||
# Finally, the authenticated message is signed using the Ed25519 keypair;
|
||||
# the 64 byte signature is appended to the message
|
||||
signature = ed_key.sign(bytes(message))
|
||||
|
||||
message += signature
|
||||
|
||||
cipher_text = encode_base64(message)
|
||||
|
||||
encrypted_payload = {
|
||||
"algorithm" : "m.megolm.v1.aes-sha2",
|
||||
"sender_key" : encode_base64(curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||
"ciphertext" : cipher_text,
|
||||
"session_id" : exported_key["session_id"],
|
||||
"device_id" : "TEST_DEVICE"
|
||||
}
|
||||
|
||||
encrypted_event = {
|
||||
"type": "m.room.encrypted",
|
||||
"room_id": "!room:id",
|
||||
"sender": "@alice:localhost",
|
||||
"content": encrypted_payload,
|
||||
"event_id": "$event1",
|
||||
"origin_server_ts": 1507753886000,
|
||||
}
|
||||
|
||||
return clear_event, encrypted_event
|
||||
|
||||
|
||||
def export_recovery_key(key_b64: str) -> str:
|
||||
"""
|
||||
Export a private recovery key as a recovery key that can be presented to users.
|
||||
As per spec https://spec.matrix.org/v1.8/client-server-api/#recovery-key
|
||||
"""
|
||||
private_key_bytes = base64.b64decode(key_b64)
|
||||
|
||||
# The 256-bit curve25519 private key is prepended by the bytes 0x8B and 0x01
|
||||
export_bytes = bytearray()
|
||||
export_bytes += b'\x8b'
|
||||
export_bytes += b'\x01'
|
||||
|
||||
export_bytes += private_key_bytes
|
||||
|
||||
# All the bytes in the string above, including the two header bytes,
|
||||
# are XORed together to form a parity byte. This parity byte is appended to the byte string.
|
||||
parity_byte = 0 #b'\x8b' ^ b'\x01'
|
||||
[parity_byte := parity_byte ^ x for x in export_bytes]
|
||||
|
||||
export_bytes += parity_byte.to_bytes(1, 'big')
|
||||
|
||||
# The byte string is encoded using base58
|
||||
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
|
||||
|
||||
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
|
||||
return ' '.join(split)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,451 @@
|
||||
/* 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, IEvent } from "../../../src";
|
||||
import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** 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": "!room:id",
|
||||
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
|
||||
"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": "!room:id",
|
||||
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
|
||||
"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": "!room:id",
|
||||
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
|
||||
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
|
||||
"session_key": "AQAAAABXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIXBlK8tAfJo3cJnlh7F4ltEOAqrdME6dU0zXTkqXmURqYqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** A ratcheted version of MEGOLM_SESSION_DATA */
|
||||
export const RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!room:id",
|
||||
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
|
||||
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
|
||||
"session_key": "AQAAAAFXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIUWvpwC7by/yg231+gyzu9lDHAU4ivCj48pt7WGiORWmIqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** The key from MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
|
||||
export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"first_message_index": 1,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
|
||||
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
|
||||
"mac": "OibmACbORhI"
|
||||
}
|
||||
};
|
||||
|
||||
/** A test clear event */
|
||||
export const CLEAR_EVENT: Partial<IEvent> = {
|
||||
"type": "m.room.message",
|
||||
"room_id": "!room:id",
|
||||
"sender": "@alice:localhost",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "Hello world"
|
||||
}
|
||||
};
|
||||
|
||||
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||
export const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"type": "m.room.encrypted",
|
||||
"room_id": "!room:id",
|
||||
"sender": "@alice:localhost",
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
|
||||
"ciphertext": "AwgAEnAkBmciEAyhh1j6DCk29UXJ7kv/kvayUNfuNT0iAioLxcXjFXOZ5ho3jF1/wrytlt0Lb298uMM67OxdVMi+/mMfYpwlvi07P9cIH6CMSj8tyhYoWl0SrKY6tkPf5GWOlRSRRKbziXa96FHXvnA3V2FCAIGtAe3G4ei5RPbhkmKAFBLAen33/D6MjJVqU8Ojr5vTkgls5eyirarlVpsmnH06alDaxO8avrU0NL+Vsw26xvlUQgEMOnUJ",
|
||||
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
|
||||
"device_id": "TEST_DEVICE"
|
||||
},
|
||||
"event_id": "$event1",
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** 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": "!room:id",
|
||||
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
|
||||
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
|
||||
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!room:id",
|
||||
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
|
||||
"session_id": "+07YOpSgdZ1X9le3n3NMByw0V1B0H0Djnbm76jgmWoo",
|
||||
"session_key": "AQAAAAAjWfIMo9+BWS8IvhfsQuomxXXXGy11tJs0ej505xxd1RzOIP4ftq3MbZYsfH8kqSMBc2l1Ym2u3Dksv2/nR0zGQeNIgOxeMuwHU3Ry7+DdV1I96blPylVCCn/f5RAy6smKoaeylptPdXgVXmw3YBBUVYpHpm+xCIUUp9foAdb8hftO2DqUoHWdV/ZXt59zTAcsNFdQdB9A4525u+o4JlqK",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
}
|
||||
];
|
||||
|
||||
/** An exported megolm session */
|
||||
export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!room:id",
|
||||
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
|
||||
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
|
||||
"session_key": "AQAAAADZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9XoinBNIWr/gkyepuAKiQqemlc8J5amD9OkmbVkmnrxP1uyYMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
|
||||
export const BOB_RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!room:id",
|
||||
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
|
||||
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
|
||||
"session_key": "AQAAAAHZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9Xoil2JdGx9oPqR0dFVh661Aqs86rJRbQ4IeRiuEm35VMxboMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** The key from BOB_MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
|
||||
export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"first_message_index": 1,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
|
||||
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
|
||||
"mac": "lEfHlqfJQwU"
|
||||
}
|
||||
};
|
||||
|
||||
/** A test clear event */
|
||||
export const BOB_CLEAR_EVENT: Partial<IEvent> = {
|
||||
"type": "m.room.message",
|
||||
"room_id": "!room:id",
|
||||
"sender": "@alice:localhost",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "Hello world"
|
||||
}
|
||||
};
|
||||
|
||||
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||
export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"type": "m.room.encrypted",
|
||||
"room_id": "!room:id",
|
||||
"sender": "@alice:localhost",
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
|
||||
"ciphertext": "AwgAEnA/mEqZm2lSrhoG11OpDqsohGSBJWsudbuoItLlivmpFZQHrKMbE6z/dhCTwUi76vwfRCtf4tyPMD845cqZH1nL0bowq3/awyzZ8Q263Y3WrLfkUTFBU6oPF/IULUFZZuw6kLdfd5g5+uigvqUhFFpICoj7KNHznv4sFNssd00/WgJquZ6PRt6e1v6ANFNiZPAwghIL+kBc6pb8i6MUWt9JnXilJhTqFDHdXiY4qkaKBWbwebC26PYM",
|
||||
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
|
||||
"device_id": "TEST_DEVICE"
|
||||
},
|
||||
"event_id": "$event1",
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
+243
-74
@@ -2,20 +2,30 @@
|
||||
import EventEmitter from "events";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
import "../olm-loader";
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
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
|
||||
* SYNCING event.
|
||||
* @param {Object} client The client
|
||||
* @param {Number=} count Number of syncs to wait for (default 1)
|
||||
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
||||
* @param client - The client
|
||||
* @param count - Number of syncs to wait for (default 1)
|
||||
* @returns Promise which resolves once the client has emitted a SYNCING event
|
||||
*/
|
||||
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
if (count <= 0) {
|
||||
@@ -40,21 +50,78 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
* @param {*} constr The class constructor (used with 'new')
|
||||
* @param {string} name The name of the class
|
||||
* @return {Object} An instantiated object with spied methods/properties.
|
||||
* 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 mock<T>(constr: { new(...args: any[]): T }, name: string): T {
|
||||
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: {},
|
||||
knock: {},
|
||||
},
|
||||
account_data: { events: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
* @param constr - The class constructor (used with 'new')
|
||||
* @param name - The name of the class
|
||||
* @returns An instantiated object with spied methods/properties.
|
||||
*/
|
||||
export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
||||
const HelperConstr = new Function(); // jshint ignore:line
|
||||
HelperConstr.prototype = constr.prototype;
|
||||
// @ts-ignore
|
||||
const result = new HelperConstr();
|
||||
result.toString = function() {
|
||||
result.toString = function () {
|
||||
return "mock" + (name ? " of " + name : "");
|
||||
};
|
||||
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
|
||||
for (const key of Object.getOwnPropertyNames(constr.prototype)) {
|
||||
// eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
@@ -84,15 +151,15 @@ interface IEventOpts {
|
||||
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
|
||||
/**
|
||||
* Create an Event.
|
||||
* @param {Object} opts Values for the event.
|
||||
* @param {string} opts.type The event.type
|
||||
* @param {string} opts.room The event.room_id
|
||||
* @param {string} opts.sender The event.sender
|
||||
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
|
||||
* @param {Object} opts.content The event.content
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @return {Object} a JSON object representing this event.
|
||||
* @param opts - Values for the event.
|
||||
* @param opts.type - The event.type
|
||||
* @param opts.room - The event.room_id
|
||||
* @param opts.sender - The event.sender
|
||||
* @param opts.skey - Optional. The state key (auto inserts empty string)
|
||||
* @param opts.content - The event.content
|
||||
* @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 a JSON object representing this event.
|
||||
*/
|
||||
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
@@ -114,15 +181,17 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
} else if ([
|
||||
EventType.RoomName,
|
||||
EventType.RoomTopic,
|
||||
EventType.RoomCreate,
|
||||
EventType.RoomJoinRules,
|
||||
EventType.RoomPowerLevels,
|
||||
EventType.RoomTopic,
|
||||
"com.example.state",
|
||||
].includes(opts.type)) {
|
||||
} else if (
|
||||
[
|
||||
EventType.RoomName,
|
||||
EventType.RoomTopic,
|
||||
EventType.RoomCreate,
|
||||
EventType.RoomJoinRules,
|
||||
EventType.RoomPowerLevels,
|
||||
EventType.RoomTopic,
|
||||
"com.example.state",
|
||||
].includes(opts.type)
|
||||
) {
|
||||
event.state_key = "";
|
||||
}
|
||||
|
||||
@@ -160,8 +229,8 @@ interface IPresenceOpts {
|
||||
|
||||
/**
|
||||
* Create an m.presence event.
|
||||
* @param {Object} opts Values for the presence.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts - Values for the presence.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
|
||||
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
|
||||
@@ -193,16 +262,16 @@ interface IMembershipOpts {
|
||||
|
||||
/**
|
||||
* Create an m.room.member event.
|
||||
* @param {Object} opts Values for the membership.
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.mship The content.membership for the event.
|
||||
* @param {string} opts.sender The sender user ID for the event.
|
||||
* @param {string} opts.skey The target user ID for the event if applicable
|
||||
* @param opts - Values for the membership.
|
||||
* @param opts.room - The room ID for the event.
|
||||
* @param opts.mship - The content.membership for the event.
|
||||
* @param opts.sender - The sender user ID for the event.
|
||||
* @param opts.skey - The target user ID for the event if applicable
|
||||
* e.g. for invites/bans.
|
||||
* @param {string} opts.name The content.displayname for the event.
|
||||
* @param {string} opts.url The content.avatar_url for the event.
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts.name - The content.displayname for the event.
|
||||
* @param opts.url - The content.avatar_url for the event.
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
|
||||
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
|
||||
@@ -228,8 +297,8 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
|
||||
}
|
||||
|
||||
export function mkMembershipCustom<T>(
|
||||
base: T & { membership: string, sender: string, content?: IContent },
|
||||
): T & { type: EventType, sender: string, state_key: string, content: IContent } & GeneratedMetadata {
|
||||
base: T & { membership: string; sender: string; content?: IContent },
|
||||
): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
|
||||
const content = base.content || {};
|
||||
return mkEventCustom({
|
||||
...base,
|
||||
@@ -250,13 +319,16 @@ export interface IMessageOpts {
|
||||
|
||||
/**
|
||||
* Create an m.room.message event.
|
||||
* @param {Object} opts Values for the message
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.user The user ID for the event.
|
||||
* @param {string} opts.msg Optional. The content.body for the event.
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts - Values for the message
|
||||
* @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.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
|
||||
*/
|
||||
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
@@ -290,14 +362,15 @@ interface IReplyMessageOpts extends IMessageOpts {
|
||||
/**
|
||||
* Create a reply message.
|
||||
*
|
||||
* @param {Object} opts Values for the message
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.user The user ID for the event.
|
||||
* @param {string} opts.msg Optional. The content.body for the event.
|
||||
* @param {MatrixEvent} opts.replyToMessage The replied message
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts - Values for the message
|
||||
* @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.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
@@ -315,7 +388,7 @@ export function mkReplyMessage(
|
||||
"rel_type": "m.in_reply_to",
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": opts.replyToMessage.getId()!,
|
||||
event_id: opts.replyToMessage.getId()!,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -328,11 +401,76 @@ export function mkReplyMessage(
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
* Create a reaction event.
|
||||
*
|
||||
* @constructor
|
||||
* @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 class MockStorageApi {
|
||||
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
|
||||
*/
|
||||
export class MockStorageApi implements Storage {
|
||||
private data: Record<string, any> = {};
|
||||
|
||||
public get length() {
|
||||
@@ -354,33 +492,52 @@ export class MockStorageApi {
|
||||
public removeItem(k: string): void {
|
||||
delete this.data[k];
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
* @returns promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
|
||||
export async function awaitDecryption(
|
||||
event: MatrixEvent,
|
||||
{ waitOnDecryptionFailure = false } = {},
|
||||
): Promise<MatrixEvent> {
|
||||
// An event is not always decrypted ahead of time
|
||||
// getClearContent is a good signal to know whether an event has been decrypted
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
return event;
|
||||
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
|
||||
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
|
||||
} else {
|
||||
return event;
|
||||
}
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being 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`);
|
||||
return new Promise((resolve) => {
|
||||
if (waitOnDecryptionFailure) {
|
||||
event.on(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
if (!err) {
|
||||
resolve(ev);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
|
||||
|
||||
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
app_display_name: "app",
|
||||
@@ -392,3 +549,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();
|
||||
}
|
||||
|
||||
+62
-33
@@ -18,21 +18,28 @@ 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 = ({ rootEventId, replyToEventId, ...props }: any & {
|
||||
rootEventId: string; replyToEventId: string; event?: boolean;
|
||||
}): MatrixEvent => mkMessage({
|
||||
...props,
|
||||
relatesTo: {
|
||||
event_id: rootEventId,
|
||||
rel_type: "m.thread",
|
||||
['m.in_reply_to']: {
|
||||
event_id: replyToEventId,
|
||||
export const makeThreadEvent = ({
|
||||
rootEventId,
|
||||
replyToEventId,
|
||||
...props
|
||||
}: any & {
|
||||
rootEventId: string;
|
||||
replyToEventId: string;
|
||||
event?: boolean;
|
||||
}): MatrixEvent =>
|
||||
mkMessage({
|
||||
...props,
|
||||
relatesTo: {
|
||||
event_id: rootEventId,
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
["m.in_reply_to"]: {
|
||||
event_id: replyToEventId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
type MakeThreadEventsProps = {
|
||||
roomId: Room["roomId"];
|
||||
@@ -50,12 +57,17 @@ type MakeThreadEventsProps = {
|
||||
};
|
||||
|
||||
export const makeThreadEvents = ({
|
||||
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
|
||||
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
roomId,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
currentUserId,
|
||||
}: MakeThreadEventsProps): { rootEvent: MatrixEvent; events: MatrixEvent[] } => {
|
||||
const rootEvent = mkMessage({
|
||||
user: authorId,
|
||||
room: roomId,
|
||||
msg: 'root event message ' + Math.random(),
|
||||
msg: "root event message " + Math.random(),
|
||||
ts,
|
||||
event: true,
|
||||
});
|
||||
@@ -67,16 +79,18 @@ export const makeThreadEvents = ({
|
||||
const prevEvent = events[i - 1];
|
||||
const replyToEventId = prevEvent.getId();
|
||||
const user = participantUserIds[i % participantUserIds.length];
|
||||
events.push(makeThreadEvent({
|
||||
user,
|
||||
room: roomId,
|
||||
event: true,
|
||||
msg: `reply ${i} by ${user}`,
|
||||
rootEventId,
|
||||
replyToEventId,
|
||||
// replies are 1ms after each other
|
||||
ts: ts + i,
|
||||
}));
|
||||
events.push(
|
||||
makeThreadEvent({
|
||||
user,
|
||||
room: roomId,
|
||||
event: true,
|
||||
msg: `reply ${i} by ${user}`,
|
||||
rootEventId,
|
||||
replyToEventId,
|
||||
// replies are 1ms after each other
|
||||
ts: ts + i,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
rootEvent.setUnsigned({
|
||||
@@ -101,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,
|
||||
@@ -108,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,
|
||||
@@ -120,15 +154,10 @@ export const mkThread = ({
|
||||
expect(rootEvent).toBeTruthy();
|
||||
|
||||
for (const evt of events) {
|
||||
room?.reEmitter.reEmit(evt, [
|
||||
MatrixEventEvent.BeforeRedaction,
|
||||
]);
|
||||
room?.reEmitter.reEmit(evt, [MatrixEventEvent.BeforeRedaction]);
|
||||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
|
||||
// So that we do not have to mock the thread loading
|
||||
thread.initialEventsFetched = true;
|
||||
thread.addEvents(events, true);
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, [rootEvent, ...events], true);
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
|
||||
+401
-81
@@ -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.
|
||||
@@ -26,21 +26,23 @@ import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call";
|
||||
import { CallEvent, CallEventHandlerMap, CallState, MatrixCall } from "../../src/webrtc/call";
|
||||
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
|
||||
import { CallFeed } from "../../src/webrtc/callFeed";
|
||||
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
|
||||
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
|
||||
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
|
||||
|
||||
export const DUMMY_SDP = (
|
||||
export const DUMMY_SDP =
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
|
||||
@@ -77,24 +79,40 @@ export const DUMMY_SDP = (
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n";
|
||||
|
||||
export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
|
||||
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
|
||||
|
||||
export const FAKE_ROOM_ID = "!fake:test.dummy";
|
||||
export const FAKE_CONF_ID = "fakegroupcallid";
|
||||
|
||||
export const FAKE_USER_ID_1 = "@alice:test.dummy";
|
||||
export const FAKE_DEVICE_ID_1 = "@AAAAAA";
|
||||
export const FAKE_SESSION_ID_1 = "alice1";
|
||||
export const FAKE_USER_ID_2 = "@bob:test.dummy";
|
||||
export const FAKE_DEVICE_ID_2 = "@BBBBBB";
|
||||
export const FAKE_SESSION_ID_2 = "bob1";
|
||||
export const FAKE_USER_ID_3 = "@charlie:test.dummy";
|
||||
|
||||
class MockMediaStreamAudioSourceNode {
|
||||
public connect() {}
|
||||
}
|
||||
|
||||
class MockAnalyser {
|
||||
public getFloatFrequencyData() { return 0.0; }
|
||||
public getFloatFrequencyData() {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockAudioContext {
|
||||
constructor() {}
|
||||
public createAnalyser() { return new MockAnalyser(); }
|
||||
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
|
||||
public createAnalyser() {
|
||||
return new MockAnalyser();
|
||||
}
|
||||
public createMediaStreamSource() {
|
||||
return new MockMediaStreamAudioSourceNode();
|
||||
}
|
||||
public close() {}
|
||||
}
|
||||
|
||||
@@ -103,12 +121,15 @@ export class MockRTCPeerConnection {
|
||||
|
||||
private negotiationNeededListener?: () => void;
|
||||
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;
|
||||
public localDescription: RTCSessionDescription;
|
||||
public signalingState: RTCSignalingState = "stable";
|
||||
public iceConnectionState: RTCIceConnectionState = "connected";
|
||||
public transceivers: MockRTCRtpTransceiver[] = [];
|
||||
|
||||
public static triggerAllNegotiations(): void {
|
||||
@@ -118,7 +139,7 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
|
||||
public static hasAnyPendingNegotiations(): boolean {
|
||||
return this.instances.some(i => i.needsNegotiation);
|
||||
return this.instances.some((i) => i.needsNegotiation);
|
||||
}
|
||||
|
||||
public static resetInstances() {
|
||||
@@ -128,11 +149,11 @@ export class MockRTCPeerConnection {
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
type: "offer",
|
||||
toJSON: function () {},
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>(resolve => {
|
||||
this.readyToNegotiate = new Promise<void>((resolve) => {
|
||||
this.onReadyToNegotiate = resolve;
|
||||
});
|
||||
|
||||
@@ -140,24 +161,30 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
|
||||
public addEventListener(type: string, listener: () => void) {
|
||||
if (type === 'negotiationneeded') {
|
||||
if (type === "negotiationneeded") {
|
||||
this.negotiationNeededListener = listener;
|
||||
} else if (type == 'icecandidate') {
|
||||
} else if (type == "icecandidate") {
|
||||
this.iceCandidateListener = listener;
|
||||
} else if (type == 'track') {
|
||||
} else if (type === "iceconnectionstatechange") {
|
||||
this.iceConnectionStateChangeListener = listener;
|
||||
} else if (type == "track") {
|
||||
this.onTrackListener = listener;
|
||||
} else if (type == "datachannel") {
|
||||
this.onDataChannelListener = listener;
|
||||
}
|
||||
}
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) {
|
||||
return { label, ...opts };
|
||||
}
|
||||
public createOffer() {
|
||||
return Promise.resolve({
|
||||
type: 'offer',
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public createAnswer() {
|
||||
return Promise.resolve({
|
||||
type: 'answer',
|
||||
type: "answer",
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
@@ -167,8 +194,10 @@ export class MockRTCPeerConnection {
|
||||
public setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
public close() { }
|
||||
public getStats() { return []; }
|
||||
public close() {}
|
||||
public getStats() {
|
||||
return [];
|
||||
}
|
||||
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
@@ -193,9 +222,11 @@ export class MockRTCPeerConnection {
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
}
|
||||
|
||||
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
|
||||
public getTransceivers(): MockRTCRtpTransceiver[] {
|
||||
return this.transceivers;
|
||||
}
|
||||
public getSenders(): MockRTCRtpSender[] {
|
||||
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
|
||||
return this.transceivers.map((t) => t.sender as unknown as MockRTCRtpSender);
|
||||
}
|
||||
|
||||
public doNegotiation() {
|
||||
@@ -204,16 +235,24 @@ export class MockRTCPeerConnection {
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
|
||||
public triggerIncomingDataChannel(): void {
|
||||
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
|
||||
}
|
||||
|
||||
public restartIce(): void {}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
|
||||
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
public replaceTrack(track: MockMediaStreamTrack) {
|
||||
this.track = track;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCRtpReceiver {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
}
|
||||
|
||||
export class MockRTCRtpTransceiver {
|
||||
@@ -230,7 +269,7 @@ export class MockRTCRtpTransceiver {
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
|
||||
|
||||
public stop = jest.fn<void, []>();
|
||||
|
||||
@@ -238,7 +277,9 @@ export class MockMediaStreamTrack {
|
||||
public isStopped = false;
|
||||
public settings?: MediaTrackSettings;
|
||||
|
||||
public getSettings(): MediaTrackSettings { return this.settings!; }
|
||||
public getSettings(): MediaTrackSettings {
|
||||
return this.settings!;
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
@@ -257,16 +298,15 @@ export class MockMediaStreamTrack {
|
||||
});
|
||||
}
|
||||
|
||||
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
|
||||
public typed(): MediaStreamTrack {
|
||||
return this as unknown as MediaStreamTrack;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
export class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
constructor(public id: string, private tracks: MockMediaStreamTrack[] = []) {}
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
@@ -277,9 +317,15 @@ export class MockMediaStream {
|
||||
c();
|
||||
});
|
||||
}
|
||||
public getTracks() { return this.tracks; }
|
||||
public getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
public getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
public getTracks() {
|
||||
return this.tracks;
|
||||
}
|
||||
public getAudioTracks() {
|
||||
return this.tracks.filter((track) => track.kind === "audio");
|
||||
}
|
||||
public getVideoTracks() {
|
||||
return this.tracks.filter((track) => track.kind === "video");
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
@@ -292,7 +338,9 @@ export class MockMediaStream {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
public removeTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.splice(this.tracks.indexOf(track), 1);
|
||||
}
|
||||
|
||||
public clone(): MediaStream {
|
||||
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
|
||||
@@ -309,11 +357,11 @@ export class MockMediaStream {
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audioinput" | "videoinput" | "audiooutput",
|
||||
) { }
|
||||
constructor(public kind: "audioinput" | "videoinput" | "audiooutput") {}
|
||||
|
||||
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
|
||||
public typed(): MediaDeviceInfo {
|
||||
return this as unknown as MediaDeviceInfo;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
@@ -343,28 +391,38 @@ export class MockMediaHandler {
|
||||
public stopScreensharingStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public hasAudioDevice() { return true; }
|
||||
public hasVideoDevice() { return true; }
|
||||
public hasAudioDevice() {
|
||||
return true;
|
||||
}
|
||||
public hasVideoDevice() {
|
||||
return true;
|
||||
}
|
||||
public stopAllStreams() {}
|
||||
|
||||
public typed(): MediaHandler { return this as unknown as MediaHandler; }
|
||||
public typed(): MediaHandler {
|
||||
return this as unknown as MediaHandler;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMediaDevices {
|
||||
public enumerateDevices = jest.fn<Promise<MediaDeviceInfo[]>, []>().mockResolvedValue([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]);
|
||||
public enumerateDevices = jest
|
||||
.fn<Promise<MediaDeviceInfo[]>, []>()
|
||||
.mockResolvedValue([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]);
|
||||
|
||||
public getUserMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_stream").typed()),
|
||||
);
|
||||
public getUserMedia = jest
|
||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
|
||||
|
||||
public getDisplayMedia = jest.fn<Promise<MediaStream>, [DisplayMediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_display_stream").typed()),
|
||||
);
|
||||
public getDisplayMedia = jest
|
||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
|
||||
|
||||
public typed(): MediaDevices { return this as unknown as MediaDevices; }
|
||||
public typed(): MediaDevices {
|
||||
return this as unknown as MediaDevices;
|
||||
}
|
||||
}
|
||||
|
||||
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
|
||||
@@ -389,21 +447,33 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
calls: new Map<string, MatrixCall>(),
|
||||
};
|
||||
|
||||
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
|
||||
roomId: string, eventType: EventType, content: any, statekey: string,
|
||||
]>();
|
||||
public sendToDevice = jest.fn<Promise<{}>, [
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]>();
|
||||
public sendStateEvent = jest.fn<
|
||||
Promise<ISendEventResponse>,
|
||||
[roomId: string, eventType: EventType, content: any, statekey: string]
|
||||
>();
|
||||
public sendToDevice = jest.fn<
|
||||
Promise<{}>,
|
||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
||||
>();
|
||||
|
||||
public getMediaHandler(): MediaHandler { return this.mediaHandler.typed(); }
|
||||
public isInitialSyncComplete(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getUserId(): string { return this.userId; }
|
||||
public getMediaHandler(): MediaHandler {
|
||||
return this.mediaHandler.typed();
|
||||
}
|
||||
|
||||
public getDeviceId(): string { return this.deviceId; }
|
||||
public getSessionId(): string { return this.sessionId; }
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public getDeviceId(): string {
|
||||
return this.deviceId;
|
||||
}
|
||||
public getSessionId(): string {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
public getTurnServers = () => [];
|
||||
public isFallbackICEServerAllowed = () => false;
|
||||
@@ -415,24 +485,72 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
public getFoci = jest.fn();
|
||||
|
||||
public typed(): MatrixClient { return this as unknown as MatrixClient; }
|
||||
public supportsThreads(): boolean {
|
||||
return true;
|
||||
}
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
|
||||
public typed(): MatrixClient {
|
||||
return this as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
public emitRoomState(event: MatrixEvent, state: RoomState): void {
|
||||
this.emit(
|
||||
RoomStateEvent.Events,
|
||||
event,
|
||||
state,
|
||||
null,
|
||||
);
|
||||
this.emit(RoomStateEvent.Events, event, state, null);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
|
||||
constructor(public roomId: string, public groupCallId?: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
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 getOpponentMember(): Partial<RoomMember> {
|
||||
return this.opponentMember;
|
||||
}
|
||||
|
||||
public getOpponentDeviceId(): string | undefined {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCallFeed {
|
||||
constructor(
|
||||
public userId: string,
|
||||
public stream: MockMediaStream,
|
||||
) {}
|
||||
constructor(public userId: string, public deviceId: string | undefined, public stream: MockMediaStream) {}
|
||||
|
||||
public measureVolumeActivity(val: boolean) {}
|
||||
public dispose() {}
|
||||
@@ -479,16 +597,22 @@ export function installWebRTCMocks() {
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
}): MatrixEvent {
|
||||
export function makeMockGroupCallStateEvent(
|
||||
roomId: string,
|
||||
groupCallId: string,
|
||||
content: IContent = {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
},
|
||||
redacted?: boolean,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -501,3 +625,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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,16 +27,14 @@ class EventSource extends EventEmitter {
|
||||
}
|
||||
|
||||
doAnError() {
|
||||
this.emit('error');
|
||||
this.emit("error");
|
||||
}
|
||||
}
|
||||
|
||||
class EventTarget extends EventEmitter {
|
||||
class EventTarget extends EventEmitter {}
|
||||
|
||||
}
|
||||
|
||||
describe("ReEmitter", function() {
|
||||
it("Re-Emits events with the same args", function() {
|
||||
describe("ReEmitter", function () {
|
||||
it("Re-Emits events with the same args", function () {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
@@ -53,18 +51,18 @@ describe("ReEmitter", function() {
|
||||
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
|
||||
});
|
||||
|
||||
it("Doesn't throw if no handler for 'error' event", function() {
|
||||
it("Doesn't throw if no handler for 'error' event", function () {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, ['error']);
|
||||
reEmitter.reEmit(src, ["error"]);
|
||||
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on('error', handler);
|
||||
tgt.on("error", handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
+581
-360
File diff suppressed because it is too large
Load Diff
+108
-103
@@ -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,24 +23,20 @@ import {
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
import { REFERENCE_RELATION } from "../../src/@types/extensible_events";
|
||||
|
||||
describe('Beacon content helpers', () => {
|
||||
describe('makeBeaconInfoContent()', () => {
|
||||
describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow);
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, 'now').mockRestore();
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
});
|
||||
it('create fully defined event content', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
LocationAssetType.Pin,
|
||||
)).toEqual({
|
||||
description: 'nice beacon_info',
|
||||
it("create fully defined event content", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
||||
description: "nice beacon_info",
|
||||
timeout: 1234,
|
||||
live: true,
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
@@ -52,78 +46,72 @@ describe('Beacon content helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults timestamp to current time', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
LocationAssetType.Pin,
|
||||
)).toEqual(expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
}));
|
||||
it("defaults timestamp to current time", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual(
|
||||
expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses timestamp when provided', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
LocationAssetType.Pin,
|
||||
99999,
|
||||
)).toEqual(expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: 99999,
|
||||
}));
|
||||
it("uses timestamp when provided", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin, 99999)).toEqual(
|
||||
expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: 99999,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults asset type to self when not set', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
// no assetType passed
|
||||
)).toEqual(expect.objectContaining({
|
||||
[M_ASSET.name]: {
|
||||
type: LocationAssetType.Self,
|
||||
},
|
||||
}));
|
||||
it("defaults asset type to self when not set", () => {
|
||||
expect(
|
||||
makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
"nice beacon_info",
|
||||
// no assetType passed
|
||||
),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
[M_ASSET.name]: {
|
||||
type: LocationAssetType.Self,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeBeaconContent()', () => {
|
||||
it('creates event content without description', () => {
|
||||
expect(makeBeaconContent(
|
||||
'geo:foo',
|
||||
123,
|
||||
'$1234',
|
||||
// no description
|
||||
)).toEqual({
|
||||
describe("makeBeaconContent()", () => {
|
||||
it("creates event content without description", () => {
|
||||
expect(
|
||||
makeBeaconContent(
|
||||
"geo:foo",
|
||||
123,
|
||||
"$1234",
|
||||
// no description
|
||||
),
|
||||
).toEqual({
|
||||
[M_LOCATION.name]: {
|
||||
description: undefined,
|
||||
uri: 'geo:foo',
|
||||
uri: "geo:foo",
|
||||
},
|
||||
[M_TIMESTAMP.name]: 123,
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: '$1234',
|
||||
event_id: "$1234",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('creates event content with description', () => {
|
||||
expect(makeBeaconContent(
|
||||
'geo:foo',
|
||||
123,
|
||||
'$1234',
|
||||
'test description',
|
||||
)).toEqual({
|
||||
it("creates event content with description", () => {
|
||||
expect(makeBeaconContent("geo:foo", 123, "$1234", "test description")).toEqual({
|
||||
[M_LOCATION.name]: {
|
||||
description: 'test description',
|
||||
uri: 'geo:foo',
|
||||
description: "test description",
|
||||
uri: "geo:foo",
|
||||
},
|
||||
[M_TIMESTAMP.name]: 123,
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: '$1234',
|
||||
event_id: "$1234",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -190,64 +178,81 @@ describe('Beacon content helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Topic content helpers', () => {
|
||||
describe('makeTopicContent()', () => {
|
||||
it('creates fully defined event content without html', () => {
|
||||
describe("Topic content helpers", () => {
|
||||
describe("makeTopicContent()", () => {
|
||||
it("creates fully defined event content without html", () => {
|
||||
expect(makeTopicContent("pizza")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
}],
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('creates fully defined event content with html', () => {
|
||||
it("creates fully defined event content with html", () => {
|
||||
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
}, {
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
}],
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTopicContent()', () => {
|
||||
it('parses event content with plain text topic without mimetype', () => {
|
||||
expect(parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
}],
|
||||
})).toEqual({
|
||||
describe("parseTopicContent()", () => {
|
||||
it("parses event content with plain text topic without mimetype", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
|
||||
it('parses event content with plain text topic', () => {
|
||||
expect(parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
}],
|
||||
})).toEqual({
|
||||
it("parses event content with plain text topic", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
|
||||
it('parses event content with html topic', () => {
|
||||
expect(parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
}],
|
||||
})).toEqual({
|
||||
it("parses event content with html topic", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
html: "<b>pizza</b>",
|
||||
});
|
||||
|
||||
@@ -16,60 +16,50 @@ limitations under the License.
|
||||
|
||||
import { getHttpUriForMxc } from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
describe("ContentRepo", function () {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
describe("getHttpUriForMxc", function () {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function () {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(
|
||||
getHttpUriForMxc(
|
||||
baseUrl, httpUrl, undefined, undefined, undefined, true,
|
||||
),
|
||||
).toEqual(httpUrl);
|
||||
expect(getHttpUriForMxc(baseUrl, httpUrl, undefined, undefined, undefined, true)).toEqual(httpUrl);
|
||||
});
|
||||
|
||||
it("should return the empty string HTTP URLs by default", function() {
|
||||
it("should return the empty string HTTP URLs by default", function () {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function() {
|
||||
expect(getHttpUriForMxc(null as any, '')).toEqual("");
|
||||
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/v3/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
it("should return the empty string for null input", function () {
|
||||
expect(getHttpUriForMxc(null as any, "")).toEqual("");
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
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/v3/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
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/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/v3/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+632
-415
File diff suppressed because it is too large
Load Diff
@@ -14,28 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import {
|
||||
CrossSigningInfo,
|
||||
createCryptoStoreCacheCallbacks,
|
||||
} from '../../../src/crypto/CrossSigning';
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import "../../olm-loader";
|
||||
import { CrossSigningInfo, createCryptoStoreCacheCallbacks } from "../../../src/crypto/CrossSigning";
|
||||
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import "fake-indexeddb/auto";
|
||||
import "jest-localstorage-mock";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { logger } from '../../../src/logger';
|
||||
import { logger } from "../../../src/logger";
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
// Private key for tests only
|
||||
const testKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
|
||||
0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
|
||||
const types = [
|
||||
@@ -50,13 +43,13 @@ badKey[0] ^= 1;
|
||||
|
||||
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
@@ -65,13 +58,12 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it.each(types)("should throw if the callback returns falsey",
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: async () => false as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
it.each(types)("should throw if the callback returns falsey", async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: async () => false as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
});
|
||||
|
||||
it("should throw if the expected key doesn't come back", async () => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
@@ -96,63 +88,8 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("should request a key from the cache callback (if set)" +
|
||||
" and does not call app if one is found" +
|
||||
" %o",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
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);
|
||||
if (shouldCache) {
|
||||
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("should store a key with the cache callback (if set)",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
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);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("does not store a bad key to the cache",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it.each(types)("does not store a value to the cache if it came from the cache",
|
||||
it.each(types)(
|
||||
"should request a key from the cache callback (if set)" + " and does not call app if one is found" + " %o",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
@@ -162,56 +99,99 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
|
||||
new Error("Tried to store a value from cache"),
|
||||
);
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(getCrossSigningKeyCache).toHaveBeenLastCalledWith(type, expect.any(String));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(types)("should store a key with the cache callback (if set)", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenLastCalledWith(type, testKey);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("does not store a bad key to the cache", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
|
||||
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it.each(types)("does not store a value to the cache if it came from the cache", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(new Error("Tried to store a value from cache"));
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
});
|
||||
|
||||
it.each(types)(
|
||||
"requests a key from the cache callback (if set) and then calls app" + " if one is not found",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
});
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
|
||||
" if one is not found", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
},
|
||||
);
|
||||
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
});
|
||||
it.each(types)(
|
||||
"requests a key from the cache callback (if set) and then" + " calls app if that key doesn't match",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
it.each(types)("requests a key from the cache callback (if set) and then" +
|
||||
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
});
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -219,20 +199,21 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
* it's not possible to get one in normal execution unless you hack as we do here.
|
||||
*/
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined!, "tests");
|
||||
// @ts-ignore set private properties
|
||||
store._backend = new MemoryCryptoStore();
|
||||
// @ts-ignore
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
return store;
|
||||
}],
|
||||
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
|
||||
let store;
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
[
|
||||
"MemoryCryptoStore",
|
||||
() => {
|
||||
const store = new IndexedDBCryptoStore(undefined!, "tests");
|
||||
// @ts-ignore set private properties
|
||||
store._backend = new MemoryCryptoStore();
|
||||
// @ts-ignore
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
return store;
|
||||
},
|
||||
],
|
||||
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function (name, dbFactory) {
|
||||
let store: IndexedDBCryptoStore;
|
||||
|
||||
beforeAll(() => {
|
||||
store = dbFactory();
|
||||
@@ -245,8 +226,10 @@ describe.each([
|
||||
it("should cache data to the store and retrieve it", async () => {
|
||||
await store.startup();
|
||||
const olmDevice = new OlmDevice(store);
|
||||
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
|
||||
createCryptoStoreCacheCallbacks(store, olmDevice);
|
||||
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = createCryptoStoreCacheCallbacks(
|
||||
store,
|
||||
olmDevice,
|
||||
);
|
||||
await storeCrossSigningKeyCache!("self_signing", testKey);
|
||||
|
||||
// If we've not saved anything, don't expect anything
|
||||
|
||||
@@ -22,33 +22,29 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store
|
||||
import { DeviceList } from "../../../src/crypto/DeviceList";
|
||||
import { IDownloadKeyResult, MatrixClient } from "../../../src";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { CryptoStore } from "../../../src/crypto/store/base";
|
||||
|
||||
const signedDeviceList: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@test1:sw1v.org": {
|
||||
"HGKAWHRVJQ": {
|
||||
"signatures": {
|
||||
HGKAWHRVJQ: {
|
||||
signatures: {
|
||||
"@test1:sw1v.org": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
|
||||
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
|
||||
},
|
||||
},
|
||||
"user_id": "@test1:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
|
||||
"curve25519:HGKAWHRVJQ":
|
||||
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
|
||||
user_id: "@test1:sw1v.org",
|
||||
keys: {
|
||||
"ed25519:HGKAWHRVJQ": "0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
|
||||
"curve25519:HGKAWHRVJQ": "mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "HGKAWHRVJQ",
|
||||
unsigned: {
|
||||
device_display_name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -56,50 +52,45 @@ const signedDeviceList: IDownloadKeyResult = {
|
||||
};
|
||||
|
||||
const signedDeviceList2: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@test2:sw1v.org": {
|
||||
"QJVRHWAKGH": {
|
||||
"signatures": {
|
||||
QJVRHWAKGH: {
|
||||
signatures: {
|
||||
"@test2:sw1v.org": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
|
||||
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
|
||||
},
|
||||
},
|
||||
"user_id": "@test2:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH":
|
||||
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
user_id: "@test2:sw1v.org",
|
||||
keys: {
|
||||
"ed25519:QJVRHWAKGH": "Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH": "YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "QJVRHWAKGH",
|
||||
unsigned: {
|
||||
device_display_name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
describe("DeviceList", function () {
|
||||
let downloadSpy: jest.Mock;
|
||||
let cryptoStore: CryptoStore;
|
||||
let deviceLists: DeviceList[] = [];
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
deviceLists = [];
|
||||
|
||||
downloadSpy = jest.fn();
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
for (const dl of deviceLists) {
|
||||
dl.stop();
|
||||
}
|
||||
@@ -108,94 +99,98 @@ describe('DeviceList', function() {
|
||||
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
getUserId: () => "@test1:sw1v.org",
|
||||
deviceId: "HGKAWHRVJQ",
|
||||
} as unknown as MatrixClient;
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
verifySignature: function (key: string, message: string, signature: string) {},
|
||||
} as unknown as OlmDevice;
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
|
||||
it("should successfully download and store device keys", function() {
|
||||
it("should successfully download and store device keys", function () {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList("@test1:sw1v.org");
|
||||
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
|
||||
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
dl.startTrackingDeviceList("@test1:sw1v.org");
|
||||
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
|
||||
downloadSpy.mockReset();
|
||||
|
||||
// outdated notif arrives while the request is in flight.
|
||||
const queryDefer2 = utils.defer();
|
||||
downloadSpy.mockReturnValue(queryDefer2.promise);
|
||||
|
||||
dl.invalidateUserDeviceList('@test1:sw1v.org');
|
||||
dl.invalidateUserDeviceList("@test1:sw1v.org");
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
dl.saveIfDirty().then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
failures: {},
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
await dl
|
||||
.saveIfDirty()
|
||||
.then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@test1:sw1v.org": {},
|
||||
},
|
||||
});
|
||||
return prom1;
|
||||
})
|
||||
.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);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
|
||||
dl2.stop();
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
// allow promise chain to complete
|
||||
return prom3;
|
||||
})
|
||||
.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
|
||||
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
|
||||
dl.stop();
|
||||
});
|
||||
return prom1;
|
||||
}).then(() => {
|
||||
// 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();
|
||||
const queryDefer3 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
dl2.stop();
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
// allow promise chain to complete
|
||||
return prom3;
|
||||
}).then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
it("should download device keys in batches", function() {
|
||||
it("should download device keys in batches", function () {
|
||||
const dl = createTestDeviceList(1);
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
dl.startTrackingDeviceList("@test1:sw1v.org");
|
||||
dl.startTrackingDeviceList("@test2:sw1v.org");
|
||||
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
@@ -203,17 +198,17 @@ describe('DeviceList', function() {
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
|
||||
expect(Object.keys(storedKeys1)).toEqual(["HGKAWHRVJQ"]);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser("@test2:sw1v.org");
|
||||
expect(Object.keys(storedKeys2)).toEqual(["QJVRHWAKGH"]);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user