Compare commits
904 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aac0023338 | |||
| e3873ddef5 | |||
| a7fd7fd539 | |||
| 300d8b026a | |||
| d5a15ac275 | |||
| bbb5294b3b | |||
| 7731579796 | |||
| 55ab38a097 | |||
| 5367ee18fb | |||
| 45db39ec88 | |||
| 32f55de383 | |||
| 7e8dfa56d0 | |||
| 32bb4b1fc4 | |||
| ae9bb6f27f | |||
| 08ab51eeac | |||
| aa130c88da | |||
| 9523978861 | |||
| 5112340040 | |||
| 6fb40d465e | |||
| 8d7eaa769a | |||
| 7a18991342 | |||
| f18c64db9e | |||
| 7c560b6daa | |||
| de2add5d5d | |||
| 24710ee2fc | |||
| 1fbfdaf221 | |||
| c4f7e4d5aa | |||
| 9a6dccb79b | |||
| 3935152d08 | |||
| 72f9a51c27 | |||
| efdda8425d | |||
| 685cab38b9 | |||
| 85a96c6467 | |||
| 2f832a9bfe | |||
| 1cb32c174b | |||
| b899fd6ccc | |||
| f4aecb317f | |||
| ee0264f77d | |||
| 9bf8b936d4 | |||
| 9f01c8d1fb | |||
| df5ab4fa91 | |||
| 39465b50cb | |||
| a745c67dec | |||
| 55bec4fbe9 | |||
| 3a40348860 | |||
| 98262853c7 | |||
| 4b3aac21db | |||
| 7521f82cac | |||
| 9b7628c103 | |||
| 7d2f4cfd42 | |||
| 5822730797 | |||
| fc946ab0fa | |||
| 9b843daf2f | |||
| ab588f0e51 | |||
| b43b4aa9f9 | |||
| d3ff7655f7 | |||
| a1ab0d42fe | |||
| b9ca3ceacd | |||
| eb8491c91b | |||
| 78db74dad8 | |||
| 4897bccdc9 | |||
| aaf508e309 | |||
| 8eeefc72e9 | |||
| 8e896c4da3 | |||
| 2c2686c910 | |||
| b20063f8a8 | |||
| 11cc2aca9d | |||
| 8f5162c40d | |||
| 2982bd79f6 | |||
| 96c35e2dd3 | |||
| cb5b2e1470 | |||
| 37d5e4a5e9 | |||
| bf30c15d77 | |||
| 8acc92770d | |||
| bfed6edf41 | |||
| 07189f0637 | |||
| aa94d5d95c | |||
| d73126ecb2 | |||
| 76797704ea | |||
| e35ede0370 | |||
| 518e16e6d5 | |||
| 012f6c56e6 | |||
| 8711499121 | |||
| b64dbdce74 | |||
| 2e27a4134c | |||
| 8412ccfa9b | |||
| c707c8632b | |||
| 2700f1d053 | |||
| 142c285063 | |||
| c0ff9756a8 | |||
| 64c6c477dc | |||
| c28939c250 | |||
| 05dd3c4967 | |||
| 8c72c5d0e6 | |||
| 93293750ce | |||
| 220f709b3a | |||
| 6c336ae470 | |||
| a4a50a4a5c | |||
| 169e865bb6 | |||
| 8803d2b7e2 | |||
| ab16adfb7d | |||
| f90adcab89 | |||
| 6090c7bbfc | |||
| 12253064d1 | |||
| b2120a0a13 | |||
| ad030bfc1f | |||
| 60d665e866 | |||
| 2fd48a607d | |||
| 2f540e31a5 | |||
| 7bd9626496 | |||
| 58a5742bd3 | |||
| d90f983438 | |||
| 81a6e48fc0 | |||
| db4a6fa9e1 | |||
| 457f063c67 | |||
| 01eee96b29 | |||
| 25afb7cb3c | |||
| c12932b2a6 | |||
| 7ac1d07cc4 | |||
| 66adc2d597 | |||
| a9516d047f | |||
| e81d84502b | |||
| 32b2c217c7 | |||
| 7da555c255 | |||
| 88348660eb | |||
| 81d884f899 | |||
| 2dccefb33a | |||
| af6915b4fc | |||
| 3786ea4ca2 | |||
| c7f6777e48 | |||
| ba371f7468 | |||
| 761facf98a | |||
| f78fed5ede | |||
| add3732450 | |||
| c6af997542 | |||
| e9e8e90a94 | |||
| f44510e65f | |||
| ba1f6ffc84 | |||
| 3e4f02b41e | |||
| af17fb27b8 | |||
| 72013341db | |||
| 6f445ca99a | |||
| e2af78d8d3 | |||
| 4721aa1d24 | |||
| 4d4d6e1411 | |||
| 67f5293d6c | |||
| 923ff4b282 | |||
| 49dd76b91e | |||
| 34cfa51104 | |||
| 685276f056 | |||
| 03a79dc6dd | |||
| 83a4881498 | |||
| e29ee105aa | |||
| 454eb7e627 | |||
| 62d77231af | |||
| 706b4d6054 | |||
| da69ca215b | |||
| 95a94cdbe3 | |||
| 6b5f4aa0a9 | |||
| dea3f52fe9 | |||
| a388fde3e2 | |||
| 1cde686a13 | |||
| 592f2931dc | |||
| 030fcb57a5 | |||
| 8be30acb11 | |||
| b86630f0e3 | |||
| 81c89f69c5 | |||
| 9b633251d5 | |||
| 70c6a4b567 | |||
| 5cd615c495 | |||
| 88f5ea675d | |||
| ac5fee0a69 | |||
| 274d6a9597 | |||
| d190cdc307 | |||
| b896111269 | |||
| 6137afeb28 | |||
| 2ebf33544f | |||
| 45f1991d4e | |||
| aa283a3c9e | |||
| 34ee566d88 | |||
| 3649cf46d3 | |||
| 8f00eb6771 | |||
| 01ec51891a | |||
| 738876a563 | |||
| 2d9b4b3896 | |||
| ba06e430c4 | |||
| ac08e52410 | |||
| 1bb82108b7 | |||
| e133005b44 | |||
| c0cb66233a | |||
| d82cdd3b19 | |||
| 6a51b02bed | |||
| 47d2c063fa | |||
| 540514c805 | |||
| db58a66e19 | |||
| 91a67bdac3 | |||
| 48d3fce22d | |||
| 9f4598638d | |||
| cde935629d | |||
| fbe81ad823 | |||
| c3d7a4977a | |||
| 9aab917836 | |||
| 54661cca95 | |||
| b58d09aa9a | |||
| 3b33237e51 | |||
| b8ec62e786 | |||
| 59763a84f8 | |||
| 1375a4849c | |||
| c0e3ad4b83 | |||
| 5305e373a0 | |||
| 877e3df71b | |||
| 286500e335 | |||
| 5937e6a6a8 | |||
| c6c22e394b | |||
| 378802a5ab | |||
| f963feab0f | |||
| d705a0ed9e | |||
| 7023fb1c99 | |||
| fe36dafcc7 | |||
| 04a6dbbedf | |||
| 29b427aab7 | |||
| 643b783dec | |||
| 872033a552 | |||
| 6d0f4e537e | |||
| 781fdf4fdc | |||
| dde4285cdf | |||
| 3322b47b6d | |||
| d457fd6db0 | |||
| 03f4700bd7 | |||
| b8321290f8 | |||
| 71b7521f42 | |||
| 106f7beb48 | |||
| d6f1c6cfdc | |||
| 4360ae7ff8 | |||
| 26cbe02a7f | |||
| 85b8d4f83a | |||
| bdc3da1fac | |||
| 95fcd5aa26 | |||
| 0ad83c43e4 | |||
| d6e4de4761 | |||
| f03a391f80 | |||
| e90f12ee32 | |||
| c541b3f1ce | |||
| 6192325fe0 | |||
| d0b964837f | |||
| 65316ffb5c | |||
| dadc19897c | |||
| bd2f1858f4 | |||
| e32b8a75ee | |||
| a6fe8797f0 | |||
| 29e54806a4 | |||
| d9f0704048 | |||
| 75674d961a | |||
| 779afbcb39 | |||
| a3f5ec1ba2 | |||
| 524322280b | |||
| 905a884f72 | |||
| 157635476b | |||
| 18943d6519 | |||
| 42b3b73551 | |||
| ee9eccb85a | |||
| 03a8d9edb6 | |||
| f1db4dc668 | |||
| 98c1710ac1 | |||
| 510833b2f2 | |||
| c2fdb4478d | |||
| 57d71ccd0f | |||
| d064d82fcc | |||
| 17f3920ddd | |||
| 9fc8048c30 | |||
| 9058dbf289 | |||
| e16e7bc098 | |||
| dbcd01bb43 | |||
| 40d1674a5c | |||
| 35a375e3d2 | |||
| 460f4f9254 | |||
| dbd6af745c | |||
| 2ac9448646 | |||
| 3141a7d7c1 | |||
| 1304e811d0 | |||
| 2ce1e7e6ef | |||
| 70efed1a58 | |||
| 9e4f109e80 | |||
| 5d54bf558c | |||
| fc5f0e8047 | |||
| 6bc584ba8b | |||
| ac4e504b8d | |||
| 0abee2a0bf | |||
| 4e4afdb795 | |||
| 9b429c1902 | |||
| b782dee2ef | |||
| 54e815085f | |||
| 0739da4ef4 | |||
| c300a6bdd6 | |||
| 901d53eb38 | |||
| a54471f737 | |||
| 1fae9cb3ef | |||
| 124bfc9328 | |||
| 53aa34fba5 | |||
| 58756a1973 | |||
| 7d9800b817 | |||
| e408590c21 | |||
| 735ccca18b | |||
| 946f47e037 | |||
| 4de1699c49 | |||
| 3d9221f054 | |||
| 1d1d59c757 | |||
| af43687354 | |||
| 2ec5acb55d | |||
| 2128f67dc3 | |||
| 55dda8420c | |||
| d595717e60 | |||
| 4603d4e578 | |||
| 12e525b664 | |||
| 1ac4cc4b11 | |||
| 7b218905fb | |||
| 080426dfdd | |||
| a89c1990d6 | |||
| 4e72290d53 | |||
| 7a7318b636 | |||
| 74d24f38f7 | |||
| 78dcae9143 | |||
| 0db640a679 | |||
| a3ddfd519b | |||
| e86d8861b9 | |||
| 2910e62bb6 | |||
| 2bb14698a5 | |||
| 2fd08d72dc | |||
| 222f62164c | |||
| bf8ee39a3f | |||
| cc1d1d76d0 | |||
| 30fdd96168 | |||
| cfad8d3614 | |||
| d9c3b880fc | |||
| 25c115739c | |||
| d97c514b8d | |||
| 6b822ccd61 | |||
| 47c5c4645e | |||
| ea0eaff212 | |||
| 41bf8c2d5f | |||
| d473a2e095 | |||
| d398ffd6df | |||
| ffab55452a | |||
| 9120b1dfd9 | |||
| 5d4e3183aa | |||
| 07171a95c4 | |||
| 12afcd3850 | |||
| 2d23330b74 | |||
| 7faff66006 | |||
| b07457726b | |||
| 6bf8142ff6 | |||
| cf0ccaf93d | |||
| 34a3955b60 | |||
| 82122872bf | |||
| 9c242a9ce6 | |||
| 51f3fac87b | |||
| 66b98844a2 | |||
| d03db00e4c | |||
| cf8d2bf6ef | |||
| 913a0b51a6 | |||
| ede0f696ee | |||
| d05214a169 | |||
| 1e93d0b19f | |||
| 5e125e646c | |||
| 1643201363 | |||
| 8e99e084a8 | |||
| 67d362fdd9 | |||
| e66430ada6 | |||
| 52ca60142a | |||
| 7d2a5afa6d | |||
| 02ba233644 | |||
| 50493a3330 | |||
| 8434f29c54 | |||
| a34426a7f6 | |||
| 033693283d | |||
| fab7b7f26a | |||
| 230e3b4ace | |||
| a50a627300 | |||
| 80930f6690 | |||
| 652b3a9208 | |||
| 0a8b825702 | |||
| f4e2a38f4b | |||
| b3474531c9 | |||
| f0597e0124 | |||
| 2fdc91bd04 | |||
| 16ca09eed8 | |||
| 129fb7f10f | |||
| 016e24472a | |||
| 4acb98c496 | |||
| 96d1b30012 | |||
| 6fc586598a | |||
| 2d9c938765 | |||
| 83d1a6c046 | |||
| 87393c7db8 | |||
| bd47667e63 | |||
| cfd865bf8b | |||
| 9b54df7b2b | |||
| 5da562fa6f | |||
| 83d952eae8 | |||
| f30be87879 | |||
| ab19480cc5 | |||
| 99451698a4 | |||
| 48dbed8001 | |||
| bae883a891 | |||
| 2aae2362e3 | |||
| f780e1dbc3 | |||
| fdc11b5e53 | |||
| 1a0bdb0f98 | |||
| e870bebd3d | |||
| dd0ca91aa9 | |||
| 36906e2ddb | |||
| 3eaed30446 | |||
| 72643026a3 | |||
| d6980cb8fa | |||
| dcd24fd516 | |||
| 75cc873d77 | |||
| dd23a1a401 | |||
| 6ac84a2465 | |||
| e4d3124e72 | |||
| df11a9e832 | |||
| e4703989fe | |||
| feb83ba161 | |||
| 22a98b3e4f | |||
| 934a0e8943 | |||
| 963c7690b6 | |||
| aeec4aa4a8 | |||
| 169b6b5572 | |||
| 6244d77d44 | |||
| f4839a3b4f | |||
| 4b21b67a45 | |||
| 799a83a115 | |||
| f3ebb2a314 | |||
| c0fbed914e | |||
| dd8c157bb9 | |||
| 8bd2d640f2 | |||
| 80aaa6c32b | |||
| 4e9fe8f53f | |||
| f926a2f777 | |||
| f8097221e6 | |||
| 95e7a76ba9 | |||
| 72bc13eb39 | |||
| 2206b80e65 | |||
| 39a8399977 | |||
| b7247fcedc | |||
| e4494707fa | |||
| 45b7cab203 | |||
| 4dfc3b9dbb | |||
| 15d05cd92f | |||
| ba1b1cc6fa | |||
| b716cde9b5 | |||
| 46a0645865 | |||
| 2b30f45f60 | |||
| 87b920698f | |||
| 4f4811e1d9 | |||
| db9936e07c | |||
| 7dcee2eb40 | |||
| 6b34ea6fe5 | |||
| b33b01df0f | |||
| 1c895432ed | |||
| ddd6a05198 | |||
| 0ccde7807f | |||
| 9f97992196 | |||
| 67b8da719f | |||
| 04fad564f7 | |||
| aee698c285 | |||
| 55b489b17a | |||
| c5d188c30a | |||
| 6fff3d6db5 | |||
| 8043d83921 | |||
| b2d83c1f80 | |||
| 1b91cd7037 | |||
| 0baf926c84 | |||
| 763e25e031 | |||
| 861023b3f6 | |||
| 1f5a83994b | |||
| af523522de | |||
| c3a266b3e7 | |||
| f41d815aa6 | |||
| ad8a93dde8 | |||
| b07d44a6c0 | |||
| 1dc899ba6e | |||
| 0b1e3edaff | |||
| 9fb4fed044 | |||
| 730b0b121d | |||
| 9d9d9e2cfa | |||
| 43bc09f392 | |||
| 195498e9db | |||
| 50332c4999 | |||
| 9e46646b16 | |||
| a74a03e414 | |||
| fe8b9eca8e | |||
| a7cc1ae630 | |||
| 14e008bbad | |||
| 6a12c265cf | |||
| c35cb57a79 | |||
| 51c776f51e | |||
| 7ce0eb26d4 | |||
| f0091fa811 | |||
| 25c88bc986 | |||
| ab06f57965 | |||
| c46209a159 | |||
| 0c47e27f9f | |||
| 0385f265e8 | |||
| 59b3960a42 | |||
| d5ab4ba2ef | |||
| f59b81f46b | |||
| 4c39ce8c96 | |||
| 94ff0ea4cd | |||
| 526758a07f | |||
| 9686fba81e | |||
| bbc547fd7f | |||
| 7d5dbeaf2d | |||
| 8c19cf45e2 | |||
| c9277de8ee | |||
| b5baca51a6 | |||
| fc800821f9 | |||
| efbc46937f | |||
| 9ceeb2d220 | |||
| 7b89862ed0 | |||
| 6804e4290a | |||
| 0a3d820fda | |||
| e2a9c95ea8 | |||
| 3aefc9f02e | |||
| 0042cb5292 | |||
| b288c97372 | |||
| a0de1740a4 | |||
| 8e2e3018d8 | |||
| 60e93024e7 | |||
| d89aa365f9 | |||
| 4241263f2c | |||
| b009f9c21b | |||
| 04ccec6de3 | |||
| e7b41fadd0 | |||
| 2dc1acef2e | |||
| b977b629f4 | |||
| 6bbd493e20 | |||
| 38ad435af5 | |||
| 8f245af317 | |||
| 9b2bfbe7d0 | |||
| f69ea85e55 | |||
| 7559a10526 | |||
| 68dbe959bb | |||
| 74a875aa23 | |||
| 4b679c5056 | |||
| cb8a260b9c | |||
| 7d7975b8bb | |||
| f12513da96 | |||
| 785a3e3381 | |||
| f5fcc20840 | |||
| 17e20c17b3 | |||
| 39fd350ffe | |||
| b4d100baff | |||
| 29bc83f4a7 | |||
| 13c8774d45 | |||
| c9212e770d | |||
| 08aa7803e3 | |||
| 770858c255 | |||
| c73bb7d408 | |||
| 15cd675354 | |||
| e8137d3f88 | |||
| c63abe9988 | |||
| a7a08fd760 | |||
| df51f95fbd | |||
| a14cf1a339 | |||
| 30758600f0 | |||
| 823242a93c | |||
| 482e3c4cb7 | |||
| 1dbd7158ad | |||
| 46b3f0babd | |||
| b4bc554d7a | |||
| 1485969493 | |||
| 92f7ddcf3e | |||
| 90d480b4cf | |||
| 2515d07c8f | |||
| 7acb9416c2 | |||
| 095e3a8a88 | |||
| 92623a246a | |||
| 0c4212ca90 | |||
| 9a06a05a75 | |||
| f05a0615f7 | |||
| 6b93687ff0 | |||
| 8d910d5e26 | |||
| 9264050f41 | |||
| eab0c54663 | |||
| 82a254b7bd | |||
| 0f6a59ed98 | |||
| afb4682987 | |||
| a18326519b | |||
| b033540f95 | |||
| ff529d733d | |||
| 5951958c05 | |||
| 886f2fb95d | |||
| f6ce0c581e | |||
| 5ce68b6c12 | |||
| c748aebfc5 | |||
| f8ec445ff0 | |||
| 5d4f347eaa | |||
| 1359c0574e | |||
| b1d239b292 | |||
| 70031415e8 | |||
| 3c97d17770 | |||
| 5a73a9758a | |||
| 6848e96244 | |||
| b53566e074 | |||
| 012b914a97 | |||
| 3c6eed0135 | |||
| f2a2e7f5dd | |||
| 15b6a9a257 | |||
| b83977e72f | |||
| 8f5082cd2f | |||
| 50c877cb60 | |||
| e3e7fd5900 | |||
| 69823f9cae | |||
| e5dcdf109a | |||
| 5c6a643436 | |||
| 1ca1a69eb8 | |||
| 76ea1ba192 | |||
| 8cd8ec134e | |||
| 6c649e2bfe | |||
| 9f42647a75 | |||
| 9747e5e0d5 | |||
| a4cacbc73a | |||
| f24840d09c | |||
| acb7991cc5 | |||
| 6addc7dd3d | |||
| 2bd60f4160 | |||
| e71362a6df | |||
| 673c09cd77 | |||
| 4606422e57 | |||
| 4bc8f968e0 | |||
| 35ee58dfe1 | |||
| 3705426b6d | |||
| 5813aea27f | |||
| 5ba836a31b | |||
| 4f387d15b9 | |||
| 8ab0246ca2 | |||
| ed68bd4108 | |||
| 3d39e8ec26 | |||
| 53bdab7cc8 | |||
| fafa55ba1d | |||
| 3a818c1419 | |||
| 422853298c | |||
| f3a2bd6b40 | |||
| 6834137f50 | |||
| c4d7fef0cd | |||
| 556b5ff628 | |||
| 2daa1ec4ce | |||
| 348ea72b70 | |||
| 4e14d810c5 | |||
| f84905b003 | |||
| 9bb87145f6 | |||
| c7675570aa | |||
| be942fd66a | |||
| 8544aca362 | |||
| c3308e1de9 | |||
| 047d7125a1 | |||
| cf4d3117c1 | |||
| 88c2844e48 | |||
| 6ca4694fe6 | |||
| 062287e270 | |||
| 50e0d76f10 | |||
| df039d3d8c | |||
| 799606b73c | |||
| c048f5d357 | |||
| f712a8cecb | |||
| e1d8d6bf4e | |||
| 14ed8a110c | |||
| cb6700f21b | |||
| d4f35bf07a | |||
| b4eb29138b | |||
| 8ca3a071f9 | |||
| 5abd211436 | |||
| 99f428e091 | |||
| a734fdf9b0 | |||
| 6f91b2bc80 | |||
| 32d7272939 | |||
| c8d85452bf | |||
| 557f800919 | |||
| 2119e88c78 | |||
| 24e7a7e6bf | |||
| 4e5847f356 | |||
| 72d580edfd | |||
| e878b7683b | |||
| fac700bf4d | |||
| 894c24880d | |||
| d742249fd1 | |||
| 9c100fea48 | |||
| 7daab62850 | |||
| fed0ced89d | |||
| df9cfaed1d | |||
| edd614dd0c | |||
| 375b228bb2 | |||
| 7143ef8a32 | |||
| 1bd7de5a18 | |||
| a1f73eee86 | |||
| d9f8710758 | |||
| 3a9d5439a2 | |||
| 5d1be6e8be | |||
| 775e85a2e8 | |||
| dc8f403c44 | |||
| cf9fa991d1 | |||
| d26dc49755 | |||
| be04559a66 | |||
| 0722dafe58 | |||
| ab906faa17 | |||
| 9d63a12469 | |||
| f40d0d24c7 | |||
| 2cdea36abf | |||
| 8d4f4c8832 | |||
| 3b4dcbb01d | |||
| b52f3b1c57 | |||
| 2cdf5629e5 | |||
| 97b4f0e369 | |||
| a508c8d236 | |||
| 7911e6371a | |||
| bbace01e9a | |||
| 87469fea63 | |||
| 48a9db0315 | |||
| 33c9471112 | |||
| 04fdcbb672 | |||
| 4d2286c5be | |||
| 620e45e4dc | |||
| fb97e74344 | |||
| 3bbb7df72d | |||
| 58b08821bf | |||
| 19bdfd5b84 | |||
| b6c857e2a2 | |||
| 0991626e85 | |||
| 7b10d4fac0 | |||
| 980dacaa87 | |||
| 3790e8abb4 | |||
| 14ba851daf | |||
| 95d80772d7 | |||
| 341e936082 | |||
| e87399c4a1 | |||
| a7678ea1e0 | |||
| 0388bd3b48 | |||
| f27327164b | |||
| a0f85075c1 | |||
| e97d18a03b | |||
| f07cdb43b1 | |||
| da8c8f166d | |||
| 0830a30498 | |||
| 68a2762b75 | |||
| 666e471369 | |||
| d89f4a74f9 | |||
| 191535d01a | |||
| 241203f804 | |||
| 26cc7a0f64 | |||
| 324f9e58ea | |||
| e71519c638 | |||
| 9b6cee0cab | |||
| 2a5f6a7b3d | |||
| cae03817cb | |||
| 37736184a0 | |||
| 9431a52abe | |||
| 13c664ad34 | |||
| d7640d9e15 | |||
| 3c36be9839 | |||
| 9dce7bbb97 | |||
| fa44300abc | |||
| 2783d162b7 | |||
| 39566dbd4f | |||
| f8186add92 | |||
| c15f22e81d | |||
| 052ac8f95b | |||
| c37f8ba4c7 | |||
| 7e2458d5b5 | |||
| fe989177bb | |||
| 412645f025 | |||
| e527c46f34 | |||
| da471b57ad | |||
| a4ff97c8df | |||
| 411fc47f28 | |||
| b97e3113d1 | |||
| f107d63fab | |||
| a65ac5a82b | |||
| c7c1db00e8 | |||
| 824f93daa3 | |||
| 9fec2f8c58 | |||
| 0b46389176 | |||
| db03f16b8e | |||
| f83fcb1a18 | |||
| ba0f1f86b9 | |||
| 81d349d993 | |||
| d65d2b94b9 | |||
| acd52bc070 | |||
| a64339763f | |||
| 4f21b95bbf | |||
| 03e4dcf1ed | |||
| 3c56d1372d | |||
| 27ecdef09b | |||
| 121e8a51c1 | |||
| 88e3ada701 | |||
| 92d822d494 | |||
| 62c93f54cc | |||
| 500c2ed4f2 | |||
| 425362edfe | |||
| 9c554f9b3d | |||
| b7a5f81f95 | |||
| 0746a64c83 | |||
| 22cd475d22 | |||
| 6904f6b36f | |||
| e25a649e1a | |||
| 0eb8cbee4d | |||
| 96e8f65af7 | |||
| 8fb036ba2d | |||
| 0a10fa12ef | |||
| e30dad7913 | |||
| c2a0c02898 | |||
| 7c1e89f86a | |||
| 29f8a4cba2 | |||
| b884accc99 | |||
| d56540320e | |||
| 526fe7e9a4 | |||
| aa696c4c15 | |||
| 3216d7e5a7 | |||
| b68f649414 | |||
| 543f35d9a8 | |||
| 03d42a98cd | |||
| 4f48554cd2 | |||
| 85d9c0f403 | |||
| 66b4279dd8 | |||
| 1ee84fbeca | |||
| c0e15b206a | |||
| 290575ba65 | |||
| bd8690de57 | |||
| 0d09f87777 | |||
| 0f45b7e516 | |||
| 4c552cc350 | |||
| 6a87f54292 | |||
| bfb2c5aad0 | |||
| db898e7bcf | |||
| dbebbe3a19 | |||
| 10e8aff46a | |||
| 81e2ea6a61 | |||
| 2594c59250 | |||
| b56a4ee6ec | |||
| ab524ad3b4 | |||
| 83bf80feec | |||
| b9e5417218 | |||
| 082169a756 | |||
| cdc87d371c | |||
| f8ec3bc591 | |||
| 85f4651e9b | |||
| da53241c48 | |||
| fa265296e8 | |||
| ccf296ea92 | |||
| d65b48204b | |||
| 977720cee4 | |||
| 3c7cdb1da8 | |||
| dd80e744ff | |||
| 0b12e37459 | |||
| 51e815f0cb | |||
| f3856d569d | |||
| e6d1909f0b | |||
| 408976a199 | |||
| 4da49d926b | |||
| 75750ed760 | |||
| 2f74779bf1 | |||
| 7b038393b1 | |||
| 768c0e7f77 | |||
| c6009b1056 | |||
| 2a280afa88 | |||
| be980f4bc9 | |||
| 78118e9442 | |||
| a2c10a7913 | |||
| 7c4f7c9dad | |||
| 62058e6d48 | |||
| a2f514b544 | |||
| 67434bc5d4 | |||
| 167183775e | |||
| 4ce6313126 | |||
| 1298ed61b6 | |||
| 14b424ee94 | |||
| 2bc1f2fe21 | |||
| b392dbf6f8 | |||
| 696b3ef1ce | |||
| 4e2ee3b3a8 | |||
| 1f9fab9a0c | |||
| 6170796ed8 | |||
| d91d1cea34 | |||
| 69ba32683c | |||
| fc78e63ad1 | |||
| db212a555d | |||
| 4607f0177c | |||
| d7dbaeba46 | |||
| 3e94db1837 | |||
| 4fd77c2f05 | |||
| 9fe05e7d40 | |||
| e5a67500e4 | |||
| 9b50455049 | |||
| a531446396 | |||
| f876c35283 | |||
| 1bcec53c6b | |||
| 0ad5ef4e9b | |||
| 18113900fe | |||
| 88b8b24629 | |||
| 58c60d85e7 | |||
| 21a357c433 | |||
| d49d6936a5 | |||
| c2b5b14d26 | |||
| f1a0c46a29 | |||
| e775bcac3c |
@@ -21,3 +21,6 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
@@ -31,6 +31,9 @@ module.exports = {
|
||||
"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", "events"],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
@@ -49,6 +52,8 @@ module.exports = {
|
||||
"@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",
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Minor white-space adjustments
|
||||
1d1d59c75744e1f6a2be1cb3e0d1bd9ded5f8025
|
||||
# Import ordering and spacing: eslint-plugin-import
|
||||
80aaa6c32b50601f82e0c991c24e5a4590f39463
|
||||
# Minor white-space adjustment
|
||||
8fb036ba2d01fab66dc4373802ccf19b5cac8541
|
||||
# Minor white-space adjustment
|
||||
b63de6a902a9e1f8ffd7697dea33820fc04f028e
|
||||
3ca84cfc491b0987eec1f13f13cae58d2032bf54
|
||||
# Conform to new typescript eslint rules
|
||||
a87858840b57514603f63e2abbbda4f107f05a77
|
||||
5cf6684129a921295f5593173f16f192336fe0a2
|
||||
# Comply with new member-delimiter-style rule
|
||||
b2ad957d298720d3e026b6bd91be0c403338361a
|
||||
# Fix semicolons in TS files
|
||||
e2ec8952e38b8fea3f0ccaa09ecb42feeba0d923
|
||||
# Migrate to `eslint-plugin-matrix-org`
|
||||
# and `babel/...` to `@babel/...` migration
|
||||
09fac77ce0d9bcf6637088c29afab84084f0e739
|
||||
102704e91a70643bcc09721e14b0d909f0ef55c6
|
||||
# Eslint formatting
|
||||
cec00cd303787fa9008b6c48826e75ed438036fa
|
||||
# Minor eslint changes
|
||||
68bb8182e4e62d8f450f80c408c4b231b8725f1b
|
||||
c979ff6696e30ab8983ac416a3590996d84d3560
|
||||
f4a7395e3a3751a1a8e92dd302c49175a3296ad2
|
||||
# eslint --fix for dangley commas on function calls
|
||||
423175f5397910b0afe3112d6fb18283fc7d27d4
|
||||
# eslint ---fix for prefer-const
|
||||
7bca05af644e8b997dae81e568a3913d8f18d7ca
|
||||
# Fix linting on tests
|
||||
cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||
# eslint --fix
|
||||
0fa9f7c6098822db1ae214f352fd1fe5c248b02c
|
||||
# eslint --fix for lots of white-space
|
||||
5abf6b9f208801c5022a47023150b5846cb0b309
|
||||
# eslint --fix
|
||||
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
|
||||
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
|
||||
|
||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
|
||||
## Checklist
|
||||
|
||||
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
|
||||
Notes:
|
||||
* [ ] 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:
|
||||
|
||||
Notes: Add super cool feature
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Release Process
|
||||
on:
|
||||
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
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
ls -lah
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
cp -a "$RUNNER_TEMP/$VERSION" .
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q "Version $VERSION" 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: .
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Notify Downstream Projects
|
||||
on:
|
||||
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
|
||||
|
||||
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 }}
|
||||
@@ -1,12 +0,0 @@
|
||||
name: Preview Changelog
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled ]
|
||||
jobs:
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Preview Changelog
|
||||
uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,91 @@
|
||||
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 }}
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
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!");
|
||||
|
||||
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 }}
|
||||
|
||||
- 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",
|
||||
});
|
||||
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
||||
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
|
||||
@@ -0,0 +1,15 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
concurrency:
|
||||
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
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build
|
||||
run: "yarn build"
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn coverage --ci --reporters github-actions"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Upgrade Dependencies
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
upgrade:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- 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 }}
|
||||
@@ -1,2 +0,0 @@
|
||||
instrumentation:
|
||||
compact: false
|
||||
+574
-4
@@ -1,12 +1,576 @@
|
||||
Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.1) (2021-07-11)
|
||||
Changes in [19.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.2.0) (2022-08-02)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Remove unstable support for `m.room_key.withheld` ([\#2512](https://github.com/matrix-org/matrix-js-sdk/pull/2512)). Fixes #2233.
|
||||
|
||||
## ✨ Features
|
||||
* Sliding sync: add missing filters from latest MSC ([\#2555](https://github.com/matrix-org/matrix-js-sdk/pull/2555)).
|
||||
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
|
||||
* Add support for MSC3575: Sliding Sync ([\#2242](https://github.com/matrix-org/matrix-js-sdk/pull/2242)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Correct the units in TURN servers expiry documentation ([\#2520](https://github.com/matrix-org/matrix-js-sdk/pull/2520)).
|
||||
* Re-insert room IDs when decrypting bundled redaction events returned by `/sync` ([\#2531](https://github.com/matrix-org/matrix-js-sdk/pull/2531)). Contributed by @duxovni.
|
||||
|
||||
Changes in [19.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.1.0) (2022-07-26)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Remove MSC3244 support ([\#2504](https://github.com/matrix-org/matrix-js-sdk/pull/2504)).
|
||||
|
||||
## ✨ Features
|
||||
* `room` now exports `KNOWN_SAFE_ROOM_VERSION` ([\#2474](https://github.com/matrix-org/matrix-js-sdk/pull/2474)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't crash with undefined room in `processBeaconEvents()` ([\#2500](https://github.com/matrix-org/matrix-js-sdk/pull/2500)). Fixes #2494.
|
||||
* Properly re-insert room ID in bundled thread relation messages from sync ([\#2505](https://github.com/matrix-org/matrix-js-sdk/pull/2505)). Fixes vector-im/element-web#22094. Contributed by @duxovni.
|
||||
* Actually store the identity server in the client when given as an option ([\#2503](https://github.com/matrix-org/matrix-js-sdk/pull/2503)). Fixes vector-im/element-web#22757.
|
||||
* Fix call.collectCallStats() ([\#2480](https://github.com/matrix-org/matrix-js-sdk/pull/2480)).
|
||||
|
||||
Changes in [19.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.0.0) (2022-07-05)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Remove unused sessionStore ([\#2455](https://github.com/matrix-org/matrix-js-sdk/pull/2455)).
|
||||
|
||||
## ✨ Features
|
||||
* Implement MSC3827: Filtering of `/publicRooms` by room type ([\#2469](https://github.com/matrix-org/matrix-js-sdk/pull/2469)).
|
||||
* expose latestLocationEvent on beacon model ([\#2467](https://github.com/matrix-org/matrix-js-sdk/pull/2467)). Contributed by @kerryarchibald.
|
||||
* Live location share - add start time leniency ([\#2465](https://github.com/matrix-org/matrix-js-sdk/pull/2465)). Contributed by @kerryarchibald.
|
||||
* Log real errors and not just their messages, traces are useful ([\#2464](https://github.com/matrix-org/matrix-js-sdk/pull/2464)).
|
||||
* Various changes to `src/crypto` files for correctness ([\#2137](https://github.com/matrix-org/matrix-js-sdk/pull/2137)). Contributed by @ShadowJonathan.
|
||||
* Update MSC3786 implementation: Check the `state_key` ([\#2429](https://github.com/matrix-org/matrix-js-sdk/pull/2429)).
|
||||
* Timeline needs to refresh when we see a MSC2716 marker event ([\#2299](https://github.com/matrix-org/matrix-js-sdk/pull/2299)). Contributed by @MadLittleMods.
|
||||
* Try to load keys from key backup when a message fails to decrypt ([\#2373](https://github.com/matrix-org/matrix-js-sdk/pull/2373)). Fixes vector-im/element-web#21026. Contributed by @duxovni.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Send call version `1` as a string ([\#2471](https://github.com/matrix-org/matrix-js-sdk/pull/2471)). Fixes vector-im/element-web#22629.
|
||||
* Fix issue with `getEventTimeline` returning undefined for thread roots in main timeline ([\#2454](https://github.com/matrix-org/matrix-js-sdk/pull/2454)). Fixes vector-im/element-web#22539.
|
||||
* Add missing `type` property on `IAuthData` ([\#2463](https://github.com/matrix-org/matrix-js-sdk/pull/2463)).
|
||||
* Clearly indicate that `lastReply` on a Thread can return falsy ([\#2462](https://github.com/matrix-org/matrix-js-sdk/pull/2462)).
|
||||
* Fix issues with getEventTimeline and thread roots ([\#2444](https://github.com/matrix-org/matrix-js-sdk/pull/2444)). Fixes vector-im/element-web#21613.
|
||||
* Live location sharing - monitor liveness of beacons yet to start ([\#2437](https://github.com/matrix-org/matrix-js-sdk/pull/2437)). Contributed by @kerryarchibald.
|
||||
* Refactor Relations to not be per-EventTimelineSet ([\#2412](https://github.com/matrix-org/matrix-js-sdk/pull/2412)). Fixes #2399 and vector-im/element-web#22298.
|
||||
* Add tests for sendEvent threadId handling ([\#2435](https://github.com/matrix-org/matrix-js-sdk/pull/2435)). Fixes vector-im/element-web#22433.
|
||||
* Make sure `encryptAndSendKeysToDevices` assumes devices are unique per-user. ([\#2136](https://github.com/matrix-org/matrix-js-sdk/pull/2136)). Fixes #2135. Contributed by @ShadowJonathan.
|
||||
* Don't bug the user while re-checking key backups after decryption failures ([\#2430](https://github.com/matrix-org/matrix-js-sdk/pull/2430)). Fixes vector-im/element-web#22416. Contributed by @duxovni.
|
||||
|
||||
Changes in [18.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.1.0) (2022-06-07)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Convert `getLocalAliases` to a stable API call ([\#2402](https://github.com/matrix-org/matrix-js-sdk/pull/2402)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix request, crypto, and bs58 imports ([\#2414](https://github.com/matrix-org/matrix-js-sdk/pull/2414)). Fixes #2415.
|
||||
* Update relations after every decryption attempt ([\#2387](https://github.com/matrix-org/matrix-js-sdk/pull/2387)). Fixes vector-im/element-web#22258. Contributed by @weeman1337.
|
||||
* Fix degraded mode for the IDBStore and test it ([\#2400](https://github.com/matrix-org/matrix-js-sdk/pull/2400)). Fixes matrix-org/element-web-rageshakes#13170.
|
||||
* Don't cancel SAS verifications if `ready` is received after `start` ([\#2250](https://github.com/matrix-org/matrix-js-sdk/pull/2250)).
|
||||
* Prevent overlapping sync accumulator persists ([\#2392](https://github.com/matrix-org/matrix-js-sdk/pull/2392)). Fixes vector-im/element-web#21541.
|
||||
* Fix behaviour of isRelation with relation m.replace for state events ([\#2389](https://github.com/matrix-org/matrix-js-sdk/pull/2389)). Fixes vector-im/element-web#22280.
|
||||
* Fixes #2384 ([\#2385](https://github.com/matrix-org/matrix-js-sdk/pull/2385)). Fixes undefined/matrix-js-sdk#2384. Contributed by @schmop.
|
||||
* Ensure rooms are recalculated on re-invites ([\#2374](https://github.com/matrix-org/matrix-js-sdk/pull/2374)). Fixes vector-im/element-web#22106.
|
||||
|
||||
Changes in [18.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0) (2022-05-24)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES (to experimental methods)
|
||||
* Implement changes to MSC2285 (private read receipts) ([\#2221](https://github.com/matrix-org/matrix-js-sdk/pull/2221)).
|
||||
|
||||
## ✨ Features
|
||||
* Add support for HTML renderings of room topics ([\#2272](https://github.com/matrix-org/matrix-js-sdk/pull/2272)).
|
||||
* Add stopClient parameter to MatrixClient::logout ([\#2367](https://github.com/matrix-org/matrix-js-sdk/pull/2367)).
|
||||
* registration: add function to re-request email token ([\#2357](https://github.com/matrix-org/matrix-js-sdk/pull/2357)).
|
||||
* Remove hacky custom status feature ([\#2350](https://github.com/matrix-org/matrix-js-sdk/pull/2350)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Remove default push rule override for MSC1930 ([\#2376](https://github.com/matrix-org/matrix-js-sdk/pull/2376)). Fixes vector-im/element-web#15439.
|
||||
* Tweak thread creation & event adding to fix bugs around relations ([\#2369](https://github.com/matrix-org/matrix-js-sdk/pull/2369)). Fixes vector-im/element-web#22162 and vector-im/element-web#22180.
|
||||
* Prune both clear & wire content on redaction ([\#2346](https://github.com/matrix-org/matrix-js-sdk/pull/2346)). Fixes vector-im/element-web#21929.
|
||||
* MSC3786: Add a default push rule to ignore `m.room.server_acl` events ([\#2333](https://github.com/matrix-org/matrix-js-sdk/pull/2333)). Fixes vector-im/element-web#20788.
|
||||
|
||||
Changes in [17.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.2.0) (2022-05-10)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Live location sharing: handle encrypted messages in processBeaconEvents ([\#2327](https://github.com/matrix-org/matrix-js-sdk/pull/2327)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race conditions around threads ([\#2331](https://github.com/matrix-org/matrix-js-sdk/pull/2331)). Fixes vector-im/element-web#21627.
|
||||
* Ignore m.replace relations on state events, they're invalid ([\#2306](https://github.com/matrix-org/matrix-js-sdk/pull/2306)). Fixes vector-im/element-web#21851.
|
||||
* fix example in readme ([\#2315](https://github.com/matrix-org/matrix-js-sdk/pull/2315)).
|
||||
* Don't decrement the length count of a thread when root redacted ([\#2314](https://github.com/matrix-org/matrix-js-sdk/pull/2314)).
|
||||
* Prevent attempt to create thread with id "undefined" ([\#2308](https://github.com/matrix-org/matrix-js-sdk/pull/2308)).
|
||||
* Update threads handling for replies-to-thread-responses as per MSC update ([\#2305](https://github.com/matrix-org/matrix-js-sdk/pull/2305)). Fixes vector-im/element-web#19678.
|
||||
|
||||
Changes in [17.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.1.0) (2022-04-26)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add MatrixClient.doesServerSupportLogoutDevices() for MSC2457 ([\#2297](https://github.com/matrix-org/matrix-js-sdk/pull/2297)).
|
||||
* Live location sharing - expose room liveBeaconIds ([\#2296](https://github.com/matrix-org/matrix-js-sdk/pull/2296)).
|
||||
* Support for MSC2457 logout_devices param for setPassword() ([\#2285](https://github.com/matrix-org/matrix-js-sdk/pull/2285)).
|
||||
* Stabilise token authenticated registration support ([\#2181](https://github.com/matrix-org/matrix-js-sdk/pull/2181)). Contributed by @govynnus.
|
||||
* Live location sharing - Aggregate beacon locations on beacons ([\#2268](https://github.com/matrix-org/matrix-js-sdk/pull/2268)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Prevent duplicated re-emitter setups in event-mapper ([\#2293](https://github.com/matrix-org/matrix-js-sdk/pull/2293)).
|
||||
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
|
||||
|
||||
Changes in [17.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0) (2022-04-11)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
|
||||
|
||||
## ✨ Features
|
||||
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
|
||||
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
|
||||
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
|
||||
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
|
||||
|
||||
Changes in [16.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.2-rc.1) (2022-04-05)
|
||||
============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
|
||||
|
||||
## ✨ Features
|
||||
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
|
||||
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
|
||||
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
|
||||
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
|
||||
|
||||
Changes in [16.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1) (2022-03-28)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* emit aggregate room beacon liveness ([\#2241](https://github.com/matrix-org/matrix-js-sdk/pull/2241)).
|
||||
* Live location sharing - create m.beacon_info events ([\#2238](https://github.com/matrix-org/matrix-js-sdk/pull/2238)).
|
||||
* Beacon event types from MSC3489 ([\#2230](https://github.com/matrix-org/matrix-js-sdk/pull/2230)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix incorrect usage of unstable variant of `is_falling_back` ([\#2227](https://github.com/matrix-org/matrix-js-sdk/pull/2227)).
|
||||
|
||||
Changes in [16.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1-rc.1) (2022-03-22)
|
||||
============================================================================================================
|
||||
|
||||
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
|
||||
|
||||
## ✨ Features
|
||||
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
|
||||
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
|
||||
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
|
||||
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
|
||||
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
|
||||
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
|
||||
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
|
||||
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
|
||||
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
|
||||
|
||||
Changes in [16.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0-rc.1) (2022-03-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
|
||||
|
||||
## ✨ Features
|
||||
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
|
||||
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
|
||||
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
|
||||
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
|
||||
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
|
||||
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
|
||||
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
|
||||
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
|
||||
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
|
||||
|
||||
Changes in [15.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0) (2022-02-28)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* [Release] Fix bug with the /hierarchy API sending invalid requests ([\#2202](https://github.com/matrix-org/matrix-js-sdk/pull/2202)).
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0-rc.1) (2022-02-22)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.2) (2022-02-17)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix synthetic read receipt handling
|
||||
|
||||
Changes in [15.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1) (2022-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1-rc.1) (2022-02-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
|
||||
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
|
||||
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
|
||||
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
|
||||
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
|
||||
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
|
||||
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
|
||||
|
||||
Changes in [15.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0-rc.1) (2022-01-11)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
|
||||
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
|
||||
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
|
||||
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
|
||||
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
|
||||
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
|
||||
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
|
||||
|
||||
Changes in [15.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0) (2021-12-20)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
|
||||
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
|
||||
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
|
||||
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
|
||||
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
|
||||
|
||||
Changes in [15.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0-rc.1) (2021-12-14)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
|
||||
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
|
||||
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
|
||||
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
|
||||
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
|
||||
|
||||
Changes in [15.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.1) (2021-12-13)
|
||||
==================================================================================================
|
||||
|
||||
* Security release with updated version of Olm to fix https://matrix.org/blog/2021/12/03/pre-disclosure-upcoming-security-release-of-libolm-and-matrix-js-sdk
|
||||
|
||||
Changes in [15.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0) (2021-12-06)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
|
||||
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
|
||||
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0-rc.1) (2021-11-30)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
|
||||
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
|
||||
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1) (2021-11-22)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
|
||||
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
|
||||
|
||||
Changes in [15.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1-rc.1) (2021-11-17)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
|
||||
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
|
||||
|
||||
Changes in [15.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0) (2021-11-08)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
|
||||
|
||||
## ✨ Features
|
||||
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
|
||||
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
|
||||
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
|
||||
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
|
||||
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
|
||||
|
||||
Changes in [15.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0-rc.1) (2021-11-02)
|
||||
============================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
|
||||
|
||||
## ✨ Features
|
||||
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
|
||||
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
|
||||
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
|
||||
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
|
||||
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
|
||||
|
||||
Changes in [15.0.0](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0) (2021-10-25)
|
||||
===================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
|
||||
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
|
||||
|
||||
Changes in [15.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0-rc.1) (2021-10-19)
|
||||
=============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
|
||||
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
|
||||
|
||||
Changes in [14.0.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.1) (2021-10-12)
|
||||
===================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
|
||||
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
|
||||
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
|
||||
|
||||
|
||||
Changes in [14.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.1) (2021-10-04)
|
||||
=============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
|
||||
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
|
||||
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
|
||||
|
||||
Changes in [13.0.0](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0) (2021-09-27)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
|
||||
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
|
||||
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
|
||||
|
||||
Changes in [13.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0-rc.1) (2021-09-21)
|
||||
=============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
|
||||
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
|
||||
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
|
||||
|
||||
Changes in [12.5.0](https://github.com/vector-im/element-desktop/releases/tag/v12.5.0) (2021-09-14)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* [Release] Exclude opt-in Element performance metrics from encryption ([\#1901](https://github.com/matrix-org/matrix-js-sdk/pull/1901)).
|
||||
* Give `MatrixCall` the capability to emit `LengthChanged` events ([\#1873](https://github.com/matrix-org/matrix-js-sdk/pull/1873)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Improve browser example ([\#1875](https://github.com/matrix-org/matrix-js-sdk/pull/1875)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Give `CallFeed` the capability to emit on volume changes ([\#1865](https://github.com/matrix-org/matrix-js-sdk/pull/1865)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix verification request cancellation ([\#1871](https://github.com/matrix-org/matrix-js-sdk/pull/1871)).
|
||||
|
||||
Changes in [12.4.1](https://github.com/vector-im/element-desktop/releases/tag/v12.4.1) (2021-09-13)
|
||||
===================================================================================================
|
||||
|
||||
## 🔒 SECURITY FIXES
|
||||
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
|
||||
for details.
|
||||
|
||||
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
|
||||
===================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate groups APIs. Groups are no longer supported, only Synapse has support. They are being replaced by Spaces which build off of Rooms and are far more flexible. ([\#1792](https://github.com/matrix-org/matrix-js-sdk/pull/1792)).
|
||||
|
||||
## ✨ Features
|
||||
* Add method for including extra fields when uploading to a tree space ([\#1850](https://github.com/matrix-org/matrix-js-sdk/pull/1850)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix broken voice calls, no ringing and broken call notifications ([\#1858](https://github.com/matrix-org/matrix-js-sdk/pull/1858)). Fixes vector-im/element-web#18578 vector-im/element-web#18538 and vector-im/element-web#18578. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Revert "Fix glare related regressions" ([\#1857](https://github.com/matrix-org/matrix-js-sdk/pull/1857)).
|
||||
* Fix glare related regressions ([\#1851](https://github.com/matrix-org/matrix-js-sdk/pull/1851)). Fixes vector-im/element-web#18538 and vector-im/element-web#18538. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix temporary call messages being handled without call ([\#1834](https://github.com/matrix-org/matrix-js-sdk/pull/1834)). Contributed by [Palid](https://github.com/Palid).
|
||||
* Fix conditional on returning file tree spaces ([\#1841](https://github.com/matrix-org/matrix-js-sdk/pull/1841)).
|
||||
|
||||
Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17)
|
||||
===================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix multiple VoIP regressions ([\#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)).
|
||||
|
||||
Changes in [12.3.0](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0) (2021-08-16)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support for MSC3291: Muting in VoIP calls ([\#1812](https://github.com/matrix-org/matrix-js-sdk/pull/1812)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Support for screen-sharing using multi-stream VoIP (MSC3077) ([\#1685](https://github.com/matrix-org/matrix-js-sdk/pull/1685)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Handle DTMF support ([\#1813](https://github.com/matrix-org/matrix-js-sdk/pull/1813)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* [Release] Fix glare related regressions ([\#1854](https://github.com/matrix-org/matrix-js-sdk/pull/1854)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503.
|
||||
* Fix error on turning off screensharing ([\#1833](https://github.com/matrix-org/matrix-js-sdk/pull/1833)). Fixes vector-im/element-web#18449. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix blank profile in join events ([\#1837](https://github.com/matrix-org/matrix-js-sdk/pull/1837)). Fixes vector-im/element-web#18321.
|
||||
* fix TURN by fixing regression preventing multiple ICE candidates from sending. ([\#1838](https://github.com/matrix-org/matrix-js-sdk/pull/1838)).
|
||||
@@ -14,7 +578,7 @@ Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/t
|
||||
* Apply hidden char check to rawDisplayName too ([\#1816](https://github.com/matrix-org/matrix-js-sdk/pull/1816)).
|
||||
* Only clear bit 63 when we create the IV ([\#1819](https://github.com/matrix-org/matrix-js-sdk/pull/1819)).
|
||||
|
||||
Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-07-02)
|
||||
Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-08-02)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
@@ -226,7 +790,7 @@ Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
|
||||
|
||||
All changes
|
||||
@@ -899,7 +1463,7 @@ BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
|
||||
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
@@ -1574,6 +2138,12 @@ All Changes
|
||||
* [BREAKING] Refactor the entire build process
|
||||
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
|
||||
|
||||
Changes in [3.42.2-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.42.2-rc.3) (2022-04-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
|
||||
|
||||
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
|
||||
|
||||
+97
-7
@@ -22,13 +22,25 @@ ask you to make any refinements needed or merge it and make them ourselves.
|
||||
Things that should go into your PR description:
|
||||
* A changelog entry in the `Notes` section (see below)
|
||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
* Notes for the reviewer that might help them to understand why the change is
|
||||
necessary or how they might better review it.
|
||||
|
||||
Things that should *not* go into your PR description:
|
||||
* Any information on how the code works or why you chose to do it the way
|
||||
you did. If this isn't obvious from your code, you haven't written enough
|
||||
comments.
|
||||
* Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch. This information is
|
||||
also helpful when we come back to look at this in 6 months and ask "why did
|
||||
we do it like that?" we have a chance of finding out.
|
||||
* Why didn't it work before? Why does it work now? What use cases does it
|
||||
unlock?
|
||||
* If you find yourself adding information on how the code works or why you
|
||||
chose to do it the way you did, make sure this information is instead
|
||||
written as comments in the code itself.
|
||||
* Sometimes a PR can change considerably as it is developed. In this case,
|
||||
the description should be updated to reflect the most recent state of
|
||||
the PR. (It can be helpful to retain the old content under a suitable
|
||||
heading, for additional context.)
|
||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
||||
what's changing.
|
||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
* Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
We rely on information in pull request to populate the information that goes
|
||||
into the changelogs our users see, both for the JS SDK itself and also for some
|
||||
@@ -92,6 +104,61 @@ We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Tests
|
||||
-----
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-react-sdk` or `element-web`, you
|
||||
must include:
|
||||
|
||||
1. Comprehensive unit tests written in Jest. These are located in `/test`.
|
||||
2. "happy path" end-to-end tests.
|
||||
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
|
||||
are run using `element-web`. Ideally, you would also include tests for edge
|
||||
and error cases.
|
||||
|
||||
Unit tests are expected even when the feature is in labs. It's good practice
|
||||
to write tests alongside the code as it ensures the code is testable from
|
||||
the start, and gives you a fast feedback loop while you're developing the
|
||||
functionality. End-to-end tests should be added prior to the feature
|
||||
leaving labs, but don't have to be present from the start (although it might
|
||||
be beneficial to have some running early, so you can test things faster).
|
||||
|
||||
For bugs in those repos, your change must include at least one unit test or
|
||||
end-to-end test; which is best depends on what sort of test most concisely
|
||||
exercises the area.
|
||||
|
||||
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
|
||||
These are located in `/spec/`.
|
||||
|
||||
When writing unit tests, please aim for a high level of test coverage
|
||||
for new code - 80% or greater. If you cannot achieve that, please document
|
||||
why it's not possible in your PR.
|
||||
|
||||
Some sections of code are not sensible to add coverage for, such as those
|
||||
which explicitly inhibit noisy logging for tests. Which can be hidden using
|
||||
an istanbul magic comment as [documented here][1]. See example:
|
||||
```javascript
|
||||
/* istanbul ignore if */
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
logger.error("Log line that is noisy enough in tests to want to skip");
|
||||
}
|
||||
```
|
||||
|
||||
Tests validate that your change works as intended and also document
|
||||
concisely what is being changed. Ideally, your new tests fail
|
||||
prior to your change, and succeed once it has been applied. You may
|
||||
find this simpler to achieve if you write the tests first.
|
||||
|
||||
If you're spiking some code that's experimental and not being used to support
|
||||
production features, exceptions can be made to requirements for tests.
|
||||
Note that tests will still be required in order to ship the feature, and it's
|
||||
strongly encouraged to think about tests early in the process, as adding
|
||||
tests later will become progressively more difficult.
|
||||
|
||||
If you're not sure how to approach writing tests for your change, ask for help
|
||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
||||
|
||||
Code style
|
||||
----------
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
@@ -192,3 +259,26 @@ on Git 2.17+ you can mass signoff using rebase:
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
Review expectations
|
||||
===================
|
||||
|
||||
See https://github.com/vector-im/element-meta/wiki/Review-process
|
||||
|
||||
|
||||
Merge Strategy
|
||||
==============
|
||||
|
||||
The preferred method for merging pull requests is squash merging to keep the
|
||||
commit history trim, but it is up to the discretion of the team member merging
|
||||
the change. We do not support rebase merges due to `allchange` being unable to
|
||||
handle them. When merging make sure to leave the default commit title, or
|
||||
at least leave the PR number at the end in brackets like by default.
|
||||
When stacking pull requests, you may wish to do the following:
|
||||
|
||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
||||
|
||||
|
||||
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
[](https://www.npmjs.com/package/matrix-js-sdk)
|
||||

|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
|
||||
@@ -16,7 +24,7 @@ attached to ``window`` through which you can access the SDK. See below for how t
|
||||
include libolm to enable end-to-end-encryption.
|
||||
|
||||
The browser bundle supports recent versions of browsers. Typically this is ES2015
|
||||
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
[browserlists](https://github.com/browserslist/browserslist).
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
@@ -26,11 +34,11 @@ In Node.js
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
|
||||
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
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``
|
||||
@@ -307,7 +315,7 @@ 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.
|
||||
|
||||
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
|
||||
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("http://matrix.org");
|
||||
var client = matrixcs.createClient("https://matrix.org");
|
||||
client.publicRooms(function (err, data) {
|
||||
if (err) {
|
||||
console.error("err %s", JSON.stringify(err));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<html>
|
||||
<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>
|
||||
|
||||
@@ -341,7 +341,7 @@ function printLine(event) {
|
||||
|
||||
var maxNameWidth = 15;
|
||||
if (name.length > maxNameWidth) {
|
||||
name = name.substr(0, maxNameWidth-1) + "\u2026";
|
||||
name = name.slice(0, maxNameWidth-1) + "\u2026";
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.message") {
|
||||
@@ -398,7 +398,7 @@ function print(str, formatter) {
|
||||
|
||||
function fixWidth(str, len) {
|
||||
if (str.length > len) {
|
||||
return str.substr(0, len-2) + "\u2026";
|
||||
return str.substring(0, len-2) + "\u2026";
|
||||
}
|
||||
else if (str.length < len) {
|
||||
return str + new Array(len - str.length).join(" ");
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
console.log("Loading browser sdk");
|
||||
var BASE_URL = "https://matrix.org";
|
||||
var TOKEN = "accesstokengoeshere";
|
||||
var USER_ID = "@username:localhost";
|
||||
var ROOM_ID = "!room:id";
|
||||
const BASE_URL = "https://matrix.org";
|
||||
const TOKEN = "accesstokengoeshere";
|
||||
const USER_ID = "@username:localhost";
|
||||
const ROOM_ID = "!room:id";
|
||||
const DEVICE_ID = "some_device_id";
|
||||
|
||||
|
||||
var client = matrixcs.createClient({
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
accessToken: TOKEN,
|
||||
userId: USER_ID
|
||||
userId: USER_ID,
|
||||
deviceId: DEVICE_ID
|
||||
});
|
||||
var call;
|
||||
let call;
|
||||
|
||||
function disableButtons(place, answer, hangup) {
|
||||
document.getElementById("hangup").disabled = hangup;
|
||||
@@ -19,7 +20,7 @@ function disableButtons(place, answer, hangup) {
|
||||
}
|
||||
|
||||
function addListeners(call) {
|
||||
var lastError = "";
|
||||
let lastError = "";
|
||||
call.on("hangup", function() {
|
||||
disableButtons(false, true, true);
|
||||
document.getElementById("result").innerHTML = (
|
||||
|
||||
+34
-21
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "12.3.0-rc.1",
|
||||
"version": "19.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
@@ -9,13 +12,13 @@
|
||||
"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 --emitDeclarationOnly",
|
||||
"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": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 7 src spec",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
@@ -53,10 +56,11 @@
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"p-retry": "^4.5.0",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
@@ -74,32 +78,35 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/jest": "^28.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"allchange": "github:matrix-org/allchange",
|
||||
"babel-jest": "^26.6.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^28.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint": "8.19.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.5.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^28.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-mock-request": "^2.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
@@ -110,8 +117,14 @@
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
]
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "jest-sonar-reporter"
|
||||
},
|
||||
"jestSonar": {
|
||||
"reportPath": "coverage",
|
||||
"sonar56x": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+15
-27
@@ -29,7 +29,7 @@ fi
|
||||
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
|
||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
@@ -37,7 +37,6 @@ $USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
-n: skip publish to NPM
|
||||
EOF
|
||||
}
|
||||
@@ -60,7 +59,6 @@ if ! git diff-files --quiet; then
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
skip_npm=
|
||||
changelog_file="CHANGELOG.md"
|
||||
expected_npm_user="matrixdotorg"
|
||||
@@ -76,9 +74,6 @@ while getopts hc:u:xzn f; do
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
@@ -128,12 +123,12 @@ if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
fi
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
# If we're on any branch starting with 'release', we don't create
|
||||
# a separate release branch (this allows us to use the same
|
||||
# We might already be on the release branch, in which case, yay
|
||||
# If we're on any branch starting with 'release', or the staging branch
|
||||
# we don't create a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* ]]; then
|
||||
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
@@ -191,7 +186,10 @@ git commit package.json $pkglock -m "$tag"
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
|
||||
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
|
||||
if [ "$?" -eq 0 ]; then
|
||||
signing_id=$result
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -252,6 +250,12 @@ if [ -n "$signing_id" ]; then
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# This uses git archive which seems to be what github uses. Specifically,
|
||||
# the header fields are set in the same way: same file mode, uid & gid
|
||||
# both zero and mtime set to the timestamp of the commit that the tag
|
||||
# references. Also note that this puts the commit into the tar headers
|
||||
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
@@ -317,22 +321,6 @@ if [ -z "$skip_npm" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
yarn gendoc
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -a ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":dependencyDashboardApproval"
|
||||
],
|
||||
"labels": ["T-Task", "Dependencies"],
|
||||
"lockFileMaintenance": { "enabled": true },
|
||||
"groupName": "all",
|
||||
"packageRules": [{
|
||||
"matchFiles": ["package.json"],
|
||||
"rangeStrategy": "update-lockfile"
|
||||
}],
|
||||
"platformAutomerge": true,
|
||||
"automerge": true,
|
||||
"automergeType": "pr"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
sonar.projectKey=matrix-js-sdk
|
||||
sonar.organization=matrix-org
|
||||
|
||||
# Encoding of the source code. Default is default system encoding
|
||||
#sonar.sourceEncoding=UTF-8
|
||||
|
||||
sonar.sources=src
|
||||
sonar.tests=spec
|
||||
sonar.exclusions=docs,examples,git-hooks
|
||||
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/test-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
@@ -1,236 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
import { WebStorageSessionStore } from "../src/store/session/webstorage";
|
||||
import { syncPromise } from "./test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} userId
|
||||
* @param {string} deviceId
|
||||
* @param {string} accessToken
|
||||
*
|
||||
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
|
||||
* session store. If undefined, we will create a MockStorageApi.
|
||||
* @param {object} options additional options to pass to the client
|
||||
*/
|
||||
export function TestClient(
|
||||
userId, deviceId, accessToken, sessionStoreBackend, options,
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new MockStorageApi();
|
||||
}
|
||||
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
options = Object.assign({
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: sessionStore,
|
||||
request: this.httpBackend.requestFn,
|
||||
}, options);
|
||||
if (!options.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
options.cryptoStore = this.cryptoStore;
|
||||
}
|
||||
this.client = createClient(options);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
this.callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
return 'TestClient[' + this.userId + ']';
|
||||
};
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
|
||||
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: 'detached',
|
||||
});
|
||||
|
||||
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
|
||||
*/
|
||||
TestClient.prototype.stop = function() {
|
||||
this.client.stopClient();
|
||||
return this.httpBackend.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
*/
|
||||
TestClient.prototype.expectDeviceKeyUpload = function() {
|
||||
const self = this;
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(self + ': received device keys');
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
|
||||
|
||||
self.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
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,
|
||||
} };
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
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);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
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) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query device keys.
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} response response to the query.
|
||||
*/
|
||||
TestClient.prototype.expectKeyQuery = function(response) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, (path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual(
|
||||
[],
|
||||
"Expected key query for " + userId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getDeviceKey = function() {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getSigningKey = function() {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @returns {Promise} promise which completes once the sync has been flushed
|
||||
*/
|
||||
TestClient.prototype.flushSync = function() {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
|
||||
TestClient.prototype.isFallbackICEServerAllowed = function() {
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
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 { 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 { IHttpOpts } from "../src/http-api";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*/
|
||||
export class TestClient {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys: IDeviceKeys;
|
||||
public oneTimeKeys: Record<string, IOneTimeKey>;
|
||||
|
||||
constructor(
|
||||
public readonly userId?: string,
|
||||
public readonly deviceId?: string,
|
||||
accessToken?: string,
|
||||
sessionStoreBackend?: Storage,
|
||||
options?: Partial<ICreateClientOpts>,
|
||||
) {
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new MockStorageApi();
|
||||
}
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
const fullOptions: ICreateClientOpts = {
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
request: this.httpBackend.requestFn as IHttpOpts["request"],
|
||||
...options,
|
||||
};
|
||||
if (!fullOptions.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
fullOptions.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
}
|
||||
this.client = createClient(fullOptions);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
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, {});
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
|
||||
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
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
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.client.stopClient();
|
||||
await this.httpBackend.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
*/
|
||||
public expectDeviceKeyUpload() {
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(this + ': received device keys');
|
||||
// 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 } };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
}
|
||||
|
||||
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,
|
||||
} };
|
||||
});
|
||||
|
||||
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);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
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) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query device keys.
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} 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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query key backups for a particular session
|
||||
*/
|
||||
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
|
||||
this.httpBackend.when('GET', encodeUri("/room_keys/keys/$roomId/$sessionId", {
|
||||
$roomId: roomId,
|
||||
$sessionId: sessionId,
|
||||
})).respond(status, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*/
|
||||
public flushSync(): Promise<void> {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
}
|
||||
|
||||
public isFallbackICEServerAllowed(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
}
|
||||
@@ -17,57 +17,37 @@ limitations under the License.
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
/* global matrixcs */
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
|
||||
async function createTestClient() {
|
||||
const sessionStoreBackend = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||
const httpBackend = new MockHttpBackend();
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
|
||||
|
||||
const options = {
|
||||
baseUrl: "http://" + USER_ID + ".test.server",
|
||||
userId: USER_ID,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
deviceId: DEVICE_ID,
|
||||
sessionStore: sessionStore,
|
||||
request: httpBackend.requestFn,
|
||||
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
|
||||
};
|
||||
|
||||
const client = matrixcs.createClient(options);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
return { client, httpBackend };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
({ client, httpBackend } = await createTestClient());
|
||||
await client.startClient();
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
await httpBackend.stop();
|
||||
httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", async function() {
|
||||
it("Sync", function() {
|
||||
const event = utils.mkMembership({
|
||||
room: ROOM_ID,
|
||||
mship: "join",
|
||||
@@ -91,10 +71,8 @@ describe("Browserify Test", function() {
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await Promise.race([
|
||||
Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
]),
|
||||
return Promise.race([
|
||||
httpBackend.flushAllExpected(),
|
||||
new Promise((_, reject) => {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../TestClient';
|
||||
import * as testUtils from '../test-utils';
|
||||
import * as testUtils from '../test-utils/test-utils';
|
||||
import { logger } from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
@@ -136,138 +136,137 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("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.
|
||||
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('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
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.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
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.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,
|
||||
);
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
// 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.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,
|
||||
);
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
// 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.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}).timeout(3000);
|
||||
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", () => {
|
||||
@@ -288,7 +287,7 @@ describe("DeviceList management:", function() {
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
@@ -324,7 +323,7 @@ describe("DeviceList management:", function() {
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
@@ -360,7 +359,7 @@ describe("DeviceList management:", function() {
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
@@ -381,7 +380,7 @@ describe("DeviceList management:", function() {
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
@@ -1,757 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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 consists of a set of integration tests which try to simulate
|
||||
* communication via an Olm-encrypted room between two users, Alice and Bob.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `megolm.spec.js`.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
|
||||
let aliTestClient;
|
||||
const roomId = "!room:localhost";
|
||||
const aliUserId = "@ali:localhost";
|
||||
const aliDeviceId = "zxcvb";
|
||||
const aliAccessToken = "aseukfgwef";
|
||||
let bobTestClient;
|
||||
const bobUserId = "@bob:localhost";
|
||||
const bobDeviceId = "bvcxz";
|
||||
const bobAccessToken = "fewgfkuesa";
|
||||
let aliMessages;
|
||||
let bobMessages;
|
||||
|
||||
function bobUploadsDeviceKeys() {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
return Promise.all([
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flush(),
|
||||
]).then(() => {
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will query bobs keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliQueryKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.deviceKeys).toBeTruthy();
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual(
|
||||
[],
|
||||
"Expected Alice to key query for " + bobUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return aliTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that bob will query alis keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} which resolves once the http request has completed.
|
||||
*/
|
||||
function expectBobQueryKeys() {
|
||||
// can't query keys before ali has uploaded them
|
||||
expect(aliTestClient.deviceKeys).toBeTruthy();
|
||||
|
||||
const aliKeys = {};
|
||||
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
|
||||
logger.log("query result will be", aliKeys);
|
||||
|
||||
bobTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual(
|
||||
[],
|
||||
"Expected Bob to key query for " + aliUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return bobTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function expectAliClaimKeys() {
|
||||
return bobTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return { one_time_keys: result };
|
||||
});
|
||||
}).then(() => {
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
// time, and make sure the claim actually happens rather than ploughing on
|
||||
// confusingly.
|
||||
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
|
||||
expect(r).toEqual(1, "Ali did not claim Bob's keys");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
|
||||
const p1 = aliTestClient.client.downloadKeys([bobUserId]).then(function() {
|
||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
}).then((devices) => {
|
||||
expect(devices.length).toEqual(1);
|
||||
expect(devices[0].deviceId).toEqual("bvcxz");
|
||||
});
|
||||
const p2 = expectAliQueryKeys();
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
return Promise.all([p1, p2]).then(() => {
|
||||
return aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function aliEnablesEncryption() {
|
||||
return aliTestClient.client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(aliTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
function bobEnablesEncryption() {
|
||||
return bobTestClient.client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(bobTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function aliSendsFirstMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function aliSendsMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function bobSendsReplyMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectAliSendMessageRequest() {
|
||||
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectBobSendMessageRequest() {
|
||||
return expectSendMessageRequest(bobTestClient.httpBackend).then(function(content) {
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(client) {
|
||||
return client.sendMessage(
|
||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||
);
|
||||
}
|
||||
|
||||
function expectSendMessageRequest(httpBackend) {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
return httpBackend.flush(path, 1).then(() => prom);
|
||||
}
|
||||
|
||||
function aliRecvMessage() {
|
||||
const message = bobMessages.shift();
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage() {
|
||||
const message = aliMessages.shift();
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function recvMessage(httpBackend, client, sender, message) {
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
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((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
logger.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
resolve(event);
|
||||
};
|
||||
client.on("event", onEvent);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
|
||||
return eventPromise.then((event) => {
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
// it may still be being decrypted
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function firstSync(testClient) {
|
||||
// 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: [],
|
||||
},
|
||||
};
|
||||
|
||||
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return testClient.flushSync();
|
||||
}
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
||||
await bobTestClient.client.initCrypto();
|
||||
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
||||
});
|
||||
|
||||
it("Bob uploads device keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys);
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs device keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(aliDownloadsKeys);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(function() {
|
||||
// tamper bob's keys
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
expectAliQueryKeys(),
|
||||
]);
|
||||
}).then(function() {
|
||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", function() {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
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',
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
return Promise.all([
|
||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||
]);
|
||||
}).then(([bobDevices, eveDevices]) => {
|
||||
// should get an empty list
|
||||
expect(bobDevices).toEqual([]);
|
||||
expect(eveDevices).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", function() {
|
||||
const bobDeviceKeys = {
|
||||
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',
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Bob starts his client and uploads device keys and one-time keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => bobTestClient.awaitOneTimeKeyUpload())
|
||||
.then((keys) => {
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali sends a message", function() {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage);
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage);
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function() {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(function() {
|
||||
const message = aliMessages.shift();
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
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((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
logger.log(bobUserId + " received event",
|
||||
event);
|
||||
resolve(event);
|
||||
};
|
||||
bobTestClient.client.once("event", onEvent);
|
||||
});
|
||||
|
||||
bobTestClient.httpBackend.flush();
|
||||
return eventPromise;
|
||||
}).then((event) => {
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
// it may still be being decrypted
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function() {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliDownloadsKeys)
|
||||
.then(function() {
|
||||
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({});
|
||||
});
|
||||
return Promise.all([p1, p2]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function() {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobRecvMessage);
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(() => firstSync(bobTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage)
|
||||
.then(bobEnablesEncryption)
|
||||
.then(bobSendsReplyMessage).then(function(ciphertext) {
|
||||
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
|
||||
}).then(aliRecvMessage);
|
||||
});
|
||||
|
||||
it("Ali does a key query when encryption is enabled", function() {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(() => {
|
||||
const syncData = {
|
||||
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',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, syncData);
|
||||
return aliTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(() => {
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[bobUserId]: {},
|
||||
},
|
||||
});
|
||||
return aliTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
});
|
||||
|
||||
it("Upload new oneTimeKeys based on a /sync request - no count-asking", function() {
|
||||
// Send a response which causes a key upload
|
||||
const httpBackend = aliTestClient.httpBackend;
|
||||
const syncDataEmpty = {
|
||||
next_batch: "a",
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// enqueue expectations:
|
||||
// * Sync with empty one_time_keys => upload keys
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
logger.log(aliTestClient + ': starting');
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
||||
|
||||
aliTestClient.client.startClient({});
|
||||
|
||||
return httpBackend.flushAllExpected().then(() => {
|
||||
logger.log(aliTestClient + ': started');
|
||||
});
|
||||
})
|
||||
.then(() => httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length)
|
||||
.toBeGreaterThanOrEqual(1);
|
||||
logger.log('received %i one-time keys',
|
||||
Object.keys(content.one_time_keys).length);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
}))
|
||||
.then(() => httpBackend.flushAllExpected());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,676 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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 consists of a set of integration tests which try to simulate
|
||||
* communication via an Olm-encrypted room between two users, Alice and Bob.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `megolm.spec.js`.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
const aliUserId = "@ali:localhost";
|
||||
const aliDeviceId = "zxcvb";
|
||||
const aliAccessToken = "aseukfgwef";
|
||||
let bobTestClient: TestClient;
|
||||
const bobUserId = "@bob:localhost";
|
||||
const bobDeviceId = "bvcxz";
|
||||
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;
|
||||
}
|
||||
|
||||
async function bobUploadsDeviceKeys(): Promise<void> {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
await Promise.all([
|
||||
bobTestClient.client.uploadKeys(),
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
expect(content.device_keys[uploader.userId]).toEqual([]);
|
||||
const result = {};
|
||||
result[uploader.userId] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
|
||||
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.
|
||||
*/
|
||||
async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(_path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return { one_time_keys: result };
|
||||
});
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
// time, and make sure the claim actually happens rather than ploughing on
|
||||
// confusingly.
|
||||
const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500);
|
||||
expect(r).toEqual(1);
|
||||
}
|
||||
|
||||
async function aliDownloadsKeys(): Promise<void> {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
|
||||
const p1 = async () => {
|
||||
await aliTestClient.client.downloadKeys([bobUserId]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
expect(devices.length).toEqual(1);
|
||||
expect(devices[0].deviceId).toEqual("bvcxz");
|
||||
};
|
||||
const p2 = expectAliQueryKeys;
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
await Promise.all([p1(), p2()]);
|
||||
await aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
// @ts-ignore - protected
|
||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
});
|
||||
}
|
||||
|
||||
async function clientEnablesEncryption(client: MatrixClient): Promise<void> {
|
||||
await client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
});
|
||||
expect(client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
}
|
||||
const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client);
|
||||
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.
|
||||
*/
|
||||
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),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async function aliSendsMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
|
||||
aliMessages.push(content);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
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((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
await httpBackend.flush(path, 1);
|
||||
return prom;
|
||||
}
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift();
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift();
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
async function recvMessage(
|
||||
httpBackend: TestClient["httpBackend"],
|
||||
client: MatrixClient,
|
||||
sender: string,
|
||||
message: IContent,
|
||||
): Promise<void> {
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
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) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
logger.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
resolve(event);
|
||||
};
|
||||
client.on(ClientEvent.Event, onEvent);
|
||||
});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
const preDecryptionEvent = await eventPromise;
|
||||
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||
// it may still be being decrypted
|
||||
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toMatchObject({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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: [],
|
||||
},
|
||||
};
|
||||
|
||||
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return testClient.flushSync();
|
||||
}
|
||||
|
||||
describe("MatrixClient crypto", () => {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
||||
await bobTestClient.client.initCrypto();
|
||||
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
||||
});
|
||||
|
||||
it("Bob uploads device keys", bobUploadsDeviceKeys);
|
||||
|
||||
it("Ali downloads Bobs device keys", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
await aliDownloadsKeys();
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
// tamper bob's keys
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
expectAliQueryKeys(),
|
||||
]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", async () => {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
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',
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
const [bobDevices, eveDevices] = await Promise.all([
|
||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||
]);
|
||||
// should get an empty list
|
||||
expect(bobDevices).toEqual([]);
|
||||
expect(eveDevices).toEqual([]);
|
||||
});
|
||||
|
||||
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',
|
||||
keys: {
|
||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Bob starts his client and uploads device keys and one-time keys", async () => {
|
||||
await bobTestClient.start();
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
|
||||
it("Ali sends a message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
});
|
||||
|
||||
it("Bob receives a message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
const message = aliMessages.shift();
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
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) {
|
||||
logger.log(bobUserId + " received event", event);
|
||||
resolve(event);
|
||||
};
|
||||
bobTestClient.client.once(ClientEvent.Event, onEvent);
|
||||
});
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
const preDecryptionEvent = await eventPromise;
|
||||
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||
// it may still be being decrypted
|
||||
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
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({});
|
||||
});
|
||||
await Promise.all([p1, p2]);
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
await aliSendsMessage();
|
||||
await bobRecvMessage();
|
||||
});
|
||||
|
||||
it("Bob replies to the message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
await aliRecvMessage();
|
||||
});
|
||||
|
||||
it("Ali does a key query when encryption is enabled", async () => {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
const syncData = {
|
||||
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',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, syncData);
|
||||
await aliTestClient.httpBackend.flush('/sync', 1);
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[bobUserId]: {},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
await aliTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => {
|
||||
// Send a response which causes a key upload
|
||||
const httpBackend = aliTestClient.httpBackend;
|
||||
const syncDataEmpty = {
|
||||
next_batch: "a",
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// enqueue expectations:
|
||||
// * Sync with empty one_time_keys => upload keys
|
||||
|
||||
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" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// 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) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1);
|
||||
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
@@ -11,6 +11,7 @@ describe("MatrixClient events", function() {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
+415
-43
@@ -1,7 +1,24 @@
|
||||
import * as utils from "../test-utils";
|
||||
import { EventTimeline } from "../../src/matrix";
|
||||
/*
|
||||
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 * as utils from "../test-utils/test-utils";
|
||||
import { ClientEvent, EventTimeline, Filter, IEvent, MatrixClient, MatrixEvent, Room } from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -9,8 +26,14 @@ const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
|
||||
const withoutRoomId = (e: Partial<IEvent>): Partial<IEvent> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { room_id: _, ...copy } = e;
|
||||
return copy;
|
||||
};
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
room: roomId, mship: "join", user: userId, name: userName, event: false,
|
||||
});
|
||||
|
||||
const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
@@ -18,34 +41,37 @@ const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
event: false,
|
||||
});
|
||||
|
||||
const INITIAL_SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello",
|
||||
user: otherUserId, msg: "hello", event: false,
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
withoutRoomId(ROOM_NAME_EVENT),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
mship: "join",
|
||||
user: otherUserId, name: "Bob",
|
||||
event: false,
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
withoutRoomId(USER_MEMBERSHIP_EVENT),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
type: "m.room.create", user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
event: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -56,21 +82,72 @@ const INITIAL_SYNC_DATA = {
|
||||
|
||||
const EVENTS = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "we",
|
||||
room: roomId, user: userId, msg: "we", event: false,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "could",
|
||||
room: roomId, user: userId, msg: "could", event: false,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "be",
|
||||
room: roomId, user: userId, msg: "be", event: false,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "heroes",
|
||||
room: roomId, user: userId, msg: "heroes", event: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const THREAD_ROOT = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread root",
|
||||
"msgtype": "m.text",
|
||||
},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"io.element.thread": {
|
||||
//"latest_event": undefined,
|
||||
"count": 1,
|
||||
"current_user_participated": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
event: false,
|
||||
});
|
||||
|
||||
const THREAD_REPLY = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: false,
|
||||
});
|
||||
|
||||
THREAD_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD_REPLY;
|
||||
|
||||
const SYNC_THREAD_ROOT = withoutRoomId(THREAD_ROOT);
|
||||
const SYNC_THREAD_REPLY = withoutRoomId(THREAD_REPLY);
|
||||
SYNC_THREAD_ROOT.unsigned = {
|
||||
"m.relations": {
|
||||
"io.element.thread": {
|
||||
"latest_event": SYNC_THREAD_REPLY,
|
||||
"count": 1,
|
||||
"current_user_participated": true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
function startClient(httpBackend, client) {
|
||||
function startClient(httpBackend: TestClient["httpBackend"], client: MatrixClient) {
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
@@ -78,8 +155,8 @@ function startClient(httpBackend, client) {
|
||||
client.startClient();
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
const prom = new Promise((resolve) => {
|
||||
client.on("sync", function(state) {
|
||||
const prom = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, function(state) {
|
||||
logger.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
@@ -95,8 +172,8 @@ function startClient(httpBackend, client) {
|
||||
}
|
||||
|
||||
describe("getEventTimeline support", function() {
|
||||
let httpBackend;
|
||||
let client;
|
||||
let httpBackend: TestClient["httpBackend"];
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(function() {
|
||||
const testClient = new TestClient(userId, "DEVICE", accessToken);
|
||||
@@ -115,9 +192,7 @@ describe("getEventTimeline support", function() {
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toThrow();
|
||||
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,17 +210,13 @@ describe("getEventTimeline support", function() {
|
||||
return startClient(httpBackend, client).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).not.toThrow();
|
||||
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function() {
|
||||
it("scrollback should be able to scroll back to before a gappy /sync", function() {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
|
||||
let room;
|
||||
let room: Room;
|
||||
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
@@ -157,7 +228,7 @@ describe("getEventTimeline support", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
withoutRoomId(EVENTS[0]),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
@@ -173,7 +244,7 @@ describe("getEventTimeline support", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[1],
|
||||
withoutRoomId(EVENTS[1]),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "f_1_2",
|
||||
@@ -208,8 +279,8 @@ describe("getEventTimeline support", function() {
|
||||
});
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let client: MatrixClient;
|
||||
let httpBackend: TestClient["httpBackend"];
|
||||
|
||||
beforeEach(function() {
|
||||
const testClient = new TestClient(
|
||||
@@ -228,6 +299,7 @@ describe("MatrixClient event timelines", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
Thread.setServerSideSupport(false, false);
|
||||
});
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
@@ -275,7 +347,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
withoutRoomId(EVENTS[0]),
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
@@ -310,7 +382,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[3],
|
||||
withoutRoomId(EVENTS[3]),
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
@@ -333,7 +405,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
const prom = new Promise((resolve, reject) => {
|
||||
client.on("sync", function() {
|
||||
client.on(ClientEvent.Sync, function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
@@ -354,8 +426,7 @@ describe("MatrixClient event timelines", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should join timelines where they overlap a previous /context",
|
||||
function() {
|
||||
it("should join timelines where they overlap a previous /context", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
@@ -477,6 +548,237 @@ describe("MatrixClient event timelines", function() {
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(true, false);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId);
|
||||
const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false);
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: THREAD_REPLY,
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
|
||||
.respond(200, function() {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
|
||||
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
const timeline = await timelinePromise;
|
||||
|
||||
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy();
|
||||
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(true, false);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId);
|
||||
const threadRoot = new MatrixEvent(THREAD_ROOT);
|
||||
const thread = room.createThread(THREAD_ROOT.event_id, threadRoot, [threadRoot], false);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
const [timeline] = await Promise.all([
|
||||
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
|
||||
expect(timeline).not.toBe(thread.liveTimeline);
|
||||
expect(timelineSet.getTimelines()).toContain(timeline);
|
||||
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(true, false);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId);
|
||||
const threadRoot = new MatrixEvent(THREAD_ROOT);
|
||||
const thread = room.createThread(THREAD_ROOT.event_id, threadRoot, [threadRoot], false);
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id)).resolves.toBeUndefined(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(true, false);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: THREAD_REPLY,
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id)).resolves.toBeUndefined(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should should add lazy loading filter when requested", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.lazyLoadMembers = true;
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
const req = httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id));
|
||||
req.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
req.check((request) => {
|
||||
expect(request.queryParams.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER));
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLatestTimeline", function() {
|
||||
it("should create a new timeline for new events", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
const latestMessageId = 'event1:bar';
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
chunk: [{
|
||||
event_id: latestMessageId,
|
||||
}],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", `/rooms/!foo%3Abar/context/${encodeURIComponent(latestMessageId)}`)
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1], EVENTS[0]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
state: [
|
||||
ROOM_NAME_EVENT,
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
],
|
||||
end: "end_token",
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
client.getLatestTimeline(timelineSet).then(function(tl) {
|
||||
// Instead of this assertion logic, we could just add a spy
|
||||
// for `getEventTimeline` and make sure it's called with the
|
||||
// correct parameters. This doesn't feel too bad to make sure
|
||||
// `getLatestTimeline` is doing the right thing though.
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
|
||||
expect(tl.getEvents()[i].sender.name).toEqual(userName);
|
||||
}
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should throw error when /messages does not return a message", () => {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.respond(200, () => {
|
||||
return {
|
||||
chunk: [
|
||||
// No messages to return
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getLatestTimeline(timelineSet)).rejects.toThrow(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline", function() {
|
||||
@@ -502,7 +804,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual(30);
|
||||
expect(params.limit).toEqual("30");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
@@ -553,7 +855,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual(20);
|
||||
expect(params.limit).toEqual("20");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
@@ -605,7 +907,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
withoutRoomId(event),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
@@ -683,11 +985,10 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
const event = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: otherUserId,
|
||||
mship: "join", user: otherUserId,
|
||||
});
|
||||
const redaction = utils.mkEvent({
|
||||
type: "m.room.redaction",
|
||||
room_id: roomId,
|
||||
sender: otherUserId,
|
||||
content: {},
|
||||
});
|
||||
@@ -729,7 +1030,7 @@ describe("MatrixClient event timelines", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "world",
|
||||
user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
@@ -748,4 +1049,75 @@ describe("MatrixClient event timelines", function() {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should re-insert room IDs for bundled thread relation events", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(true, false);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_ROOT,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
const room = client.getRoom(roomId);
|
||||
const thread = room.getThread(THREAD_ROOT.event_id);
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id))
|
||||
.respond(200, {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
state: [],
|
||||
end: "end_token",
|
||||
});
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
await Promise.all([
|
||||
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_REPLY,
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
@@ -7,7 +8,6 @@ import { MatrixError } from "../../src/http-api";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
@@ -64,6 +64,7 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
describe("without opts.store", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
@@ -104,10 +105,12 @@ describe("MatrixClient opts", function() {
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
);
|
||||
});
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await client.startClient();
|
||||
client.startClient();
|
||||
await httpBackend.flush("/versions", 1);
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([
|
||||
@@ -121,6 +124,7 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
describe("without opts.scheduler", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
@@ -132,6 +136,10 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
|
||||
+33
-11
@@ -1,16 +1,32 @@
|
||||
import { EventStatus } from "../../src/matrix";
|
||||
/*
|
||||
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 { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: TestClient["httpBackend"] = null;
|
||||
let scheduler;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let room;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = new MatrixScheduler();
|
||||
@@ -23,7 +39,7 @@ describe("MatrixClient retrying", function() {
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
room = new Room(roomId);
|
||||
room = new Room(roomId, client, userId);
|
||||
client.store.storeRoom(room);
|
||||
});
|
||||
|
||||
@@ -50,17 +66,23 @@ describe("MatrixClient retrying", 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, "m1").then(function(ev) {
|
||||
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');
|
||||
}, (e) => {
|
||||
}, () => {
|
||||
// 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, "m2");
|
||||
client.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
const tl = room.getLiveTimeline().getEvents();
|
||||
@@ -72,7 +94,7 @@ 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(rq) {
|
||||
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
@@ -88,8 +110,8 @@ describe("MatrixClient retrying", function() {
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventStatus } from "../../src/models/event";
|
||||
import { RoomEvent } from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
@@ -96,7 +97,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(async function() {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
@@ -109,6 +110,7 @@ describe("MatrixClient room timelines", function() {
|
||||
client = testClient.client;
|
||||
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
@@ -116,9 +118,10 @@ describe("MatrixClient room timelines", function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
return httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
await httpBackend.flush("/versions");
|
||||
await httpBackend.flush("/pushrules");
|
||||
await httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -551,6 +554,7 @@ describe("MatrixClient room timelines", function() {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/versions", 1),
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
@@ -576,7 +580,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit a 'Room.timelineReset' event", function() {
|
||||
it("should emit a `RoomEvent.TimelineReset` event when the sync response is `limited`", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
@@ -605,4 +609,271 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Refresh live timeline', () => {
|
||||
const initialSyncEventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
|
||||
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
|
||||
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
|
||||
const contextResponse = {
|
||||
start: "start_token",
|
||||
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
|
||||
event: initialSyncEventData[2],
|
||||
events_after: [],
|
||||
state: [
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
],
|
||||
end: "end_token",
|
||||
};
|
||||
|
||||
let room;
|
||||
beforeEach(async () => {
|
||||
setNextSyncData(initialSyncEventData);
|
||||
|
||||
// Create a room from the sync
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
room = client.getRoom(roomId);
|
||||
expect(room).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should clear and refresh messages in timeline', async () => {
|
||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||
// to construct a new timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
|
||||
return contextResponse;
|
||||
});
|
||||
|
||||
// Refresh the timeline.
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure the message are visible
|
||||
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
|
||||
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
|
||||
expect(resultantEventIdsInTimeline).toEqual([
|
||||
initialSyncEventData[0].event_id,
|
||||
initialSyncEventData[1].event_id,
|
||||
initialSyncEventData[2].event_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Perfectly merges timelines if a sync finishes while refreshing the timeline', async () => {
|
||||
// `/context` request for `refreshLiveTimeline()` ->
|
||||
// `getEventTimeline()` to construct a new timeline from.
|
||||
//
|
||||
// We only resolve this request after we detect that the timeline
|
||||
// was reset(when it goes blank) and force a sync to happen in the
|
||||
// middle of all of this refresh timeline logic. We want to make
|
||||
// sure the sync pagination still works as expected after messing
|
||||
// the refresh timline logic messes with the pagination tokens.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
.respond(200, () => {
|
||||
// Now finally return and make the `/context` request respond
|
||||
return contextResponse;
|
||||
});
|
||||
|
||||
// Wait for the timeline to reset(when it goes blank) which means
|
||||
// it's in the middle of the refrsh logic right before the
|
||||
// `getEventTimeline()` -> `/context`. Then simulate a racey `/sync`
|
||||
// to happen in the middle of all of this refresh timeline logic. We
|
||||
// want to make sure the sync pagination still works as expected
|
||||
// after messing the refresh timline logic messes with the
|
||||
// pagination tokens.
|
||||
//
|
||||
// We define this here so the event listener is in place before we
|
||||
// call `room.refreshLiveTimeline()`.
|
||||
const racingSyncEventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
|
||||
let eventFired = false;
|
||||
// Throw a more descriptive error if this part of the test times out.
|
||||
const failTimeout = setTimeout(() => {
|
||||
if (eventFired) {
|
||||
reject(new Error(
|
||||
'TestError: `RoomEvent.TimelineReset` fired but we timed out trying to make' +
|
||||
'a `/sync` happen in time.',
|
||||
));
|
||||
} else {
|
||||
reject(new Error(
|
||||
'TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire.',
|
||||
));
|
||||
}
|
||||
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
|
||||
|
||||
room.on(RoomEvent.TimelineReset, async () => {
|
||||
try {
|
||||
eventFired = true;
|
||||
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.getUnfilteredTimelineSet().getLiveTimeline().getEvents().length).toEqual(0);
|
||||
|
||||
// Then make a `/sync` happen by sending a message and seeing that it
|
||||
// shows up (simulate a /sync naturally racing with us).
|
||||
setNextSyncData(racingSyncEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client, 1),
|
||||
]);
|
||||
// Make sure the timeline has the racey sync data
|
||||
const afterRaceySyncTimelineEvents = room
|
||||
.getUnfilteredTimelineSet()
|
||||
.getLiveTimeline()
|
||||
.getEvents();
|
||||
const afterRaceySyncTimelineEventIds = afterRaceySyncTimelineEvents
|
||||
.map((event) => event.getId());
|
||||
expect(afterRaceySyncTimelineEventIds).toEqual([
|
||||
racingSyncEventData[0].event_id,
|
||||
]);
|
||||
|
||||
clearTimeout(failTimeout);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Refresh the timeline. Just start the function, we will wait for
|
||||
// it to finish after the racey sync.
|
||||
const refreshLiveTimelinePromise = room.refreshLiveTimeline();
|
||||
|
||||
await waitForRaceySyncAfterResetPromise;
|
||||
|
||||
await Promise.all([
|
||||
refreshLiveTimelinePromise,
|
||||
// Then flush the remaining `/context` to left the refresh logic complete
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure sync pagination still works by seeing a new message show up
|
||||
// after refreshing the timeline.
|
||||
const afterRefreshEventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
]);
|
||||
|
||||
// Make sure the timeline includes the the events from the `/sync`
|
||||
// that raced and beat us in the middle of everything and the
|
||||
// `/sync` after the refresh. Since the `/sync` beat us to create
|
||||
// the timeline, `initialSyncEventData` won't be visible unless we
|
||||
// paginate backwards with `/messages`.
|
||||
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
|
||||
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
|
||||
expect(resultantEventIdsInTimeline).toEqual([
|
||||
racingSyncEventData[0].event_id,
|
||||
afterRefreshEventData[0].event_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
|
||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||
// to construct a new timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
.respond(500, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
|
||||
return {
|
||||
errcode: 'TEST_FAKE_ERROR',
|
||||
error: 'We purposely intercepted this /context request to make it fail ' +
|
||||
'in order to test whether the refresh timeline code is resilient',
|
||||
};
|
||||
});
|
||||
|
||||
// Refresh the timeline and expect it to fail
|
||||
const settledFailedRefreshPromises = await Promise.allSettled([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
// We only expect `TEST_FAKE_ERROR` here. Anything else is
|
||||
// unexpected and should fail the test.
|
||||
if (settledFailedRefreshPromises[0].status === 'fulfilled') {
|
||||
throw new Error('Expected the /context request to fail with a 500');
|
||||
} else if (settledFailedRefreshPromises[0].reason.errcode !== 'TEST_FAKE_ERROR') {
|
||||
throw settledFailedRefreshPromises[0].reason;
|
||||
}
|
||||
|
||||
// The timeline will be empty after we refresh the timeline and fail
|
||||
// to construct a new timeline.
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
|
||||
// `/messages` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` to construct a new timeline from.
|
||||
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||
.respond(200, function() {
|
||||
return {
|
||||
chunk: [{
|
||||
// The latest message in the room
|
||||
event_id: initialSyncEventData[2].event_id,
|
||||
}],
|
||||
};
|
||||
});
|
||||
// `/context` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
|
||||
// timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
|
||||
return contextResponse;
|
||||
});
|
||||
|
||||
// Refresh the timeline again but this time it should pass
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure sync pagination still works by seeing a new message show up
|
||||
// after refreshing the timeline.
|
||||
const afterRefreshEventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
]);
|
||||
|
||||
// Make sure the message are visible
|
||||
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
|
||||
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
|
||||
expect(resultantEventIdsInTimeline).toEqual([
|
||||
initialSyncEventData[0].event_id,
|
||||
initialSyncEventData[1].event_id,
|
||||
initialSyncEventData[2].event_id,
|
||||
afterRefreshEventData[0].event_id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import * as utils from "../test-utils";
|
||||
/*
|
||||
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 { EventTimeline, MatrixEvent, RoomEvent, RoomStateEvent, RoomMemberEvent } from "../../src";
|
||||
import { UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
@@ -19,6 +35,7 @@ describe("MatrixClient syncing", function() {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
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" });
|
||||
});
|
||||
@@ -59,6 +76,112 @@ describe("MatrixClient syncing", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
const inviteSyncRoomSection = {
|
||||
invite: {
|
||||
[roomId]: {
|
||||
invite_state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// Second sync: a leave (reject of some kind)
|
||||
httpBackend.when("POST", "/leave").respond(200, {});
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: {
|
||||
leave: {
|
||||
[roomId]: {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
}],
|
||||
},
|
||||
timeline: {
|
||||
limited: false,
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Third sync: another invite
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial invite
|
||||
let fires = 0;
|
||||
client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { // Room, string, string
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(oldMembership).toBeFalsy();
|
||||
|
||||
// Second fire: a leave
|
||||
client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("leave");
|
||||
expect(oldMembership).toBe("invite");
|
||||
|
||||
// Third/final fire: a second invite
|
||||
client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(oldMembership).toBe("leave");
|
||||
});
|
||||
});
|
||||
|
||||
// For maximum safety, "leave" the room after we register the handler
|
||||
client.leave(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
client.startClient();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolving invites to profile info", function() {
|
||||
@@ -176,7 +299,7 @@ describe("MatrixClient syncing", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
let latestFiredName = null;
|
||||
client.on("RoomMember.name", function(event, m) {
|
||||
client.on(RoomMemberEvent.Name, function(event, m) {
|
||||
if (m.userId === userC && m.roomId === roomOne) {
|
||||
latestFiredName = m.name;
|
||||
}
|
||||
@@ -460,6 +583,477 @@ describe("MatrixClient syncing", function() {
|
||||
xit("should update the room topic", function() {
|
||||
|
||||
});
|
||||
|
||||
describe("onMarkerStateEvent", () => {
|
||||
const normalMessageEvent = utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
});
|
||||
|
||||
it('new marker event *NOT* from the room creator in a subsequent syncs ' +
|
||||
'should *NOT* mark the timeline as needing a refresh', async () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
creator: otherUserId,
|
||||
room_version: '9',
|
||||
},
|
||||
});
|
||||
const normalFirstSync = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
normalFirstSync.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [normalMessageEvent],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
state: {
|
||||
events: [roomCreateEvent],
|
||||
},
|
||||
};
|
||||
|
||||
const nextSyncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
nextSyncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
// In subsequent syncs, a marker event in timeline
|
||||
// range should normally trigger
|
||||
// `timelineNeedsRefresh=true` but this marker isn't
|
||||
// being sent by the room creator so it has no
|
||||
// special meaning in existing room versions.
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_MSC2716_MARKER.name,
|
||||
room: roomOne,
|
||||
// The important part we're testing is here!
|
||||
// `userC` is not the room creator.
|
||||
user: userC,
|
||||
skey: "",
|
||||
content: {
|
||||
"m.insertion_id": "$abc",
|
||||
},
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
// Ensure the marker is being sent by someone who is not the room creator
|
||||
// because this is the main thing we're testing in this spec.
|
||||
const markerEvent = nextSyncData.rooms.join[roomOne].timeline.events[0];
|
||||
expect(markerEvent.sender).toBeDefined();
|
||||
expect(markerEvent.sender).not.toEqual(roomCreateEvent.sender);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]);
|
||||
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
[{
|
||||
label: 'In existing room versions (when the room creator sends the MSC2716 events)',
|
||||
roomVersion: '9',
|
||||
}, {
|
||||
label: 'In a MSC2716 supported room version',
|
||||
roomVersion: 'org.matrix.msc2716v3',
|
||||
}].forEach((testMeta) => {
|
||||
describe(testMeta.label, () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
creator: otherUserId,
|
||||
room_version: testMeta.roomVersion,
|
||||
},
|
||||
});
|
||||
|
||||
const markerEventFromRoomCreator = utils.mkEvent({
|
||||
type: UNSTABLE_MSC2716_MARKER.name, room: roomOne, user: otherUserId,
|
||||
skey: "",
|
||||
content: {
|
||||
"m.insertion_id": "$abc",
|
||||
},
|
||||
});
|
||||
|
||||
const normalFirstSync = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
normalFirstSync.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [normalMessageEvent],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
state: {
|
||||
events: [roomCreateEvent],
|
||||
},
|
||||
};
|
||||
|
||||
it('no marker event in sync response '+
|
||||
'should *NOT* mark the timeline as needing a refresh (check for a sane default)', async () => {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [normalMessageEvent],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
state: {
|
||||
events: [roomCreateEvent],
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
it('marker event already sent within timeline range when you join ' +
|
||||
'should *NOT* mark the timeline as needing a refresh (timelineWasEmpty)', async () => {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [markerEventFromRoomCreator],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
state: {
|
||||
events: [roomCreateEvent],
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
it('marker event already sent before joining (in state) ' +
|
||||
'should *NOT* mark the timeline as needing a refresh (timelineWasEmpty)', async () => {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [normalMessageEvent],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
roomCreateEvent,
|
||||
markerEventFromRoomCreator,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
it('new marker event in a subsequent syncs timeline range ' +
|
||||
'should mark the timeline as needing a refresh', async () => {
|
||||
const nextSyncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
nextSyncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
// In subsequent syncs, a marker event in timeline
|
||||
// range should trigger `timelineNeedsRefresh=true`
|
||||
markerEventFromRoomCreator,
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
const markerEventId = nextSyncData.rooms.join[roomOne].timeline.events[0].event_id;
|
||||
|
||||
// Only do the first sync
|
||||
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client.getRoom(roomOne);
|
||||
|
||||
let emitCount = 0;
|
||||
room.on(RoomEvent.HistoryImportedWithinTimeline, function(markerEvent, room) {
|
||||
expect(markerEvent.getId()).toEqual(markerEventId);
|
||||
expect(room.roomId).toEqual(roomOne);
|
||||
emitCount += 1;
|
||||
});
|
||||
|
||||
// Now do a subsequent sync with the marker event
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(true);
|
||||
// Make sure `RoomEvent.HistoryImportedWithinTimeline` was emitted
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
// Mimic a marker event being sent far back in the scroll back but since our last sync
|
||||
it('new marker event in sync state should mark the timeline as needing a refresh', async () => {
|
||||
const nextSyncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
nextSyncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello again",
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
// In subsequent syncs, a marker event in state
|
||||
// should trigger `timelineNeedsRefresh=true`
|
||||
markerEventFromRoomCreator,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]);
|
||||
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Make sure the state listeners work and events are re-emitted properly from
|
||||
// the client regardless if we reset and refresh the timeline.
|
||||
describe('state listeners and re-registered when RoomEvent.CurrentStateUpdated is fired', () => {
|
||||
const EVENTS = [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: userA, msg: "we",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: userA, msg: "could",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: userA, msg: "be",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: userA, msg: "heroes",
|
||||
}),
|
||||
];
|
||||
|
||||
const SOME_STATE_EVENT = utils.mkEvent({
|
||||
event: true,
|
||||
type: 'org.matrix.test_state',
|
||||
room: roomOne,
|
||||
user: userA,
|
||||
skey: "",
|
||||
content: {
|
||||
"foo": "bar",
|
||||
},
|
||||
});
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: userA,
|
||||
});
|
||||
|
||||
// This appears to work even if we comment out
|
||||
// `RoomEvent.CurrentStateUpdated` part which triggers everything to
|
||||
// re-listen after the `room.currentState` reference changes. I'm
|
||||
// not sure how it's getting re-emitted.
|
||||
it("should be able to listen to state events even after " +
|
||||
"the timeline is reset during `limited` sync response", async () => {
|
||||
// Create a room from the sync
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room).toBeTruthy();
|
||||
|
||||
let stateEventEmitCount = 0;
|
||||
client.on(RoomStateEvent.Update, () => {
|
||||
stateEventEmitCount += 1;
|
||||
});
|
||||
|
||||
// Cause `RoomStateEvent.Update` to be fired
|
||||
room.currentState.setStateEvents([SOME_STATE_EVENT]);
|
||||
// Make sure we can listen to the room state events before the reset
|
||||
expect(stateEventEmitCount).toEqual(1);
|
||||
|
||||
// Make a `limited` sync which will cause a `room.resetLiveTimeline`
|
||||
const limitedSyncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
limitedSyncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
// The important part, make the sync `limited`
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, limitedSyncData);
|
||||
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
// This got incremented again from processing the sync response
|
||||
expect(stateEventEmitCount).toEqual(2);
|
||||
|
||||
// Cause `RoomStateEvent.Update` to be fired
|
||||
room.currentState.setStateEvents([SOME_STATE_EVENT]);
|
||||
// Make sure we can still listen to the room state events after the reset
|
||||
expect(stateEventEmitCount).toEqual(3);
|
||||
});
|
||||
|
||||
// Make sure it re-registers the state listeners after the
|
||||
// `room.currentState` reference changes
|
||||
it("should be able to listen to state events even after " +
|
||||
"refreshing the timeline", async () => {
|
||||
const testClientWithTimelineSupport = new TestClient(
|
||||
selfUserId,
|
||||
"DEVICE",
|
||||
selfAccessToken,
|
||||
undefined,
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
httpBackend = testClientWithTimelineSupport.httpBackend;
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
client = testClientWithTimelineSupport.client;
|
||||
|
||||
// Create a room from the sync
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
client.startClient();
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room).toBeTruthy();
|
||||
|
||||
let stateEventEmitCount = 0;
|
||||
client.on(RoomStateEvent.Update, () => {
|
||||
stateEventEmitCount += 1;
|
||||
});
|
||||
|
||||
// Cause `RoomStateEvent.Update` to be fired
|
||||
room.currentState.setStateEvents([SOME_STATE_EVENT]);
|
||||
// Make sure we can listen to the room state events before the reset
|
||||
expect(stateEventEmitCount).toEqual(1);
|
||||
|
||||
const eventsInRoom = syncData.rooms.join[roomOne].timeline.events;
|
||||
const contextUrl = `/rooms/${encodeURIComponent(roomOne)}/context/` +
|
||||
`${encodeURIComponent(eventsInRoom[0].event_id)}`;
|
||||
httpBackend.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1], EVENTS[0]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
state: [
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
],
|
||||
end: "end_token",
|
||||
};
|
||||
});
|
||||
|
||||
// Refresh the timeline. This will cause the `room.currentState`
|
||||
// reference to change
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Cause `RoomStateEvent.Update` to be fired
|
||||
room.currentState.setStateEvents([SOME_STATE_EVENT]);
|
||||
// Make sure we can still listen to the room state events after the reset
|
||||
expect(stateEventEmitCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline", function() {
|
||||
@@ -515,7 +1109,7 @@ describe("MatrixClient syncing", function() {
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
const tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("roomtwotok");
|
||||
@@ -544,7 +1138,7 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
let resetCallCount = 0;
|
||||
// the token should be set *before* timelineReset is emitted
|
||||
client.on("Room.timelineReset", function(room) {
|
||||
client.on(RoomEvent.TimelineReset, function(room) {
|
||||
resetCallCount++;
|
||||
|
||||
const tl = room.getLiveTimeline();
|
||||
@@ -734,8 +1328,7 @@ describe("MatrixClient syncing", function() {
|
||||
expect(tok).toEqual("pagTok");
|
||||
}),
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
// first flush the filter request; this will make syncLeftRooms make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
return httpBackend.flushAllExpected();
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
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: Account;
|
||||
let aliceTestClient: TestClient;
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,732 @@
|
||||
/*
|
||||
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-next-line no-restricted-imports
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
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 {
|
||||
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
|
||||
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent,
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
let sdk: SlidingSyncSdk = null;
|
||||
let mockSlidingSync: SlidingSync = null;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||
s.getList = 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();
|
||||
s.setList = jest.fn();
|
||||
s.setListRanges = jest.fn();
|
||||
s.start = jest.fn();
|
||||
s.stop = jest.fn();
|
||||
s.resend = jest.fn();
|
||||
return s;
|
||||
};
|
||||
|
||||
// shorthand way to make events without filling in all the fields
|
||||
let eventIdCounter = 0;
|
||||
const mkOwnEvent = (evType: string, content: object): IRoomEvent => {
|
||||
eventIdCounter++;
|
||||
return {
|
||||
type: evType,
|
||||
content: content,
|
||||
sender: selfUserId,
|
||||
origin_server_ts: Date.now(),
|
||||
event_id: "$" + eventIdCounter,
|
||||
};
|
||||
};
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => {
|
||||
eventIdCounter++;
|
||||
return {
|
||||
type: evType,
|
||||
state_key: stateKey,
|
||||
content: content,
|
||||
sender: selfUserId,
|
||||
origin_server_ts: Date.now(),
|
||||
event_id: "$" + eventIdCounter,
|
||||
};
|
||||
};
|
||||
const assertTimelineEvents = (got: MatrixEvent[], want: IRoomEvent[]): void => {
|
||||
expect(got.length).toEqual(want.length);
|
||||
got.forEach((m, i) => {
|
||||
expect(m.getType()).toEqual(want[i].type);
|
||||
expect(m.getSender()).toEqual(want[i].sender);
|
||||
expect(m.getId()).toEqual(want[i].event_id);
|
||||
expect(m.getContent()).toEqual(want[i].content);
|
||||
expect(m.getTs()).toEqual(want[i].origin_server_ts);
|
||||
if (want[i].unsigned) {
|
||||
expect(m.getUnsigned()).toEqual(want[i].unsigned);
|
||||
}
|
||||
const maybeStateEvent = want[i] as IStateEvent;
|
||||
if (maybeStateEvent.state_key) {
|
||||
expect(m.getStateKey()).toEqual(maybeStateEvent.state_key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => {
|
||||
testOpts = testOpts || {};
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client.initCrypto();
|
||||
testOpts.crypto = client.crypto;
|
||||
}
|
||||
httpBackend.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
};
|
||||
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension => {
|
||||
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) {
|
||||
return calledExtension;
|
||||
}
|
||||
}
|
||||
fail("cannot find extension " + name);
|
||||
};
|
||||
|
||||
describe("sync/stop", () => {
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
});
|
||||
afterAll(teardownClient);
|
||||
it("can sync()", async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
await hasSynced;
|
||||
expect(mockSlidingSync.start).toBeCalled();
|
||||
});
|
||||
it("can stop()", async () => {
|
||||
sdk.stop();
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rooms", () => {
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
});
|
||||
afterAll(teardownClient);
|
||||
|
||||
describe("initial", () => {
|
||||
beforeAll(async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
// inject some rooms with different fields set.
|
||||
// All rooms are new so they all have initial: true
|
||||
const roomA = "!a_state_and_timeline:localhost";
|
||||
const roomB = "!b_timeline_only:localhost";
|
||||
const roomC = "!c_with_highlight_count:localhost";
|
||||
const roomD = "!d_with_notif_count:localhost";
|
||||
const roomE = "!e_with_invite:localhost";
|
||||
const roomF = "!f_calc_room_name:localhost";
|
||||
const data: Record<string, MSC3575RoomData> = {
|
||||
[roomA]: {
|
||||
name: "A",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
|
||||
],
|
||||
timeline: [
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello A" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world A" }),
|
||||
],
|
||||
initial: true,
|
||||
},
|
||||
[roomB]: {
|
||||
name: "B",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
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,
|
||||
},
|
||||
[roomC]: {
|
||||
name: "C",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world C" }),
|
||||
],
|
||||
highlight_count: 5,
|
||||
initial: true,
|
||||
},
|
||||
[roomD]: {
|
||||
name: "D",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world D" }),
|
||||
],
|
||||
notification_count: 5,
|
||||
initial: true,
|
||||
},
|
||||
[roomE]: {
|
||||
name: "E",
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
invite_state: [
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
content: { membership: "invite" },
|
||||
state_key: selfUserId,
|
||||
sender: "@bob:localhost",
|
||||
event_id: "$room_e_invite",
|
||||
origin_server_ts: 123456,
|
||||
},
|
||||
{
|
||||
type: "m.room.join_rules",
|
||||
content: { join_rule: "invite" },
|
||||
state_key: "",
|
||||
sender: "@bob:localhost",
|
||||
event_id: "$room_e_join_rule",
|
||||
origin_server_ts: 123456,
|
||||
},
|
||||
],
|
||||
initial: true,
|
||||
},
|
||||
[roomF]: {
|
||||
name: "#foo:localhost",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "This should be ignored" }, ""),
|
||||
],
|
||||
timeline: [
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello A" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world A" }),
|
||||
],
|
||||
initial: true,
|
||||
},
|
||||
};
|
||||
|
||||
it("can be created with required_state and timeline", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
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", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
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", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(data[roomC].highlight_count);
|
||||
});
|
||||
|
||||
it("can be created with a notification_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(data[roomD].notification_count);
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
|
||||
it("uses the 'name' field to caluclate the room name", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(
|
||||
gotRoom.name,
|
||||
).toEqual(data[roomF].name);
|
||||
});
|
||||
|
||||
describe("updating", () => {
|
||||
it("can update with a new timeline event", async () => {
|
||||
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: [newEvent],
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
const newTimeline = data[roomA].timeline;
|
||||
newTimeline.push(newEvent);
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
});
|
||||
|
||||
it("can update with a new required_state event", async () => {
|
||||
let gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||
],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||
name: data[roomC].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("lifecycle", () => {
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
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: {} }, null,
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
|
||||
|
||||
mockSlidingSync.emit(
|
||||
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"),
|
||||
);
|
||||
}
|
||||
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: {} },
|
||||
null,
|
||||
);
|
||||
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(sdk.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("opts", () => {
|
||||
afterEach(teardownClient);
|
||||
it("can resolveProfilesToInvites", async () => {
|
||||
await setupClient({
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
const roomId = "!resolveProfilesToInvites:localhost";
|
||||
const invitee = "@invitee:localhost";
|
||||
const inviteeProfile = {
|
||||
avatar_url: "mxc://foobar",
|
||||
displayname: "The Invitee",
|
||||
};
|
||||
httpBackend.when("GET", "/profile").respond(200, inviteeProfile);
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
initial: true,
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
],
|
||||
});
|
||||
await httpBackend.flush("/profile", 1, 1000);
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room).toBeDefined();
|
||||
const inviteeMember = room.getMember(invitee);
|
||||
expect(inviteeMember).toBeDefined();
|
||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionE2EE", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient({
|
||||
withCrypto: true,
|
||||
});
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
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", () => {
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
},
|
||||
});
|
||||
// TODO: more assertions?
|
||||
});
|
||||
it("can update OTK counts", () => {
|
||||
client.crypto.updateOneTimeKeyCount = 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);
|
||||
});
|
||||
});
|
||||
describe("ExtensionAccountData", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
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 = {
|
||||
info: "here",
|
||||
};
|
||||
let globalData = client.getAccountData(globalType);
|
||||
expect(globalData).toBeUndefined();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
{
|
||||
type: globalType,
|
||||
content: globalContent,
|
||||
},
|
||||
],
|
||||
});
|
||||
globalData = client.getAccountData(globalType);
|
||||
expect(globalData).toBeDefined();
|
||||
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.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const roomContent = {
|
||||
foo: "bar",
|
||||
};
|
||||
const roomType = "test";
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: [
|
||||
{
|
||||
type: roomType,
|
||||
content: roomContent,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room).toBeDefined();
|
||||
const event = room.getAccountData(roomType);
|
||||
expect(event).toBeDefined();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
it("doesn't crash for unknown room account data", async () => {
|
||||
const unknownRoomId = "!unknown:id";
|
||||
const roomType = "tester";
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[unknownRoomId]: [
|
||||
{
|
||||
type: roomType,
|
||||
content: {
|
||||
foo: "Bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const room = client.getRoom(unknownRoomId);
|
||||
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,
|
||||
}],
|
||||
},
|
||||
};
|
||||
let pushRule = client.getRoomPushRule("global", roomId);
|
||||
expect(pushRule).toBeUndefined();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
{
|
||||
type: EventType.PushRules,
|
||||
content: pushRulesContent,
|
||||
},
|
||||
],
|
||||
});
|
||||
pushRule = client.getRoomPushRule("global", roomId);
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]);
|
||||
});
|
||||
});
|
||||
describe("ExtensionToDevice", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
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",
|
||||
events: [],
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual({
|
||||
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 = {
|
||||
foo: "bar",
|
||||
};
|
||||
let called = false;
|
||||
client.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||
expect(ev.getType()).toEqual(toDeviceType);
|
||||
called = true;
|
||||
});
|
||||
ext.onResponse({
|
||||
next_batch: "34567",
|
||||
events: [
|
||||
{
|
||||
type: toDeviceType,
|
||||
content: toDeviceContent,
|
||||
},
|
||||
],
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
ext.onResponse({
|
||||
next_batch: "45678",
|
||||
events: [
|
||||
// someone tries to verify keys
|
||||
{
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
transaction_id: "a",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
transaction_id: "a",
|
||||
},
|
||||
},
|
||||
// then gives up
|
||||
{
|
||||
type: "m.key.verification.cancel",
|
||||
content: {
|
||||
transaction_id: "a",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,758 @@
|
||||
/*
|
||||
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-next-line no-restricted-imports
|
||||
import EventEmitter from "events";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src";
|
||||
import { sleep } from "../../src/utils";
|
||||
|
||||
/**
|
||||
* Tests for sliding sync. These tests are broken down into sub-tests which are reliant upon one another.
|
||||
* Each test suite (describe block) uses a single MatrixClient/HTTPBackend and a single SlidingSync class.
|
||||
* Each test will call different functions on SlidingSync which may depend on state from previous tests.
|
||||
*/
|
||||
describe("SlidingSync", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const proxyBaseUrl = "http://localhost:8008";
|
||||
const syncUrl = proxyBaseUrl + "/_matrix/client/unstable/org.matrix.msc3575/sync";
|
||||
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = () => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
};
|
||||
|
||||
describe("start/stop", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
it("should start the sync loop upon calling start()", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
const fakeResp = {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).respond(200, fakeResp);
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
expect(state).toEqual(SlidingSyncState.RequestFinished);
|
||||
expect(resp).toEqual(fakeResp);
|
||||
expect(err).toBeFalsy();
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should stop the sync loop upon calling stop()", () => {
|
||||
slidingSync.stop();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
});
|
||||
|
||||
describe("room subscriptions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
const roomId = "!foo:bar";
|
||||
const anotherRoomID = "!another:room";
|
||||
let roomSubInfo = {
|
||||
timeline_limit: 1,
|
||||
required_state: [
|
||||
["m.room.name", ""],
|
||||
],
|
||||
};
|
||||
const wantRoomData = {
|
||||
name: "foo bar",
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
};
|
||||
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
it("should be able to subscribe to a room", async () => {
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("room sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomId]).toEqual(roomSubInfo);
|
||||
}).respond(200, {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: wantRoomData,
|
||||
},
|
||||
});
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.RoomData", (gotRoomId, gotRoomData) => {
|
||||
expect(gotRoomId).toEqual(roomId);
|
||||
expect(gotRoomData).toEqual(wantRoomData);
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should be possible to adjust room subscription info whilst syncing", async () => {
|
||||
// listen for updated request
|
||||
const newSubInfo = {
|
||||
timeline_limit: 100,
|
||||
required_state: [
|
||||
["m.room.member", "*"],
|
||||
],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("adjusted sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomId]).toEqual(newSubInfo);
|
||||
}).respond(200, {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: wantRoomData,
|
||||
},
|
||||
});
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.RoomData", (gotRoomId, gotRoomData) => {
|
||||
expect(gotRoomId).toEqual(roomId);
|
||||
expect(gotRoomData).toEqual(wantRoomData);
|
||||
return true;
|
||||
});
|
||||
|
||||
slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
// need to set what the new subscription info is for subsequent tests
|
||||
roomSubInfo = newSubInfo;
|
||||
});
|
||||
|
||||
it("should be possible to add room subscriptions whilst syncing", async () => {
|
||||
// listen for updated request
|
||||
const anotherRoomData = {
|
||||
name: "foo bar 2",
|
||||
room_id: anotherRoomID,
|
||||
// we should not fall over if fields are missing.
|
||||
// required_state: [],
|
||||
// timeline: [],
|
||||
};
|
||||
const anotherRoomDataFixed = {
|
||||
name: anotherRoomData.name,
|
||||
room_id: anotherRoomID,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("new subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
// only the new room is sent, the other is sticky
|
||||
expect(body.room_subscriptions[anotherRoomID]).toEqual(roomSubInfo);
|
||||
expect(body.room_subscriptions[roomId]).toBeUndefined();
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[anotherRoomID]: anotherRoomData,
|
||||
},
|
||||
});
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.RoomData", (gotRoomId, gotRoomData) => {
|
||||
expect(gotRoomId).toEqual(anotherRoomID);
|
||||
expect(gotRoomData).toEqual(anotherRoomDataFixed);
|
||||
return true;
|
||||
});
|
||||
|
||||
const subs = slidingSync.getRoomSubscriptions();
|
||||
subs.add(anotherRoomID);
|
||||
slidingSync.modifyRoomSubscriptions(subs);
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should be able to unsubscribe from a room", async () => {
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("unsub request", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.unsubscribe_rooms).toEqual([roomId]);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
});
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
|
||||
// remove the subscription for the first room
|
||||
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("lists", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
|
||||
const roomA = "!a:localhost";
|
||||
const roomB = "!b:localhost";
|
||||
const roomC = "!c:localhost";
|
||||
const rooms = {
|
||||
[roomA]: {
|
||||
name: "A",
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
},
|
||||
[roomB]: {
|
||||
name: "B",
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
},
|
||||
[roomC]: {
|
||||
name: "C",
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
},
|
||||
};
|
||||
const newRanges = [[0, 2], [3, 5]];
|
||||
|
||||
let slidingSync: SlidingSync;
|
||||
it("should be possible to subscribe to a list", async () => {
|
||||
// request first 3 rooms
|
||||
const listReq = {
|
||||
ranges: [[0, 2]],
|
||||
sort: ["by_name"],
|
||||
timeline_limit: 1,
|
||||
required_state: [
|
||||
["m.room.topic", ""],
|
||||
],
|
||||
filters: {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1);
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual(listReq);
|
||||
}).respond(200, {
|
||||
pos: "a",
|
||||
lists: [{
|
||||
count: 500,
|
||||
ops: [{
|
||||
op: "SYNC",
|
||||
range: [0, 2],
|
||||
room_ids: Object.keys(rooms),
|
||||
}],
|
||||
}],
|
||||
rooms: rooms,
|
||||
});
|
||||
const listenerData = {};
|
||||
const dataListener = (roomId, roomData) => {
|
||||
expect(listenerData[roomId]).toBeFalsy();
|
||||
listenerData[roomId] = roomData;
|
||||
};
|
||||
slidingSync.on(SlidingSyncEvent.RoomData, dataListener);
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
|
||||
expect(listenerData[roomA]).toEqual(rooms[roomA]);
|
||||
expect(listenerData[roomB]).toEqual(rooms[roomB]);
|
||||
expect(listenerData[roomC]).toEqual(rooms[roomC]);
|
||||
|
||||
expect(slidingSync.listLength()).toEqual(1);
|
||||
slidingSync.off(SlidingSyncEvent.RoomData, dataListener);
|
||||
});
|
||||
|
||||
it("should be possible to retrieve list data", () => {
|
||||
expect(slidingSync.getList(0)).toBeDefined();
|
||||
expect(slidingSync.getList(5)).toBeNull();
|
||||
expect(slidingSync.getListData(5)).toBeNull();
|
||||
const syncData = slidingSync.getListData(0);
|
||||
expect(syncData.joinedCount).toEqual(500); // from previous test
|
||||
expect(syncData.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
});
|
||||
|
||||
it("should be possible to adjust list ranges", async () => {
|
||||
// modify the list ranges
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("next ranges", body.lists[0].ranges);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [{
|
||||
count: 500,
|
||||
ops: [{
|
||||
op: "SYNC",
|
||||
range: [0, 2],
|
||||
room_ids: Object.keys(rooms),
|
||||
}],
|
||||
}],
|
||||
});
|
||||
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.RequestFinished;
|
||||
});
|
||||
slidingSync.setListRanges(0, newRanges);
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
it("should be possible to add an extra list", async () => {
|
||||
// add extra list
|
||||
const extraListReq = {
|
||||
ranges: [[0, 100]],
|
||||
sort: ["by_name"],
|
||||
filters: {
|
||||
"is_dm": true,
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("extra list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
expect(body.lists[1]).toEqual(extraListReq);
|
||||
}).respond(200, {
|
||||
pos: "c",
|
||||
lists: [
|
||||
{
|
||||
count: 500,
|
||||
},
|
||||
{
|
||||
count: 50,
|
||||
ops: [{
|
||||
op: "SYNC",
|
||||
range: [0, 2],
|
||||
room_ids: Object.keys(rooms),
|
||||
}],
|
||||
},
|
||||
],
|
||||
});
|
||||
listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(1);
|
||||
expect(joinedCount).toEqual(50);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.setList(1, extraListReq);
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
it("should be possible to get list DELETE/INSERTs", async () => {
|
||||
// move C (2) to A (0)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
ops: [{
|
||||
op: "DELETE",
|
||||
index: 2,
|
||||
}, {
|
||||
op: "INSERT",
|
||||
index: 0,
|
||||
room_id: roomC,
|
||||
}],
|
||||
},
|
||||
{
|
||||
count: 50,
|
||||
}],
|
||||
});
|
||||
let listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomA,
|
||||
2: roomB,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
// move C (0) back to A (2)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
lists: [{
|
||||
count: 500,
|
||||
ops: [{
|
||||
op: "DELETE",
|
||||
index: 0,
|
||||
}, {
|
||||
op: "INSERT",
|
||||
index: 2,
|
||||
room_id: roomC,
|
||||
}],
|
||||
},
|
||||
{
|
||||
count: 50,
|
||||
}],
|
||||
});
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should ignore invalid list indexes", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
ops: [{
|
||||
op: "DELETE",
|
||||
index: 2324324,
|
||||
}],
|
||||
},
|
||||
{
|
||||
count: 50,
|
||||
}],
|
||||
});
|
||||
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should be possible to update a list", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [{
|
||||
count: 42,
|
||||
ops: [
|
||||
{
|
||||
op: "INVALIDATE",
|
||||
range: [0, 2],
|
||||
},
|
||||
{
|
||||
op: "SYNC",
|
||||
range: [0, 1],
|
||||
room_ids: [roomB, roomC],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
count: 50,
|
||||
}],
|
||||
});
|
||||
// update the list with a new filter
|
||||
slidingSync.setList(0, {
|
||||
filters: {
|
||||
is_encrypted: true,
|
||||
},
|
||||
ranges: [[0, 100]],
|
||||
});
|
||||
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
expect(joinedCount).toEqual(42);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extensions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
let slidingSync: SlidingSync;
|
||||
const extReq = {
|
||||
foo: "bar",
|
||||
};
|
||||
const extResp = {
|
||||
baz: "quuz",
|
||||
};
|
||||
|
||||
// Pre-extensions get called BEFORE processing the sync response
|
||||
const preExtName = "foobar";
|
||||
let onPreExtensionRequest;
|
||||
let onPreExtensionResponse;
|
||||
|
||||
// Post-extensions get called AFTER processing the sync response
|
||||
const postExtName = "foobar2";
|
||||
let onPostExtensionRequest;
|
||||
let onPostExtensionResponse;
|
||||
|
||||
const extPre = {
|
||||
name: () => preExtName,
|
||||
onRequest: (initial) => { return onPreExtensionRequest(initial); },
|
||||
onResponse: (res) => { return onPreExtensionResponse(res); },
|
||||
when: () => ExtensionState.PreProcess,
|
||||
};
|
||||
const extPost = {
|
||||
name: () => postExtName,
|
||||
onRequest: (initial) => { return onPostExtensionRequest(initial); },
|
||||
onResponse: (res) => { return onPostExtensionResponse(res); },
|
||||
when: () => ExtensionState.PostProcess,
|
||||
};
|
||||
|
||||
it("should be able to register an extension", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
|
||||
const callbackOrder = [];
|
||||
let extensionOnResponseCalled = false;
|
||||
onPreExtensionRequest = () => {
|
||||
return extReq;
|
||||
};
|
||||
onPreExtensionResponse = (resp) => {
|
||||
extensionOnResponseCalled = true;
|
||||
callbackOrder.push("onPreExtensionResponse");
|
||||
expect(resp).toEqual(extResp);
|
||||
};
|
||||
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions[preExtName]).toEqual(extReq);
|
||||
}).respond(200, {
|
||||
pos: "a",
|
||||
ops: [],
|
||||
counts: [],
|
||||
extensions: {
|
||||
[preExtName]: extResp,
|
||||
},
|
||||
});
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
if (state === SlidingSyncState.Complete) {
|
||||
callbackOrder.push("Lifecycle");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
expect(extensionOnResponseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
|
||||
});
|
||||
|
||||
it("should be able to send nothing in an extension request/response", async () => {
|
||||
onPreExtensionRequest = () => {
|
||||
return undefined;
|
||||
};
|
||||
let responseCalled = false;
|
||||
onPreExtensionResponse = (resp) => {
|
||||
responseCalled = true;
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req nothing", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions[preExtName]).toBeUndefined();
|
||||
}).respond(200, {
|
||||
pos: "a",
|
||||
ops: [],
|
||||
counts: [],
|
||||
extensions: {},
|
||||
});
|
||||
// we need to resend as sliding sync will already have a buffered request with the old
|
||||
// extension values from the previous test.
|
||||
slidingSync.resend();
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(false);
|
||||
});
|
||||
|
||||
it("is possible to register extensions after start() has been called", async () => {
|
||||
slidingSync.registerExtension(extPost);
|
||||
onPostExtensionRequest = () => {
|
||||
return extReq;
|
||||
};
|
||||
let responseCalled = false;
|
||||
const callbackOrder = [];
|
||||
onPostExtensionResponse = (resp) => {
|
||||
expect(resp).toEqual(extResp);
|
||||
responseCalled = true;
|
||||
callbackOrder.push("onPostExtensionResponse");
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req after start", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions[preExtName]).toBeUndefined(); // from the earlier test
|
||||
expect(body.extensions[postExtName]).toEqual(extReq);
|
||||
}).respond(200, {
|
||||
pos: "c",
|
||||
ops: [],
|
||||
counts: [],
|
||||
extensions: {
|
||||
[postExtName]: extResp,
|
||||
},
|
||||
});
|
||||
// we need to resend as sliding sync will already have a buffered request with the old
|
||||
// extension values from the previous test.
|
||||
slidingSync.resend();
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
if (state === SlidingSyncState.Complete) {
|
||||
callbackOrder.push("Lifecycle");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("is not possible to register the same extension name twice", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function timeout(delayMs: number, reason: string): Promise<never> {
|
||||
await sleep(delayMs);
|
||||
throw new Error(`timeout: ${delayMs}ms - ${reason}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen until a callback returns data.
|
||||
* @param {EventEmitter} emitter The event emitter
|
||||
* @param {string} eventName The event to listen for
|
||||
* @param {function} callback The callback which will be invoked when events fire. Return something truthy from this to resolve the promise.
|
||||
* @param {number} timeoutMs The number of milliseconds to wait for the callback to return data. Default: 500ms.
|
||||
* @returns {Promise} A promise which will be resolved when the callback returns data. If the callback throws or the timeout is reached,
|
||||
* the promise is rejected.
|
||||
*/
|
||||
function listenUntil<T>(
|
||||
emitter: EventEmitter,
|
||||
eventName: string,
|
||||
callback: (...args: any[]) => T,
|
||||
timeoutMs = 500,
|
||||
): Promise<T> {
|
||||
const trace = new Error().stack.split(`\n`)[2];
|
||||
return Promise.race([new Promise<T>((resolve, reject) => {
|
||||
const wrapper = (...args) => {
|
||||
try {
|
||||
const data = callback(...args);
|
||||
if (data) {
|
||||
emitter.off(eventName, wrapper);
|
||||
resolve(data);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
emitter.on(eventName, wrapper);
|
||||
}), timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace)]);
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import { logger } from '../src/logger';
|
||||
import { MatrixEvent } from "../src/models/event";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function syncPromise(client, count) {
|
||||
if (count === undefined) {
|
||||
count = 1;
|
||||
}
|
||||
if (count <= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const cb = (state) => {
|
||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
if (state === 'SYNCING') {
|
||||
resolve();
|
||||
} else {
|
||||
client.once('sync', cb);
|
||||
}
|
||||
};
|
||||
client.once('sync', cb);
|
||||
});
|
||||
|
||||
return p.then(() => {
|
||||
return syncPromise(client, count-1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function mock(constr, name) {
|
||||
// Based on
|
||||
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
||||
const HelperConstr = new Function(); // jshint ignore:line
|
||||
HelperConstr.prototype = constr.prototype;
|
||||
const result = new HelperConstr();
|
||||
result.toString = function() {
|
||||
return "mock" + (name ? " of " + name : "");
|
||||
};
|
||||
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();
|
||||
}
|
||||
} catch (ex) {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
// cause exceptions.
|
||||
// Overwriting will not work either in that case.
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {Object} a JSON object representing this event.
|
||||
*/
|
||||
export function mkEvent(opts) {
|
||||
if (!opts.type || !opts.content) {
|
||||
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
||||
}
|
||||
const event = {
|
||||
type: opts.type,
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||
"m.room.power_levels", "m.room.topic",
|
||||
"com.example.state"].includes(opts.type)) {
|
||||
event.state_key = "";
|
||||
}
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an m.presence event.
|
||||
* @param {Object} opts Values for the presence.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
export function mkPresence(opts) {
|
||||
if (!opts.user) {
|
||||
throw new Error("Missing user");
|
||||
}
|
||||
const event = {
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
type: "m.presence",
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: {
|
||||
avatar_url: opts.url,
|
||||
displayname: opts.name,
|
||||
last_active_ago: opts.ago,
|
||||
presence: opts.presence || "offline",
|
||||
},
|
||||
};
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
*/
|
||||
export function mkMembership(opts) {
|
||||
opts.type = "m.room.member";
|
||||
if (!opts.skey) {
|
||||
opts.skey = opts.sender || opts.user;
|
||||
}
|
||||
if (!opts.mship) {
|
||||
throw new Error("Missing .mship => " + JSON.stringify(opts));
|
||||
}
|
||||
opts.content = {
|
||||
membership: opts.mship,
|
||||
};
|
||||
if (opts.name) {
|
||||
opts.content.displayname = opts.name;
|
||||
}
|
||||
if (opts.url) {
|
||||
opts.content.avatar_url = opts.url;
|
||||
}
|
||||
return mkEvent(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
export function mkMessage(opts) {
|
||||
opts.type = "m.room.message";
|
||||
if (!opts.msg) {
|
||||
opts.msg = "Random->" + Math.random();
|
||||
}
|
||||
if (!opts.room || !opts.user) {
|
||||
throw new Error("Missing .room or .user from %s", opts);
|
||||
}
|
||||
opts.content = {
|
||||
msgtype: "m.text",
|
||||
body: opts.msg,
|
||||
};
|
||||
return mkEvent(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export function MockStorageApi() {
|
||||
this.data = {};
|
||||
}
|
||||
MockStorageApi.prototype = {
|
||||
get length() {
|
||||
return Object.keys(this.data).length;
|
||||
},
|
||||
key: function(i) {
|
||||
return Object.keys(this.data)[i];
|
||||
},
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function awaitDecryption(event) {
|
||||
// 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;
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function HttpResponse(
|
||||
httpLookups, acceptKeepalives, ignoreUnhandledSync,
|
||||
) {
|
||||
this.httpLookups = httpLookups;
|
||||
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
|
||||
this.ignoreUnhandledSync = ignoreUnhandledSync;
|
||||
this.pendingLookup = null;
|
||||
}
|
||||
|
||||
HttpResponse.prototype.request = function(
|
||||
cb, method, path, qp, data, prefix,
|
||||
) {
|
||||
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const next = this.httpLookups.shift();
|
||||
const logLine = (
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
logger.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
||||
logger.log("MatrixClient[UT] Ignoring.");
|
||||
return new Promise(() => {});
|
||||
}
|
||||
if (this.pendingLookup) {
|
||||
if (this.pendingLookup.method === method
|
||||
&& this.pendingLookup.path === path) {
|
||||
return this.pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path,
|
||||
);
|
||||
}
|
||||
this.pendingLookup = {
|
||||
promise: new Promise(() => {}),
|
||||
method: method,
|
||||
path: path,
|
||||
};
|
||||
return this.pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
logger.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response",
|
||||
);
|
||||
if (next.expectBody) {
|
||||
expect(next.expectBody).toEqual(data);
|
||||
}
|
||||
if (next.expectQueryParams) {
|
||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
||||
});
|
||||
}
|
||||
|
||||
if (next.thenCall) {
|
||||
process.nextTick(next.thenCall, 0); // next tick so we return first.
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
});
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
||||
logger.log("MatrixClient[UT] Ignoring.");
|
||||
this.httpLookups.unshift(next);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
return new Promise(() => {});
|
||||
};
|
||||
|
||||
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
HttpResponse.PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {},
|
||||
};
|
||||
|
||||
HttpResponse.USER_ID = "@alice:bar";
|
||||
|
||||
HttpResponse.filterResponse = function(userId) {
|
||||
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
return {
|
||||
method: "POST",
|
||||
path: filterPath,
|
||||
data: { filter_id: "f1lt3r" },
|
||||
};
|
||||
};
|
||||
|
||||
HttpResponse.SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: { events: [] },
|
||||
rooms: {},
|
||||
};
|
||||
|
||||
HttpResponse.SYNC_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: HttpResponse.SYNC_DATA,
|
||||
};
|
||||
|
||||
HttpResponse.defaultResponses = function(userId) {
|
||||
return [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
HttpResponse.filterResponse(userId),
|
||||
HttpResponse.SYNC_RESPONSE,
|
||||
];
|
||||
};
|
||||
|
||||
export function setHttpResponses(
|
||||
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
) {
|
||||
const httpResponseObj = new HttpResponse(
|
||||
responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
);
|
||||
|
||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||
client.http = [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
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 { 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";
|
||||
|
||||
type InfoContentProps = {
|
||||
timeout: number;
|
||||
isLive?: boolean;
|
||||
assetType?: LocationAssetType;
|
||||
description?: string;
|
||||
timestamp?: number;
|
||||
};
|
||||
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
|
||||
timeout: 3600000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an m.beacon_info event
|
||||
* all required properties are mocked
|
||||
* override with contentProps
|
||||
*/
|
||||
export const makeBeaconInfoEvent = (
|
||||
sender: string,
|
||||
roomId: string,
|
||||
contentProps: Partial<InfoContentProps> = {},
|
||||
eventId?: string,
|
||||
): MatrixEvent => {
|
||||
const {
|
||||
timeout,
|
||||
isLive,
|
||||
description,
|
||||
assetType,
|
||||
timestamp,
|
||||
} = {
|
||||
...DEFAULT_INFO_CONTENT_PROPS,
|
||||
...contentProps,
|
||||
};
|
||||
const event = new MatrixEvent({
|
||||
type: M_BEACON_INFO.name,
|
||||
room_id: roomId,
|
||||
state_key: sender,
|
||||
content: makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp),
|
||||
});
|
||||
|
||||
event.event.origin_server_ts = timestamp || Date.now();
|
||||
|
||||
// live beacons use the beacon_info event id
|
||||
// set or default this
|
||||
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
uri: string;
|
||||
timestamp: number;
|
||||
beaconInfoId: string;
|
||||
description?: string;
|
||||
};
|
||||
const DEFAULT_CONTENT_PROPS: ContentProps = {
|
||||
uri: 'geo:-36.24484561954707,175.46884959563613;u=10',
|
||||
timestamp: 123,
|
||||
beaconInfoId: '$123',
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an m.beacon event
|
||||
* all required properties are mocked
|
||||
* override with contentProps
|
||||
*/
|
||||
export const makeBeaconEvent = (
|
||||
sender: string,
|
||||
contentProps: Partial<ContentProps> = {},
|
||||
): MatrixEvent => {
|
||||
const { uri, timestamp, beaconInfoId, description } = {
|
||||
...DEFAULT_CONTENT_PROPS,
|
||||
...contentProps,
|
||||
};
|
||||
|
||||
return new MatrixEvent({
|
||||
type: M_BEACON.name,
|
||||
sender,
|
||||
content: makeBeaconContent(uri, timestamp, beaconInfoId, description),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a mock geolocation position
|
||||
* defaults all required properties
|
||||
*/
|
||||
export const makeGeolocationPosition = (
|
||||
{ timestamp, coords }:
|
||||
{ timestamp?: number, coords: Partial<GeolocationCoordinates> },
|
||||
): GeolocationPosition => ({
|
||||
timestamp: timestamp ?? 1647256791840,
|
||||
coords: {
|
||||
accuracy: 1,
|
||||
latitude: 54.001927,
|
||||
longitude: -8.253491,
|
||||
altitude: null,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
...coords,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Filter emitter.emit mock calls to find relevant events
|
||||
* eg:
|
||||
* ```
|
||||
* const emitSpy = jest.spyOn(state, 'emit');
|
||||
* << actions >>
|
||||
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
|
||||
* expect(beaconLivenessEmits.length).toBe(1);
|
||||
* ```
|
||||
*/
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
|
||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||
@@ -0,0 +1,373 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import EventEmitter from "events";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
if (count <= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const p = new Promise<void>((resolve) => {
|
||||
const cb = (state: SyncState) => {
|
||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
if (state === SyncState.Syncing) {
|
||||
resolve();
|
||||
} else {
|
||||
client.once(ClientEvent.Sync, cb);
|
||||
}
|
||||
};
|
||||
client.once(ClientEvent.Sync, cb);
|
||||
});
|
||||
|
||||
return p.then(() => {
|
||||
return syncPromise(client, count - 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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() {
|
||||
return "mock" + (name ? " of " + name : "");
|
||||
};
|
||||
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();
|
||||
}
|
||||
} catch (ex) {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
// cause exceptions.
|
||||
// Overwriting will not work either in that case.
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface IEventOpts {
|
||||
type: EventType | string;
|
||||
room?: string;
|
||||
sender?: string;
|
||||
skey?: string;
|
||||
content: IContent;
|
||||
user?: string;
|
||||
unsigned?: IUnsigned;
|
||||
redacts?: string;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): Partial<IEvent> | MatrixEvent {
|
||||
if (!opts.type || !opts.content) {
|
||||
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
||||
}
|
||||
const event: Partial<IEvent> = {
|
||||
type: opts.type as string,
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
unsigned: opts.unsigned || {},
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
redacts: opts.redacts,
|
||||
};
|
||||
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)) {
|
||||
event.state_key = "";
|
||||
}
|
||||
|
||||
if (opts.event && client) {
|
||||
return eventMapperFor(client, {})(event);
|
||||
}
|
||||
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
}
|
||||
|
||||
type GeneratedMetadata = {
|
||||
event_id: string;
|
||||
txn_id: string;
|
||||
origin_server_ts: number;
|
||||
};
|
||||
|
||||
export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
|
||||
return {
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
origin_server_ts: Date.now(),
|
||||
...base,
|
||||
};
|
||||
}
|
||||
|
||||
interface IPresenceOpts {
|
||||
user?: string;
|
||||
sender?: string;
|
||||
url: string;
|
||||
name: string;
|
||||
ago: number;
|
||||
presence?: string;
|
||||
event?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an m.presence event.
|
||||
* @param {Object} opts Values for the presence.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
|
||||
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
|
||||
export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
|
||||
const event = {
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
type: "m.presence",
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: {
|
||||
avatar_url: opts.url,
|
||||
displayname: opts.name,
|
||||
last_active_ago: opts.ago,
|
||||
presence: opts.presence || "offline",
|
||||
},
|
||||
};
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
}
|
||||
|
||||
interface IMembershipOpts {
|
||||
room?: string;
|
||||
mship: string;
|
||||
sender?: string;
|
||||
user?: string;
|
||||
skey?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
event?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
*/
|
||||
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
|
||||
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
|
||||
export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
|
||||
const eventOpts: IEventOpts = {
|
||||
...opts,
|
||||
type: EventType.RoomMember,
|
||||
content: {
|
||||
membership: opts.mship,
|
||||
},
|
||||
};
|
||||
|
||||
if (!opts.skey) {
|
||||
eventOpts.skey = opts.sender || opts.user;
|
||||
}
|
||||
if (opts.name) {
|
||||
eventOpts.content.displayname = opts.name;
|
||||
}
|
||||
if (opts.url) {
|
||||
eventOpts.content.avatar_url = opts.url;
|
||||
}
|
||||
return mkEvent(eventOpts);
|
||||
}
|
||||
|
||||
export function mkMembershipCustom<T>(
|
||||
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,
|
||||
content: { ...content, membership: base.membership },
|
||||
type: EventType.RoomMember,
|
||||
state_key: base.sender,
|
||||
});
|
||||
}
|
||||
|
||||
interface IMessageOpts {
|
||||
room?: string;
|
||||
user: string;
|
||||
msg?: string;
|
||||
event?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
export function mkMessage(
|
||||
opts: IMessageOpts & { event?: boolean },
|
||||
client?: MatrixClient,
|
||||
): Partial<IEvent> | MatrixEvent {
|
||||
const eventOpts: IEventOpts = {
|
||||
...opts,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: opts.msg,
|
||||
},
|
||||
};
|
||||
|
||||
if (!eventOpts.content.body) {
|
||||
eventOpts.content.body = "Random->" + Math.random();
|
||||
}
|
||||
return mkEvent(eventOpts, client);
|
||||
}
|
||||
|
||||
interface IReplyMessageOpts extends IMessageOpts {
|
||||
replyToMessage: MatrixEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
export function mkReplyMessage(
|
||||
opts: IReplyMessageOpts & { event?: boolean },
|
||||
client?: MatrixClient,
|
||||
): Partial<IEvent> | MatrixEvent {
|
||||
const eventOpts: IEventOpts = {
|
||||
...opts,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": opts.msg,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.in_reply_to",
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!eventOpts.content.body) {
|
||||
eventOpts.content.body = "Random->" + Math.random();
|
||||
}
|
||||
return mkEvent(eventOpts, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export class MockStorageApi {
|
||||
private data: Record<string, any> = {};
|
||||
|
||||
public get length() {
|
||||
return Object.keys(this.data).length;
|
||||
}
|
||||
|
||||
public key(i: number): any {
|
||||
return Object.keys(this.data)[i];
|
||||
}
|
||||
|
||||
public setItem(k: string, v: any): void {
|
||||
this.data[k] = v;
|
||||
}
|
||||
|
||||
public getItem(k: string): any {
|
||||
return this.data[k] || null;
|
||||
}
|
||||
|
||||
public removeItem(k: string): void {
|
||||
delete this.data[k];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export async function awaitDecryption(event: MatrixEvent): 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;
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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" +
|
||||
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
|
||||
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
|
||||
"a=ice-options:trickle\r\n" +
|
||||
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
|
||||
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
|
||||
"a=setup:active\r\n" +
|
||||
"a=mid:0\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
|
||||
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
|
||||
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
|
||||
"a=sendrecv\r\n" +
|
||||
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"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"
|
||||
);
|
||||
|
||||
export class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener() { }
|
||||
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() { }
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
|
||||
|
||||
stop() { }
|
||||
}
|
||||
|
||||
// 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[] = [],
|
||||
) {}
|
||||
|
||||
listeners: [string, (...args: any[]) => any][] = [];
|
||||
|
||||
dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audio" | "video",
|
||||
) { }
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
}
|
||||
stopUserMediaStream() { }
|
||||
hasAudioDevice() { return true; }
|
||||
}
|
||||
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
const EVENTNAME = "UnknownEntry";
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as sdk from "../../src";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
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 { 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 {
|
||||
makeBeaconContent,
|
||||
makeBeaconInfoContent,
|
||||
makeTopicContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
|
||||
describe('Beacon content helpers', () => {
|
||||
describe('makeBeaconInfoContent()', () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
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',
|
||||
timeout: 1234,
|
||||
live: true,
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
[M_ASSET.name]: {
|
||||
type: LocationAssetType.Pin,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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('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({
|
||||
[M_LOCATION.name]: {
|
||||
description: undefined,
|
||||
uri: 'geo:foo',
|
||||
},
|
||||
[M_TIMESTAMP.name]: 123,
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: '$1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('creates event content with description', () => {
|
||||
expect(makeBeaconContent(
|
||||
'geo:foo',
|
||||
123,
|
||||
'$1234',
|
||||
'test description',
|
||||
)).toEqual({
|
||||
[M_LOCATION.name]: {
|
||||
description: 'test description',
|
||||
uri: 'geo:foo',
|
||||
},
|
||||
[M_TIMESTAMP.name]: 123,
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: '$1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
}],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
|
||||
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>",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+202
-133
@@ -1,6 +1,8 @@
|
||||
import '../olm-loader';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { TestClient } from "../TestClient";
|
||||
@@ -8,12 +10,50 @@ import { MatrixEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import * as olmlib from "../../src/crypto/olmlib";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { logger } from '../../src/logger';
|
||||
import { MemoryStore } from "../../src";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
function awaitEvent(emitter, event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
emitter.once(event, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function keyshareEventForEvent(client, event, index) {
|
||||
const roomId = event.getRoomId();
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
eventContent.sender_key,
|
||||
eventContent.session_id,
|
||||
index,
|
||||
);
|
||||
const ksEvent = new MatrixEvent({
|
||||
type: "m.forwarded_room_key",
|
||||
sender: client.getUserId(),
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: eventContent.sender_key,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: eventContent.session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain:
|
||||
key.forwarding_curve_key_chain,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
ksEvent.senderCurve25519Key = "akey";
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
@@ -113,7 +153,7 @@ describe("Crypto", function() {
|
||||
|
||||
beforeEach(async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const clientStore = new MemoryStore({ localStorage: mockStorage });
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
cryptoStore.storeEndToEndDeviceData({
|
||||
@@ -140,10 +180,9 @@ describe("Crypto", function() {
|
||||
|
||||
crypto = new Crypto(
|
||||
mockBaseApis,
|
||||
sessionStore,
|
||||
"@alice:home.server",
|
||||
"FLIBBLE",
|
||||
sessionStore,
|
||||
clientStore,
|
||||
cryptoStore,
|
||||
mockRoomList,
|
||||
);
|
||||
@@ -200,136 +239,141 @@ describe("Crypto", function() {
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it(
|
||||
"does not cancel keyshare requests if some messages are not decrypted",
|
||||
async function() {
|
||||
function awaitEvent(emitter, event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
emitter.once(event, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function keyshareEventForEvent(event, index) {
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await aliceClient.crypto.olmDevice
|
||||
.getInboundGroupSessionKey(
|
||||
roomId, eventContent.sender_key, eventContent.session_id,
|
||||
index,
|
||||
);
|
||||
const ksEvent = new MatrixEvent({
|
||||
type: "m.forwarded_room_key",
|
||||
sender: "@alice:example.com",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: eventContent.sender_key,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: eventContent.session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain:
|
||||
key.forwarding_curve_key_chain,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
ksEvent.senderCurve25519Key = "akey";
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
event.clearEvent = undefined;
|
||||
event.senderCurve25519Key = null;
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
let eventPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the second message, so
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(events[1], 1);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
it("does not cancel keyshare requests if some messages are not decrypted", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
// the room key request should still be there, since we haven't
|
||||
// decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toBeDefined();
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
event.clearEvent = undefined;
|
||||
event.senderCurve25519Key = null;
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the first message, so
|
||||
// that it can now be decrypted
|
||||
eventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
ksEvent = await keyshareEventForEvent(events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
await sleep(1);
|
||||
// the room key request should be gone since we've now decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toBeFalsy();
|
||||
},
|
||||
);
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
let eventPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the second message, so
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
// the room key request should still be there, since we haven't
|
||||
// decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
|
||||
|
||||
// keyshare the session key starting at the first message, so
|
||||
// that it can now be decrypted
|
||||
eventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
await sleep(1);
|
||||
// the room key request should be gone since we've now decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should error if a forwarded room key lacks a content.sender_key", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
});
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
event.clearEvent = undefined;
|
||||
event.senderCurve25519Key = null;
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
|
||||
ksEvent.getContent().sender_key = undefined; // test
|
||||
bobClient.crypto.addInboundGroupSession = jest.fn();
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
expect(bobClient.crypto.addInboundGroupSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a new keyshare request if we request a keyshare", async function() {
|
||||
// make sure that cancelAndResend... creates a new keyshare request
|
||||
@@ -398,4 +442,29 @@ describe("Crypto", function() {
|
||||
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Secret storage', function() {
|
||||
it("creates secret storage even if there is no keyInfo", async function() {
|
||||
jest.spyOn(logger, 'log').mockImplementation(() => {});
|
||||
jest.setTimeout(10000);
|
||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||
await client.initCrypto();
|
||||
client.crypto.getSecretStorageKey = async () => null;
|
||||
client.crypto.isCrossSigningReady = async () => false;
|
||||
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
|
||||
client.crypto.baseApis.setAccountData = () => null;
|
||||
client.crypto.baseApis.uploadKeySignatures = () => null;
|
||||
client.crypto.baseApis.http.authedRequest = () => null;
|
||||
const createSecretStorageKey = async () => {
|
||||
return {
|
||||
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||
privateKey: Uint8Array.of(32, 33),
|
||||
};
|
||||
};
|
||||
await client.crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+58
-56
@@ -66,23 +66,23 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
});
|
||||
|
||||
it.each(types)("should throw if the callback returns falsey",
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => false,
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: async () => false as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
});
|
||||
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, {
|
||||
getCrossSigningKey: () => masterKeyPub,
|
||||
getCrossSigningKey: async () => masterKeyPub as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should return a key from its callback", async () => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => testKey,
|
||||
getCrossSigningKey: async () => testKey,
|
||||
});
|
||||
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
@@ -99,7 +99,7 @@ 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 }) => {
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
@@ -122,58 +122,58 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
});
|
||||
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 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);
|
||||
});
|
||||
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 }) => {
|
||||
@@ -220,12 +220,14 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
*/
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
() => 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;
|
||||
}],
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
@@ -20,8 +20,10 @@ import { logger } from "../../../src/logger";
|
||||
import * as utils from "../../../src/utils";
|
||||
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";
|
||||
|
||||
const signedDeviceList = {
|
||||
const signedDeviceList: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test1:sw1v.org": {
|
||||
@@ -45,13 +47,15 @@ const signedDeviceList = {
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {},
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const signedDeviceList2 = {
|
||||
const signedDeviceList2: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test2:sw1v.org": {
|
||||
@@ -75,7 +79,9 @@ const signedDeviceList2 = {
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {},
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -104,10 +110,10 @@ describe('DeviceList', function() {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
};
|
||||
} as unknown as MatrixClient;
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
} as unknown as OlmDevice;
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
@@ -118,7 +124,7 @@ describe('DeviceList', function() {
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
@@ -128,6 +134,7 @@ describe('DeviceList', function() {
|
||||
return prom1.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,7 +144,7 @@ describe('DeviceList', function() {
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
@@ -154,6 +161,7 @@ describe('DeviceList', function() {
|
||||
dl.saveIfDirty().then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
failures: {},
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
@@ -165,11 +173,12 @@ describe('DeviceList', function() {
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.mockReset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = utils.defer();
|
||||
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));
|
||||
|
||||
@@ -178,6 +187,7 @@ describe('DeviceList', function() {
|
||||
}).then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,9 +197,9 @@ describe('DeviceList', function() {
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
const queryDefer2 = utils.defer();
|
||||
const queryDefer2 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
@@ -204,6 +214,7 @@ describe('DeviceList', function() {
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import '../../../olm-loader';
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../../MockStorageApi";
|
||||
import * as testUtils from "../../../test-utils";
|
||||
import * as testUtils from "../../../test-utils/test-utils";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import { Crypto } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
@@ -256,97 +256,172 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
describe("session reuse and key reshares", () => {
|
||||
const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
let megolmEncryption;
|
||||
let aliceDeviceInfo;
|
||||
let mockRoom;
|
||||
let olmDevice;
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
beforeEach(async () => {
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
}));
|
||||
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
aliceDeviceInfo = {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
};
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: aliceDeviceInfo,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}));
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
});
|
||||
|
||||
megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: rotationPeriodMs,
|
||||
},
|
||||
});
|
||||
mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
});
|
||||
|
||||
const megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
},
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
it("should use larger otkTimeout when preparing to encrypt room", async () => {
|
||||
megolmEncryption.prepareToEncrypt(mockRoom);
|
||||
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 10000,
|
||||
);
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
it("should generate a new session if this one needs rotation", async () => {
|
||||
const session = await megolmEncryption.prepareNewSession(false);
|
||||
session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time
|
||||
// Inject expired session which needs rotation
|
||||
megolmEncryption.setupPromise = Promise.resolve(session);
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession");
|
||||
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(prepareNewSessionSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
});
|
||||
|
||||
it("re-shares keys to devices it's already sent to", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
|
||||
mockBaseApis.sendToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
'@alice:home.server',
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-share keys to devices whose keys have changed", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
|
||||
aliceDeviceInfo.getIdentityKey = jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
|
||||
);
|
||||
|
||||
mockBaseApis.sendToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
'@alice:home.server',
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -414,7 +489,7 @@ describe("MegolmDecryption", function() {
|
||||
let run = false;
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
run = true;
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
expect(contentMap).toStrictEqual({
|
||||
@@ -524,7 +599,7 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
const sendPromise = new Promise((resolve, reject) => {
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice: {
|
||||
@@ -548,6 +623,8 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await sendPromise;
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error describing why it doesn't have a key", async function() {
|
||||
@@ -566,12 +643,12 @@ describe("MegolmDecryption", function() {
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id",
|
||||
session_id: "session_id1",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
code: "m.blacklisted",
|
||||
reason: "You have been blocked",
|
||||
@@ -588,9 +665,38 @@ describe("MegolmDecryption", function() {
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id",
|
||||
session_id: "session_id1",
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id2",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
code: "m.blacklisted",
|
||||
reason: "You have been blocked",
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id2",
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error describing the lack of an olm session", async function() {
|
||||
@@ -612,12 +718,12 @@ describe("MegolmDecryption", function() {
|
||||
const now = Date.now();
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id",
|
||||
session_id: "session_id1",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
code: "m.no_olm",
|
||||
reason: "Unable to establish a secure channel.",
|
||||
@@ -638,10 +744,44 @@ describe("MegolmDecryption", function() {
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id",
|
||||
session_id: "session_id1",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id2",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
code: "m.no_olm",
|
||||
reason: "Unable to establish a secure channel.",
|
||||
},
|
||||
}));
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id2",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error to indicate a wedged olm session", async function() {
|
||||
@@ -692,5 +832,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// After a single tick through the first task, it should have
|
||||
// claimed ownership of all devices to avoid deadlocking others.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||
@@ -254,7 +254,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// The second task should not have changed the ownership count, as
|
||||
// it's waiting on the first task.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
// Track the tasks, but don't await them yet.
|
||||
const promises = Promise.all([
|
||||
|
||||
@@ -15,20 +15,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockedObject } from "jest-mock";
|
||||
|
||||
import '../../olm-loader';
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import * as algorithms from "../../../src/crypto/algorithms";
|
||||
import { WebStorageSessionStore } from "../../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../MockStorageApi";
|
||||
import * as testUtils from "../../test-utils";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BackupManager } from "../../../src/crypto/backup";
|
||||
import { StubStore } from "../../../src/store/stub";
|
||||
import { IAbortablePromise, MatrixScheduler } from '../../../src';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -93,8 +95,8 @@ const AES256_KEY_BACKUP_DATA = {
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
@@ -102,7 +104,7 @@ const CURVE25519_BACKUP_INFO = {
|
||||
|
||||
const AES256_BACKUP_INFO = {
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: 1,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
// FIXME: add iv and mac
|
||||
},
|
||||
@@ -118,30 +120,22 @@ function saveCrossSigningKeys(k) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
function makeTestClient(sessionStore, cryptoStore) {
|
||||
function makeTestClient(cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
|
||||
const store = new StubStore();
|
||||
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
request: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
|
||||
});
|
||||
@@ -160,8 +154,6 @@ describe("MegolmBackup", function() {
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockStorage;
|
||||
let sessionStore;
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
@@ -173,9 +165,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
@@ -188,7 +178,6 @@ describe("MegolmBackup", function() {
|
||||
|
||||
describe("backup", function() {
|
||||
let mockBaseApis;
|
||||
let realSetTimeout;
|
||||
|
||||
beforeEach(function() {
|
||||
mockBaseApis = {};
|
||||
@@ -206,14 +195,14 @@ describe("MegolmBackup", function() {
|
||||
// clobber the setTimeout function to run 100x faster.
|
||||
// ideally we would use lolex, but we have no oportunity
|
||||
// to tick the clock between the first try and the retry.
|
||||
realSetTimeout = global.setTimeout;
|
||||
global.setTimeout = function(f, n) {
|
||||
const realSetTimeout = global.setTimeout;
|
||||
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
|
||||
return realSetTimeout(f, n/100);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.setTimeout = realSetTimeout;
|
||||
jest.spyOn(global, 'setTimeout').mockRestore();
|
||||
});
|
||||
|
||||
it('automatically calls the key back up', function() {
|
||||
@@ -261,7 +250,7 @@ describe("MegolmBackup", function() {
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(sessionStore, cryptoStore);
|
||||
const client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
@@ -288,21 +277,21 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
.then(async () => {
|
||||
await client.enableKeyBackup({
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
@@ -311,17 +300,17 @@ describe("MegolmBackup", function() {
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
@@ -329,6 +318,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -339,7 +329,7 @@ describe("MegolmBackup", function() {
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(sessionStore, cryptoStore);
|
||||
const client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
@@ -369,22 +359,22 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
.then(async () => {
|
||||
await client.enableKeyBackup({
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: 1,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
|
||||
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
@@ -393,17 +383,17 @@ describe("MegolmBackup", function() {
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
@@ -411,6 +401,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -421,7 +412,7 @@ describe("MegolmBackup", function() {
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(sessionStore, cryptoStore);
|
||||
const client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
@@ -434,19 +425,12 @@ describe("MegolmBackup", function() {
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
await client.initCrypto();
|
||||
let privateKeys;
|
||||
client.uploadDeviceSigningKeys = async function(e) {return;};
|
||||
client.uploadKeySignatures = async function(e) {return;};
|
||||
client.on("crossSigning.saveCrossSigningKeys", function(e) {
|
||||
privateKeys = e;
|
||||
});
|
||||
client.on("crossSigning.getKey", function(e) {
|
||||
e.done(privateKeys[e.type]);
|
||||
});
|
||||
client.uploadDeviceSigningKeys = async function(e) {return {};};
|
||||
client.uploadKeySignatures = async function(e) {return { failures: {} };};
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let backupInfo;
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
@@ -463,33 +447,34 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
}
|
||||
backupInfo = data;
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
} else if (numCalls === 2) {
|
||||
expect(method).toBe("GET");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
resolve();
|
||||
return Promise.resolve(backupInfo);
|
||||
return Promise.resolve(backupInfo) as IAbortablePromise<any>;
|
||||
} else {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
}
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(numCalls).toBe(2);
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
it('retries when a backup fails', async function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
@@ -498,26 +483,17 @@ describe("MegolmBackup", function() {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
|
||||
const store = new StubStore();
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
request: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
});
|
||||
|
||||
@@ -531,70 +507,68 @@ describe("MegolmBackup", function() {
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
await client.initCrypto();
|
||||
await cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
);
|
||||
}
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(2);
|
||||
});
|
||||
txn);
|
||||
});
|
||||
|
||||
await client.enableKeyBackup({
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
) as IAbortablePromise<any>;
|
||||
}
|
||||
};
|
||||
return client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
});
|
||||
expect(numCalls).toBe(2);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -602,7 +576,7 @@ describe("MegolmBackup", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = makeTestClient(sessionStore, cryptoStore);
|
||||
client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
@@ -17,16 +17,45 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import anotherjson from 'another-json';
|
||||
import { PkSigning } from '@matrix-org/olm';
|
||||
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { HttpResponse, setHttpResponses } from '../../test-utils';
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
import { logger } from '../../../src/logger';
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client';
|
||||
import { CryptoEvent } from '../../../src/crypto';
|
||||
import { IDevice } from '../../../src/crypto/deviceinfo';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
const PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {},
|
||||
};
|
||||
|
||||
const filterResponse = function(userId) {
|
||||
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
return {
|
||||
method: "POST",
|
||||
path: filterPath,
|
||||
data: { filter_id: "f1lt3r" },
|
||||
};
|
||||
};
|
||||
|
||||
function setHttpResponses(httpBackend, responses) {
|
||||
responses.forEach(response => {
|
||||
httpBackend
|
||||
.when(response.method, response.path)
|
||||
.respond(200, response.data);
|
||||
});
|
||||
}
|
||||
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string, deviceId: string},
|
||||
options: Partial<ICreateClientOpts> = {},
|
||||
keys = {},
|
||||
) {
|
||||
function getCrossSigningKey(type) {
|
||||
return keys[type];
|
||||
}
|
||||
@@ -35,17 +64,17 @@ async function makeTestClient(userInfo, options, keys) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
if (!options) options = {};
|
||||
options.cryptoCallbacks = Object.assign(
|
||||
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
|
||||
);
|
||||
const client = (new TestClient(
|
||||
const testClient = new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
);
|
||||
const client = testClient.client;
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
return client;
|
||||
return { client, httpBackend: testClient.httpBackend };
|
||||
}
|
||||
|
||||
describe("Cross Signing", function() {
|
||||
@@ -59,27 +88,28 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
|
||||
alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => {
|
||||
await olmlib.verifySignature(
|
||||
alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => {};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T>() => ({} as T);
|
||||
// set Alice's cross-signing key
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
@@ -109,9 +139,9 @@ describe("Cross Signing", function() {
|
||||
error.httpStatus == 401;
|
||||
throw error;
|
||||
};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => { };
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T> => ({} as T);
|
||||
const authUploadDeviceSigningKeys = async func => await func({});
|
||||
|
||||
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
|
||||
@@ -127,14 +157,15 @@ describe("Cross Signing", function() {
|
||||
}
|
||||
}
|
||||
expect(bootstrapDidThrow).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
@@ -148,19 +179,23 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
// Alice verifies Bob's key
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = (...args) => {
|
||||
alice.uploadKeySignatures = async (...args) => {
|
||||
resolve(...args);
|
||||
return { failures: {} };
|
||||
};
|
||||
});
|
||||
await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true);
|
||||
// Alice should send a signature of Bob's key to the server
|
||||
await promise;
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should get cross-signing keys from sync", async function() {
|
||||
it.skip("should get cross-signing keys from sync", async function() {
|
||||
const masterKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
@@ -174,12 +209,12 @@ describe("Cross Signing", function() {
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
// will be called to sign our own device
|
||||
getCrossSigningKey: type => {
|
||||
getCrossSigningKey: async type => {
|
||||
if (type === 'master') {
|
||||
return masterKey;
|
||||
} else {
|
||||
@@ -191,7 +226,7 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.once("crossSigning.keysChanged", async (e) => {
|
||||
alice.once(CryptoEvent.KeysChanged, async (e) => {
|
||||
resolve(e);
|
||||
await alice.checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests: true,
|
||||
@@ -199,14 +234,14 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
const uploadSigsPromise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = jest.fn(async (content) => {
|
||||
const uploadSigsPromise = new Promise<void>((resolve, reject) => {
|
||||
alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => {
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
alice.crypto.olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
|
||||
);
|
||||
@@ -222,20 +257,26 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
keys: deviceInfo.keys,
|
||||
algorithms: deviceInfo.algorithms,
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
|
||||
olmlib.pkSign(
|
||||
aliceDevice as ISignedKey,
|
||||
selfSigningKey as unknown as PkSigning,
|
||||
"@alice:example.com",
|
||||
'',
|
||||
);
|
||||
|
||||
// feed sync result that includes master key, ssk, device key
|
||||
const responses = [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload",
|
||||
@@ -246,7 +287,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpResponse.filterResponse("@alice:example.com"),
|
||||
filterResponse("@alice:example.com"),
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -310,9 +351,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses, true, true);
|
||||
setHttpResponses(httpBackend, responses);
|
||||
|
||||
await alice.startClient();
|
||||
alice.startClient();
|
||||
httpBackend.flushAllExpected();
|
||||
|
||||
// once ssk is confirmed, device key should be trusted
|
||||
await keyChangePromise;
|
||||
@@ -328,14 +370,15 @@ describe("Cross Signing", function() {
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isTofu()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isVerified()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
@@ -345,7 +388,7 @@ describe("Cross Signing", function() {
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK = {
|
||||
const bobSSK: ICrossSigningKey = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -369,10 +412,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
const bobDevice = {
|
||||
const bobDeviceUnsigned = {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
@@ -381,11 +424,16 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const sig = bobSigning.sign(anotherjson.stringify(bobDevice));
|
||||
bobDevice.signatures = {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobPubkey]: sig,
|
||||
const sig = bobSigning.sign(anotherjson.stringify(bobDeviceUnsigned));
|
||||
const bobDevice: IDevice = {
|
||||
...bobDeviceUnsigned,
|
||||
signatures: {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobPubkey]: sig,
|
||||
},
|
||||
},
|
||||
verified: 0,
|
||||
known: false,
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
@@ -400,7 +448,7 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// Alice verifies Bob's SSK
|
||||
alice.uploadKeySignatures = () => {};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
|
||||
|
||||
// Bob's device key should be trusted
|
||||
@@ -412,19 +460,20 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys = {};
|
||||
const alice = await makeTestClient(
|
||||
it.skip("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys: Record<string, PkSigning> = {};
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
null,
|
||||
aliceKeys,
|
||||
);
|
||||
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
@@ -436,28 +485,29 @@ describe("Cross Signing", function() {
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.crypto.deviceList.once("userCrossSigningUpdated", (userId) => {
|
||||
const keyChangePromise = new Promise<void>((resolve, reject) => {
|
||||
alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
|
||||
if (userId === "@bob:example.com") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
keys: deviceInfo.keys,
|
||||
algorithms: deviceInfo.algorithms,
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
|
||||
const bobDevice = {
|
||||
const bobDeviceUnsigned = {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
@@ -466,15 +516,25 @@ describe("Cross Signing", function() {
|
||||
"curve25519:Dynabook": bobKeys.curve25519,
|
||||
},
|
||||
};
|
||||
const deviceStr = anotherjson.stringify(bobDevice);
|
||||
bobDevice.signatures = {
|
||||
"@bob:example.com": {
|
||||
"ed25519:Dynabook": bobOlmAccount.sign(deviceStr),
|
||||
const deviceStr = anotherjson.stringify(bobDeviceUnsigned);
|
||||
const bobDevice: IDevice = {
|
||||
...bobDeviceUnsigned,
|
||||
signatures: {
|
||||
"@bob:example.com": {
|
||||
"ed25519:Dynabook": bobOlmAccount.sign(deviceStr),
|
||||
},
|
||||
},
|
||||
verified: 0,
|
||||
known: false,
|
||||
};
|
||||
olmlib.pkSign(bobDevice, selfSigningKey, "@bob:example.com");
|
||||
olmlib.pkSign(
|
||||
bobDevice,
|
||||
selfSigningKey as unknown as PkSigning,
|
||||
"@bob:example.com",
|
||||
'',
|
||||
);
|
||||
|
||||
const bobMaster = {
|
||||
const bobMaster: ICrossSigningKey = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
@@ -482,7 +542,7 @@ describe("Cross Signing", function() {
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
},
|
||||
};
|
||||
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com");
|
||||
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com", '');
|
||||
|
||||
// Alice downloads Bob's keys
|
||||
// - device key
|
||||
@@ -490,7 +550,7 @@ describe("Cross Signing", function() {
|
||||
// - master key signed by her usk (pretend that it was signed by another
|
||||
// of Alice's devices)
|
||||
const responses = [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload",
|
||||
@@ -501,7 +561,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpResponse.filterResponse("@alice:example.com"),
|
||||
filterResponse("@alice:example.com"),
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -560,10 +620,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses);
|
||||
|
||||
await alice.startClient();
|
||||
setHttpResponses(httpBackend, responses);
|
||||
|
||||
alice.startClient();
|
||||
httpBackend.flushAllExpected();
|
||||
await keyChangePromise;
|
||||
|
||||
// Bob's device key should be trusted
|
||||
@@ -575,14 +635,15 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
@@ -593,7 +654,7 @@ describe("Cross Signing", function() {
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK = {
|
||||
const bobSSK: ICrossSigningKey = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -617,8 +678,8 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
const bobDevice = {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -630,7 +691,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
Dynabook: bobDevice as unknown as IDevice,
|
||||
});
|
||||
// Bob's device key should be untrusted
|
||||
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
@@ -644,14 +705,15 @@ describe("Cross Signing", function() {
|
||||
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
@@ -660,7 +722,7 @@ describe("Cross Signing", function() {
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK = {
|
||||
const bobSSK: ICrossSigningKey = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -684,10 +746,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
const bobDevice = {
|
||||
const bobDeviceUnsigned = {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
@@ -696,16 +758,23 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const bobDeviceString = anotherjson.stringify(bobDevice);
|
||||
const bobDeviceString = anotherjson.stringify(bobDeviceUnsigned);
|
||||
const sig = bobSigning.sign(bobDeviceString);
|
||||
bobDevice.signatures = {};
|
||||
bobDevice.signatures["@bob:example.com"] = {};
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
|
||||
const bobDevice: IDevice = {
|
||||
...bobDeviceUnsigned,
|
||||
verified: 0,
|
||||
known: false,
|
||||
signatures: {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobPubkey]: sig,
|
||||
},
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Alice verifies Bob's SSK
|
||||
alice.uploadKeySignatures = () => {};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
|
||||
|
||||
// Bob's device key should be trusted
|
||||
@@ -720,7 +789,7 @@ describe("Cross Signing", function() {
|
||||
const bobSigning2 = new global.Olm.PkSigning();
|
||||
const bobPrivkey2 = bobSigning2.generate_seed();
|
||||
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
|
||||
const bobSSK2 = {
|
||||
const bobSSK2: ICrossSigningKey = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -744,8 +813,8 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
self_signing: bobSSK2,
|
||||
},
|
||||
firstUse: 0,
|
||||
unsigned: {},
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
// Bob's and his device should be untrusted
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
@@ -757,7 +826,7 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
|
||||
|
||||
// Alice verifies Bob's SSK
|
||||
alice.uploadKeySignatures = () => {};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true);
|
||||
|
||||
// Bob should be trusted but not his device
|
||||
@@ -780,29 +849,31 @@ describe("Cross Signing", function() {
|
||||
|
||||
const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should offer to upgrade device verifications to cross-signing", async function() {
|
||||
let upgradeResolveFunc;
|
||||
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
shouldUpgradeDeviceVerifications: (verifs) => {
|
||||
shouldUpgradeDeviceVerifications: async (verifs) => {
|
||||
expect(verifs.users["@bob:example.com"]).toBeDefined();
|
||||
upgradeResolveFunc();
|
||||
return ["@bob:example.com"];
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
);
|
||||
const bob = await makeTestClient(
|
||||
const { client: bob } = await makeTestClient(
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
);
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Bob's cross-signing key
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
@@ -821,8 +892,8 @@ describe("Cross Signing", function() {
|
||||
bob.crypto.crossSigningInfo.toStorage(),
|
||||
);
|
||||
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// when alice sets up cross-signing, she should notice that bob's
|
||||
// cross-signing key is signed by his Dynabook, which alice has
|
||||
// verified, and ask if the device verification should be upgraded to a
|
||||
@@ -848,14 +919,159 @@ describe("Cross Signing", function() {
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice.crypto.deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||
alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
|
||||
await new Promise((resolve) => {
|
||||
alice.crypto.on("userTrustStatusChanged", resolve);
|
||||
alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve);
|
||||
});
|
||||
await upgradePromise;
|
||||
|
||||
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobTrust3.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it(
|
||||
"should observe that our own device is cross-signed, even if this device doesn't trust the key",
|
||||
async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: ICrossSigningKey = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + alicePubkey]: alicePubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||
aliceSSK.signatures = {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys, but doesn't trust them yet
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: aliceSSK,
|
||||
},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
|
||||
// Alice has a second device that's cross-signed
|
||||
const aliceDeviceId = 'Dynabook';
|
||||
const aliceUnsignedDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: aliceDeviceId,
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const sig = aliceSigning.sign(anotherjson.stringify(aliceUnsignedDevice));
|
||||
const aliceCrossSignedDevice: IDevice = {
|
||||
...aliceUnsignedDevice,
|
||||
verified: 0,
|
||||
known: false,
|
||||
signatures: {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + alicePubkey]: sig,
|
||||
},
|
||||
} };
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
[aliceDeviceId]: aliceCrossSignedDevice,
|
||||
});
|
||||
|
||||
// We don't trust the cross-signing keys yet...
|
||||
expect(
|
||||
alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified(),
|
||||
).toBeFalsy();
|
||||
// ... but we do acknowledge that the device is signed by them
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy();
|
||||
alice.stopClient();
|
||||
},
|
||||
);
|
||||
|
||||
it("should observe that our own device isn't cross-signed", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: ICrossSigningKey = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + alicePubkey]: alicePubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||
aliceSSK.signatures = {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: aliceSSK,
|
||||
},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
|
||||
const deviceId = "Dynabook";
|
||||
const aliceNotCrossSignedDevice: IDevice = {
|
||||
verified: 0,
|
||||
known: false,
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
[deviceId]: aliceNotCrossSignedDevice,
|
||||
});
|
||||
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import { IRecoveryKey } from '../../../src/crypto/api';
|
||||
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning';
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
// needs to be phased out and replaced with bootstrapSecretStorage,
|
||||
// but that is doing too much extra stuff for it to be an easy transition.
|
||||
export async function resetCrossSigningKeys(client, {
|
||||
level,
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
export async function resetCrossSigningKeys(
|
||||
client,
|
||||
{ level }: { level?: CrossSigningLevel} = {},
|
||||
): Promise<void> {
|
||||
const crypto = client.crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
|
||||
@@ -26,18 +28,18 @@ export async function resetCrossSigningKeys(client, {
|
||||
crypto.crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto.baseApis.emit("crossSigning.keysChanged", {});
|
||||
crypto.emit("crossSigning.keysChanged", {});
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey() {
|
||||
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
keyInfo: { pubkey: storagePublicKey, key: undefined },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
+21
-19
@@ -18,11 +18,11 @@ import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
@@ -43,13 +43,15 @@ const requests = [
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
||||
store._backend = new MemoryCryptoStore();
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
// @ts-ignore set private properties
|
||||
store.backend = new MemoryCryptoStore();
|
||||
// @ts-ignore
|
||||
store.backendPromise = Promise.resolve(store.backend);
|
||||
return store;
|
||||
}],
|
||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||
@@ -64,22 +66,22 @@ describe.each([
|
||||
});
|
||||
|
||||
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
|
||||
async () => {
|
||||
const r = await
|
||||
async () => {
|
||||
const r = await
|
||||
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
|
||||
expect(r).toHaveLength(2);
|
||||
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
|
||||
expect(r).toContainEqual(e);
|
||||
expect(r).toHaveLength(2);
|
||||
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
|
||||
expect(r).toContainEqual(e);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
async () => {
|
||||
const r =
|
||||
async () => {
|
||||
const r =
|
||||
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
@@ -23,17 +23,19 @@ import { makeTestClients } from './verification/util';
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
import { ICreateClientOpts } from '../../../src/client';
|
||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
logger.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
|
||||
async function makeTestClient(userInfo, options) {
|
||||
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
@@ -47,7 +49,7 @@ async function makeTestClient(userInfo, options) {
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
client.crypto.downloadKeys = async function() {};
|
||||
jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({});
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -55,7 +57,7 @@ async function makeTestClient(userInfo, options) {
|
||||
// Wrapper around pkSign to return a signed object. pkSign returns the
|
||||
// signature, rather than the signed object.
|
||||
function sign(obj, key, userId) {
|
||||
olmlib.pkSign(obj, key, userId);
|
||||
olmlib.pkSign(obj, key, userId, '');
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
};
|
||||
|
||||
const getKey = jest.fn(e => {
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
||||
return ['abc', key];
|
||||
});
|
||||
@@ -94,7 +96,7 @@ describe("Secrets", function() {
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => signingKey,
|
||||
getCrossSigningKey: async t => signingKey,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
@@ -105,17 +107,19 @@ describe("Secrets", function() {
|
||||
|
||||
const secretStorage = alice.crypto.secretStorage;
|
||||
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
jest.spyOn(alice, 'setAccountData').mockImplementation(
|
||||
async function(eventType, contents, callback) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
if (callback) {
|
||||
callback(undefined, undefined);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
@@ -137,6 +141,7 @@ describe("Secrets", function() {
|
||||
expect(await secretStorage.get("foo")).toBe("bar");
|
||||
|
||||
expect(getKey).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should throw if given a key that doesn't exist", async function() {
|
||||
@@ -151,6 +156,7 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
@@ -163,12 +169,13 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should encrypt with default key if keys is null", async function() {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn(e => {
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
expect(Object.keys(e.keys)).toEqual([newKeyId]);
|
||||
return [newKeyId, key];
|
||||
});
|
||||
@@ -191,11 +198,12 @@ describe("Secrets", function() {
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
return {};
|
||||
};
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, { pubkey: undefined, key: undefined },
|
||||
);
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
// which won't happen in the test setup
|
||||
@@ -204,6 +212,7 @@ describe("Secrets", function() {
|
||||
|
||||
const accountData = alice.getAccountData('foo');
|
||||
expect(accountData.getContent().encrypted).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt if no keys given and no default key", async function() {
|
||||
@@ -216,10 +225,11 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should request secrets from other clients", async function() {
|
||||
const [osborne2, vax] = await makeTestClients(
|
||||
const [[osborne2, vax], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@alice:example.com", deviceId: "VAX" },
|
||||
@@ -274,6 +284,9 @@ describe("Secrets", function() {
|
||||
const secret = await request.promise;
|
||||
|
||||
expect(secret).toBe("bar");
|
||||
osborne2.stop();
|
||||
vax.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
describe("bootstrap", function() {
|
||||
@@ -299,7 +312,7 @@ describe("Secrets", function() {
|
||||
it("bootstraps when no storage or cross-signing keys locally", async function() {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn(e => {
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
return [Object.keys(e.keys)[0], key];
|
||||
});
|
||||
|
||||
@@ -314,8 +327,8 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
@@ -325,10 +338,11 @@ describe("Secrets", function() {
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
@@ -341,6 +355,7 @@ describe("Secrets", function() {
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function() {
|
||||
@@ -407,10 +422,11 @@ describe("Secrets", function() {
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("adds passphrase checking if it's lacking", async function() {
|
||||
let crossSigningKeys = {
|
||||
let crossSigningKeys: Record<string, Uint8Array> = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
@@ -422,9 +438,9 @@ describe("Secrets", function() {
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
getCrossSigningKey: async t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
getSecretStorageKey: async ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
@@ -480,6 +496,8 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -520,14 +538,15 @@ describe("Secrets", function() {
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
|
||||
.getContent();
|
||||
.getContent() as ISecretStorageKeyInfo;
|
||||
expect(keyInfo.algorithm)
|
||||
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
@@ -539,9 +558,10 @@ describe("Secrets", function() {
|
||||
expect(keyInfo).toHaveProperty("mac");
|
||||
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
|
||||
.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
it("fixes backup keys in the wrong format", async function() {
|
||||
let crossSigningKeys = {
|
||||
let crossSigningKeys: Record<string, Uint8Array> = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
@@ -553,9 +573,9 @@ describe("Secrets", function() {
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
getCrossSigningKey: async t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
getSecretStorageKey: async ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
@@ -620,6 +640,8 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -660,15 +682,17 @@ describe("Secrets", function() {
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
expect(backupKey.encrypted).toHaveProperty("key_id");
|
||||
expect(await alice.getSecret("m.megolm_backup.v1"))
|
||||
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("verification request integration tests with crypto layer", function()
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
@@ -69,7 +69,7 @@ describe("verification request integration tests with crypto layer", function()
|
||||
bobVerifier.verify();
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
bobVerifier._endTimer();
|
||||
bobVerifier.endTimer();
|
||||
});
|
||||
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
@@ -77,6 +77,10 @@ describe("verification request integration tests with crypto layer", function()
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier._endTimer();
|
||||
aliceVerifier.endTimer();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,9 +75,10 @@ describe("SAS verification", function() {
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
let clearTestClientTimeouts;
|
||||
|
||||
beforeEach(async () => {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
@@ -178,6 +179,8 @@ describe("SAS verification", function() {
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
it("should verify a key", async () => {
|
||||
@@ -215,7 +218,7 @@ describe("SAS verification", function() {
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
|
||||
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
@@ -227,6 +230,62 @@ describe("SAS verification", function() {
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be able to verify using the old base64", async () => {
|
||||
// pretend that Alice can only understand the old (incorrect) base64
|
||||
// encoding, and make sure that she can still verify with Bob
|
||||
let macMethod;
|
||||
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
|
||||
alice.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.start") {
|
||||
// Note: this modifies not only the message that Bob
|
||||
// receives, but also the copy of the message that Alice
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()][bob.client.deviceId]
|
||||
.message_authentication_codes = ['hkdf-hmac-sha256'];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
|
||||
alice.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@bob:example.com": BOB_DEVICES,
|
||||
},
|
||||
});
|
||||
bob.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": ALICE_DEVICES,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be able to verify using the old MAC", async () => {
|
||||
// pretend that Alice can only understand the old (incorrect) MAC,
|
||||
// and make sure that she can still verify with Bob
|
||||
@@ -334,7 +393,7 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
@@ -377,6 +436,10 @@ describe("SAS verification", function() {
|
||||
.not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
describe("verification in DM", function() {
|
||||
@@ -386,9 +449,10 @@ describe("SAS verification", function() {
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
let clearTestClientTimeouts;
|
||||
|
||||
beforeEach(async function() {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
@@ -488,6 +552,8 @@ describe("SAS verification", function() {
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("self-verifications", () => {
|
||||
undefined, // startEvent
|
||||
request,
|
||||
);
|
||||
verification._resolve = () => undefined;
|
||||
verification.resolve = () => undefined;
|
||||
|
||||
const result = await verification.done();
|
||||
|
||||
|
||||
@@ -15,13 +15,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
import { logger } from '../../../../src/logger';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const timeouts = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
@@ -65,7 +67,7 @@ export async function makeTestClients(userInfos, options) {
|
||||
},
|
||||
}));
|
||||
|
||||
setImmediate(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
for (const tc of clients) {
|
||||
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
|
||||
logger.log("sending remote echo!!");
|
||||
@@ -76,6 +78,8 @@ export async function makeTestClients(userInfos, options) {
|
||||
}
|
||||
});
|
||||
|
||||
timeouts.push(timeout);
|
||||
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
};
|
||||
|
||||
@@ -102,7 +106,11 @@ export async function makeTestClients(userInfos, options) {
|
||||
|
||||
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
|
||||
|
||||
return clients;
|
||||
const destroy = () => {
|
||||
timeouts.forEach((t) => clearTimeout(t));
|
||||
};
|
||||
|
||||
return [clients, destroy];
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
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 { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } from "../../src";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { IStore } from "../../src/store";
|
||||
|
||||
describe("eventMapperFor", function() {
|
||||
let rooms: Room[] = [];
|
||||
|
||||
const userId = "@test:example.org";
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {} as any, // NOP
|
||||
store: {
|
||||
getRoom(roomId: string): Room | null {
|
||||
return rooms.find(r => r.roomId === roomId);
|
||||
},
|
||||
} as IStore,
|
||||
scheduler: {
|
||||
setProcessFunction: jest.fn(),
|
||||
} as unknown as MatrixScheduler,
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
rooms = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const room = new Room(roomId, client, userId);
|
||||
rooms.push(room);
|
||||
|
||||
const mapper = eventMapperFor(client, {
|
||||
preventReEmit: true,
|
||||
decrypt: false,
|
||||
});
|
||||
|
||||
const eventId = "$event1:server";
|
||||
const eventDefinition = {
|
||||
type: "m.room.message",
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
content: {
|
||||
body: "body",
|
||||
},
|
||||
unsigned: {},
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const event = mapper(eventDefinition);
|
||||
expect(event).toBeInstanceOf(MatrixEvent);
|
||||
|
||||
room.addLiveEvents([event]);
|
||||
expect(room.findEventById(eventId)).toBe(event);
|
||||
|
||||
const event2 = mapper(eventDefinition);
|
||||
expect(event).toBe(event2);
|
||||
});
|
||||
|
||||
it("should not de-duplicate state events due to directionality of sentinel members", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const room = new Room(roomId, client, userId);
|
||||
rooms.push(room);
|
||||
|
||||
const mapper = eventMapperFor(client, {
|
||||
preventReEmit: true,
|
||||
decrypt: false,
|
||||
});
|
||||
|
||||
const eventId = "$event1:server";
|
||||
const eventDefinition = {
|
||||
type: "m.room.name",
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
content: {
|
||||
name: "Room name",
|
||||
},
|
||||
unsigned: {},
|
||||
event_id: eventId,
|
||||
state_key: "",
|
||||
};
|
||||
|
||||
const event = mapper(eventDefinition);
|
||||
expect(event).toBeInstanceOf(MatrixEvent);
|
||||
|
||||
room.oldState.setStateEvents([event]);
|
||||
room.currentState.setStateEvents([event]);
|
||||
room.addLiveEvents([event]);
|
||||
expect(room.findEventById(eventId)).toBe(event);
|
||||
|
||||
const event2 = mapper(eventDefinition);
|
||||
expect(event).not.toBe(event2);
|
||||
});
|
||||
|
||||
it("should decrypt appropriately", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const room = new Room(roomId, client, userId);
|
||||
rooms.push(room);
|
||||
|
||||
const eventId = "$event1:server";
|
||||
const eventDefinition = {
|
||||
type: "m.room.encrypted",
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
content: {
|
||||
ciphertext: "",
|
||||
},
|
||||
unsigned: {},
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
|
||||
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
||||
|
||||
const mapper = eventMapperFor(client, {
|
||||
decrypt: true,
|
||||
});
|
||||
const event = mapper(eventDefinition);
|
||||
expect(event).toBeInstanceOf(MatrixEvent);
|
||||
expect(decryptEventIfNeededSpy).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it("should configure re-emitter appropriately", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const room = new Room(roomId, client, userId);
|
||||
rooms.push(room);
|
||||
|
||||
const eventId = "$event1:server";
|
||||
const eventDefinition = {
|
||||
type: "m.room.message",
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
content: {
|
||||
body: "body",
|
||||
},
|
||||
unsigned: {},
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const evListener = jest.fn();
|
||||
client.on(MatrixEventEvent.Replaced, evListener);
|
||||
|
||||
const noReEmitMapper = eventMapperFor(client, {
|
||||
preventReEmit: true,
|
||||
});
|
||||
const event1 = noReEmitMapper(eventDefinition);
|
||||
expect(event1).toBeInstanceOf(MatrixEvent);
|
||||
event1.emit(MatrixEventEvent.Replaced, event1);
|
||||
expect(evListener).not.toHaveBeenCalled();
|
||||
|
||||
const reEmitMapper = eventMapperFor(client, {
|
||||
preventReEmit: false,
|
||||
});
|
||||
const event2 = reEmitMapper(eventDefinition);
|
||||
expect(event2).toBeInstanceOf(MatrixEvent);
|
||||
event2.emit(MatrixEventEvent.Replaced, event2);
|
||||
expect(evListener.mock.calls[0][0]).toEqual(event2);
|
||||
|
||||
expect(event1).not.toBe(event2); // the event wasn't added to a room so de-duplication wouldn't occur
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
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 * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
Room,
|
||||
DuplicateStrategy,
|
||||
} from '../../src';
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
describe('EventTimelineSet', () => {
|
||||
const roomId = '!foo:bar';
|
||||
const userA = "@alice:bar";
|
||||
|
||||
let room: Room;
|
||||
let eventTimeline: EventTimeline;
|
||||
let eventTimelineSet: EventTimelineSet;
|
||||
let client: MatrixClient;
|
||||
|
||||
let messageEvent: MatrixEvent;
|
||||
let replyEvent: MatrixEvent;
|
||||
|
||||
const itShouldReturnTheRelatedEvents = () => {
|
||||
it('should return the related events', () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId(),
|
||||
"m.in_reply_to",
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
expect(relations).toBeDefined();
|
||||
expect(relations.getRelations().length).toBe(1);
|
||||
expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId());
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, 'MatrixClient');
|
||||
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
|
||||
room = new Room(roomId, client, userA);
|
||||
eventTimelineSet = new EventTimelineSet(room);
|
||||
eventTimeline = new EventTimeline(eventTimelineSet);
|
||||
messageEvent = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: 'Hi!',
|
||||
event: true,
|
||||
});
|
||||
replyEvent = utils.mkReplyMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: 'Hoo!',
|
||||
event: true,
|
||||
replyToMessage: messageEvent,
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLiveEvent', () => {
|
||||
it("Adds event to the live timeline in the timeline set", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addLiveEvent(messageEvent);
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("should replace a timeline event if dupe strategy is 'replace'", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addLiveEvent(messageEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(1);
|
||||
|
||||
// make a duplicate
|
||||
const duplicateMessageEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "dupe", event: true,
|
||||
});
|
||||
duplicateMessageEvent.event.event_id = messageEvent.getId();
|
||||
|
||||
// Adding the duplicate event should replace the `messageEvent`
|
||||
// because it has the same `event_id` and duplicate strategy is
|
||||
// replace.
|
||||
eventTimelineSet.addLiveEvent(duplicateMessageEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
});
|
||||
|
||||
const eventsInLiveTimeline = liveTimeline.getEvents();
|
||||
expect(eventsInLiveTimeline.length).toStrictEqual(1);
|
||||
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
|
||||
});
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Replace, false)).not.toThrow();
|
||||
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Ignore, true)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEventToTimeline', () => {
|
||||
it("Adds event to timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(
|
||||
messageEvent,
|
||||
liveTimeline,
|
||||
true,
|
||||
);
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(
|
||||
messageEvent,
|
||||
liveTimeline,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateRelations', () => {
|
||||
describe('with unencrypted events', () => {
|
||||
beforeEach(() => {
|
||||
eventTimelineSet.addEventsToTimeline(
|
||||
[
|
||||
messageEvent,
|
||||
replyEvent,
|
||||
],
|
||||
true,
|
||||
eventTimeline,
|
||||
'foo',
|
||||
);
|
||||
});
|
||||
|
||||
itShouldReturnTheRelatedEvents();
|
||||
});
|
||||
|
||||
describe('with events to be decrypted', () => {
|
||||
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
|
||||
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, 'shouldAttemptDecryption');
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
|
||||
|
||||
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, 'shouldAttemptDecryption');
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
|
||||
|
||||
eventTimelineSet.addEventsToTimeline(
|
||||
[
|
||||
messageEvent,
|
||||
replyEvent,
|
||||
],
|
||||
true,
|
||||
eventTimeline,
|
||||
'foo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return the related events', () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId(),
|
||||
"m.in_reply_to",
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
expect(relations).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('after decryption', () => {
|
||||
beforeEach(() => {
|
||||
// simulate decryption failure once
|
||||
messageEventIsDecryptionFailureSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy.mockReturnValue(true);
|
||||
|
||||
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
|
||||
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
|
||||
|
||||
// simulate decryption
|
||||
messageEventIsDecryptionFailureSpy.mockReturnValue(false);
|
||||
replyEventIsDecryptionFailureSpy.mockReturnValue(false);
|
||||
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(false);
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(false);
|
||||
|
||||
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
|
||||
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
|
||||
});
|
||||
|
||||
itShouldReturnTheRelatedEvents();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("canContain", () => {
|
||||
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
content: {
|
||||
"body": "Thread response :: " + Math.random(),
|
||||
"m.relates_to": {
|
||||
"event_id": root.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": root.getId(),
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}, room.client);
|
||||
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("should throw if timeline set has no room", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(undefined, {}, client);
|
||||
expect(() => eventTimelineSet.canContain(messageEvent)).toThrowError();
|
||||
});
|
||||
|
||||
it("should return false if timeline set is for thread but event is not threaded", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
|
||||
expect(eventTimelineSet.canContain(replyEvent)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if timeline set it for thread but event it for a different thread", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
|
||||
const event = mkThreadResponse(replyEvent);
|
||||
expect(eventTimelineSet.canContain(event)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if timeline set is not for a thread but event is a thread response", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client);
|
||||
const event = mkThreadResponse(replyEvent);
|
||||
expect(eventTimelineSet.canContain(event)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true if the timeline set is not for a thread and the event is a thread root", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client);
|
||||
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return true if the timeline set is for a thread and the event is its thread root", () => {
|
||||
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
|
||||
messageEvent.setThread(thread);
|
||||
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return true if the timeline set is for a thread and the event is a response to it", () => {
|
||||
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
|
||||
messageEvent.setThread(thread);
|
||||
const event = mkThreadResponse(messageEvent);
|
||||
expect(eventTimelineSet.canContain(event)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
|
||||
@@ -50,9 +50,11 @@ describe("EventTimeline", function() {
|
||||
timeline.initialiseState(events);
|
||||
expect(timeline.startState.setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
expect(timeline.endState.setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -73,7 +75,7 @@ describe("EventTimeline", function() {
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
}).not.toThrow();
|
||||
timeline.addEvent(event, false);
|
||||
timeline.addEvent(event, { toStartOfTimeline: false });
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
}).toThrow();
|
||||
@@ -149,9 +151,9 @@ describe("EventTimeline", function() {
|
||||
];
|
||||
|
||||
it("should be able to add events to the end", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[0]);
|
||||
@@ -159,9 +161,9 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("should be able to add events to the start", function() {
|
||||
timeline.addEvent(events[0], true);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], true);
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[1]);
|
||||
@@ -203,9 +205,9 @@ describe("EventTimeline", function() {
|
||||
content: { name: "Old Room Name" },
|
||||
});
|
||||
|
||||
timeline.addEvent(newEv, false);
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
||||
expect(newEv.sender).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, true);
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
||||
expect(oldEv.sender).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
@@ -242,9 +244,9 @@ describe("EventTimeline", function() {
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
|
||||
});
|
||||
timeline.addEvent(newEv, false);
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, true);
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
@@ -262,13 +264,13 @@ describe("EventTimeline", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]]);
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
@@ -291,13 +293,13 @@ describe("EventTimeline", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], true);
|
||||
timeline.addEvent(events[1], true);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]]);
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
@@ -305,6 +307,11 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
|
||||
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState() })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeEvent", function() {
|
||||
@@ -324,8 +331,8 @@ describe("EventTimeline", function() {
|
||||
];
|
||||
|
||||
it("should remove events", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
|
||||
let ev = timeline.removeEvent(events[0].getId());
|
||||
@@ -338,9 +345,9 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("should update baseIndex", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], true);
|
||||
timeline.addEvent(events[2], false);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
expect(timeline.getEvents().length).toEqual(3);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
@@ -358,11 +365,11 @@ describe("EventTimeline", function() {
|
||||
// further addEvent(ev, false) calls made the index increase.
|
||||
it("should not make baseIndex assplode when removing the last event",
|
||||
function() {
|
||||
timeline.addEvent(events[0], true);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.removeEvent(events[0].getId());
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[2], false);
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundaction C.I.C.
|
||||
Copyright 2019 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.
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { RelationType } from "../../src";
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils/test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
related_by_senders: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootNotParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
user: '@someone-else:server.org',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
count: 2,
|
||||
current_user_participated: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
related_by_senders: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: '@someone-else:server.org',
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
related_by_rel_types: ["m.thread"],
|
||||
});
|
||||
|
||||
const referenceRelationEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Reference]: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
related_by_rel_types: ["m.thread"],
|
||||
});
|
||||
|
||||
const threadRootEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const eventWithMultipleRelations = mkEvent({
|
||||
"type": "m.room.message",
|
||||
"content": {},
|
||||
"unsigned": {
|
||||
"m.relations": {
|
||||
"testtesttest": {},
|
||||
"m.annotation": {
|
||||
"chunk": [
|
||||
{
|
||||
"type": "m.reaction",
|
||||
"key": "🤫",
|
||||
"count": 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
"m.thread": {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"room": 'roomId',
|
||||
"event": true,
|
||||
});
|
||||
|
||||
const noMatchEvent = mkEvent({
|
||||
"type": "m.room.message",
|
||||
"content": {},
|
||||
"unsigned": {
|
||||
"m.relations": {
|
||||
"testtesttest": {},
|
||||
},
|
||||
},
|
||||
"room": 'roomId',
|
||||
"event": true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootEvent)).toBe(true);
|
||||
expect(filter.check(eventWithMultipleRelations)).toBe(true);
|
||||
expect(filter.check(noMatchEvent)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,8 @@ limitations under the License.
|
||||
import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth } from "../../src/interactive-auth";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
@@ -172,4 +174,107 @@ describe("InteractiveAuth", function() {
|
||||
expect(error.message).toBe('No appropriate authentication flow found');
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestEmailToken", () => {
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("passes errors through", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => {
|
||||
throw new Error("unspecific network error");
|
||||
});
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error");
|
||||
});
|
||||
|
||||
it("only starts one request at a time", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]);
|
||||
expect(requestEmailToken).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("stores result in email sid", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const sid = randomString(24);
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(ia.getEmailSid()).toEqual(sid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
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 { makeLocationContent, parseLocationEvent } from "../../src/content-helpers";
|
||||
import {
|
||||
M_ASSET,
|
||||
LocationAssetType,
|
||||
M_LOCATION,
|
||||
M_TIMESTAMP,
|
||||
LocationEventWireContent,
|
||||
} from "../../src/@types/location";
|
||||
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
|
||||
import { MsgType } from "../../src/@types/event";
|
||||
|
||||
describe("Location", function() {
|
||||
const defaultContent = {
|
||||
"body": "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
|
||||
"msgtype": "m.location",
|
||||
"geo_uri": "geo:-36.24484561954707,175.46884959563613;u=10",
|
||||
[M_LOCATION.name]: { "uri": "geo:-36.24484561954707,175.46884959563613;u=10", "description": null },
|
||||
[M_ASSET.name]: { "type": "m.self" },
|
||||
[TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
|
||||
[M_TIMESTAMP.name]: 1646823712443,
|
||||
} as any;
|
||||
|
||||
const backwardsCompatibleEventContent = { ...defaultContent };
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const { body, msgtype, geo_uri, ...modernProperties } = defaultContent;
|
||||
const modernEventContent = { ...modernProperties };
|
||||
|
||||
const legacyEventContent = {
|
||||
// eslint-disable-next-line camelcase
|
||||
body, msgtype, geo_uri,
|
||||
} as LocationEventWireContent;
|
||||
|
||||
it("should create a valid location with defaults", function() {
|
||||
const loc = makeLocationContent(undefined, "geo:foo", 134235435);
|
||||
expect(loc.body).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z');
|
||||
expect(loc.msgtype).toEqual(MsgType.Location);
|
||||
expect(loc.geo_uri).toEqual("geo:foo");
|
||||
expect(M_LOCATION.findIn(loc)).toEqual({
|
||||
uri: "geo:foo",
|
||||
description: undefined,
|
||||
});
|
||||
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z');
|
||||
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
it("should create a valid location with explicit properties", function() {
|
||||
const loc = makeLocationContent(
|
||||
undefined, "geo:bar", 134235436, "desc", LocationAssetType.Pin);
|
||||
|
||||
expect(loc.body).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
|
||||
expect(loc.msgtype).toEqual(MsgType.Location);
|
||||
expect(loc.geo_uri).toEqual("geo:bar");
|
||||
expect(M_LOCATION.findIn(loc)).toEqual({
|
||||
uri: "geo:bar",
|
||||
description: "desc",
|
||||
});
|
||||
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
|
||||
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
|
||||
it('parses backwards compatible event correctly', () => {
|
||||
const eventContent = parseLocationEvent(backwardsCompatibleEventContent);
|
||||
|
||||
expect(eventContent).toEqual(backwardsCompatibleEventContent);
|
||||
});
|
||||
|
||||
it('parses modern correctly', () => {
|
||||
const eventContent = parseLocationEvent(modernEventContent);
|
||||
|
||||
expect(eventContent).toEqual(backwardsCompatibleEventContent);
|
||||
});
|
||||
|
||||
it('parses legacy event correctly', () => {
|
||||
const eventContent = parseLocationEvent(legacyEventContent);
|
||||
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[M_TIMESTAMP.name]: timestamp,
|
||||
...expectedResult
|
||||
} = defaultContent;
|
||||
expect(eventContent).toEqual({
|
||||
...expectedResult,
|
||||
[M_LOCATION.name]: {
|
||||
...expectedResult[M_LOCATION.name],
|
||||
description: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// don't infer timestamp from legacy event
|
||||
expect(M_TIMESTAMP.findIn(eventContent)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,705 +0,0 @@
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
EventType,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
UNSTABLE_MSC3088_ENABLED,
|
||||
UNSTABLE_MSC3088_PURPOSE,
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Preset } from "../../src/@types/partials";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const userId = "@alice:bar";
|
||||
const identityServerUrl = "https://identity.server";
|
||||
const identityServerDomain = "identity.server";
|
||||
let client;
|
||||
let store;
|
||||
let scheduler;
|
||||
|
||||
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
const PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {},
|
||||
};
|
||||
|
||||
const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
|
||||
const FILTER_RESPONSE = {
|
||||
method: "POST",
|
||||
path: FILTER_PATH,
|
||||
data: { filter_id: "f1lt3r" },
|
||||
};
|
||||
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: { events: [] },
|
||||
rooms: {},
|
||||
};
|
||||
|
||||
const SYNC_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
let httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
// method: "GET",
|
||||
// path: "/initialSync",
|
||||
// data: {},
|
||||
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
|
||||
// expectBody: {} // additional expects on the body
|
||||
// expectQueryParams: {} // additional expects on query params
|
||||
// thenCall: function(){} // function to call *AFTER* returning response.
|
||||
// }
|
||||
// items are popped off when processed and block if no items left.
|
||||
];
|
||||
let acceptKeepalives;
|
||||
let pendingLookup = null;
|
||||
function httpReq(cb, method, path, qp, data, prefix) {
|
||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const next = httpLookups.shift();
|
||||
const logLine = (
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
logger.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (pendingLookup) {
|
||||
if (pendingLookup.method === method && pendingLookup.path === path) {
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path,
|
||||
);
|
||||
}
|
||||
pendingLookup = {
|
||||
promise: new Promise(() => {}),
|
||||
method: method,
|
||||
path: path,
|
||||
};
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
logger.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response",
|
||||
);
|
||||
if (next.expectBody) {
|
||||
expect(next.expectBody).toEqual(data);
|
||||
}
|
||||
if (next.expectQueryParams) {
|
||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
||||
});
|
||||
}
|
||||
|
||||
if (next.thenCall) {
|
||||
process.nextTick(next.thenCall, 0); // next tick so we return first.
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
});
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
|
||||
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
|
||||
"getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
});
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = [
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
acceptKeepalives = true;
|
||||
pendingLookup = null;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
// need to re-stub the requests with NOPs because there are no guarantees
|
||||
// clients from previous tests will be GC'd before the next test. This
|
||||
// means they may call /events and then fail an expect() which will fail
|
||||
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
||||
// promises to stop the client from continuing to run.
|
||||
client.http.authedRequest.mockImplementation(function() {
|
||||
return new Promise(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
const userId = "@test:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
const roomName = "Test Tree";
|
||||
const mockRoom = {};
|
||||
const fn = jest.fn().mockImplementation((opts) => {
|
||||
expect(opts).toMatchObject({
|
||||
name: roomName,
|
||||
preset: Preset.PrivateChat,
|
||||
power_level_content_override: {
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users: {
|
||||
[userId]: 100,
|
||||
},
|
||||
},
|
||||
creation_content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
initial_state: [
|
||||
{
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
type: UNSTABLE_MSC3088_PURPOSE.unstable,
|
||||
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: EventType.RoomEncryption,
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: MEGOLM_ALGORITHM,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return { room_id: roomId };
|
||||
});
|
||||
client.getUserId = () => userId;
|
||||
client.createRoom = fn;
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = await client.unstableCreateFileTree(roomName);
|
||||
expect(tree).toBeDefined();
|
||||
expect(tree.roomId).toEqual(roomId);
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should get (unstable) file trees with valid state", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeDefined();
|
||||
expect(tree.roomId).toEqual(roomId);
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: "org.example.not_space",
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
client.removeListener("sync", syncListener);
|
||||
resolve();
|
||||
} else if (state === "ERROR") {
|
||||
reject(new Error("sync error"));
|
||||
}
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
await syncPromise;
|
||||
});
|
||||
|
||||
describe("getSyncState", function() {
|
||||
it("should return null if the client isn't started", function() {
|
||||
expect(client.getSyncState()).toBe(null);
|
||||
});
|
||||
|
||||
it("should return the same sync state as emitted sync events", async function() {
|
||||
const syncingPromise = new Promise((resolve) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
client.removeListener("sync", syncListener);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
await syncingPromise;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateFilter", function() {
|
||||
it("should POST createFilter if no id is present in localStorage", function() {
|
||||
});
|
||||
it("should use an existing filter if id is present in localStorage", function() {
|
||||
});
|
||||
it("should handle localStorage filterId missing from the server", function(done) {
|
||||
function getFilterName(userId, suffix) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
||||
}
|
||||
const invalidFilterId = 'invalidF1lt3r';
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: FILTER_PATH + '/' + invalidFilterId,
|
||||
error: {
|
||||
errcode: "M_UNKNOWN",
|
||||
name: "M_UNKNOWN",
|
||||
message: "No row found",
|
||||
data: { errcode: "M_UNKNOWN", error: "No row found" },
|
||||
httpStatus: 404,
|
||||
},
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
store.getFilterIdByName.mockReturnValue(invalidFilterId);
|
||||
|
||||
const filterName = getFilterName(client.credentials.userId);
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
|
||||
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", async function() {
|
||||
await client.startClient();
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
it("should work on /filter", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should work on /sync", function(done) {
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false",
|
||||
);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jest.advanceTimersByTime(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should work on /pushrules", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("emitted sync events", function() {
|
||||
function syncChecker(expectedStates, done) {
|
||||
return function syncListener(state, old) {
|
||||
const expected = expectedStates.shift();
|
||||
logger.log(
|
||||
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
|
||||
);
|
||||
if (!expected) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
expect(state).toEqual(expected[0]);
|
||||
expect(old).toEqual(expected[1]);
|
||||
if (expectedStates.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
// standard retry time is 5 to 10 seconds
|
||||
jest.advanceTimersByTime(10000);
|
||||
};
|
||||
}
|
||||
|
||||
it("should transition null -> PREPARED after the first /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition null -> ERROR after a failed /filter", function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
expectedStates.push(["ERROR", null]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> CATCHUP after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("inviteByEmail", function() {
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
it("should send an invite HTTP POST", function() {
|
||||
httpLookups = [{
|
||||
method: "POST",
|
||||
path: "/rooms/!foo%3Abar/invite",
|
||||
data: {},
|
||||
expectBody: {
|
||||
id_server: identityServerDomain,
|
||||
medium: "email",
|
||||
address: "alice@gmail.com",
|
||||
},
|
||||
}];
|
||||
client.inviteByEmail(roomId, "alice@gmail.com");
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("guest rooms", function() {
|
||||
it("should only do /sync calls (without filter/pushrules)", function(done) {
|
||||
httpLookups = []; // no /pushrules or /filter
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: SYNC_DATA,
|
||||
thenCall: function() {
|
||||
done();
|
||||
},
|
||||
});
|
||||
client.setGuest(true);
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should be able to peek into a room using peekInRoom", function(done) {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPresence", function() {
|
||||
it("should send a presence HTTP GET", function() {
|
||||
httpLookups = [{
|
||||
method: "GET",
|
||||
path: `/presence/${encodeURIComponent(userId)}/status`,
|
||||
data: {
|
||||
"presence": "unavailable",
|
||||
"last_active_ago": 420845,
|
||||
},
|
||||
}];
|
||||
client.getPresence(userId);
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,21 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../../src";
|
||||
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
|
||||
import { EventTimeline } from "../../../src/models/event-timeline";
|
||||
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
|
||||
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
|
||||
|
||||
describe("MSC3089Branch", () => {
|
||||
let client: MatrixClient;
|
||||
// @ts-ignore - TS doesn't know that this is a type
|
||||
let indexEvent: any;
|
||||
let directory: MSC3089TreeSpace;
|
||||
let branch: MSC3089Branch;
|
||||
let branch2: MSC3089Branch;
|
||||
|
||||
const branchRoomId = "!room:example.org";
|
||||
const fileEventId = "$file";
|
||||
const fileEventId2 = "$second_file";
|
||||
|
||||
const staticTimelineSets = {} as EventTimelineSet;
|
||||
const staticRoom = {
|
||||
@@ -50,7 +54,12 @@ describe("MSC3089Branch", () => {
|
||||
getRoomId: () => branchRoomId,
|
||||
getStateKey: () => fileEventId,
|
||||
});
|
||||
branch = new MSC3089Branch(client, indexEvent);
|
||||
directory = new MSC3089TreeSpace(client, branchRoomId);
|
||||
branch = new MSC3089Branch(client, indexEvent, directory);
|
||||
branch2 = new MSC3089Branch(client, {
|
||||
getRoomId: () => branchRoomId,
|
||||
getStateKey: () => fileEventId2,
|
||||
} as MatrixEvent, directory);
|
||||
});
|
||||
|
||||
it('should know the file event ID', () => {
|
||||
@@ -69,13 +78,15 @@ describe("MSC3089Branch", () => {
|
||||
});
|
||||
|
||||
it('should be able to delete the file', async () => {
|
||||
const eventIdOrder = [fileEventId, fileEventId2];
|
||||
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({});
|
||||
expect(content['active']).toBeUndefined();
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
expect(stateKey).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
@@ -83,16 +94,19 @@ describe("MSC3089Branch", () => {
|
||||
|
||||
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
expect(eventId).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.redactEvent = redactFn;
|
||||
|
||||
branch.getVersionHistory = () => Promise.resolve([branch, branch2]);
|
||||
branch2.getVersionHistory = () => Promise.resolve([branch2]);
|
||||
|
||||
await branch.delete();
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
expect(redactFn).toHaveBeenCalledTimes(1);
|
||||
expect(stateFn).toHaveBeenCalledTimes(2);
|
||||
expect(redactFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should know its name', async () => {
|
||||
@@ -127,16 +141,71 @@ describe("MSC3089Branch", () => {
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be v1 by default', () => {
|
||||
indexEvent.getContent = () => ({ active: true });
|
||||
|
||||
const res = branch.version;
|
||||
|
||||
expect(res).toEqual(1);
|
||||
});
|
||||
|
||||
it('should be vN when set', () => {
|
||||
indexEvent.getContent = () => ({ active: true, version: 3 });
|
||||
|
||||
const res = branch.version;
|
||||
|
||||
expect(res).toEqual(3);
|
||||
});
|
||||
|
||||
it('should be unlocked by default', async () => {
|
||||
indexEvent.getContent = () => ({ active: true });
|
||||
|
||||
const res = branch.isLocked();
|
||||
|
||||
expect(res).toEqual(false);
|
||||
});
|
||||
|
||||
it('should use lock status from index event', async () => {
|
||||
indexEvent.getContent = () => ({ active: true, locked: true });
|
||||
|
||||
const res = branch.isLocked();
|
||||
|
||||
expect(res).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be able to change its locked status', async () => {
|
||||
const locked = true;
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: true,
|
||||
locked: locked,
|
||||
});
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
await branch.setLocked(locked);
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be able to return event information', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
const eventsArr = [
|
||||
{ getId: () => "$not-file", getContent: () => ({}) },
|
||||
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
|
||||
];
|
||||
client.getEventTimeline = () => Promise.resolve({
|
||||
getEvents: () => eventsArr,
|
||||
}) as any as Promise<EventTimeline>; // partial
|
||||
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
|
||||
staticRoom.getUnfilteredTimelineSet = () => ({
|
||||
findEventById: (eventId) => {
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
}) as EventTimelineSet;
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
@@ -151,4 +220,112 @@ describe("MSC3089Branch", () => {
|
||||
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to return the event object', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
|
||||
staticRoom.getUnfilteredTimelineSet = () => ({
|
||||
findEventById: (eventId) => {
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
}) as EventTimelineSet;
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
};
|
||||
client.decryptEventIfNeeded = () => Promise.resolve();
|
||||
|
||||
const res = await branch.getFileEvent();
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toBe(fileEvent);
|
||||
});
|
||||
|
||||
it('should create new versions of itself', async () => {
|
||||
const canaryName = "canary";
|
||||
const canaryContents = "contents go here";
|
||||
const canaryFile = {} as IEncryptedFile;
|
||||
const canaryAddl = { canary: true };
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateKeyOrder = [fileEventId2, fileEventId];
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
|
||||
if (stateKey === fileEventId) {
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: false,
|
||||
});
|
||||
} else if (stateKey === fileEventId2) {
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
version: 2,
|
||||
name: canaryName,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected state key: " + stateKey);
|
||||
}
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
const createFn = jest.fn().mockImplementation((
|
||||
name: string,
|
||||
contents: ArrayBuffer,
|
||||
info: Partial<IEncryptedFile>,
|
||||
addl: IContent,
|
||||
) => {
|
||||
expect(name).toEqual(canaryName);
|
||||
expect(contents).toBe(canaryContents);
|
||||
expect(info).toBe(canaryFile);
|
||||
expect(addl).toMatchObject({
|
||||
...canaryAddl,
|
||||
"m.new_content": true,
|
||||
"m.relates_to": {
|
||||
"rel_type": RelationType.Replace,
|
||||
"event_id": fileEventId,
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId2 });
|
||||
});
|
||||
directory.createFile = createFn;
|
||||
|
||||
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(2);
|
||||
expect(createFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fetch file history', async () => {
|
||||
branch2.getFileEvent = () => Promise.resolve({
|
||||
replacingEventId: () => undefined,
|
||||
getId: () => fileEventId2,
|
||||
} as MatrixEvent);
|
||||
branch.getFileEvent = () => Promise.resolve({
|
||||
replacingEventId: () => fileEventId2,
|
||||
getId: () => fileEventId,
|
||||
} as MatrixEvent);
|
||||
|
||||
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
|
||||
replacingEventId: (): string => null,
|
||||
getId: () => "$unknown",
|
||||
}];
|
||||
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
|
||||
|
||||
directory.getFile = (evId: string) => {
|
||||
expect(evId).toEqual(fileEventId);
|
||||
return branch;
|
||||
};
|
||||
|
||||
const results = await branch2.getVersionHistory();
|
||||
expect(results).toMatchObject([
|
||||
branch2,
|
||||
branch,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
TreePermissions,
|
||||
} from "../../../src/models/MSC3089TreeSpace";
|
||||
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||
import { MockBlob } from "../../MockBlob";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
|
||||
describe("MSC3089TreeSpace", () => {
|
||||
@@ -227,40 +226,61 @@ describe("MSC3089TreeSpace", () => {
|
||||
[targetUser]: expectedPl,
|
||||
},
|
||||
});
|
||||
|
||||
// Store new power levels so the `getPermissions()` test passes
|
||||
makePowerLevels(content);
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.sendStateEvent = fn;
|
||||
await tree.setPermissions(targetUser, role);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
|
||||
const finalPermissions = tree.getPermissions(targetUser);
|
||||
expect(finalPermissions).toEqual(role);
|
||||
}
|
||||
|
||||
it('should support setting Viewer permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Viewer, 1024);
|
||||
});
|
||||
|
||||
it('should support setting Editor permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
}, TreePermissions.Editor, 1024);
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Editor, 1025);
|
||||
});
|
||||
|
||||
it('should support setting Owner permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1024,
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Owner, 1024);
|
||||
}, TreePermissions.Owner, 1026);
|
||||
});
|
||||
|
||||
it('should support demoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
users: {
|
||||
[targetUser]: 2222,
|
||||
},
|
||||
@@ -270,11 +290,15 @@ describe("MSC3089TreeSpace", () => {
|
||||
it('should support promoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
users: {
|
||||
[targetUser]: 5,
|
||||
},
|
||||
}, TreePermissions.Editor, 1024);
|
||||
}, TreePermissions.Editor, 1025);
|
||||
});
|
||||
|
||||
it('should support defaults: Viewer', () => {
|
||||
@@ -862,12 +886,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
// Mock out Blob for the test environment
|
||||
(<any>global).Blob = MockBlob;
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Blob, opts: any) => {
|
||||
expect(contents).toBeInstanceOf(Blob);
|
||||
expect(contents.size).toEqual(fileContents.length);
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
@@ -883,6 +903,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
body: fileName,
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
metadata: true, // additional content from test
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
@@ -900,14 +921,78 @@ describe("MSC3089TreeSpace", () => {
|
||||
name: fileName,
|
||||
});
|
||||
|
||||
return Promise.resolve(); // return value not used.
|
||||
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
|
||||
const buf = Buffer.from(fileContents);
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
await tree.createFile(fileName, buf, Object.assign({}, fileInfo));
|
||||
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { metadata: true });
|
||||
expect(result).toMatchObject({ event_id: fileEventId });
|
||||
|
||||
expect(uploadFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendStateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should upload file versions', async () => {
|
||||
const mxc = "mxc://example.org/file";
|
||||
const fileInfo = {
|
||||
mimetype: "text/plain",
|
||||
// other fields as required by encryption, but ignored here
|
||||
};
|
||||
const fileEventId = "$file";
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
const content = {
|
||||
msgtype: MsgType.File,
|
||||
body: fileName,
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
};
|
||||
expect(contents).toMatchObject({
|
||||
...content,
|
||||
"m.new_content": content,
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
});
|
||||
client.sendMessage = sendMsgFn;
|
||||
|
||||
const sendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
name: fileName,
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Buffer.from(fileContents);
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { "m.new_content": true });
|
||||
expect(result).toMatchObject({ event_id: fileEventId });
|
||||
|
||||
expect(uploadFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
||||
@@ -933,7 +1018,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
it('should return falsy for unknown files', () => {
|
||||
const fileEventId = "$file";
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
return null;
|
||||
@@ -960,4 +1045,22 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(files.length).toEqual(1);
|
||||
expect(files[0].indexEvent).toBe(firstFile);
|
||||
});
|
||||
|
||||
it('should list all files', () => {
|
||||
const firstFile = { getContent: () => ({ active: true }) };
|
||||
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [firstFile, secondFile];
|
||||
},
|
||||
};
|
||||
|
||||
const files = tree.listAllFiles();
|
||||
expect(files).toBeDefined();
|
||||
expect(files.length).toEqual(2);
|
||||
expect(files[0].indexEvent).toBe(firstFile);
|
||||
expect(files[1].indexEvent).toBe(secondFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
/*
|
||||
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 { MatrixEvent } from "../../../src";
|
||||
import {
|
||||
isTimestampInDuration,
|
||||
Beacon,
|
||||
BeaconEvent,
|
||||
} from "../../../src/models/beacon";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('Beacon', () => {
|
||||
describe('isTimestampInDuration()', () => {
|
||||
const startTs = new Date('2022-03-11T12:07:47.592Z').getTime();
|
||||
const HOUR_MS = 3600000;
|
||||
it('returns false when timestamp is before start time', () => {
|
||||
// day before
|
||||
const timestamp = new Date('2022-03-10T12:07:47.592Z').getTime();
|
||||
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when timestamp is after start time + duration', () => {
|
||||
// 1 second later
|
||||
const timestamp = new Date('2022-03-10T12:07:48.592Z').getTime();
|
||||
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when timestamp is exactly start time', () => {
|
||||
expect(isTimestampInDuration(startTs, HOUR_MS, startTs)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when timestamp is exactly the end of the duration', () => {
|
||||
expect(isTimestampInDuration(startTs, HOUR_MS, startTs + HOUR_MS)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when timestamp is within the duration', () => {
|
||||
const twoHourDuration = HOUR_MS * 2;
|
||||
const now = startTs + HOUR_MS;
|
||||
expect(isTimestampInDuration(startTs, twoHourDuration, now)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Beacon', () => {
|
||||
const userId = '@user:server.org';
|
||||
const userId2 = '@user2:server.org';
|
||||
const roomId = '$room:server.org';
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
const HOUR_MS = 3600000;
|
||||
|
||||
// beacon_info events
|
||||
// created 'an hour ago'
|
||||
// without timeout of 3 hours
|
||||
let liveBeaconEvent: MatrixEvent;
|
||||
let notLiveBeaconEvent: MatrixEvent;
|
||||
let user2BeaconEvent: MatrixEvent;
|
||||
|
||||
const advanceDateAndTime = (ms: number) => {
|
||||
// bc liveness check uses Date.now we have to advance this mock
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(Date.now() + ms);
|
||||
// then advance time for the interval by the same amount
|
||||
jest.advanceTimersByTime(ms);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
liveBeaconEvent = makeBeaconInfoEvent(
|
||||
userId,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS * 3,
|
||||
isLive: true,
|
||||
timestamp: now - HOUR_MS,
|
||||
},
|
||||
'$live123',
|
||||
);
|
||||
notLiveBeaconEvent = makeBeaconInfoEvent(
|
||||
userId,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS * 3,
|
||||
isLive: false,
|
||||
timestamp: now - HOUR_MS,
|
||||
},
|
||||
'$dead123',
|
||||
);
|
||||
user2BeaconEvent = makeBeaconInfoEvent(
|
||||
userId2,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS * 3,
|
||||
isLive: true,
|
||||
timestamp: now - HOUR_MS,
|
||||
},
|
||||
'$user2live123',
|
||||
);
|
||||
|
||||
// back to 'now'
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, 'now').mockRestore();
|
||||
});
|
||||
|
||||
it('creates beacon from event', () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
|
||||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||
expect(beacon.roomId).toEqual(roomId);
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
expect(beacon.beaconInfoOwner).toEqual(userId);
|
||||
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
|
||||
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
|
||||
expect(beacon.beaconInfo).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('isLive()', () => {
|
||||
it('returns false when beacon is explicitly set to not live', () => {
|
||||
const beacon = new Beacon(notLiveBeaconEvent);
|
||||
expect(beacon.isLive).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when beacon is expired', () => {
|
||||
const expiredBeaconEvent = makeBeaconInfoEvent(
|
||||
userId2,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS,
|
||||
isLive: true,
|
||||
timestamp: now - HOUR_MS * 2,
|
||||
},
|
||||
'$user2live123',
|
||||
);
|
||||
const beacon = new Beacon(expiredBeaconEvent);
|
||||
expect(beacon.isLive).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when beacon timestamp is in future by an hour', () => {
|
||||
const beaconStartsInHour = makeBeaconInfoEvent(
|
||||
userId2,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS,
|
||||
isLive: true,
|
||||
timestamp: now + HOUR_MS,
|
||||
},
|
||||
'$user2live123',
|
||||
);
|
||||
const beacon = new Beacon(beaconStartsInHour);
|
||||
expect(beacon.isLive).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when beacon timestamp is one minute in the future', () => {
|
||||
const beaconStartsInOneMin = makeBeaconInfoEvent(
|
||||
userId2,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS,
|
||||
isLive: true,
|
||||
timestamp: now + 60000,
|
||||
},
|
||||
'$user2live123',
|
||||
);
|
||||
const beacon = new Beacon(beaconStartsInOneMin);
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true when beacon timestamp is one minute before expiry', () => {
|
||||
// this test case is to check the start time leniency doesn't affect
|
||||
// strict expiry time checks
|
||||
const expiresInOneMin = makeBeaconInfoEvent(
|
||||
userId2,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS,
|
||||
isLive: true,
|
||||
timestamp: now - HOUR_MS + 60000,
|
||||
},
|
||||
'$user2live123',
|
||||
);
|
||||
const beacon = new Beacon(expiresInOneMin);
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when beacon timestamp is one minute after expiry', () => {
|
||||
// this test case is to check the start time leniency doesn't affect
|
||||
// strict expiry time checks
|
||||
const expiredOneMinAgo = makeBeaconInfoEvent(
|
||||
userId2,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS,
|
||||
isLive: true,
|
||||
timestamp: now - HOUR_MS - 60000,
|
||||
},
|
||||
'$user2live123',
|
||||
);
|
||||
const beacon = new Beacon(expiredOneMinAgo);
|
||||
expect(beacon.isLive).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when beacon was created in past and not yet expired', () => {
|
||||
// liveBeaconEvent was created 1 hour ago
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update()', () => {
|
||||
it('does not update with different event', () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
|
||||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||
|
||||
expect(() => beacon.update(user2BeaconEvent)).toThrow();
|
||||
// didnt update
|
||||
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
|
||||
});
|
||||
|
||||
it('does not update with an older event', () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
|
||||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||
|
||||
const oldUpdateEvent = makeBeaconInfoEvent(
|
||||
userId,
|
||||
roomId,
|
||||
);
|
||||
// less than the original event
|
||||
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
|
||||
|
||||
beacon.update(oldUpdateEvent);
|
||||
// didnt update
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||
});
|
||||
|
||||
it('updates event', () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
|
||||
const updatedBeaconEvent = makeBeaconInfoEvent(
|
||||
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123');
|
||||
|
||||
beacon.update(updatedBeaconEvent);
|
||||
expect(beacon.isLive).toEqual(false);
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Update, updatedBeaconEvent, beacon);
|
||||
});
|
||||
|
||||
it('emits livenesschange event when beacon liveness changes', () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
|
||||
const updatedBeaconEvent = makeBeaconInfoEvent(
|
||||
userId,
|
||||
roomId,
|
||||
{ timeout: HOUR_MS * 3, isLive: false },
|
||||
beacon.beaconInfoId,
|
||||
);
|
||||
|
||||
beacon.update(updatedBeaconEvent);
|
||||
expect(beacon.isLive).toEqual(false);
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('monitorLiveness()', () => {
|
||||
it('does not set a monitor interval when beacon is not live', () => {
|
||||
// beacon was created an hour ago
|
||||
// and has a 3hr duration
|
||||
const beacon = new Beacon(notLiveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.monitorLiveness();
|
||||
|
||||
// @ts-ignore
|
||||
expect(beacon.livenessWatchTimeout).toBeFalsy();
|
||||
advanceDateAndTime(HOUR_MS * 2 + 1);
|
||||
|
||||
// no emit
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checks liveness of beacon at expected start time', () => {
|
||||
const futureBeaconEvent = makeBeaconInfoEvent(
|
||||
userId,
|
||||
roomId,
|
||||
{
|
||||
timeout: HOUR_MS * 3,
|
||||
isLive: true,
|
||||
// start timestamp hour in future
|
||||
timestamp: now + HOUR_MS,
|
||||
},
|
||||
'$live123',
|
||||
);
|
||||
|
||||
const beacon = new Beacon(futureBeaconEvent);
|
||||
expect(beacon.isLive).toBeFalsy();
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.monitorLiveness();
|
||||
|
||||
// advance to the start timestamp of the beacon
|
||||
advanceDateAndTime(HOUR_MS + 1);
|
||||
|
||||
// beacon is in live period now
|
||||
expect(emitSpy).toHaveBeenCalledTimes(1);
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, true, beacon);
|
||||
|
||||
// check the expiry monitor is still setup ok
|
||||
// advance to the expiry
|
||||
advanceDateAndTime(HOUR_MS * 3 + 100);
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledTimes(2);
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
|
||||
});
|
||||
|
||||
it('checks liveness of beacon at expected expiry time', () => {
|
||||
// live beacon was created an hour ago
|
||||
// and has a 3hr duration
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
expect(beacon.isLive).toBeTruthy();
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.monitorLiveness();
|
||||
advanceDateAndTime(HOUR_MS * 2 + 1);
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledTimes(1);
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
|
||||
});
|
||||
|
||||
it('clears monitor interval when re-monitoring liveness', () => {
|
||||
// live beacon was created an hour ago
|
||||
// and has a 3hr duration
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
expect(beacon.isLive).toBeTruthy();
|
||||
|
||||
beacon.monitorLiveness();
|
||||
// @ts-ignore
|
||||
const oldMonitor = beacon.livenessWatchTimeout;
|
||||
|
||||
beacon.monitorLiveness();
|
||||
|
||||
// @ts-ignore
|
||||
expect(beacon.livenessWatchTimeout).not.toEqual(oldMonitor);
|
||||
});
|
||||
|
||||
it('destroy kills liveness monitor and emits', () => {
|
||||
// live beacon was created an hour ago
|
||||
// and has a 3hr duration
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
expect(beacon.isLive).toBeTruthy();
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.monitorLiveness();
|
||||
|
||||
// destroy the beacon
|
||||
beacon.destroy();
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Destroy, beacon.identifier);
|
||||
// live forced to false
|
||||
expect(beacon.isLive).toBe(false);
|
||||
|
||||
advanceDateAndTime(HOUR_MS * 2 + 1);
|
||||
|
||||
// no additional calls
|
||||
expect(emitSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLocations', () => {
|
||||
it('ignores locations when beacon is not live', () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.addLocations([
|
||||
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
|
||||
]);
|
||||
|
||||
expect(beacon.latestLocationState).toBeFalsy();
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores locations outside the beacon live duration', () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.addLocations([
|
||||
// beacon has now + 60000 live period
|
||||
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 100000 }),
|
||||
]);
|
||||
|
||||
expect(beacon.latestLocationState).toBeFalsy();
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when beacon is live with a start timestamp is in the future', () => {
|
||||
it('ignores locations before the beacon start timestamp', () => {
|
||||
const startTimestamp = now + 60000;
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(
|
||||
userId,
|
||||
roomId,
|
||||
{ isLive: true, timeout: 60000, timestamp: startTimestamp },
|
||||
));
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.addLocations([
|
||||
// beacon has now + 60000 live period
|
||||
makeBeaconEvent(
|
||||
userId,
|
||||
{
|
||||
beaconInfoId: beacon.beaconInfoId,
|
||||
// now < location timestamp < beacon timestamp
|
||||
timestamp: now + 10,
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
expect(beacon.latestLocationState).toBeFalsy();
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it('sets latest location when location timestamp is after startTimestamp', () => {
|
||||
const startTimestamp = now + 60000;
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(
|
||||
userId,
|
||||
roomId,
|
||||
{ isLive: true, timeout: 600000, timestamp: startTimestamp },
|
||||
));
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
beacon.addLocations([
|
||||
// beacon has now + 600000 live period
|
||||
makeBeaconEvent(
|
||||
userId,
|
||||
{
|
||||
beaconInfoId: beacon.beaconInfoId,
|
||||
// now < beacon timestamp < location timestamp
|
||||
timestamp: startTimestamp + 10,
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
expect(beacon.latestLocationState).toBeTruthy();
|
||||
expect(emitSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets latest location state to most recent location', () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
const locations = [
|
||||
// older
|
||||
makeBeaconEvent(
|
||||
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
|
||||
),
|
||||
// newer
|
||||
makeBeaconEvent(
|
||||
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
|
||||
),
|
||||
// not valid
|
||||
makeBeaconEvent(
|
||||
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:baz', timestamp: now - 5 },
|
||||
),
|
||||
];
|
||||
|
||||
beacon.addLocations(locations);
|
||||
|
||||
const expectedLatestLocation = {
|
||||
description: undefined,
|
||||
timestamp: now + 10000,
|
||||
uri: 'geo:bar',
|
||||
};
|
||||
|
||||
// the newest valid location
|
||||
expect(beacon.latestLocationState).toEqual(expectedLatestLocation);
|
||||
expect(beacon.latestLocationEvent).toEqual(locations[1]);
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation);
|
||||
});
|
||||
|
||||
it('ignores locations that are less recent that the current latest location', () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
|
||||
const olderLocation = makeBeaconEvent(
|
||||
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
|
||||
);
|
||||
const newerLocation = makeBeaconEvent(
|
||||
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
|
||||
);
|
||||
|
||||
beacon.addLocations([newerLocation]);
|
||||
// latest location set to newerLocation
|
||||
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
|
||||
uri: 'geo:bar',
|
||||
}));
|
||||
expect(beacon.latestLocationEvent).toEqual(newerLocation);
|
||||
|
||||
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
|
||||
|
||||
// add older location
|
||||
beacon.addLocations([olderLocation]);
|
||||
|
||||
// no change
|
||||
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
|
||||
uri: 'geo:bar',
|
||||
}));
|
||||
// no emit
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -57,4 +57,31 @@ describe('MatrixEvent', () => {
|
||||
expect(a.toSnapshot().isEquivalentTo(a)).toBe(true);
|
||||
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
|
||||
});
|
||||
|
||||
it("should prune clearEvent when being redacted", () => {
|
||||
const ev = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "Test",
|
||||
},
|
||||
event_id: "$event1:server",
|
||||
});
|
||||
|
||||
expect(ev.getContent().body).toBe("Test");
|
||||
expect(ev.getWireContent().body).toBe("Test");
|
||||
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
|
||||
expect(ev.getContent().body).toBe("Test");
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBe("xyz");
|
||||
|
||||
const redaction = new MatrixEvent({
|
||||
type: "m.room.redaction",
|
||||
redacts: ev.getId(),
|
||||
});
|
||||
|
||||
ev.makeRedacted(redaction);
|
||||
expect(ev.getContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 { Thread } from "../../../src/models/thread";
|
||||
|
||||
describe('Thread', () => {
|
||||
describe("constructor", () => {
|
||||
it("should explode for element-web#22141 logging", () => {
|
||||
// Logging/debugging for https://github.com/vector-im/element-web/issues/22141
|
||||
expect(() => {
|
||||
new Thread("$event", undefined, {} as any); // deliberate cast to test error case
|
||||
}).toThrow("element-web#22141: A thread requires a room in order to function");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType } from "../../src";
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
@@ -208,6 +209,7 @@ describe('NotificationService', function() {
|
||||
msgtype: "m.text",
|
||||
},
|
||||
});
|
||||
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules);
|
||||
pushProcessor = new PushProcessor(matrixClient);
|
||||
});
|
||||
|
||||
@@ -295,6 +297,22 @@ describe('NotificationService', function() {
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not bing on room server ACL changes', function() {
|
||||
testEvent = utils.mkEvent({
|
||||
type: EventType.RoomServerAcl,
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
skey: "",
|
||||
event: true,
|
||||
content: {},
|
||||
});
|
||||
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toBeFalsy();
|
||||
expect(actions.tweaks.sound).toBeFalsy();
|
||||
expect(actions.notify).toBeFalsy();
|
||||
});
|
||||
|
||||
// invalid
|
||||
|
||||
it('should gracefully handle bad input.', function() {
|
||||
@@ -302,4 +320,20 @@ describe('NotificationService', function() {
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
it("a rule with no conditions matches every event.", function() {
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
conditions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as callbacks from "../../src/realtime-callbacks";
|
||||
|
||||
let wallTime = 1234567890;
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers().setSystemTime(wallTime);
|
||||
|
||||
describe("realtime-callbacks", function() {
|
||||
function tick(millis) {
|
||||
@@ -9,14 +9,6 @@ describe("realtime-callbacks", function() {
|
||||
jest.advanceTimersByTime(millis);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
callbacks.setNow(() => wallTime);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
callbacks.setNow();
|
||||
});
|
||||
|
||||
describe("setTimeout", function() {
|
||||
it("should call the callback after the timeout", function() {
|
||||
const callback = jest.fn();
|
||||
|
||||
+62
-13
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
|
||||
@@ -96,19 +96,14 @@ describe("Relations", function() {
|
||||
},
|
||||
});
|
||||
|
||||
// Stub the room
|
||||
|
||||
const room = new Room("room123", null, null);
|
||||
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const room = new Room("room123", null, null);
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
});
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
|
||||
@@ -117,17 +112,71 @@ describe("Relations", function() {
|
||||
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const room = new Room("room123", null, null);
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
});
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
|
||||
await relationsCreated;
|
||||
}
|
||||
});
|
||||
|
||||
it("should re-use Relations between all timeline sets in a room", async () => {
|
||||
const room = new Room("room123", null, null);
|
||||
const timelineSet1 = new EventTimelineSet(room);
|
||||
const timelineSet2 = new EventTimelineSet(room);
|
||||
expect(room.relations).toBe(timelineSet1.relations);
|
||||
expect(room.relations).toBe(timelineSet2.relations);
|
||||
});
|
||||
|
||||
it("should ignore m.replace for state events", async () => {
|
||||
const userId = "@bob:example.com";
|
||||
const room = new Room("room123", null, userId);
|
||||
const relations = new Relations("m.replace", "m.room.topic", room);
|
||||
|
||||
// Create an instance of a state event with rel_type m.replace
|
||||
const originalTopic = new MatrixEvent({
|
||||
"sender": userId,
|
||||
"type": "m.room.topic",
|
||||
"event_id": "$orig",
|
||||
"room_id": room.roomId,
|
||||
"content": {
|
||||
"topic": "orig",
|
||||
},
|
||||
"state_key": "",
|
||||
});
|
||||
const badlyEditedTopic = new MatrixEvent({
|
||||
"sender": userId,
|
||||
"type": "m.room.topic",
|
||||
"event_id": "$orig",
|
||||
"room_id": room.roomId,
|
||||
"content": {
|
||||
"topic": "topic",
|
||||
"m.new_content": {
|
||||
"topic": "edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$orig",
|
||||
"rel_type": "m.replace",
|
||||
},
|
||||
},
|
||||
"state_key": "",
|
||||
});
|
||||
|
||||
await relations.setTargetEvent(originalTopic);
|
||||
expect(originalTopic.replacingEvent()).toBe(null);
|
||||
expect(originalTopic.getContent().topic).toBe("orig");
|
||||
expect(badlyEditedTopic.isRelation()).toBe(false);
|
||||
expect(badlyEditedTopic.isRelation("m.replace")).toBe(false);
|
||||
|
||||
await relations.addEvent(badlyEditedTopic);
|
||||
expect(originalTopic.replacingEvent()).toBe(null);
|
||||
expect(originalTopic.getContent().topic).toBe("orig");
|
||||
expect(badlyEditedTopic.replacingEvent()).toBe(null);
|
||||
expect(badlyEditedTopic.getContent().topic).toBe("topic");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
|
||||
describe("RoomMember", function() {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import * as utils from "../test-utils";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
||||
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
||||
import {
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../src/models/event";
|
||||
import { M_BEACON } from "../../src/@types/beacon";
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -120,7 +129,7 @@ describe("RoomState", function() {
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
@@ -248,6 +257,116 @@ describe("RoomState", function() {
|
||||
memberEvent, state,
|
||||
);
|
||||
});
|
||||
|
||||
it("should emit `RoomStateEvent.Marker` for each marker event", function() {
|
||||
const events = [
|
||||
utils.mkEvent({
|
||||
event: true,
|
||||
type: UNSTABLE_MSC2716_MARKER.name,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
skey: "",
|
||||
content: {
|
||||
"m.insertion_id": "$abc",
|
||||
},
|
||||
}),
|
||||
];
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) {
|
||||
expect(markerEvent).toEqual(events[emitCount]);
|
||||
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
|
||||
emitCount += 1;
|
||||
});
|
||||
state.setStateEvents(events, { timelineWasEmpty: true });
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
describe('beacon events', () => {
|
||||
it('adds new beacon info events to state and emits', () => {
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
|
||||
expect(state.beacons.size).toEqual(1);
|
||||
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
|
||||
expect(beaconInstance).toBeTruthy();
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
|
||||
});
|
||||
|
||||
it('does not add redacted beacon info events to state', () => {
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const redactionEvent = { event: { type: 'm.room.redaction' } };
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
|
||||
// no beacon added
|
||||
expect(state.beacons.size).toEqual(0);
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
|
||||
// no new beacon emit
|
||||
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
|
||||
});
|
||||
|
||||
it('updates existing beacon info events in state', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
expect(beaconInstance.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([updatedBeaconEvent]);
|
||||
|
||||
// same Beacon
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
||||
// updated liveness
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false);
|
||||
});
|
||||
|
||||
it('destroys and removes redacted beacon events', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
|
||||
expect(beaconInstance.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
|
||||
expect(destroySpy).toHaveBeenCalled();
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
|
||||
});
|
||||
|
||||
it('updates live beacon ids once after setting state events', () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
|
||||
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
|
||||
|
||||
// called once
|
||||
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
|
||||
|
||||
// live beacon is now not live
|
||||
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
|
||||
);
|
||||
|
||||
state.setStateEvents([updatedLiveBeaconEvent]);
|
||||
|
||||
expect(state.hasLiveBeacons).toBe(false);
|
||||
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
|
||||
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setOutOfBandMembers", function() {
|
||||
@@ -622,4 +741,243 @@ describe("RoomState", function() {
|
||||
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processBeaconEvents', () => {
|
||||
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
|
||||
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
|
||||
|
||||
const mockClient = { decryptEventIfNeeded: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.decryptEventIfNeeded.mockClear();
|
||||
});
|
||||
|
||||
it('does nothing when state has no beacons', () => {
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when there are no events', () => {
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('without encryption', () => {
|
||||
it('discards events for beacons that are not in state', () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
const otherRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
['m.relates_to']: {
|
||||
event_id: 'whatever',
|
||||
},
|
||||
},
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('discards events that are not beacon type', () => {
|
||||
// related to beacon1
|
||||
const otherRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
['m.relates_to']: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: beacon1.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([otherRelatedEvent], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adds locations to beacons', () => {
|
||||
const location1 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
|
||||
});
|
||||
const location2 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
|
||||
});
|
||||
const location3 = makeBeaconEvent(userB, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
|
||||
state.setStateEvents([beacon1, beacon2], mockClient);
|
||||
|
||||
expect(state.beacons.size).toEqual(2);
|
||||
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
|
||||
|
||||
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
|
||||
// only called with locations for beacon1
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([location1]);
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([location2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with encryption', () => {
|
||||
const beacon1RelationContent = { ['m.relates_to']: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: beacon1.getId(),
|
||||
} };
|
||||
const relatedEncryptedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
|
||||
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
|
||||
|
||||
it('discards events without relations', () => {
|
||||
const unrelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([unrelatedEvent], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('discards events for beacons that are not in state', () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
const otherRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: {
|
||||
['m.relates_to']: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: 'whatever',
|
||||
},
|
||||
},
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('decrypts related events if needed', () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: beacon1.getId(),
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('listens for decryption on events that are being decrypted', () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('listens for decryption on events that have decryption failure', () => {
|
||||
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('discard events that are not m.beacon type after decryption', () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// this event is a message after decryption
|
||||
decryptingRelatedEvent.type = EventType.RoomMessage;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adds locations to beacons after decryption', () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
const locationEvent = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// update type after '''decryption'''
|
||||
decryptingRelatedEvent.event.type = M_BEACON.name;
|
||||
decryptingRelatedEvent.event.content = locationEvent.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
import { defer } from '../../src/utils';
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
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 'jest-localstorage-mock';
|
||||
|
||||
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||
|
||||
describe("IndexedDBStore", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const roomId = "!room:id";
|
||||
it("should degrade to MemoryStore on IDB errors", async () => {
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
const member1: IStateEventWithRoomId = {
|
||||
room_id: roomId,
|
||||
event_id: "!ev1:id",
|
||||
sender: "@user1:id",
|
||||
state_key: "@user1:id",
|
||||
type: "m.room.member",
|
||||
origin_server_ts: 123,
|
||||
content: {},
|
||||
};
|
||||
const member2: IStateEventWithRoomId = {
|
||||
room_id: roomId,
|
||||
event_id: "!ev2:id",
|
||||
sender: "@user2:id",
|
||||
state_key: "@user2:id",
|
||||
type: "m.room.member",
|
||||
origin_server_ts: 123,
|
||||
content: {},
|
||||
};
|
||||
|
||||
expect(await store.getOutOfBandMembers(roomId)).toBe(null);
|
||||
await store.setOutOfBandMembers(roomId, [member1]);
|
||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
|
||||
|
||||
// Simulate a broken IDB
|
||||
(store.backend as LocalIndexedDBStoreBackend)["db"].transaction = (): IDBTransaction => {
|
||||
const err = new Error("Failed to execute 'transaction' on 'IDBDatabase': " +
|
||||
"The database connection is closing.");
|
||||
err.name = "InvalidStateError";
|
||||
throw err;
|
||||
};
|
||||
|
||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
|
||||
await Promise.all([
|
||||
emitPromise(store["emitter"], "degraded"),
|
||||
store.setOutOfBandMembers(roomId, [member1, member2]),
|
||||
]);
|
||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should use MemoryStore methods for pending events if no localStorage", async () => {
|
||||
jest.spyOn(MemoryStore.prototype, "setPendingEvents");
|
||||
jest.spyOn(MemoryStore.prototype, "getPendingEvents");
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage: undefined,
|
||||
});
|
||||
|
||||
const events = [{ type: "test" }];
|
||||
await store.setPendingEvents(roomId, events);
|
||||
expect(MemoryStore.prototype.setPendingEvents).toHaveBeenCalledWith(roomId, events);
|
||||
await expect(store.getPendingEvents(roomId)).resolves.toEqual(events);
|
||||
expect(MemoryStore.prototype.getPendingEvents).toHaveBeenCalledWith(roomId);
|
||||
});
|
||||
|
||||
it("should persist pending events to localStorage if available", async () => {
|
||||
jest.spyOn(MemoryStore.prototype, "setPendingEvents");
|
||||
jest.spyOn(MemoryStore.prototype, "getPendingEvents");
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
});
|
||||
|
||||
await expect(store.getPendingEvents(roomId)).resolves.toEqual([]);
|
||||
const events = [{ type: "test" }];
|
||||
await store.setPendingEvents(roomId, events);
|
||||
expect(MemoryStore.prototype.setPendingEvents).not.toHaveBeenCalled();
|
||||
await expect(store.getPendingEvents(roomId)).resolves.toEqual(events);
|
||||
expect(MemoryStore.prototype.getPendingEvents).not.toHaveBeenCalled();
|
||||
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBe(JSON.stringify(events));
|
||||
await store.setPendingEvents(roomId, []);
|
||||
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { SyncAccumulator } from "../../src/sync-accumulator";
|
||||
|
||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||
@@ -294,10 +295,13 @@ describe("SyncAccumulator", function() {
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
"m.read": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 1 },
|
||||
"@bob:localhost": { ts: 2 },
|
||||
},
|
||||
[ReceiptType.ReadPrivate]: {
|
||||
"@dan:localhost": { ts: 4 },
|
||||
},
|
||||
"some.other.receipt.type": {
|
||||
"@should_be_ignored:localhost": { key: "val" },
|
||||
},
|
||||
@@ -309,7 +313,7 @@ describe("SyncAccumulator", function() {
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event2:localhost": {
|
||||
"m.read": {
|
||||
[ReceiptType.Read]: {
|
||||
"@bob:localhost": { ts: 2 }, // clobbers event1 receipt
|
||||
"@charlie:localhost": { ts: 3 },
|
||||
},
|
||||
@@ -337,12 +341,15 @@ describe("SyncAccumulator", function() {
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
"m.read": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 1 },
|
||||
},
|
||||
[ReceiptType.ReadPrivate]: {
|
||||
"@dan:localhost": { ts: 4 },
|
||||
},
|
||||
},
|
||||
"$event2:localhost": {
|
||||
"m.read": {
|
||||
[ReceiptType.Read]: {
|
||||
"@bob:localhost": { ts: 2 },
|
||||
"@charlie:localhost": { ts: 3 },
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { TimelineIndex, TimelineWindow } from "../../src/timeline-window";
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
|
||||
const ROOM_ID = "roomId";
|
||||
const USER_ID = "userId";
|
||||
@@ -35,13 +35,14 @@ function createTimeline(numEvents, baseIndex) {
|
||||
return timeline;
|
||||
}
|
||||
|
||||
function addEventsToTimeline(timeline, numEvents, atStart) {
|
||||
function addEventsToTimeline(timeline, numEvents, toStartOfTimeline) {
|
||||
for (let i = 0; i < numEvents; i++) {
|
||||
timeline.addEvent(
|
||||
utils.mkMessage({
|
||||
room: ROOM_ID, user: USER_ID,
|
||||
event: true,
|
||||
}), atStart,
|
||||
}),
|
||||
{ toStartOfTimeline },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { User } from "../../src/models/user";
|
||||
import * as utils from "../test-utils";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
|
||||
describe("User", function() {
|
||||
const userId = "@alice:bar";
|
||||
|
||||
+44
-78
@@ -10,8 +10,11 @@ import {
|
||||
prevString,
|
||||
simpleRetryOperation,
|
||||
stringToBase,
|
||||
sortEventsByLatestContentTimestamp,
|
||||
} from "../../src/utils";
|
||||
import { logger } from "../../src/logger";
|
||||
import { mkMessage } from "../test-utils/test-utils";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
|
||||
// TODO: Fix types throughout
|
||||
|
||||
@@ -26,6 +29,15 @@ describe("utils", function() {
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle boolean and numeric values", function() {
|
||||
const params = {
|
||||
string: "foobar",
|
||||
number: 12345,
|
||||
boolean: false,
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeUri", function() {
|
||||
@@ -97,24 +109,12 @@ describe("utils", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasNoAdditionalKeys", function() {
|
||||
it("should throw for extra keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({ foo: "bar", baz: 4 }, ["foo"]);
|
||||
}).toThrow();
|
||||
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({ foo: "bar" }, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepCompare", function() {
|
||||
const assert = {
|
||||
isTrue: function(x) {
|
||||
isTrue: function(x: any) {
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function(x) {
|
||||
isFalse: function(x: any) {
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
@@ -176,10 +176,10 @@ describe("utils", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
const func = function(x) {
|
||||
const func = function() {
|
||||
return true;
|
||||
};
|
||||
const func2 = function(x) {
|
||||
const func2 = function() {
|
||||
return true;
|
||||
};
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
@@ -189,66 +189,6 @@ describe("utils", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend", function() {
|
||||
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
const source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Fix type
|
||||
const target: any = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkPromises", function() {
|
||||
it("should execute promises in chunks", async function() {
|
||||
let promiseCount = 0;
|
||||
@@ -273,7 +213,7 @@ describe("utils", function() {
|
||||
it('should retry', async () => {
|
||||
let count = 0;
|
||||
const val = {};
|
||||
const fn = (attempt) => {
|
||||
const fn = (attempt: any) => {
|
||||
count++;
|
||||
|
||||
// If this expectation fails then it can appear as a Jest Timeout due to
|
||||
@@ -480,7 +420,7 @@ describe("utils", function() {
|
||||
},
|
||||
[72]: "test",
|
||||
};
|
||||
const output = [
|
||||
const output: any = [
|
||||
["72", "test"],
|
||||
["a", 42],
|
||||
["b", [
|
||||
@@ -557,4 +497,30 @@ describe("utils", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortEventsByLatestContentTimestamp', () => {
|
||||
const roomId = '!room:server';
|
||||
const userId = '@user:server';
|
||||
const eventWithoutContentTimestamp = mkMessage({ room: roomId, user: userId, event: true });
|
||||
// m.beacon events have timestamp in content
|
||||
const beaconEvent1 = makeBeaconEvent(userId, { timestamp: 1648804528557 });
|
||||
const beaconEvent2 = makeBeaconEvent(userId, { timestamp: 1648804528558 });
|
||||
const beaconEvent3 = makeBeaconEvent(userId, { timestamp: 1648804528000 });
|
||||
const beaconEvent4 = makeBeaconEvent(userId, { timestamp: 0 });
|
||||
|
||||
it('sorts events with timestamps as later than events without', () => {
|
||||
expect(
|
||||
[beaconEvent4, eventWithoutContentTimestamp, beaconEvent1]
|
||||
.sort(utils.sortEventsByLatestContentTimestamp),
|
||||
).toEqual([
|
||||
beaconEvent1, beaconEvent4, eventWithoutContentTimestamp,
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts by content timestamps correctly', () => {
|
||||
expect(
|
||||
[beaconEvent1, beaconEvent2, beaconEvent3].sort(sortEventsByLatestContentTimestamp),
|
||||
).toEqual([beaconEvent2, beaconEvent1, beaconEvent3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+494
-99
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 - 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.
|
||||
@@ -15,73 +15,25 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
|
||||
import { MatrixCall, CallErrorCode, CallEvent, supportsMatrixCall, CallType } from '../../../src/webrtc/call';
|
||||
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
|
||||
import {
|
||||
DUMMY_SDP,
|
||||
MockMediaHandler,
|
||||
MockMediaStream,
|
||||
MockMediaStreamTrack,
|
||||
MockMediaDeviceInfo,
|
||||
MockRTCPeerConnection,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
|
||||
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" +
|
||||
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
|
||||
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
|
||||
"a=ice-options:trickle\r\n" +
|
||||
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
|
||||
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
|
||||
"a=setup:active\r\n" +
|
||||
"a=mid:0\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
|
||||
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
|
||||
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
|
||||
"a=sendrecv\r\n" +
|
||||
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"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"
|
||||
);
|
||||
const startVoiceCall = async (client: TestClient, call: MatrixCall): Promise<void> => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush("");
|
||||
await callPromise;
|
||||
|
||||
class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener() {}
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
}
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
};
|
||||
|
||||
describe('Call', function() {
|
||||
let client;
|
||||
@@ -98,13 +50,9 @@ describe('Call', function() {
|
||||
global.navigator = {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => {
|
||||
return {
|
||||
getTracks: () => [],
|
||||
getAudioTracks: () => [],
|
||||
getVideoTracks: () => [],
|
||||
};
|
||||
},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
// @ts-ignore Mock
|
||||
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -115,7 +63,7 @@ describe('Call', function() {
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
@@ -124,6 +72,8 @@ describe('Call', function() {
|
||||
// We just stub out sendEvent: we're not interested in testing the client's
|
||||
// event sending code here
|
||||
client.client.sendEvent = () => {};
|
||||
client.client.mediaHandler = new MockMediaHandler;
|
||||
client.client.getMediaHandler = () => client.client.mediaHandler;
|
||||
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
|
||||
call = new MatrixCall({
|
||||
client: client.client,
|
||||
@@ -141,9 +91,8 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should ignore candidate events from non-matching party ID', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
@@ -197,9 +146,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should add candidates received before answer if party ID is correct', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
@@ -257,9 +204,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should map asserted identity messages to remoteAssertedIdentity', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
@@ -301,13 +246,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it("should map SDPStreamMetadata to feeds", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
call.getOpponentMember = () => {
|
||||
return { userId: "@bob:bar.uk" };
|
||||
};
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
@@ -319,7 +258,7 @@ describe('Call', function() {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {
|
||||
"stream_id": {
|
||||
"remote_stream": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: true,
|
||||
video_muted: false,
|
||||
@@ -329,21 +268,23 @@ describe('Call', function() {
|
||||
},
|
||||
});
|
||||
|
||||
call.pushRemoteFeed({ id: "stream_id", getAudioTracks: () => ["track1"], getVideoTracks: () => ["track1"] });
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "stream_id");
|
||||
call.pushRemoteFeed(
|
||||
new MockMediaStream(
|
||||
"remote_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("remote_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("remote_video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
|
||||
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
|
||||
expect(feed?.isAudioMuted()).toBeTruthy();
|
||||
expect(feed?.isVideoMuted()).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it("should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
call.getOpponentMember = () => {
|
||||
return { userId: "@bob:bar.uk" };
|
||||
};
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
@@ -363,4 +304,458 @@ describe('Call', function() {
|
||||
call.setScreensharingEnabled(true);
|
||||
expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fallback to answering with no video", async () => {
|
||||
await client.httpBackend.flush();
|
||||
|
||||
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
|
||||
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject");
|
||||
|
||||
await call.answer(true, true);
|
||||
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
|
||||
});
|
||||
|
||||
it("should handle mid-call device changes", async () => {
|
||||
client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue(
|
||||
new MockMediaStream(
|
||||
"stream", [
|
||||
new MockMediaStreamTrack("audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.updateLocalUsermediaStream(
|
||||
new MockMediaStream(
|
||||
"replacement_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("new_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(call.localUsermediaStream.id).toBe("stream");
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("new_audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("new_audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
|
||||
it("should handle upgrade to video call", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.upgradeCall(false, true);
|
||||
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
|
||||
describe("should handle stream replacement", () => {
|
||||
it("with both purpose and id", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream1": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream1");
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream2": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream2", []));
|
||||
|
||||
expect(feed?.stream?.id).toBe("remote_stream2");
|
||||
});
|
||||
|
||||
it("with just purpose", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream1": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream1");
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream2": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream2", []));
|
||||
|
||||
expect(feed?.stream?.id).toBe("remote_stream2");
|
||||
});
|
||||
|
||||
it("should not replace purpose is different", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream1": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream1");
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream2": {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream2", []));
|
||||
|
||||
expect(feed?.stream?.id).toBe("remote_stream1");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle SDPStreamMetadata changes", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: false,
|
||||
video_muted: false,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
|
||||
|
||||
call.onSDPStreamMetadataChangedReceived({
|
||||
getContent: () => ({
|
||||
[SDPStreamMetadataKey]: {
|
||||
"remote_stream": {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
audio_muted: true,
|
||||
video_muted: true,
|
||||
id: "feed_id2",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Screenshare);
|
||||
expect(feed?.audioMuted).toBe(true);
|
||||
expect(feed?.videoMuted).toBe(true);
|
||||
});
|
||||
|
||||
it("should choose opponent member", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
const opponentMember = {
|
||||
roomId: call.roomId,
|
||||
userId: "opponentUserId",
|
||||
};
|
||||
const opponentCaps = {
|
||||
"m.call.transferee": true,
|
||||
"m.call.dtmf": false,
|
||||
};
|
||||
call.chooseOpponent({
|
||||
getContent: () => ({
|
||||
version: 1,
|
||||
party_id: "party_id",
|
||||
capabilities: opponentCaps,
|
||||
}),
|
||||
sender: opponentMember,
|
||||
});
|
||||
|
||||
expect(call.getOpponentMember()).toBe(opponentMember);
|
||||
expect(call.opponentPartyId).toBe("party_id");
|
||||
expect(call.opponentCaps).toBe(opponentCaps);
|
||||
expect(call.opponentCanBeTransferred()).toBe(true);
|
||||
expect(call.opponentSupportsDTMF()).toBe(false);
|
||||
});
|
||||
|
||||
describe("should deduce the call type correctly", () => {
|
||||
it("if no video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
expect(call.type).toBe(CallType.Voice);
|
||||
});
|
||||
|
||||
it("if remote video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]));
|
||||
expect(call.type).toBe(CallType.Video);
|
||||
});
|
||||
|
||||
it("if local video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushNewLocalFeed(
|
||||
new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]),
|
||||
SDPStreamMetadataPurpose.Usermedia,
|
||||
false,
|
||||
);
|
||||
expect(call.type).toBe(CallType.Video);
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate local SDPStreamMetadata", async () => {
|
||||
const callPromise = call.placeCallWithCallFeeds([new CallFeed({
|
||||
client,
|
||||
// @ts-ignore Mock
|
||||
stream: new MockMediaStream("local_stream1", [new MockMediaStreamTrack("track_id", "audio")]),
|
||||
roomId: call.roomId,
|
||||
userId: client.getUserId(),
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
})]);
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushNewLocalFeed(
|
||||
new MockMediaStream("local_stream2", [new MockMediaStreamTrack("track_id", "video")]),
|
||||
SDPStreamMetadataPurpose.Screenshare, "feed_id2",
|
||||
);
|
||||
await call.setMicrophoneMuted(true);
|
||||
|
||||
expect(call.getLocalSDPStreamMetadata()).toStrictEqual({
|
||||
"local_stream1": {
|
||||
"purpose": SDPStreamMetadataPurpose.Usermedia,
|
||||
"audio_muted": true,
|
||||
"video_muted": true,
|
||||
},
|
||||
"local_stream2": {
|
||||
"purpose": SDPStreamMetadataPurpose.Screenshare,
|
||||
"audio_muted": true,
|
||||
"video_muted": false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("feed and stream getters return correctly", async () => {
|
||||
const localUsermediaStream = new MockMediaStream("local_usermedia_stream_id", []);
|
||||
const localScreensharingStream = new MockMediaStream("local_screensharing_stream_id", []);
|
||||
const remoteUsermediaStream = new MockMediaStream("remote_usermedia_stream_id", []);
|
||||
const remoteScreensharingStream = new MockMediaStream("remote_screensharing_stream_id", []);
|
||||
|
||||
const callPromise = call.placeCallWithCallFeeds([
|
||||
new CallFeed({
|
||||
client,
|
||||
userId: client.getUserId(),
|
||||
// @ts-ignore Mock
|
||||
stream: localUsermediaStream,
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
id: "local_usermedia_feed_id",
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
}),
|
||||
new CallFeed({
|
||||
client,
|
||||
userId: client.getUserId(),
|
||||
// @ts-ignore Mock
|
||||
stream: localScreensharingStream,
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
id: "local_screensharing_feed_id",
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
}),
|
||||
]);
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_usermedia_stream_id": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
id: "remote_usermedia_feed_id",
|
||||
audio_muted: false,
|
||||
video_muted: false,
|
||||
},
|
||||
"remote_screensharing_stream_id": {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
id: "remote_screensharing_feed_id",
|
||||
audio_muted: false,
|
||||
video_muted: false,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(remoteUsermediaStream);
|
||||
call.pushRemoteFeed(remoteScreensharingStream);
|
||||
|
||||
expect(call.localUsermediaFeed.stream).toBe(localUsermediaStream);
|
||||
expect(call.localUsermediaStream).toBe(localUsermediaStream);
|
||||
expect(call.localScreensharingFeed.stream).toBe(localScreensharingStream);
|
||||
expect(call.localScreensharingStream).toBe(localScreensharingStream);
|
||||
expect(call.remoteUsermediaFeed.stream).toBe(remoteUsermediaStream);
|
||||
expect(call.remoteUsermediaStream).toBe(remoteUsermediaStream);
|
||||
expect(call.remoteScreensharingFeed.stream).toBe(remoteScreensharingStream);
|
||||
expect(call.remoteScreensharingStream).toBe(remoteScreensharingStream);
|
||||
expect(call.hasRemoteUserMediaAudioTrack).toBe(false);
|
||||
});
|
||||
|
||||
it("should end call after receiving a select event with a different party id", async () => {
|
||||
const callPromise = call.initWithInvite({
|
||||
getContent: () => ({
|
||||
version: 1,
|
||||
call_id: "call_id",
|
||||
party_id: "remote_party_id",
|
||||
offer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
}),
|
||||
getLocalAge: () => null,
|
||||
});
|
||||
call.feeds.push(new CallFeed({
|
||||
client,
|
||||
userId: "remote_user_id",
|
||||
// @ts-ignore Mock
|
||||
stream: new MockMediaStream("remote_stream_id", [new MockMediaStreamTrack("remote_tack_id")]),
|
||||
id: "remote_feed_id",
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
}));
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
const callHangupCallback = jest.fn();
|
||||
call.on(CallEvent.Hangup, callHangupCallback);
|
||||
|
||||
await call.onSelectAnswerReceived({
|
||||
getContent: () => ({
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
selected_party_id: "different_party_id",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(callHangupCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("turn servers", () => {
|
||||
it("should fallback if allowed", async () => {
|
||||
client.client.isFallbackICEServerAllowed = () => true;
|
||||
const localCall = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!room_id',
|
||||
});
|
||||
|
||||
expect((localCall as any).turnServers).toStrictEqual([{ urls: ["stun:turn.matrix.org"] }]);
|
||||
});
|
||||
|
||||
it("should not fallback if not allowed", async () => {
|
||||
client.client.isFallbackICEServerAllowed = () => false;
|
||||
const localCall = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!room_id',
|
||||
});
|
||||
|
||||
expect((localCall as any).turnServers).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should not fallback if we supplied turn servers", async () => {
|
||||
client.client.isFallbackICEServerAllowed = () => true;
|
||||
const turnServers = [{ urls: ["turn.server.org"] }];
|
||||
const localCall = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!room_id',
|
||||
turnServers,
|
||||
});
|
||||
|
||||
expect((localCall as any).turnServers).toStrictEqual(turnServers);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle creating a data channel", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
const dataChannelCallback = jest.fn();
|
||||
call.on(CallEvent.DataChannel, dataChannelCallback);
|
||||
|
||||
const dataChannel = call.createDataChannel("data_channel_label", { id: 123 });
|
||||
|
||||
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel);
|
||||
expect(dataChannel.label).toBe("data_channel_label");
|
||||
expect(dataChannel.id).toBe(123);
|
||||
});
|
||||
|
||||
describe("supportsMatrixCall", () => {
|
||||
it("should return true when the environment is right", () => {
|
||||
expect(supportsMatrixCall()).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if window or document are undefined", () => {
|
||||
global.window = undefined;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
global.window = prevWindow;
|
||||
global.document = undefined;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if RTCPeerConnection throws", () => {
|
||||
// @ts-ignore - writing to window as we are simulating browser edge-cases
|
||||
global.window = {};
|
||||
Object.defineProperty(global.window, "RTCPeerConnection", {
|
||||
get: () => {
|
||||
throw Error("Secure mode, naaah!");
|
||||
},
|
||||
});
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if RTCPeerConnection & RTCSessionDescription " +
|
||||
"& RTCIceCandidate & mediaDevices are unavailable",
|
||||
() => {
|
||||
global.window.RTCPeerConnection = undefined;
|
||||
global.window.RTCSessionDescription = undefined;
|
||||
global.window.RTCIceCandidate = undefined;
|
||||
// @ts-ignore - writing to a read-only property as we are simulating faulty browsers
|
||||
global.navigator.mediaDevices = undefined;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 { TestClient } from '../../TestClient';
|
||||
import {
|
||||
ClientEvent,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
IRoomTimelineData,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from "../../../src";
|
||||
import { CallEventHandler, CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
|
||||
import { SyncState } from "../../../src/sync";
|
||||
|
||||
describe("callEventHandler", () => {
|
||||
it("should ignore a call if invite & hangup come within a single sync", () => {
|
||||
const testClient = new TestClient();
|
||||
const client = testClient.client;
|
||||
const room = new Room("!room:id", client, "@user:id");
|
||||
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
client.callEventHandler = new CallEventHandler(client);
|
||||
client.callEventHandler.start();
|
||||
|
||||
// Fire off call invite then hangup within a single sync
|
||||
const callInvite = new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
content: {
|
||||
call_id: "123",
|
||||
},
|
||||
});
|
||||
client.emit(RoomEvent.Timeline, callInvite, room, false, false, timelineData);
|
||||
|
||||
const callHangup = new MatrixEvent({
|
||||
type: EventType.CallHangup,
|
||||
content: {
|
||||
call_id: "123",
|
||||
},
|
||||
});
|
||||
client.emit(RoomEvent.Timeline, callHangup, room, false, false, timelineData);
|
||||
|
||||
const incomingCallEmitted = jest.fn();
|
||||
client.on(CallEventHandlerEvent.Incoming, incomingCallEmitted);
|
||||
|
||||
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
|
||||
client.emit(ClientEvent.Sync, SyncState.Syncing);
|
||||
|
||||
expect(incomingCallEmitted).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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 { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
|
||||
import { CallFeed, CallFeedEvent } from "../../../src/webrtc/callFeed";
|
||||
import { MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
|
||||
import { TestClient } from "../../TestClient";
|
||||
|
||||
describe("CallFeed", () => {
|
||||
const roomId = "room_id";
|
||||
|
||||
let client;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it("should handle stream replacement", () => {
|
||||
const feedNewStreamCallback = jest.fn();
|
||||
const feed = new CallFeed({
|
||||
client,
|
||||
roomId,
|
||||
userId: "user1",
|
||||
// @ts-ignore Mock
|
||||
stream: new MockMediaStream("stream1"),
|
||||
id: "id",
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
});
|
||||
feed.on(CallFeedEvent.NewStream, feedNewStreamCallback);
|
||||
|
||||
const replacementStream = new MockMediaStream("stream2");
|
||||
// @ts-ignore Mock
|
||||
feed.setNewStream(replacementStream);
|
||||
expect(feedNewStreamCallback).toHaveBeenCalledWith(replacementStream);
|
||||
expect(feed.stream).toBe(replacementStream);
|
||||
|
||||
feedNewStreamCallback.mockReset();
|
||||
|
||||
replacementStream.addTrack(new MockMediaStreamTrack("track_id", "audio"));
|
||||
expect(feedNewStreamCallback).toHaveBeenCalledWith(replacementStream);
|
||||
});
|
||||
});
|
||||
@@ -90,8 +90,9 @@ export interface ISenderNotificationPermissionCondition
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type PushRuleCondition = IPushRuleCondition<string>
|
||||
| IEventMatchCondition
|
||||
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
|
||||
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
|
||||
export type PushRuleCondition = IEventMatchCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition;
|
||||
@@ -146,12 +147,12 @@ export interface IPusher {
|
||||
app_display_name: string;
|
||||
app_id: string;
|
||||
data: {
|
||||
format?: string; // TODO: Types
|
||||
format?: string;
|
||||
url?: string; // TODO: Required if kind==http
|
||||
brand?: string; // TODO: For email notifications only?
|
||||
brand?: string; // TODO: For email notifications only? Unspecced field
|
||||
};
|
||||
device_display_name: string;
|
||||
kind: string; // TODO: Types
|
||||
kind: "http" | string;
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
/**
|
||||
* Represents a response to the CSAPI `/refresh` endpoint.
|
||||
*/
|
||||
export interface IRefreshTokenResponse {
|
||||
access_token: string;
|
||||
expires_in_ms: number;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
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 { RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
|
||||
|
||||
/**
|
||||
* Beacon info and beacon event types as described in MSC3672
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||
*/
|
||||
|
||||
/**
|
||||
* Beacon info events are state events.
|
||||
* We have two requirements for these events:
|
||||
* 1. they can only be written by their owner
|
||||
* 2. a user can have an arbitrary number of beacon_info events
|
||||
*
|
||||
* 1. is achieved by setting the state_key to the owners mxid.
|
||||
* Event keys in room state are a combination of `type` + `state_key`.
|
||||
* To achieve an arbitrary number of only owner-writable state events
|
||||
* we introduce a variable suffix to the event type
|
||||
*
|
||||
* Eg
|
||||
* {
|
||||
* "type": "m.beacon_info.@matthew:matrix.org.1",
|
||||
* "state_key": "@matthew:matrix.org",
|
||||
* "content": {
|
||||
* "m.beacon_info": {
|
||||
* "description": "The Matthew Tracker",
|
||||
* "timeout": 86400000,
|
||||
* },
|
||||
* // more content as described below
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* "type": "m.beacon_info.@matthew:matrix.org.2",
|
||||
* "state_key": "@matthew:matrix.org",
|
||||
* "content": {
|
||||
* "m.beacon_info": {
|
||||
* "description": "Another different Matthew tracker",
|
||||
* "timeout": 400000,
|
||||
* },
|
||||
* // more content as described below
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Non-variable type for m.beacon_info event content
|
||||
*/
|
||||
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3672.beacon_info");
|
||||
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3672.beacon");
|
||||
|
||||
export type MBeaconInfoContent = {
|
||||
description?: string;
|
||||
// how long from the last event until we consider the beacon inactive in milliseconds
|
||||
timeout: number;
|
||||
// true when this is a live location beacon
|
||||
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||
live?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* m.beacon_info Event example from the spec
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||
* {
|
||||
"type": "m.beacon_info",
|
||||
"state_key": "@matthew:matrix.org",
|
||||
"content": {
|
||||
"m.beacon_info": {
|
||||
"description": "The Matthew Tracker", // same as an `m.location` description
|
||||
"timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds
|
||||
},
|
||||
"m.ts": 1436829458432, // creation timestamp of the beacon on the client
|
||||
"m.asset": {
|
||||
"type": "m.self" // the type of asset being tracked as per MSC3488
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* m.beacon_info.* event content
|
||||
*/
|
||||
export type MBeaconInfoEventContent = &
|
||||
MBeaconInfoContent &
|
||||
// creation timestamp of the beacon on the client
|
||||
MTimestampEvent &
|
||||
// the type of asset being tracked as per MSC3488
|
||||
MAssetEvent;
|
||||
|
||||
/**
|
||||
* m.beacon event example
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||
*
|
||||
* {
|
||||
"type": "m.beacon",
|
||||
"sender": "@matthew:matrix.org",
|
||||
"content": {
|
||||
"m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674
|
||||
"rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267
|
||||
"event_id": "$beacon_info"
|
||||
},
|
||||
"m.location": {
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"description": "Arbitrary beacon information"
|
||||
},
|
||||
"m.ts": 1636829458432,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Content of an m.beacon event
|
||||
*/
|
||||
export type MBeaconEventContent = &
|
||||
MLocationEvent &
|
||||
// timestamp when location was taken
|
||||
MTimestampEvent &
|
||||
// relates to a beacon_info event
|
||||
RELATES_TO_RELATIONSHIP<typeof REFERENCE_RELATION>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user