Compare commits
2639 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07476a0ae0 | |||
| 6348704bec | |||
| f84a33910c | |||
| 0ccf7c50f2 | |||
| ead33003b7 | |||
| 7d5360a00f | |||
| 4b283015ba | |||
| 887e15aac5 | |||
| a83d80f502 | |||
| 6166a8f7fd | |||
| 3efc18cfde | |||
| 5520aa3e2a | |||
| 5afe373446 | |||
| 9bb5afe5c0 | |||
| f349663329 | |||
| f398e3564d | |||
| 91171afddd | |||
| b54c9d689a | |||
| 4e69d7c9ac | |||
| bf9f595984 | |||
| f410e71bfa | |||
| 83fca5b57d | |||
| 90052670e7 | |||
| db49a1a623 | |||
| 45330c6418 | |||
| 4ba083e6af | |||
| 0403e4bedc | |||
| 14aa7846a5 | |||
| 2d067ad957 | |||
| 418aa3ff6a | |||
| a587d7c360 | |||
| 45348a354e | |||
| fa3339fc84 | |||
| b64a30f0ad | |||
| 5451f6139a | |||
| b332c6c4b9 | |||
| 209a101be7 | |||
| efbf5479d1 | |||
| dacef048be | |||
| caadc6f95b | |||
| 39bc7e2bb3 | |||
| 040f012350 | |||
| 8b2b677f92 | |||
| 2b1fab928b | |||
| 516f52c5a4 | |||
| 8599a98b47 | |||
| 7e24cb6cae | |||
| 38aa8d18c0 | |||
| 2967ee6309 | |||
| 2e10b6065c | |||
| 69057ee035 | |||
| f34e568a98 | |||
| 6fd80ed3ed | |||
| 9ff11d1f32 | |||
| 41ab3337b5 | |||
| 1432e094c2 | |||
| e09936cc9b | |||
| d32d190a8a | |||
| 53de8d5690 | |||
| 829110b580 | |||
| 59c82cb679 | |||
| 6d3741d55c | |||
| 43213fec78 | |||
| c8438ff6da | |||
| a1d0f037e2 | |||
| fb565f301b | |||
| afa3b37ad5 | |||
| a57c430b09 | |||
| 583e48086b | |||
| b22c671fee | |||
| 1b86acb2fb | |||
| d2f7a2575e | |||
| 8490f72488 | |||
| d87e53858b | |||
| 917e8c01d8 | |||
| eb3309db43 | |||
| 65741d7860 | |||
| a7a264f4e7 | |||
| 0fa125b60a | |||
| 8aee884d03 | |||
| a8fd0f3d13 | |||
| 876491e38d | |||
| 71fcd9f35b | |||
| 193ff0b4d1 | |||
| f616022d07 | |||
| 3c206c0b85 | |||
| a8c4ff473a | |||
| 289a930cda | |||
| 8ba30bc4ef | |||
| 6e28634819 | |||
| b1e70c5404 | |||
| b11c502a40 | |||
| 167f51c8cd | |||
| 5d2753241e | |||
| 274fe447fd | |||
| c0f1849a83 | |||
| 7b2b618d26 | |||
| 37187ef347 | |||
| e87ce873b0 | |||
| 207171efd6 | |||
| 8cc5efdf46 | |||
| cbcf47d5c0 | |||
| bbaa0e6536 | |||
| 1efeb1ec0e | |||
| 06e8d98911 | |||
| 8716c1ab9b | |||
| ac7f505a2b | |||
| b5576758e4 | |||
| c32a83fdac | |||
| 2d9556c1bb | |||
| 818b70554a | |||
| 725336ffc6 | |||
| 1fbd8983ed | |||
| 1c77816dbd | |||
| c1160f40c2 | |||
| b789cc5933 | |||
| 5e4474b959 | |||
| 8e646ea584 | |||
| 528e9343ae | |||
| 6571b6a1ab | |||
| 1df329df7c | |||
| 760eeaeed7 | |||
| 438fc70615 | |||
| de3b3960d2 | |||
| b265d795a4 | |||
| eb79f6246d | |||
| 37f8f736e0 | |||
| 4b1a443f90 | |||
| 3ae974e23e | |||
| 566b4ba56c | |||
| 13291f33d2 | |||
| 8502759e3e | |||
| 24f9075a84 | |||
| d18aae09c8 | |||
| be3e731499 | |||
| a9f2ae6b55 | |||
| b254ca7fc8 | |||
| 3f6f5b69c7 | |||
| 0e8bd3f02d | |||
| 478270b225 | |||
| 9eb72908a7 | |||
| 2728d74771 | |||
| edcef9364c | |||
| 1635ac9971 | |||
| 8f13df2dd9 | |||
| fa9f078a75 | |||
| 055af933cc | |||
| 1ba2730e25 | |||
| 1c9d644a23 | |||
| e29e0d15a5 | |||
| 9ee94c9902 | |||
| 1645867ea6 | |||
| 24d4181a08 | |||
| f576a9f2e9 | |||
| fed121b0aa | |||
| 3e37c74264 | |||
| 3762c20aad | |||
| 2596999cb8 | |||
| a3248c0aa1 | |||
| c96f1ba22b | |||
| 43c81358b2 | |||
| e05f9b5815 | |||
| 6316a6ae44 | |||
| 575b416856 | |||
| 7b7f8c1592 | |||
| 3907d1c28f | |||
| c629d2f60e | |||
| 43b453804b | |||
| d867affc40 | |||
| c36bfc821c | |||
| 7e784da00a | |||
| b79f469008 | |||
| cf33569a21 | |||
| fb0a0c66c8 | |||
| aac0023338 | |||
| e3873ddef5 | |||
| f0991348e2 | |||
| fa6708c27e | |||
| 4427201326 | |||
| 4a4241806e | |||
| 3824f65d15 | |||
| 3c17e4a6d6 | |||
| 75513d08de | |||
| 7cb3b40493 | |||
| ab89804c55 | |||
| ab6cf93c2b | |||
| 4c80762e22 | |||
| 1f7e80c68d | |||
| e91b879a69 | |||
| 14885ba7a2 | |||
| 0dda187d96 | |||
| 680d8cac4d | |||
| 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 | |||
| 429f32dacb | |||
| ad036104e6 | |||
| 4607f0177c | |||
| d7dbaeba46 | |||
| 3e94db1837 | |||
| 4fd77c2f05 | |||
| 9fe05e7d40 | |||
| 946dcd0301 | |||
| 97d718e850 | |||
| 913b2d148c | |||
| 916f6a6126 | |||
| 7dbee19456 | |||
| 2375b00b52 | |||
| 13cdca028f | |||
| e5a67500e4 | |||
| 264636c879 | |||
| 9b50455049 | |||
| a531446396 | |||
| f876c35283 | |||
| 1bcec53c6b | |||
| 0ad5ef4e9b | |||
| 94726aa1d5 | |||
| 8da2f91e22 | |||
| b03555a50b | |||
| b61312feca | |||
| ea9459d330 | |||
| 3edfe70a7b | |||
| 5482a19979 | |||
| e1678aaf11 | |||
| 2bdb570f3c | |||
| 3723b2da39 | |||
| b63af00579 | |||
| 4ed6fbe5fe | |||
| 026260502b | |||
| c45bc91389 | |||
| 18113900fe | |||
| 69358f8372 | |||
| 8800c6d2e0 | |||
| b63de6a902 | |||
| b1cec7d6a5 | |||
| 08821e499c | |||
| 3ca84cfc49 | |||
| be8ecce995 | |||
| 775c3465bb | |||
| 7484e3c032 | |||
| b93e79274f | |||
| fee6a96350 | |||
| ac2c09108f | |||
| 5b13785115 | |||
| dd3c53edac | |||
| 904071ebe6 | |||
| 7e0aeb425d | |||
| f4e985b5cc | |||
| 03134832e9 | |||
| dbb0f66094 | |||
| 4631cd1fe3 | |||
| bc07ad5909 | |||
| 8b0c4a0efb | |||
| eac77a6695 | |||
| aae8f1c706 | |||
| 1e281f6838 | |||
| aef89e4a26 | |||
| 8a1bc98d4e | |||
| 8b2d5959ec | |||
| 4891446dc3 | |||
| cace8f232f | |||
| 88b8b24629 | |||
| b15ba8c5e3 | |||
| 58c60d85e7 | |||
| 529fe93ab1 | |||
| 600438d02e | |||
| 21a357c433 | |||
| 9c05077c26 | |||
| d49d6936a5 | |||
| e6696f78f4 | |||
| f2da3ba6ae | |||
| db5de4c6e4 | |||
| ca042b3647 | |||
| 87fd3521ea | |||
| b3c66848e2 | |||
| 606aa29381 | |||
| 2d25150a21 | |||
| ca7a457094 | |||
| 23ab3e3ec0 | |||
| b11a8459d8 | |||
| 057eb0f2a5 | |||
| 8de6c5aad1 | |||
| 6fda2a0c57 | |||
| efeeba265f | |||
| 35ca7fdc48 | |||
| 959e3f68a4 | |||
| f20b3211e6 | |||
| 9b5302ebf2 | |||
| 19f90f56be | |||
| a69d9a9f20 | |||
| c5e113d0ca | |||
| 1df90f8fc7 | |||
| 8e6040ad6f | |||
| 21f107d734 | |||
| e6d40f0c17 | |||
| 6b8bb46790 | |||
| 05c3236e50 | |||
| 4f8c20ddae | |||
| 8aa283d994 | |||
| e85b3b6a8d | |||
| 04cd9b55f7 | |||
| faf237e588 | |||
| 019abc7ad8 | |||
| db4b6fe2af | |||
| c340e6f1ba | |||
| f227a307c1 | |||
| 011a2acfd2 | |||
| c2b5b14d26 | |||
| ad79a64f1c | |||
| 91f5df1e48 | |||
| 980d6fc2ae | |||
| 5aa60b8056 | |||
| dc154a00a8 | |||
| a087acf65d | |||
| 05d0755b6a | |||
| cd86aa7d9e | |||
| fcbbcc0398 | |||
| 2bd99b5cb0 | |||
| 82d248cb8e | |||
| ce2803ebaa | |||
| e85038e888 | |||
| ceafb1c5d0 | |||
| a87858840b | |||
| f708c47385 | |||
| 9eb918f701 | |||
| b4bffd2700 | |||
| 80d7c88b40 | |||
| fb315ac75b | |||
| bee840ce16 | |||
| afa67688f8 | |||
| 0c06e47782 | |||
| ea646c673b | |||
| 5cf6684129 | |||
| cb2b9619ab | |||
| b8c2a57829 | |||
| 718a22f574 | |||
| 324cd886a2 | |||
| 4cf655785e | |||
| f686d70157 | |||
| b936b4a2ce | |||
| 026839d7e7 | |||
| 830a4a741e | |||
| a3d9ed19a4 | |||
| 8f9f5fd5b0 | |||
| 805267f97f | |||
| 4a1cd159a4 | |||
| 7129c7c7af | |||
| ecc9fda28b | |||
| 7f7c3cbea5 | |||
| fc8cd39d1a | |||
| 589c66bb12 | |||
| 16fe5a91d5 | |||
| b57b7a65ee | |||
| 019e93e76c | |||
| 69415ba2c3 | |||
| 1607ad2111 | |||
| f1a0c46a29 | |||
| 9b81b805be | |||
| 8379bb818e | |||
| c62f2a0965 | |||
| d383e71aca | |||
| dbe93f598a | |||
| 2bc5f7132d | |||
| b17f339011 | |||
| e2b04d543a | |||
| 0601f98265 | |||
| bae5650e7a | |||
| ff40b4dc4a | |||
| 6098f74d8f | |||
| 45c153321c | |||
| 19aedebff1 | |||
| 5392f616b4 | |||
| c4ccb9d493 | |||
| b0726b5008 | |||
| 9c221419ef | |||
| fddf0c47fb | |||
| b17862e212 | |||
| e133898554 | |||
| ee5a0e7edf | |||
| b538c06fcb | |||
| 49cbaa579f | |||
| 20bd9b525b | |||
| 3038fc523c | |||
| 52a85e3f82 | |||
| 3df1c4b507 | |||
| 6ada02e245 | |||
| 9184a0d902 | |||
| 4bc4e1b76f | |||
| cb192d3c74 | |||
| 11dabf9a68 | |||
| ea4dfd003f | |||
| 326b14402f | |||
| d71ca8a9e0 | |||
| 45259f0773 | |||
| 83337eee57 | |||
| aed861036b | |||
| 3ff05c0947 | |||
| 5b2a53a2b4 | |||
| 3f6aee1443 | |||
| dba70d4ce2 | |||
| ab3dfd48ae | |||
| ab59e9134b | |||
| 56921f0961 | |||
| f9005c33e9 | |||
| b7c5b36556 | |||
| 33deef9f2c | |||
| d5c3cb1b7f | |||
| 19d8f2bbac | |||
| 1aa103d8d2 | |||
| 75f630e45d | |||
| 1defde0f5e | |||
| e38595e735 | |||
| c0e16ac98c | |||
| b33429317c | |||
| e3f7c848d7 | |||
| 5f2ed4450e | |||
| 11e25ea9a2 | |||
| fb7184e6fb | |||
| 3c77a8772a | |||
| 5ce787c6ea | |||
| 67189ad323 | |||
| 3bff5430f6 | |||
| 17efcad6fe | |||
| d7e6cee19c | |||
| 116085e927 | |||
| 96e6e7b1e8 | |||
| 061199ec3c | |||
| 2d67a35af5 | |||
| 4cb6ad9e70 | |||
| d639a29501 | |||
| ac02f30dc8 | |||
| 05a20ab56f | |||
| 964aa6d94a | |||
| 1a17b138ce | |||
| 2e27e247f3 | |||
| ef8e4d6d35 | |||
| 8bee38367a | |||
| 0318c65847 | |||
| ab5b69bbdb | |||
| 9f4c5af665 | |||
| e275301e8b | |||
| c8a7820cb3 | |||
| 17dc1bda19 | |||
| 8d9de37099 | |||
| 29b403da45 | |||
| 1fd1ba2ada | |||
| 5fbce3b928 | |||
| 9019bce843 | |||
| fd2d106f2a | |||
| 64a1c83acc | |||
| 8cc250e19d | |||
| dbd737c87d | |||
| 33398e78cd | |||
| 79c6d0180c | |||
| e7a198dc0c | |||
| ef08c35c6f | |||
| 41b816538e | |||
| 87d9fe24f5 | |||
| d11c95a23f | |||
| 3c9b846116 | |||
| c701e2e6d3 | |||
| 11b490c77f | |||
| 0544ab2617 | |||
| 5c6d3724b0 | |||
| c34d4e777f | |||
| 588d783a55 | |||
| 79f74fdff4 | |||
| 6f5dbb782e | |||
| f0ae9b0100 | |||
| fd260a023c | |||
| e775bcac3c | |||
| 641c852ee2 | |||
| ab679c2216 | |||
| 96420a75ab | |||
| c4263692f0 | |||
| 0074b71cbf | |||
| d219c2f462 | |||
| 4700dc0375 | |||
| a248bbc46b | |||
| d60affc1b8 | |||
| 8c0f5b4e31 | |||
| a617f6cc55 | |||
| 9e66026bdc | |||
| dc90115a1b | |||
| 13b250d291 | |||
| 834ab22923 | |||
| 17be68a39d | |||
| 48ee03e278 | |||
| 5b86d8b837 | |||
| 521fce59ea | |||
| bbe1ba7328 | |||
| ce7f20219f | |||
| aad33bd1b7 | |||
| 9923c7e577 | |||
| ae870b1cc1 | |||
| e3cb199540 | |||
| d9d19fdc63 | |||
| f506882bf8 | |||
| e782183a49 | |||
| 6699c4d8af | |||
| f027cbf170 | |||
| e0012d9b81 | |||
| b2ad957d29 | |||
| cab334ed73 | |||
| a99c1e96d6 | |||
| 261ac8b2d6 | |||
| 399237e781 | |||
| 4b29f02f1c | |||
| 558da5528b | |||
| e3a00c2cb4 | |||
| 12deaa80a6 | |||
| a6507768bc | |||
| 8f19ab066c | |||
| 17decea576 | |||
| a2442add5b | |||
| 7dbe95fa92 | |||
| 3339a2874d | |||
| 393047dec5 | |||
| 55fb3d4e8e | |||
| 047180edc9 | |||
| 1b0a388eb3 | |||
| d50e559f97 | |||
| 835aafcb17 | |||
| 48ad9ba3d7 | |||
| 3675e95970 | |||
| 1ca13f4ce9 | |||
| 40aa6ba96a | |||
| b4dc1e1555 | |||
| 3effeb7dc4 | |||
| 8c11839e4d | |||
| 41817995bd | |||
| 8c9799d64c | |||
| 3a5e4ffa91 | |||
| 02afcc7d4b | |||
| e9007429dd | |||
| 664d920dd1 | |||
| 5a8299f1a5 | |||
| 6017fead19 | |||
| 69050ed338 | |||
| 2664848545 | |||
| b18f0ff738 | |||
| 1171c33f7a | |||
| 6deb2bc7a9 | |||
| 7bda13aba6 | |||
| 75719c3e0f | |||
| 2fb033b5ba | |||
| 9f1db7a707 | |||
| a93de99d42 | |||
| 3d5774ac9e | |||
| 1d8e0398e9 | |||
| cf76375ce0 | |||
| 284e2bb911 | |||
| ecb790c179 | |||
| 467b75f2dc | |||
| 1f728cab92 | |||
| 66b17aa019 | |||
| 27a6d1f878 | |||
| fc67dc6497 | |||
| fedd9a981a | |||
| 47e972a66c | |||
| 36bd4b5408 | |||
| f502233ddc | |||
| 59c1edb623 | |||
| 9dd00c7731 | |||
| 311edb4e4c | |||
| 924b8629d8 | |||
| 9e11da1fa5 | |||
| b760fa0ff5 | |||
| cbce2f46c3 | |||
| 7aa5a79c86 | |||
| fc029a9b9e | |||
| 86ef80ae9a | |||
| e5b475ad89 | |||
| 2c6858f149 | |||
| c1bff0b2ea | |||
| 39892c98f9 | |||
| b15487ec03 | |||
| 303119e113 | |||
| 09b729beb5 | |||
| 6b73a77b2d | |||
| a2449ff6a7 | |||
| b1b7522b80 | |||
| 608b0e7b93 | |||
| 7c61b9cf7e | |||
| 50a973409a | |||
| bfea882416 | |||
| f5e8fe836e | |||
| c4664a185f | |||
| c5f093c42f | |||
| 64f369b5de | |||
| 4b5653c09b | |||
| 837e739ec6 | |||
| b780ee8373 | |||
| fe5bfbf76f | |||
| d924617672 | |||
| c545c7ca70 | |||
| 9eb9f3a117 | |||
| 7862fd9679 | |||
| 17402e8475 | |||
| aac77440db | |||
| 13c9c4bea5 | |||
| 68c1171294 | |||
| f4f01913fe | |||
| eb5908d5d2 | |||
| e0c36498e6 | |||
| 913710dd99 | |||
| 2f0d96d030 | |||
| 265802acb1 | |||
| 045f31a0dc | |||
| 9f6ed4fb33 | |||
| 434117c771 | |||
| cd95b8724a | |||
| ec2a4d473e | |||
| 47eaf3cd58 | |||
| d7b23a8634 | |||
| 6db7972f04 | |||
| 6840ee077c | |||
| 3c85bcc3c9 | |||
| 759eca6479 | |||
| 4488f174aa | |||
| 991a255041 | |||
| cfef635e1b | |||
| d3027e1fe8 | |||
| d99ea1c6b4 | |||
| 9af214007e | |||
| 0541b7f3c5 | |||
| 63fa774af7 | |||
| 4b19b36de1 | |||
| 5715df6b18 | |||
| 22bcacc715 | |||
| f1e270ca9d | |||
| f535e7535c | |||
| 912c5e13e1 | |||
| 4eb44ee2ea | |||
| e41a2beb65 | |||
| 1f6ba31a3f | |||
| bcccc909c5 | |||
| b3a11030f2 | |||
| baaf76668f | |||
| ca37ae6794 | |||
| cf21d64aa8 | |||
| 36e2533164 | |||
| c04d79d9a0 | |||
| 9084b4e7aa | |||
| f724da7e84 | |||
| 4b8f47e2b4 | |||
| 35ecbed29d | |||
| 30ed153ad1 | |||
| a1098989ff | |||
| 42bb63e07d | |||
| 683aae5ed5 | |||
| 49ef274a83 | |||
| af16bba1ec | |||
| 781086608e | |||
| c1625e5c27 | |||
| 0e05f9fd73 | |||
| ef7595bb06 | |||
| e9c98b03b0 | |||
| 75370f7855 | |||
| 4a79e13410 | |||
| 220061f022 | |||
| 090c2c0891 | |||
| 6c317b7f28 | |||
| 3788fbf607 | |||
| 382854c04c | |||
| c2fae3bad8 | |||
| 92ebd39391 | |||
| f53a32a6b4 | |||
| 6c882e6605 | |||
| ca85dfc6ff | |||
| e22ecc6b6d | |||
| 2608dd2d64 | |||
| 7d20d24249 | |||
| a3ac3692af | |||
| 0070c8f843 | |||
| b360fc8308 | |||
| bf9ba65ac4 | |||
| 43abfbc537 | |||
| 2700f0acf6 | |||
| 9156bed961 | |||
| 71dc0bac56 | |||
| 40f55b2964 | |||
| 9307f9f345 | |||
| dfb918adc3 | |||
| 2f87a4859e | |||
| 191f73e0d0 | |||
| 263e55f25d | |||
| 5c55dce13e | |||
| a1a6ec6dfa | |||
| e1edd84700 | |||
| 07ee256756 | |||
| 48888e530e | |||
| 4ef50bef55 | |||
| 486369e97c | |||
| 67994f7a53 | |||
| f027ddaf35 | |||
| f3b27d1e06 | |||
| 92e18b32dc | |||
| 4030ec9c8b | |||
| 497c2dc8df | |||
| 8a1d34c419 | |||
| caab5befaa | |||
| 0d316e3d3e | |||
| d8fd12103a | |||
| 8f9a682d34 | |||
| 102d5acf09 | |||
| e2ec8952e3 | |||
| b4eff9b996 | |||
| d050261fa9 | |||
| d29330815e | |||
| 801b4022de | |||
| bcb4071993 | |||
| e78fbd1dff | |||
| c543358826 | |||
| ff2954839b | |||
| 1a91b88968 | |||
| 5724462c2c | |||
| 0a0489750c | |||
| f46190509a | |||
| 25ec81b6c7 | |||
| a50802a63f | |||
| ce1d374a82 | |||
| 6dca8ac460 | |||
| 4dc21674d5 | |||
| 8805dd8c01 | |||
| 73b624e761 | |||
| c44fd972b6 | |||
| ff344bc110 | |||
| b0e2a38325 | |||
| fbb741ab10 | |||
| 75321220fd | |||
| 43198b0425 | |||
| 4aa2c03ff0 | |||
| 3a3be36f4c | |||
| cb91c4292c | |||
| 80722ce145 | |||
| 07bfa5532e | |||
| cea1a3ff91 | |||
| 270be2df7a | |||
| e73b969066 | |||
| 98e2154f0f | |||
| 2c0549a772 | |||
| acb9bc8cc5 | |||
| 6f44aa39e8 | |||
| c706618229 | |||
| c26b571b1c | |||
| cb3075b084 | |||
| 66fb21bd0b | |||
| 498109bd53 | |||
| 6711ab5f9a | |||
| ac8fa58845 | |||
| 095f656998 | |||
| 9e30c4f7dd | |||
| 56a2eeac77 | |||
| d104f2f4a7 | |||
| e8367ad241 | |||
| f17cd142d5 | |||
| db4c6af472 | |||
| 87689ca733 | |||
| a1a5d85979 | |||
| b1459a43ef | |||
| e8edc554a6 | |||
| bd8aca83ac | |||
| 7ebc1cfac5 | |||
| 2422204d6a | |||
| 6c98c3c662 | |||
| 1d1310f034 | |||
| 841888c480 | |||
| 8797381f44 | |||
| 92e89ffbcf | |||
| 52c031f160 | |||
| 155113b75d | |||
| 2a863025c6 | |||
| b6763ce89f | |||
| f346cd6b8d | |||
| 0c47412c75 | |||
| ea1ef3dbec | |||
| 61fd62ff81 | |||
| 32197ea903 | |||
| 65de184d88 | |||
| 52764045ce | |||
| c2da4376e0 | |||
| 4b0db6c472 | |||
| 78ebf8f117 | |||
| 3ec89a89df | |||
| 9e6b72bf38 | |||
| 747723c8fb | |||
| 40cd4629db | |||
| 52a893a811 | |||
| 7cc94e1e1e | |||
| 88945a6d6d | |||
| 7203c5aaf0 | |||
| c305058c46 | |||
| 91bbf7d1a8 | |||
| 2d5857f145 | |||
| 5de189bfa3 | |||
| 91af9a411d | |||
| 9a9ed124f5 | |||
| 44fc820f99 | |||
| 4e5442d972 | |||
| 5a8e5a9785 | |||
| 960c5da3b2 | |||
| b5fa54f91e | |||
| 2246aede4b | |||
| 4b2d409c69 | |||
| 684511ff06 | |||
| 88b328df7e | |||
| e3583dd04e | |||
| e484a2ebb5 | |||
| 5caf05cfa1 | |||
| 72f86258d0 | |||
| 874cb3b779 | |||
| f21e0228b4 | |||
| 00a28e743d | |||
| 2596c25ccc | |||
| 01cd82bc6a | |||
| 582aafa552 | |||
| d2a6a8b283 | |||
| f242e460ed | |||
| 61e7d4f807 | |||
| 95e08253a6 | |||
| 202a4fa6f1 | |||
| 576f46cb88 | |||
| 2d73805ca3 | |||
| eedfa550a6 | |||
| 4ca718adc4 | |||
| 7c4ced8f46 | |||
| 7018f4ab25 | |||
| fda13875ef | |||
| 8005917452 | |||
| f2c215311f | |||
| a13cf0e1e0 | |||
| ff60bbac9d | |||
| 16f569136b | |||
| 27c172361f | |||
| 1e0d6b9d4a | |||
| b67cd94ee2 | |||
| c6764490c6 | |||
| 18580624e6 | |||
| 7b333a34b5 | |||
| d0707e183d | |||
| fa3b246de5 | |||
| df28d87d25 | |||
| fc68bb3ae0 | |||
| 82c530da95 | |||
| d250e7387c | |||
| cbc74815d8 | |||
| e9b802deb3 | |||
| 377ca0c678 | |||
| 972aef7a9d | |||
| a35559be65 | |||
| 0e6b43a769 | |||
| 8c8a68d3ae | |||
| 4d74b5cdad | |||
| b1ace49f9a | |||
| 91eee8587e | |||
| e9132abc25 | |||
| 640d13af99 | |||
| 50e0f6353a | |||
| 444eac5c6e | |||
| 30f2263443 | |||
| 25eb6de220 | |||
| cebdc44689 | |||
| 23f5c2e03f | |||
| 4d4a6ede21 | |||
| 6a920fe623 | |||
| 631faa2046 | |||
| a4e853e1d4 | |||
| a449c5f8c2 | |||
| 19d6dbaa52 | |||
| 8820619e82 | |||
| fb33bc7e07 | |||
| 57d1fa8410 | |||
| 838e38d84c | |||
| fc29056530 | |||
| 4e967c979c | |||
| 62a34848c7 | |||
| 01fe6cc542 | |||
| 3fdc25777d | |||
| 3b7d6f8334 | |||
| 4b3c8b2969 | |||
| ad80d69369 | |||
| e2d2249686 | |||
| dc64c34ccb | |||
| 0bf50659ab | |||
| ec26b16ddf | |||
| a044b74a1d | |||
| 1e7a1dce90 | |||
| 41c2772cff | |||
| 1bba2bc0ed | |||
| e11c523a75 | |||
| b911a890cf | |||
| c8f69c0b79 | |||
| 8a6248f120 | |||
| 340fa6c63e | |||
| 1177bf39a2 | |||
| 88b310c394 | |||
| 973de2db55 | |||
| 1fe92f10c1 | |||
| 0e2e906b24 | |||
| 4667f8be03 | |||
| 4a51ac7a74 | |||
| 6099efe41a | |||
| 4254d595fc | |||
| e4fbbd56a9 | |||
| 069ca4a89d | |||
| 4290e8e56b | |||
| e3ba08fbbc | |||
| dd84e51161 | |||
| bca8568d64 | |||
| 5407717534 | |||
| 74ef760591 | |||
| b435b582bd | |||
| d46021a05e | |||
| 1b31d0622e | |||
| a416fd562b | |||
| 323a096dd7 | |||
| 628dd7bf41 | |||
| 71c6d71cae | |||
| e049edd449 | |||
| 68206a6e19 | |||
| 56797948af | |||
| c0af2f25a1 | |||
| 0fdfc3ff53 | |||
| dc12b1df00 | |||
| b13f5aebfd | |||
| 338301bb5d | |||
| 72a0931663 | |||
| 67584b9cc3 | |||
| 1bfaa28f9c | |||
| cbe9b59222 | |||
| 276b52f0fe | |||
| 07f49bcc37 | |||
| 09fac77ce0 | |||
| 102704e91a | |||
| c5fb351baa | |||
| 98f8d4414d | |||
| 1dddcd4925 | |||
| 2666a271a5 | |||
| e277de6e3d | |||
| daa17b3287 | |||
| c7f887131e | |||
| 58546b80d0 | |||
| 466f749b71 | |||
| 4c2a83c470 | |||
| d534ab18c7 | |||
| 64aaed833b | |||
| 2f05be599c | |||
| 7371f8dd3a | |||
| 79aefe9707 | |||
| 6bc80577ee | |||
| 837764190f | |||
| 61948d70e3 | |||
| 9fb0385694 | |||
| 193de8d3a9 | |||
| 1f2b3512c1 | |||
| ddc5bd3b36 | |||
| ee828d454a | |||
| 2916a73f4c | |||
| ae69af7e70 | |||
| 1c6459fe65 | |||
| adaeb42416 | |||
| f1e1daa194 | |||
| 401e89ef78 | |||
| 59e0bd467c | |||
| 7f4397f8ca | |||
| 32830b93f1 | |||
| cdc0d5623b | |||
| e78b415832 | |||
| ff1379fd29 | |||
| 3820c15ecf | |||
| 26ef33e4f3 | |||
| 0534a4ed1b | |||
| f29a24a915 | |||
| cecbcd941e | |||
| 6be99d6397 | |||
| 4e5947af51 | |||
| 4204b2170a | |||
| 0a5ad489b6 | |||
| 5de34a5c99 | |||
| 08da6b8800 | |||
| 02b283be78 | |||
| 10c49c0fd1 | |||
| 9ecc0f5d95 | |||
| 972b59b99e | |||
| 34bb05bd88 | |||
| 37fb21f726 | |||
| b42efa4a07 | |||
| 20b20738a7 | |||
| b28a191c4e | |||
| f92b620434 | |||
| ae6e2cca27 | |||
| bd920eef1f | |||
| bf25cb68da | |||
| 0b063f6b8b | |||
| ed6d4e5f6c | |||
| accfa325b5 | |||
| b6ef8d95cd | |||
| c1144e3810 | |||
| 8663fd402b | |||
| d8134aa168 | |||
| 702b3e8473 | |||
| f34052fd31 | |||
| 27d75a269f | |||
| d208a7fc5f | |||
| 702e16e3df | |||
| 6381018658 | |||
| 12050b14f0 | |||
| 1c191b2278 | |||
| 2d4a4f1736 | |||
| cd38fb9b4c | |||
| 7941b16ec4 | |||
| 3ff517e76e | |||
| 9559b26310 | |||
| 56ea4b8741 | |||
| a489691151 | |||
| 6dabfcda6f | |||
| 0b7b35f800 | |||
| 0bfcb5071d | |||
| ceb162eb01 | |||
| 0ffdf7c0f1 | |||
| 13b6db8eb4 | |||
| 481acb2a1a | |||
| 683092140d | |||
| 1bb8c2d1a5 | |||
| 60fd3b0786 | |||
| b307a177f4 | |||
| 059430bd0a | |||
| 530b60cbc2 | |||
| cd4abc4e9b | |||
| e6a21cc487 | |||
| ba8577f268 | |||
| 6c5fc153bf | |||
| 07f15b41a2 | |||
| 8375638d76 | |||
| bed7543b46 | |||
| dc55236263 | |||
| 5f3427c5d1 | |||
| 66e5af185d | |||
| 0ff611e033 | |||
| 737cadaabc | |||
| 9fb2fbaeec | |||
| 51e817a3a2 | |||
| 59c93b59bf | |||
| c18ef051fc | |||
| 1ac5c9acbd | |||
| a034ca171e | |||
| 977682d37f | |||
| 0bafe263d7 | |||
| 1a8fced80e | |||
| 1c4d0b5e99 | |||
| 844a2b457c | |||
| ccf06f2216 | |||
| f630a9f297 | |||
| 2f71c93b53 | |||
| 92032a17a8 | |||
| e531456d42 | |||
| f0b2d2fe4d | |||
| 427500220d | |||
| 32e19ead74 | |||
| d11adb6f43 | |||
| f6155a50f6 | |||
| 4efee9445d | |||
| 20746a433f | |||
| 31dacc4206 | |||
| 88e5c59a85 | |||
| cf74920b36 | |||
| 972c900b58 | |||
| 12d5fd79f7 | |||
| a29f6979b2 | |||
| 3a7146c77b | |||
| b178d8f629 | |||
| 0c94ee62a3 | |||
| e7562898cd | |||
| fb73ab6878 | |||
| 38f978791d | |||
| 5dd60de57d | |||
| 5efbfc2dba | |||
| fcd1dbad89 | |||
| ad521bf4c2 | |||
| 8152fa44e0 | |||
| e217bf9e37 | |||
| bfad21f811 | |||
| 81e68abce3 | |||
| 7963bb352d | |||
| 14d3882059 | |||
| dede508e89 | |||
| ea39b69f65 | |||
| eafecd36bc | |||
| 198c9a2507 | |||
| d07563013b | |||
| 9e967832cd | |||
| bfe1987cd9 | |||
| 1045538f1f | |||
| fccf08edcf | |||
| f43fe366b5 | |||
| 0f75f2ef9c | |||
| 2cdc68f9c3 | |||
| 6a7d58e22e | |||
| 203829c1cd | |||
| b55e6c4ef0 | |||
| 8d779e8aec | |||
| dd1d48f688 | |||
| 8d14dc9ee3 | |||
| 5849ea8e63 | |||
| 20afebf339 | |||
| 20eaba191e | |||
| ba58d3c544 | |||
| a8b9d8e3ae | |||
| 83d1e61b2f | |||
| 8e0fc8d460 | |||
| f547fa732f | |||
| e24b1519a4 | |||
| 3028fe9c87 | |||
| 0b970b05b6 | |||
| f7bfb1e49e | |||
| 371ca009e9 | |||
| 4a0f848551 | |||
| 5e8b7b2a62 | |||
| 0f27b703bd | |||
| 61e19c30cb | |||
| c82bc35202 | |||
| 65934227c3 | |||
| 7519becd43 | |||
| fe83c15bc6 | |||
| 07e6b47fa7 | |||
| b026e1c2f7 | |||
| f8194d9418 | |||
| 1ecd7f274f | |||
| 66bf0ec7af | |||
| 1b22df2b7b | |||
| 9f993f1f67 | |||
| 975518bd88 | |||
| 66a863456c | |||
| 91290c0d25 | |||
| 8a23e89c87 | |||
| 9e9cf85ba1 | |||
| 3dd365bbea | |||
| 33a824b980 | |||
| 8571884304 | |||
| 4f1067e66c | |||
| 7b5b851db0 | |||
| ed0be0cf84 | |||
| d3775e5cb1 | |||
| 2c8f658810 | |||
| 5c52f5f579 | |||
| 0a81bb3fdc | |||
| f33196bc51 | |||
| 6beb90a835 | |||
| 9d45e6acd6 | |||
| 516c464458 | |||
| 6ad3fb16b3 | |||
| 277fdd9b8c | |||
| 7d56993b39 | |||
| 4e1442fcf6 | |||
| 4777bf3e75 | |||
| 14cd37ec56 | |||
| 8bcdfd50c9 | |||
| 6776df8e80 | |||
| fbec079c9b | |||
| 93f6bc3780 | |||
| dde8f23cc3 | |||
| 7cfbd0da95 | |||
| a27ddfaaaf | |||
| b53f616015 | |||
| 22dc175879 | |||
| 5f23e4699c | |||
| a1bd258a7b | |||
| 39a9c54589 | |||
| 5f68370e07 | |||
| dae2de703d | |||
| fa19c40868 | |||
| 1df69d259a | |||
| 90dda0ca68 | |||
| e2d138cac6 | |||
| 15f968d5f8 | |||
| 4a3b68de8f | |||
| f6aec7f763 | |||
| 212b6c3a0f | |||
| 820256d451 | |||
| 3aba538db3 | |||
| 4820cf8cac | |||
| c289effba0 | |||
| 3edccf496a | |||
| 97b4171b3e | |||
| 4a073a7ba5 | |||
| 9f275d57a9 | |||
| d23bbaeb06 | |||
| c64f7a9ec4 | |||
| 214a9df382 | |||
| 90f6620f1e | |||
| 45f3a2f909 | |||
| 5904378170 | |||
| f6e8048d9e | |||
| 5afca17d27 | |||
| 2d7f5ae279 | |||
| 458384d658 | |||
| 159b98132d | |||
| 349bb2730a | |||
| c13813348d | |||
| 26e70d6b30 | |||
| f6d3b50b08 | |||
| 50ee489079 | |||
| b60e5f40ab | |||
| 5b1fdb7b37 | |||
| 65d4015300 | |||
| b692cd109e | |||
| 0f90f055ba | |||
| 28d5ce288c | |||
| c701bf279f | |||
| 6039066e7f | |||
| c9a2f8b170 | |||
| b34a36d853 | |||
| f8f76f6806 | |||
| e25ae546fc | |||
| 5b73bf3e5d | |||
| c16b093bd7 | |||
| e406f32386 | |||
| c4e7c149a4 | |||
| f91edfabbb | |||
| f410004d45 | |||
| 49e238d580 | |||
| 489d188966 | |||
| 79fb7bab0b | |||
| c410954bad | |||
| 53a8a7d50f | |||
| 1c7f95c0ee | |||
| 1717fcf499 | |||
| b25453cf87 | |||
| 07b596bf30 | |||
| 1166947c21 | |||
| 6e7b9ca6c0 | |||
| 5b1e3537cc | |||
| 44843d418d | |||
| a103ffa038 | |||
| 712335789e | |||
| cdf8186f44 | |||
| d06942d602 | |||
| 45ac3a60dc | |||
| 4f244da3ec | |||
| 4eefa05d3f | |||
| 40fb31099a | |||
| bcd85f5397 | |||
| 89aeda45c6 | |||
| 7581e5ffdc | |||
| 7046fa3224 | |||
| ef392785e8 | |||
| 5bd029115c | |||
| b6f42b25dd | |||
| 75dd9625a0 | |||
| fd6110679e | |||
| c761019aca | |||
| 853363fdf5 | |||
| f7753f8be3 | |||
| d2dfa29556 | |||
| c0a88b7f4e | |||
| 150e5fede4 | |||
| bb3ec322fb | |||
| f3ee164a7d | |||
| 035cb9fe08 | |||
| 58d0018174 | |||
| 52ed0f8615 | |||
| 2a46513dfd | |||
| 907567182d | |||
| 46cebcd1ca | |||
| 40198f95dc | |||
| 736b934b18 | |||
| b009811ac9 | |||
| ff6612f9d0 | |||
| 565d446b1d | |||
| 044d334398 | |||
| e31ef2dfb5 | |||
| c20566083a | |||
| 9845553a5f | |||
| 03737546fe | |||
| 579cb00c98 | |||
| a307950213 | |||
| 0f5c469be6 | |||
| 6f7e409e9a | |||
| d707912d81 | |||
| 97ab680f4e | |||
| ea35a29bd8 | |||
| 63c182bc3e | |||
| b1a0a12a5f | |||
| cc242230be | |||
| aef9211ea8 | |||
| de5d557882 | |||
| fa9adf199d | |||
| 0807066f1f | |||
| 142e79941d | |||
| 5e9ce38a24 | |||
| f42e6373c4 | |||
| bc46609caa | |||
| cc44abe2d3 | |||
| db6398acdd | |||
| 6661bde608 | |||
| 69f6bba964 | |||
| 2a4e722f0f | |||
| dd20828ded | |||
| 5993dd588c | |||
| 30f86e2437 | |||
| 3ef91e16d8 | |||
| 45e9b3ac68 | |||
| a076e3f0fc | |||
| 99bff04ccc | |||
| bd906e619d | |||
| 34882cc438 | |||
| 5ac00e3465 | |||
| 622dd065ff | |||
| c5c98a6ac1 | |||
| da423ed508 | |||
| 11c4337cfc | |||
| 458164384d | |||
| 13c7f55a79 | |||
| 4ab675863a | |||
| 5414b3b39d | |||
| c54db30dc8 | |||
| f11103bfcc | |||
| b56936003d | |||
| f61604a51e | |||
| ae77f900ef | |||
| 645842f0fd | |||
| 39d3640973 | |||
| 66aa9c4831 | |||
| 7de0ca2048 | |||
| 5bbc5cad9f | |||
| 0a7a80d5a8 | |||
| 33d1a33a17 | |||
| 7f130949c8 | |||
| ba7ee37899 | |||
| f8863d5c24 | |||
| 1d7954c831 | |||
| 2bb4e91dfd | |||
| abe5bf4240 | |||
| c493bf7866 | |||
| 06bf0f22be | |||
| 02d9fe1d30 | |||
| c3091c5aa4 | |||
| 677a427f1f | |||
| c416dd01a7 | |||
| 662fcb426b | |||
| fd0fe9e225 | |||
| 54a03b234a | |||
| 0ca8613896 | |||
| 4b1817719e | |||
| 295c591e95 | |||
| 9df0480b78 | |||
| 5260f40451 | |||
| 0cbe35e41f | |||
| 502745271d | |||
| 95baa3cd27 | |||
| 8fe4a29176 | |||
| 1a1a0e7324 | |||
| d00d07a1c1 | |||
| f27db16e30 | |||
| d4e107b3cd | |||
| 881b60c85f | |||
| c5e1aade12 | |||
| 28198a6f40 | |||
| 30a01e26de | |||
| 8712703f7c | |||
| e3a8631faa | |||
| 3eab51ce79 | |||
| 9db0fe0795 | |||
| 228af037e3 | |||
| 654f250fd8 | |||
| a8693d9d68 | |||
| f9f345e428 | |||
| e678706414 | |||
| 8018259480 | |||
| 9f713781cd | |||
| 48d56dc5c0 | |||
| 701dfa09b4 | |||
| d965648fd7 | |||
| 08c15e7203 | |||
| 9b9e52d0a2 | |||
| 38cc8fe7dc | |||
| 590fac0fa9 | |||
| 9590c8aaf0 | |||
| e2b79e4e7e | |||
| 2df588f95a | |||
| 7c3af91b42 | |||
| ad2f537887 | |||
| df36f0dab4 | |||
| 608d7faa29 | |||
| 7845957d0d | |||
| 13c5c4e4f5 | |||
| 62114028d5 | |||
| 4cc4b28c47 | |||
| fad9b4c67f | |||
| ade3b3a021 | |||
| d8c4101fdd | |||
| a12c250f2b | |||
| 57eef2d832 | |||
| b67a179a54 | |||
| 06044b39c3 | |||
| d16cf26c5f | |||
| b060c5af38 | |||
| b28bad651e | |||
| 5c4b7a3213 | |||
| 7f21c591ae | |||
| 65b24f595c | |||
| 9d9c2720c2 | |||
| f845100062 | |||
| e6155f9e37 | |||
| e9590e9093 | |||
| 49f2d1501c | |||
| c6819e0450 | |||
| f518ea95f4 | |||
| 92c6332143 | |||
| 0df1a7da21 | |||
| 487a9c0967 | |||
| fb89761671 | |||
| d1d3ae074d | |||
| 7dedaf90c3 | |||
| 687b98a09d | |||
| c6b2e9873c | |||
| 1e80491675 | |||
| a727da9193 | |||
| 0b7754581a | |||
| 452e8ea385 | |||
| a189de9a2e | |||
| 8632ca6e37 | |||
| 293860b6c5 | |||
| 07667172cd | |||
| f5240cdac6 | |||
| 7529d2b638 | |||
| 03fc12e888 | |||
| e05a50528e | |||
| 356ee90417 | |||
| 5dced57724 | |||
| 8ec3b88c5d | |||
| d7ef128510 | |||
| bb2502409b | |||
| aa6aab4245 | |||
| 12aab0caeb | |||
| 2f316c558d | |||
| f447273b75 | |||
| c8a18d51e6 | |||
| d77af1e67a | |||
| 81c95224d1 | |||
| a0e66291df | |||
| 5733f46f4c | |||
| da2128feff | |||
| d413faefdb | |||
| 0d1d767d96 | |||
| 5619554023 | |||
| 53880a4bb2 | |||
| 812ae227b6 | |||
| c6b44098ac | |||
| 92b95a98d0 | |||
| a3505ff42d | |||
| 15b2e3ff1d | |||
| 1845d1ac55 | |||
| 463819caa7 | |||
| a0317d9587 | |||
| fa6ce0cb70 | |||
| 7471ff4b0d | |||
| 65a5bfac88 | |||
| d2321410f8 | |||
| f90b5a99cd | |||
| cc4656b36a | |||
| 34bc63a146 | |||
| 09bd91a588 | |||
| bb3e082e5b | |||
| 60c863f829 | |||
| 2d2a73bf52 | |||
| 7b9f73709d | |||
| 1dc89f642d | |||
| a78b59010a | |||
| a2e1d94fcf | |||
| b181c83b93 | |||
| cf7c84c4ba | |||
| 6e5230f9f9 | |||
| f03f7c0acb | |||
| aa9b807b82 | |||
| fa9921e091 | |||
| a9a6b2de48 | |||
| 1ef746658f | |||
| fe0099b497 | |||
| 7e9c4146a5 | |||
| 3e978c64e4 | |||
| 13c5920a46 | |||
| 3b19203fd2 | |||
| 84cd05b218 | |||
| 9b9a9642ee | |||
| 639d2317ed | |||
| 52379d7655 | |||
| 29827362d6 | |||
| f33315f610 | |||
| aec92a41da | |||
| d570811ddc | |||
| 9cd015a218 | |||
| 8d2cc5096e | |||
| 7ec0bf69f3 | |||
| f3a8306107 | |||
| 2b29e9934c | |||
| 4b1104e463 | |||
| 60b9ef959d | |||
| 0074c2cf57 | |||
| b0204ab54e | |||
| bc5b07aa75 | |||
| cfe90dbed5 | |||
| fa913655bc | |||
| af0b6fc6ac | |||
| 6347031f61 | |||
| 3eda039898 | |||
| 5447d99481 | |||
| 9d08744fe2 | |||
| 848fa257ea | |||
| 54553fb671 | |||
| aad7484c8d | |||
| ad658ead37 | |||
| c787f0e9d0 | |||
| 6f5da701aa | |||
| a01368bd60 | |||
| 16dccd75c1 | |||
| 5151b52688 | |||
| f682097e45 | |||
| 89ebffd69f | |||
| cdeb4ead9e | |||
| 79cc835874 | |||
| 8df866865d | |||
| fa7eff50e5 | |||
| 6558881952 | |||
| 9cbe2c985d | |||
| 7769c53cd6 | |||
| 7a5a47ecc7 | |||
| 492c1d77d8 | |||
| 8c315ebd15 | |||
| 6044b2cbad | |||
| 7acfd0b423 | |||
| e201aab440 | |||
| 3952768667 | |||
| fb25fa3a27 | |||
| 857ad9b180 | |||
| 14843a1bca | |||
| 6331b34cf7 | |||
| 2ebe1dfa16 | |||
| 6bdbee533c | |||
| b9886d4f34 | |||
| 666cbbce08 | |||
| 69c575a4be | |||
| c920de0d28 | |||
| 53cdf53f63 | |||
| d9dda6aebc | |||
| 3d4a9a24ce | |||
| de339d3098 | |||
| 0f83234be9 | |||
| bcd9b45589 | |||
| 19f3996e09 | |||
| 25c2cc1768 | |||
| eec7c4c61b | |||
| 25b4b049b7 | |||
| 2191eb3f41 | |||
| bebdbf7e05 | |||
| 646c091966 | |||
| 952729cb1b | |||
| c6992e2056 | |||
| 77ed79e9a9 | |||
| c4f4add0ec | |||
| 7eeb60c838 | |||
| 438861ae5e | |||
| d42cdbbc5b | |||
| 66237e1ea6 | |||
| a51c0450c3 | |||
| 7a2416bb6d | |||
| a1528e9e33 | |||
| 71cc4d535e | |||
| c06723df3d | |||
| 06b285c013 | |||
| 49c06ef0ca | |||
| 5f92357fec | |||
| 3e0dd3d918 | |||
| f19d76b08d | |||
| 22713d8f89 | |||
| 8b6b16067b | |||
| a2da0de17d | |||
| 93ff3edb6b | |||
| 7c67fd69dd | |||
| 5ef5412a55 | |||
| e88a384aa7 | |||
| 9067feeafb | |||
| ed978f69fb | |||
| 743f2465ea | |||
| 41fffa233a | |||
| e45377166b | |||
| 24939bf0b0 | |||
| 3221be4855 | |||
| 3135f1ed24 | |||
| 1b0834ffb0 | |||
| ad85740ae2 | |||
| d79d613cb7 | |||
| d8cc1f7b7a | |||
| d7c8856fdd | |||
| 9d80a332aa | |||
| e14f7b63c7 | |||
| 3bd2880923 | |||
| 6ec7e3a0b7 | |||
| 1a1fe759c3 | |||
| 2401ad7159 | |||
| a919c798f8 | |||
| 80fe66c481 | |||
| 4577dc8f44 | |||
| cf0a5305e0 | |||
| e69e6e1981 | |||
| 0febd99fbe | |||
| 6e8e3e4150 | |||
| 5d95398621 | |||
| 64cdd73b93 | |||
| 48a9236ea8 | |||
| 8b3126e9d8 | |||
| 74d497cd2d | |||
| 5070a5c598 | |||
| 2e30b08e74 | |||
| 8bf63f5f0b | |||
| 11665d18ee | |||
| a8a9fc0c9d | |||
| 098cd1b8d4 | |||
| 3166a4880d | |||
| 9d1c7136cc | |||
| e100943edf | |||
| a6fe4cdf1c | |||
| 8b5213c09a | |||
| 23a133c825 | |||
| 69c4496dfe | |||
| 8a4440c314 | |||
| 4d74cca206 | |||
| 955e081699 | |||
| b7e73422ab | |||
| a9a4ba33aa | |||
| 7116ad9f58 | |||
| 3d18bdb2aa | |||
| 4c14581606 | |||
| a9c9ec3977 | |||
| 694d1f9631 | |||
| 2b9cfae18a | |||
| 800d8380ce | |||
| 621ca28f68 | |||
| 4792241ff6 | |||
| 0b984df5f9 | |||
| 2e260155ea | |||
| b15c8a2d1c | |||
| 94ab317f23 | |||
| 02b37e1219 | |||
| 9d25848a21 | |||
| 3958768e1f | |||
| cec00cd303 | |||
| 45cfef4294 | |||
| 2a01e99635 | |||
| a9c3aee447 | |||
| c669382e12 | |||
| 19251c427a | |||
| a7aec9e2a4 | |||
| 99d7622b42 | |||
| 8728619d2c | |||
| f6d51fdfb8 | |||
| 0ffaa8d617 | |||
| 8a974172ab | |||
| 72675f7266 | |||
| 0f559050d8 | |||
| 58084774bf | |||
| c7aee7cebd | |||
| c68d135ae8 | |||
| 9ed6c99ec8 | |||
| 7d398d41d0 | |||
| 0af763252c | |||
| 1c9dbbbb19 | |||
| 2a688bdac8 | |||
| 3c53c818cb | |||
| 8e53cb324e | |||
| efbd454775 | |||
| 68c7273f56 | |||
| 8b56ff4eff | |||
| 8134eedd93 | |||
| a87ae770cc | |||
| 355da0f9a9 | |||
| 10bdd63762 | |||
| 69dc518c2c | |||
| fa1dddf06c | |||
| f683f4544a | |||
| bc5b587651 | |||
| 8083031029 | |||
| 7b8102a42c | |||
| dbe2f5e4db | |||
| f27791b7de | |||
| 2a78170395 | |||
| cfca1c7b06 | |||
| fb7a67025f | |||
| 8a6cd48b8e | |||
| e21d1f539d | |||
| f62049559c | |||
| 1fe9dd03a3 | |||
| c3283a7297 | |||
| f423164a1c | |||
| 75fe596e24 | |||
| c5eb290e66 | |||
| d3b2c8246d | |||
| c11796af4b | |||
| 8591815d66 | |||
| b82870adb2 | |||
| 3c5b304b6b | |||
| f656698061 | |||
| d4b4bc5031 | |||
| 9bcf33b6d3 | |||
| fa550e8f03 | |||
| 2d73564eba | |||
| 1bd80247fd | |||
| 1c194e8163 | |||
| db7848c9ba | |||
| 97497ed9d5 | |||
| 60fcc652de | |||
| cb57717424 | |||
| 590f7786eb | |||
| 0024edcb7f | |||
| aa2d0d9a08 | |||
| fd126b8563 | |||
| bc97e7a5ea | |||
| 3dece4f46b | |||
| be05452c70 | |||
| 583650cf7d | |||
| 505915528f | |||
| ace8a787b4 | |||
| ed2ea9ac8e | |||
| 8d09a4abe6 | |||
| 1da959ab02 | |||
| 997dd9b88a | |||
| ebe66bdd6e | |||
| 12b573bc63 | |||
| cc8c163e0f | |||
| abc7f76679 | |||
| eee04895fe | |||
| f520b88f79 | |||
| f0f1c113e4 | |||
| 84a15761ad | |||
| 013fbb87a7 | |||
| 73764d23dc | |||
| 8f62703bf2 | |||
| 6dedae2e4d | |||
| d32131b2b8 | |||
| 0a790b2ae3 | |||
| ef1d5e3d76 | |||
| c525a19df5 | |||
| a987a31667 | |||
| 10329c3436 | |||
| 29f10bcd44 | |||
| 19fe9b8ac7 | |||
| 76da708352 | |||
| 145f01ff2d | |||
| 18b1e00875 | |||
| d021498fa9 | |||
| b83aa54661 | |||
| 429550ca3e | |||
| 2a6d8c2b1d | |||
| 01c9159830 | |||
| 3b3ed5159c | |||
| 636661dd45 | |||
| cc8e8434ec | |||
| 1b94b3c4de |
@@ -21,3 +21,6 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
+68
-67
@@ -1,71 +1,26 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint", // now needed for class properties
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
"import",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
||||
// babel's transform-runtime converts references to ES6 globals such as
|
||||
// Promise and Map to core-js polyfills, so we can use ES6 globals.
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"jest",
|
||||
],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
// NOTE: These rules are frozen and new rules should not be added here.
|
||||
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
"max-len": ["error", {
|
||||
code: 90,
|
||||
ignoreComments: true,
|
||||
}],
|
||||
curly: ["error", "multi-line"],
|
||||
"prefer-const": ["error"],
|
||||
"comma-dangle": ["error", {
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "always-multiline",
|
||||
}],
|
||||
|
||||
// loosen jsdoc requirements a little
|
||||
"require-jsdoc": ["error", {
|
||||
require: {
|
||||
FunctionDeclaration: false,
|
||||
}
|
||||
}],
|
||||
"valid-jsdoc": ["error", {
|
||||
requireParamDescription: false,
|
||||
requireReturn: false,
|
||||
requireReturnDescription: false,
|
||||
}],
|
||||
|
||||
// rules we do not want from eslint-recommended
|
||||
"no-console": ["off"],
|
||||
"no-constant-condition": ["off"],
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }],
|
||||
|
||||
// rules we do not want from the google styleguide
|
||||
"object-curly-spacing": ["off"],
|
||||
"spaced-comment": ["off"],
|
||||
"guard-for-in": ["off"],
|
||||
|
||||
// in principle we prefer single quotes, but life is too short
|
||||
quotes: ["off"],
|
||||
|
||||
// rules we'd ideally like to adhere to, but the current
|
||||
// code does not (in most cases because it's still ES5)
|
||||
// we set these to warnings, and assert that the number
|
||||
// of warnings doesn't exceed a given threshold
|
||||
"no-var": ["warn"],
|
||||
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
@@ -79,10 +34,56 @@ module.exports = {
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// eslint's built in no-invalid-this rule breaks with class properties
|
||||
"no-invalid-this": "off",
|
||||
// so we replace it with a version that is class property aware
|
||||
"babel/no-invalid-this": "error",
|
||||
}
|
||||
}
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", {
|
||||
name: "events",
|
||||
message: "Please use TypedEventEmitter instead"
|
||||
}],
|
||||
|
||||
"import/no-restricted-paths": ["error", {
|
||||
"zones": [{
|
||||
"target": "./src/",
|
||||
"from": "./src/index.ts",
|
||||
"message": "The package index is dynamic between src and lib depending on " +
|
||||
"whether release or development, target the specific module or matrix.ts instead",
|
||||
}],
|
||||
}],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
"**/*.ts",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/typescript",
|
||||
],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* @matrix-org/element-web
|
||||
@@ -0,0 +1,13 @@
|
||||
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] Tests written for new code (and old code if feasible)
|
||||
* [ ] Linter and other CI checks pass
|
||||
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
|
||||
<!--
|
||||
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,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>matrix-org/renovate-config-element-web"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -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 }}
|
||||
@@ -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,41 @@
|
||||
# Must only be called from `release#published` triggers
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
tag: next
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: github.event.release.prerelease == false
|
||||
run: |
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
env:
|
||||
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
|
||||
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.version }}
|
||||
@@ -0,0 +1,58 @@
|
||||
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: |
|
||||
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</a>" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
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
|
||||
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@@ -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,101 @@
|
||||
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"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
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"
|
||||
|
||||
tsc-strict:
|
||||
name: Typescript Strict Error Checker
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
checks: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get diff lines
|
||||
id: diff
|
||||
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
|
||||
with:
|
||||
include: '["\\.tsx?$"]'
|
||||
|
||||
- name: Detecting files changed
|
||||
id: files
|
||||
uses: futuratrepadeira/changed-files@v4.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: '^.*\.tsx?$'
|
||||
|
||||
- uses: t3chguy/typescript-check-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-check: false
|
||||
check-fail-mode: added
|
||||
output-behaviour: annotate
|
||||
ts-extra-args: '--strict'
|
||||
files-changed: ${{ steps.files.outputs.files_updated }}
|
||||
files-added: ${{ steps.files.outputs.files_created }}
|
||||
files-deleted: ${{ steps.files.outputs.files_deleted }}
|
||||
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
|
||||
@@ -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 }}
|
||||
+3
-1
@@ -12,8 +12,10 @@ lib-cov
|
||||
out
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
instrumentation:
|
||||
compact: false
|
||||
+1668
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
|
||||
|
||||
How to contribute
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
**The single biggest thing you need to know is: please base your changes on
|
||||
the develop branch - /not/ master.**
|
||||
|
||||
We use the master branch to track the most recent release, so that folks who
|
||||
blindly clone the repo and automatically check out master get something that
|
||||
works. Develop is the unstable branch where all the development actually
|
||||
happens: the workflow is that contributors should fork the develop branch to
|
||||
make a 'feature' branch for a particular contribution, and then make a pull
|
||||
request to merge this back into the matrix.org 'official' develop branch. We
|
||||
use GitHub's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
We use Travis for continuous integration, and all pull requests get
|
||||
automatically tested by Travis: if your change breaks the build, then the PR
|
||||
will show that there are failed checks, so please check back after a few
|
||||
minutes.
|
||||
|
||||
Code style
|
||||
~~~~~~~~~~
|
||||
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
TypeScript and existing files should use ES6 principles where possible.
|
||||
|
||||
Members should not be exported as a default export in general - it causes problems
|
||||
with the architecture of the SDK (index file becomes less clear) and could
|
||||
introduce naming problems (as default exports get aliased upon import). In
|
||||
general, avoid using `export default`.
|
||||
|
||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
||||
contributors are encouraged to read the code style document for matrix-react-sdk
|
||||
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
|
||||
and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and **never** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
Attribution
|
||||
~~~~~~~~~~~
|
||||
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
Sign off
|
||||
~~~~~~~~
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix::
|
||||
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment::
|
||||
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the ``-s`` flag to
|
||||
``git commit``, which uses the name and email set in your ``user.name`` and
|
||||
``user.email`` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase::
|
||||
|
||||
git rebase --signoff origin/develop
|
||||
@@ -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://yarnpkg.com/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``
|
||||
@@ -182,10 +190,8 @@ you can pass the result of the promise into it with something like:
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is an error to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved. If
|
||||
you have nothing better to do with the result, just call ``.done()`` on it. See
|
||||
http://documentup.com/kriskowal/q/#the-end for more information.
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
@@ -309,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 necessry 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>
|
||||
|
||||
@@ -288,7 +288,7 @@ function printMemberList(room) {
|
||||
}
|
||||
|
||||
function printRoomInfo(room) {
|
||||
var eventDict = room.currentState.events;
|
||||
var eventMap = room.currentState.events;
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
@@ -300,14 +300,15 @@ function printRoomInfo(room) {
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
Object.keys(eventDict).forEach(function(eventType) {
|
||||
eventMap.keys().forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventDict[eventType][stateKey];
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
@@ -340,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") {
|
||||
@@ -397,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 = (
|
||||
@@ -31,6 +32,23 @@ function addListeners(call) {
|
||||
call.hangup();
|
||||
disableButtons(false, true, true);
|
||||
});
|
||||
call.on("feeds_changed", function(feeds) {
|
||||
const localFeed = feeds.find((feed) => feed.isLocal());
|
||||
const remoteFeed = feeds.find((feed) => !feed.isLocal());
|
||||
|
||||
const remoteElement = document.getElementById("remote");
|
||||
const localElement = document.getElementById("local");
|
||||
|
||||
if (remoteFeed) {
|
||||
remoteElement.srcObject = remoteFeed.stream;
|
||||
remoteElement.play();
|
||||
}
|
||||
if (localFeed) {
|
||||
localElement.muted = true;
|
||||
localElement.srcObject = localFeed.stream;
|
||||
localElement.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
@@ -62,10 +80,7 @@ function syncComplete() {
|
||||
);
|
||||
console.log("Call => %s", call);
|
||||
addListeners(call);
|
||||
call.placeVideoCall(
|
||||
document.getElementById("remote"),
|
||||
document.getElementById("local")
|
||||
);
|
||||
call.placeVideoCall();
|
||||
document.getElementById("result").innerHTML = "<p>Placed call.</p>";
|
||||
disableButtons(true, true, false);
|
||||
};
|
||||
|
||||
+21
-13
@@ -1,26 +1,34 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
constants in <code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="remote"></video>
|
||||
</div>
|
||||
</div>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="local"></video>
|
||||
</div>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.video-background {
|
||||
height: 500px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
+8
-1
@@ -18,6 +18,13 @@
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"verbose": true,
|
||||
"template": "node_modules/better-docs"
|
||||
"template": "node_modules/docdash"
|
||||
},
|
||||
"docdash": {
|
||||
"static": true,
|
||||
"private": false,
|
||||
"search": true,
|
||||
"collapse": true,
|
||||
"typedefs": true
|
||||
}
|
||||
}
|
||||
|
||||
+85
-52
@@ -1,24 +1,29 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "6.2.0",
|
||||
"version": "20.1.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:compile-browser && yarn build:minify-browser && yarn build:types",
|
||||
"build:types": "tsc --emitDeclarationOnly",
|
||||
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"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:ts && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 81 src spec",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,10 +33,11 @@
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
@@ -47,52 +53,79 @@
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.2",
|
||||
"loglevel": "^1.6.4",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.4",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@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/content-type": "^1.1.5",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^1.4.7",
|
||||
"browserify": "^16.5.0",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-jest": "^23.0.4",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.0.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-localstorage-mock": "^2.4.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
||||
"rimraf": "^3.0.0",
|
||||
"terser": "^4.4.3",
|
||||
"tsify": "^4.0.1",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.7.3"
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.23.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^0.6.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^2.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "jest-sonar-reporter"
|
||||
},
|
||||
"jestSonar": {
|
||||
"reportPath": "coverage",
|
||||
"sonar56x": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a post-release steps of matrix-js-sdk.
|
||||
#
|
||||
# Requires:
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||
for i in main typings
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
|
||||
git push origin develop
|
||||
fi
|
||||
+113
-112
@@ -3,19 +3,16 @@
|
||||
# Script to perform a release of matrix-js-sdk and downstream projects.
|
||||
#
|
||||
# Requires:
|
||||
# github-changelog-generator; install via:
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# npm; typically installed by Node.js
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
#
|
||||
# Note: this script is also used to release matrix-react-sdk and riot-web.
|
||||
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||
@@ -26,10 +23,9 @@ else
|
||||
echo "hub is required: please install it"
|
||||
exit
|
||||
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,18 +33,9 @@ $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
|
||||
}
|
||||
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached HEAD; then
|
||||
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
||||
exit
|
||||
@@ -60,11 +47,8 @@ if ! git diff-files --quiet; then
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
skip_npm=
|
||||
changelog_file="CHANGELOG.md"
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xzn f; do
|
||||
while getopts hc:x f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
@@ -76,43 +60,81 @@ while getopts hc:u:xzn f; do
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
shift $(expr $OPTIND - 1)
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $USAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
function check_dependency {
|
||||
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
|
||||
if [ "$depver" == "null" ]; then return 0; fi
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
if [ -z "$skip_npm" ]; then
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
echo "Checking version of $1..."
|
||||
local latestver=$(yarn info -s "$1" dist-tags.next)
|
||||
if [ "$depver" != "$latestver" ]
|
||||
then
|
||||
echo "The latest version of $1 is $latestver but package.json depends on $depver."
|
||||
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
|
||||
read resp
|
||||
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
|
||||
then
|
||||
echo "Aborting."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$resp" == "u" ]
|
||||
then
|
||||
echo "Upgrading $1 to $latestver..."
|
||||
yarn add -E "$1@$latestver"
|
||||
git add -u
|
||||
git commit -m "Upgrade $1 to $latestver"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function reset_dependency {
|
||||
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
|
||||
if [ "$depver" == "null" ]; then return 0; fi
|
||||
|
||||
echo "Resetting $1 to develop branch..."
|
||||
yarn add "github:matrix-org/$1#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $1 back to develop branch"
|
||||
}
|
||||
|
||||
has_subprojects=0
|
||||
if [ -f release_config.yaml ]; then
|
||||
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
|
||||
if [ "$?" -eq 0 ]; then
|
||||
has_subprojects=1
|
||||
echo "Checking subprojects for upgrades"
|
||||
for proj in $subprojects; do
|
||||
check_dependency "$proj"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
@@ -123,32 +145,15 @@ echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
if ! command -v update_changelog >/dev/null 2>&1; then
|
||||
echo "release.sh requires github-changelog-generator. Try:" >&2
|
||||
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
echo "Using current branch ($curbranch) for release"
|
||||
rel_branch=$curbranch
|
||||
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
|
||||
fi
|
||||
|
||||
rel_branch=$(git symbolic-ref --short HEAD)
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
update_changelog -f "$changelog_file" "$release"
|
||||
yarn run allchange "$release"
|
||||
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
||||
|
||||
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
|
||||
@@ -156,8 +161,8 @@ if [ -z "$skip_changelog" ]; then
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
latest_changes=$(mktemp)
|
||||
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
@@ -170,8 +175,21 @@ echo "yarn version"
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
# For the published and dist versions of the package, we copy the
|
||||
# `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
done
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
|
||||
then
|
||||
pkglock='yarn.lock'
|
||||
else
|
||||
@@ -183,7 +201,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
|
||||
|
||||
|
||||
@@ -198,18 +219,13 @@ assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
||||
projdir=$(pwd)
|
||||
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
yarn install
|
||||
yarn install --pure-lockfile
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
@@ -229,7 +245,7 @@ fi
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
@@ -249,6 +265,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"
|
||||
|
||||
@@ -289,7 +311,7 @@ if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
|
||||
release_text=`mktemp`
|
||||
release_text=$(mktemp)
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
@@ -301,34 +323,6 @@ fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
# Tag both releases and prereleases as `next` so the last stable release remains
|
||||
# the default.
|
||||
if [ -z "$skip_npm" ]; then
|
||||
npm publish --tag next
|
||||
if [ $prerelease -eq 0 ]; then
|
||||
# For a release, also add the default `latest` tag.
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
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"
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
@@ -339,18 +333,25 @@ fi
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge "$rel_branch"
|
||||
git merge "$rel_branch" --no-edit
|
||||
|
||||
# push master and docs (if generated) to github
|
||||
# push master to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# finally, merge master back onto develop (if it exists)
|
||||
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master
|
||||
git merge master --no-edit
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
[ -x ./post-release.sh ] && ./post-release.sh
|
||||
|
||||
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
|
||||
echo "Resetting subprojects to develop"
|
||||
for proj in $subprojects; do
|
||||
reset_dependency "$proj"
|
||||
done
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require('fs/promises');
|
||||
|
||||
const PKGJSON = 'package.json';
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
|
||||
for (const field of ['main', 'typings']) {
|
||||
if (pkgJson["matrix_lib_"+field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_"+field];
|
||||
}
|
||||
}
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -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,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
@@ -17,31 +17,32 @@ limitations under the License.
|
||||
|
||||
/**
|
||||
* A mock implementation of the webstorage api
|
||||
* @constructor
|
||||
*/
|
||||
export function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
export class MockStorageApi {
|
||||
public data: Record<string, string> = {};
|
||||
public keys: string[] = [];
|
||||
public length = 0;
|
||||
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
public setItem(k: string, v: string): void {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
public getItem(k: string): string | null {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
}
|
||||
|
||||
public removeItem(k: string): void {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
public key(index: number): string {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
const keys = [];
|
||||
}
|
||||
|
||||
private recalc(): void {
|
||||
const keys: string[] = [];
|
||||
for (const k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) {
|
||||
continue;
|
||||
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,232 +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 = {};
|
||||
}
|
||||
|
||||
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`);
|
||||
});
|
||||
};
|
||||
@@ -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() as unknown as Storage;
|
||||
}
|
||||
|
||||
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,13 +71,11 @@ 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);
|
||||
}),
|
||||
]);
|
||||
}, 10000);
|
||||
}, 20000); // additional timeout as this test can take quite a while
|
||||
});
|
||||
@@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../TestClient';
|
||||
import * as testUtils from '../test-utils';
|
||||
import {logger} from '../../src/logger';
|
||||
import { TestClient } from '../TestClient';
|
||||
import * as testUtils from '../test-utils/test-utils';
|
||||
import { logger } from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -67,7 +67,6 @@ function getSyncResponse(roomMembers) {
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
@@ -98,7 +97,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
@@ -137,141 +136,139 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-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.client.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.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
});
|
||||
// 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.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,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
});
|
||||
// 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.client.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.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
}).timeout(3000);
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/4983
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
await aliceTestClient.start();
|
||||
@@ -288,9 +285,9 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
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(
|
||||
@@ -323,11 +320,10 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
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(
|
||||
@@ -361,9 +357,9 @@ describe("DeviceList management:", function() {
|
||||
);
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
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(
|
||||
@@ -382,9 +378,9 @@ describe("DeviceList management:", function() {
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
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,762 +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 * as utils from "../../src/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(utils.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(utils.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,682 @@
|
||||
/*
|
||||
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();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
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();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
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();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
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();
|
||||
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {},
|
||||
);
|
||||
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,5 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
let client;
|
||||
@@ -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" });
|
||||
});
|
||||
|
||||
+460
-52
@@ -1,7 +1,24 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/matrix";
|
||||
import {logger} from "../../src/logger";
|
||||
import {TestClient} from "../TestClient";
|
||||
/*
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,7 +202,7 @@ describe("getEventTimeline support", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
@@ -135,18 +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);
|
||||
@@ -158,7 +228,7 @@ describe("getEventTimeline support", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
withoutRoomId(EVENTS[0]),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
@@ -174,7 +244,7 @@ describe("getEventTimeline support", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[1],
|
||||
withoutRoomId(EVENTS[1]),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "f_1_2",
|
||||
@@ -209,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(
|
||||
@@ -218,7 +288,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
@@ -229,6 +299,7 @@ describe("MatrixClient event timelines", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
Thread.setServerSideSupport(false, false);
|
||||
});
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
@@ -276,7 +347,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
withoutRoomId(EVENTS[0]),
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
@@ -311,7 +382,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[3],
|
||||
withoutRoomId(EVENTS[3]),
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
@@ -334,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);
|
||||
@@ -355,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];
|
||||
|
||||
@@ -478,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() {
|
||||
@@ -503,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]],
|
||||
@@ -516,7 +817,7 @@ describe("MatrixClient event timelines", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
return client.paginateEventTimeline(tl, { backwards: true });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
@@ -532,6 +833,44 @@ describe("MatrixClient event timelines", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should stop paginating when it encounters no `end` token", () => {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, () => ({
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
}));
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual("30");
|
||||
}).respond(200, () => ({
|
||||
start: "start_token0",
|
||||
chunk: [],
|
||||
}));
|
||||
|
||||
return Promise.all([
|
||||
(async () => {
|
||||
const tl = await client.getEventTimeline(timelineSet, EVENTS[0].event_id);
|
||||
const success = await client.paginateEventTimeline(tl, { backwards: true });
|
||||
expect(success).toBeFalsy();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual(null);
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0");
|
||||
})(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should allow you to paginate forwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
@@ -555,7 +894,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]],
|
||||
@@ -569,7 +908,7 @@ describe("MatrixClient event timelines", function() {
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
tl, {backwards: false, limit: 20});
|
||||
tl, { backwards: false, limit: 20 });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
@@ -591,7 +930,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body",
|
||||
});
|
||||
event.unsigned = {transaction_id: TXN_ID};
|
||||
event.unsigned = { transaction_id: TXN_ID };
|
||||
|
||||
beforeEach(function() {
|
||||
// set up handlers for both the message send, and the
|
||||
@@ -607,7 +946,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
withoutRoomId(event),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
@@ -680,17 +1019,15 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function() {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
// 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: {},
|
||||
});
|
||||
@@ -732,7 +1069,7 @@ describe("MatrixClient event timelines", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "world",
|
||||
user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
@@ -751,4 +1088,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,13 +1,13 @@
|
||||
import * as utils from "../test-utils";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
import {MatrixClient} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {MemoryStore} from "../../src/store/memory";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
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",
|
||||
|
||||
+38
-16
@@ -1,16 +1,32 @@
|
||||
import {EventStatus} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {Room} from "../../src/models/room";
|
||||
import {TestClient} from "../TestClient";
|
||||
/*
|
||||
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();
|
||||
@@ -19,11 +35,11 @@ describe("MatrixClient retrying", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{scheduler},
|
||||
{ scheduler },
|
||||
);
|
||||
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,9 +110,9 @@ 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) => {
|
||||
if(ev0 === ev1) {
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventStatus} from "../../src/models/event";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
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() {
|
||||
let client = null;
|
||||
@@ -97,19 +97,20 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(async function() {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
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);
|
||||
@@ -117,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() {
|
||||
@@ -166,7 +168,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -198,7 +200,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -396,8 +398,8 @@ describe("MatrixClient room timelines", function() {
|
||||
describe("new events", function() {
|
||||
it("should be added to the right place in the timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
@@ -434,11 +436,11 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set the right event.sender values", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
||||
setNextSyncData(eventData);
|
||||
@@ -546,12 +548,13 @@ describe("MatrixClient room timelines", function() {
|
||||
describe("gappy sync", function() {
|
||||
it("should copy the last known state to the new timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/versions", 1),
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
@@ -577,9 +580,9 @@ 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}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
@@ -606,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,756 +0,0 @@
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const otherUserId = "@bob:localhost";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bob:bar";
|
||||
const userC = "@claire:bar";
|
||||
const roomOne = "!foo:localhost";
|
||||
const roomTwo = "!bar:localhost";
|
||||
|
||||
beforeEach(function() {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("startClient", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {},
|
||||
presence: {},
|
||||
};
|
||||
|
||||
it("should /sync after /pushrules and /filter.", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flushAllExpected().then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass the 'next_batch' token from /sync to the since= param " +
|
||||
" of the next /sync", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.since).toEqual(syncData.next_batch);
|
||||
}).respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flushAllExpected().then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolving invites to profile info", function() {
|
||||
const syncData = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
syncData.presence.events = [];
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("should resolve incoming invites from /sync", function() {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
|
||||
200, {
|
||||
avatar_url: "mxc://flibble/wibble",
|
||||
displayname: "The Boss",
|
||||
},
|
||||
);
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const member = client.getRoom(roomOne).getMember(userC);
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use cached values from m.presence wherever possible", function() {
|
||||
syncData.presence.events = [
|
||||
utils.mkPresence({
|
||||
user: userC, presence: "online", name: "The Ghost",
|
||||
}),
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const member = client.getRoom(roomOne).getMember(userC);
|
||||
expect(member.name).toEqual("The Ghost");
|
||||
});
|
||||
});
|
||||
|
||||
it("should result in events on the room member firing", function() {
|
||||
syncData.presence.events = [
|
||||
utils.mkPresence({
|
||||
user: userC, presence: "online", name: "The Ghost",
|
||||
}),
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
let latestFiredName = null;
|
||||
client.on("RoomMember.name", function(event, m) {
|
||||
if (m.userId === userC && m.roomId === roomOne) {
|
||||
latestFiredName = m.name;
|
||||
}
|
||||
});
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
expect(latestFiredName).toEqual("The Ghost");
|
||||
});
|
||||
});
|
||||
|
||||
it("should no-op if resolveInvitesToProfiles is not set", function() {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const member = client.getRoom(roomOne).getMember(userC);
|
||||
expect(member.name).toEqual(userC);
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false),
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("users", function() {
|
||||
const syncData = {
|
||||
next_batch: "nb",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: userA, presence: "online",
|
||||
}),
|
||||
utils.mkPresence({
|
||||
user: userB, presence: "unavailable",
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should create users for presence events from /sync",
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
expect(client.getUser(userA).presence).toEqual("online");
|
||||
expect(client.getUser(userB).presence).toEqual("unavailable");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("room state", function() {
|
||||
const msgText = "some text here";
|
||||
const otherDisplayName = "Bob Smith";
|
||||
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hiii",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: otherUserId,
|
||||
name: otherDisplayName,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomTwo, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const nextSyncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
nextSyncData.rooms.join[roomOne] = {
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: selfUserId,
|
||||
content: { name: "A new room name" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
nextSyncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: msgText,
|
||||
}),
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: roomTwo,
|
||||
content: { user_ids: [otherUserId] },
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should continually recalculate the right room name.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
// should have clobbered the name to the one from /events
|
||||
expect(room.name).toEqual(
|
||||
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should store the right events in the timeline.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
// should have added the message from /events
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getContent().body).toEqual(msgText);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right room name.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
// should use the display name of the other person.
|
||||
expect(room.name).toEqual(otherDisplayName);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right user's typing flag.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
let member = room.getMember(otherUserId);
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(true);
|
||||
member = room.getMember(selfUserId);
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
// XXX: This test asserts that the js-sdk obeys the spec and treats state
|
||||
// events that arrive in the incremental sync as if they preceeded the
|
||||
// timeline events, however this breaks peeking, so it's disabled
|
||||
// (see sync.js)
|
||||
xit("should correctly interpret state in incremental sync.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
const stateAtStart = room.getLiveTimeline().getState(
|
||||
EventTimeline.BACKWARDS,
|
||||
);
|
||||
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
|
||||
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
|
||||
|
||||
const stateAtEnd = room.getLiveTimeline().getState(
|
||||
EventTimeline.FORWARDS,
|
||||
);
|
||||
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
|
||||
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
|
||||
});
|
||||
});
|
||||
|
||||
xit("should update power levels for users in a room", function() {
|
||||
|
||||
});
|
||||
|
||||
xit("should update the room topic", function() {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline", function() {
|
||||
beforeEach(function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on new rooms", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "roomtwo",
|
||||
}),
|
||||
],
|
||||
prev_batch: "roomtwotok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
expect(room).toBeDefined();
|
||||
const tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("roomtwotok");
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on gappy syncs", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
let resetCallCount = 0;
|
||||
// the token should be set *before* timelineReset is emitted
|
||||
client.on("Room.timelineReset", function(room) {
|
||||
resetCallCount++;
|
||||
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(0);
|
||||
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("newerTok");
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(resetCallCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("receipts", function() {
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
syncData.rooms.join[roomOne].ephemeral = {
|
||||
events: [],
|
||||
};
|
||||
});
|
||||
|
||||
it("should sync receipts from /sync.", function() {
|
||||
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
|
||||
const receipt = {};
|
||||
receipt[ackEvent.event_id] = {
|
||||
"m.read": {},
|
||||
};
|
||||
receipt[ackEvent.event_id]["m.read"][userC] = {
|
||||
ts: 176592842636,
|
||||
};
|
||||
syncData.rooms.join[roomOne].ephemeral.events = [{
|
||||
content: receipt,
|
||||
room_id: roomOne,
|
||||
type: "m.receipt",
|
||||
}];
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
|
||||
type: "m.read",
|
||||
userId: userC,
|
||||
data: {
|
||||
ts: 176592842636,
|
||||
},
|
||||
}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", function() {
|
||||
xit("should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)", function() {
|
||||
|
||||
});
|
||||
|
||||
xit("should sync when the user explicitly calls joinRoom", function() {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncLeftRooms", function() {
|
||||
beforeEach(function(done) {
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flushAllExpected().then(function() {
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
httpBackend.when("GET", "/sync").respond(200, {});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and use an appropriate filter", function() {
|
||||
httpBackend.when("POST", "/filter").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
room: { timeline: {limit: 1},
|
||||
include_leave: true }});
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
resolve();
|
||||
}).respond(200, {});
|
||||
});
|
||||
|
||||
client.syncLeftRooms();
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
return Promise.all([
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
// flush the syncs
|
||||
return httpBackend.flushAllExpected();
|
||||
}),
|
||||
prom,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on left rooms", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
leave: {},
|
||||
},
|
||||
};
|
||||
|
||||
syncData.rooms.leave[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("POST", "/filter").respond(200, {
|
||||
filter_id: "another_id",
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
return Promise.all([
|
||||
client.syncLeftRooms().then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
const tok = room.getLiveTimeline().getPaginationToken(
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
expect(tok).toEqual("pagTok");
|
||||
}),
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
return httpBackend.flushAllExpected();
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* waits for the MatrixClient to emit one or more 'sync' events.
|
||||
*
|
||||
* @param {Number?} numSyncs number of syncs to wait for
|
||||
* @returns {Promise} promise which resolves after the sync events have happened
|
||||
*/
|
||||
function awaitSyncEvent(numSyncs) {
|
||||
return utils.syncPromise(client, numSyncs);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,974 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import * as utils from "../../src/utils";
|
||||
import * as testUtils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {logger} from "../../src/logger";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* start an Olm session with a given recipient
|
||||
*
|
||||
* @param {Olm.Account} olmAccount
|
||||
* @param {TestClient} recipientTestClient
|
||||
* @return {Promise} promise for Olm.Session
|
||||
*/
|
||||
function createOlmSession(olmAccount, recipientTestClient) {
|
||||
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
const otkId = utils.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
session.create_outbound(
|
||||
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
|
||||
);
|
||||
return session;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt an event with olm
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string=} opts.sender
|
||||
* @param {string} opts.senderKey
|
||||
* @param {Olm.Session} opts.p2pSession
|
||||
* @param {TestClient} opts.recipient
|
||||
* @param {object=} opts.plaincontent
|
||||
* @param {string=} opts.plaintype
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptOlmEvent(opts) {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.p2pSession).toBeTruthy();
|
||||
expect(opts.recipient).toBeTruthy();
|
||||
|
||||
const plaintext = {
|
||||
content: opts.plaincontent || {},
|
||||
recipient: opts.recipient.userId,
|
||||
recipient_keys: {
|
||||
ed25519: opts.recipient.getSigningKey(),
|
||||
},
|
||||
sender: opts.sender || '@bob:xyz',
|
||||
type: opts.plaintype || 'm.test',
|
||||
};
|
||||
|
||||
const event = {
|
||||
content: {
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
ciphertext: {},
|
||||
sender_key: opts.senderKey,
|
||||
},
|
||||
sender: opts.sender || '@bob:xyz',
|
||||
type: 'm.room.encrypted',
|
||||
};
|
||||
event.content.ciphertext[opts.recipient.getDeviceKey()] =
|
||||
opts.p2pSession.encrypt(JSON.stringify(plaintext));
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt an event with megolm
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.senderKey
|
||||
* @param {Olm.OutboundGroupSession} opts.groupSession
|
||||
* @param {object=} opts.plaintext
|
||||
* @param {string=} opts.room_id
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptMegolmEvent(opts) {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.groupSession).toBeTruthy();
|
||||
|
||||
const plaintext = opts.plaintext || {};
|
||||
if (!plaintext.content) {
|
||||
plaintext.content = {
|
||||
body: '42',
|
||||
msgtype: "m.text",
|
||||
};
|
||||
}
|
||||
if (!plaintext.type) {
|
||||
plaintext.type = "m.room.message";
|
||||
}
|
||||
if (!plaintext.room_id) {
|
||||
expect(opts.room_id).toBeTruthy();
|
||||
plaintext.room_id = opts.room_id;
|
||||
}
|
||||
|
||||
return {
|
||||
event_id: 'test_megolm_event',
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
|
||||
device_id: "testDevice",
|
||||
sender_key: opts.senderKey,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* build an encrypted room_key event to share a group session
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.senderKey
|
||||
* @param {TestClient} opts.recipient
|
||||
* @param {Olm.Session} opts.p2pSession
|
||||
* @param {Olm.OutboundGroupSession} opts.groupSession
|
||||
* @param {string=} opts.room_id
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptGroupSessionKey(opts) {
|
||||
return encryptOlmEvent({
|
||||
senderKey: opts.senderKey,
|
||||
recipient: opts.recipient,
|
||||
p2pSession: opts.p2pSession,
|
||||
plaincontent: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: opts.room_id,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
session_key: opts.groupSession.session_key(),
|
||||
},
|
||||
plaintype: 'm.room_key',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single room (ROOM_ID),
|
||||
* with the members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: roomMembers[i],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = roomResponse;
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running megolm tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount;
|
||||
let testSenderKey;
|
||||
let aliceTestClient;
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
* response to /keys/query
|
||||
*
|
||||
* @param {string} userId The user ID to query for
|
||||
* @returns {Object} The fake query response
|
||||
*/
|
||||
function getTestKeysQueryResponse(userId) {
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
const testDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'DEVICE_ID',
|
||||
keys: {
|
||||
'curve25519:DEVICE_ID': testE2eKeys.curve25519,
|
||||
'ed25519:DEVICE_ID': testE2eKeys.ed25519,
|
||||
},
|
||||
user_id: userId,
|
||||
};
|
||||
const j = anotherjson.stringify(testDeviceKeys);
|
||||
const sig = testOlmAccount.sign(j);
|
||||
testDeviceKeys.signatures = {};
|
||||
testDeviceKeys.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
const queryResponse = {
|
||||
device_keys: {},
|
||||
};
|
||||
|
||||
queryResponse.device_keys[userId] = {
|
||||
'DEVICE_ID': testDeviceKeys,
|
||||
};
|
||||
|
||||
return queryResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a one-time key for testOlmAccount in a format suitable for a
|
||||
* response to /keys/claim
|
||||
|
||||
* @param {string} userId The user ID to query for
|
||||
* @returns {Object} The fake key claim response
|
||||
*/
|
||||
function getTestKeysClaimResponse(userId) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
const keyId = utils.keys(testOneTimeKeys.curve25519)[0];
|
||||
const oneTimeKey = testOneTimeKeys.curve25519[keyId];
|
||||
const keyResult = {
|
||||
'key': oneTimeKey,
|
||||
};
|
||||
const j = anotherjson.stringify(keyResult);
|
||||
const sig = testOlmAccount.sign(j);
|
||||
keyResult.signatures = {};
|
||||
keyResult.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
const claimResponse = {one_time_keys: {}};
|
||||
claimResponse.one_time_keys[userId] = {
|
||||
'DEVICE_ID': {},
|
||||
};
|
||||
claimResponse.one_time_keys[userId].DEVICE_ID['signed_curve25519:' + keyId] =
|
||||
keyResult;
|
||||
return claimResponse;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
await aliceTestClient.client.initCrypto();
|
||||
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", function() {
|
||||
return aliceTestClient.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
const messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message before the session keys", function() {
|
||||
// https://github.com/vector-im/riot-web/issues/2273
|
||||
let roomKeyEncrypted;
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
const messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice just gets the message event to start with
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().msgtype).toEqual('m.bad.encrypted');
|
||||
|
||||
// now she gets the room_key event
|
||||
const syncResponse = {
|
||||
next_batch: 2,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent().msgtype != 'm.bad.encrypted') {
|
||||
return event;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}).then((event) => {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice gets a second room_key message", function() {
|
||||
return aliceTestClient.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
const messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// make a second room_key event now that we have advanced the group
|
||||
// session.
|
||||
const roomKeyEncrypted2 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// on the first sync, send the best room key
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted1],
|
||||
},
|
||||
});
|
||||
|
||||
// on the second sync, send the advanced room key, along with the
|
||||
// message. This simulates the situation where Alice has been sent a
|
||||
// later copy of the room key and is reloading the client.
|
||||
const syncResponse2 = {
|
||||
next_batch: 2,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted2],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse2.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
|
||||
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it('Alice sends a megolm message', function() {
|
||||
let p2pSession;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((_p2pSession) => {
|
||||
p2pSession = _p2pSession;
|
||||
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
// start out with the device unknown - the send should be rejected.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||
throw new Error("sendTextMessage failed on an unknown device");
|
||||
}, (e) => {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
}),
|
||||
aliceTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
}).then(function() {
|
||||
// mark the device as known, and resend.
|
||||
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
let inboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
const ct = content.ciphertext;
|
||||
const r = inboundGroupSession.decrypt(ct);
|
||||
logger.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
const decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const pendingMsg = room.getPendingEvents()[0];
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.resendEvent(pendingMsg, room),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
logger.log('Telling alice to send a megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
|
||||
).respond(200, {});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("We should start a new megolm session when a device is blocked", function() {
|
||||
let p2pSession;
|
||||
let megolmSessionId;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((_p2pSession) => {
|
||||
p2pSession = _p2pSession;
|
||||
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log("Fetching bob's devices and marking known");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flushAllExpected(),
|
||||
]).then((keys) => {
|
||||
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
|
||||
});
|
||||
}).then(function() {
|
||||
logger.log('Telling alice to send a megolm message');
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log('sendToDevice: ', content);
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(1); // normal message
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
logger.log('decrypted sendToDevice:', decrypted);
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
megolmSessionId = decrypted.content.session_id;
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log('/send:', content);
|
||||
expect(content.session_id).toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
logger.log('Telling alice to send another megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log('/send:', content);
|
||||
expect(content.session_id).not.toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
|
||||
).respond(200, {});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
|
||||
aliceTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/2676
|
||||
it("Alice should send to her other devices", function() {
|
||||
// for this test, we make the testOlmAccount be another of Alice's devices.
|
||||
// it ought to get included in messages Alice sends.
|
||||
|
||||
let p2pSession;
|
||||
let inboundGroupSession;
|
||||
let decrypted;
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
// an encrypted room with just alice
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: aliceTestClient.userId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
// the completion of the first initialsync hould make Alice
|
||||
// invalidate the device cache for all members in e2e rooms (ie,
|
||||
// herself), and do a key query.
|
||||
aliceTestClient.expectKeyQuery(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId),
|
||||
);
|
||||
|
||||
return aliceTestClient.httpBackend.flushAllExpected();
|
||||
}).then(function() {
|
||||
// start out with the device unknown - the send should be rejected.
|
||||
return aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||
throw new Error("sendTextMessage failed on an unknown device");
|
||||
}, (e) => {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
|
||||
expect(Object.keys(e.devices[aliceTestClient.userId])).
|
||||
toEqual(['DEVICE_ID']);
|
||||
});
|
||||
}).then(function() {
|
||||
// mark the device as known, and resend.
|
||||
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
||||
200, function(path, content) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
||||
.toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log("sendToDevice: ", content);
|
||||
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
p2pSession = new Olm.Session();
|
||||
p2pSession.create_inbound(testOlmAccount, ct.body);
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
const ct = content.ciphertext;
|
||||
const r = inboundGroupSession.decrypt(ct);
|
||||
logger.log('Decrypted received megolm message', r);
|
||||
decrypted = JSON.parse(r.plaintext);
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
// Grab the event that we'll need to resend
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const pendingEvents = room.getPendingEvents();
|
||||
expect(pendingEvents.length).toEqual(1);
|
||||
const unsentEvent = pendingEvents[0];
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.resendEvent(unsentEvent, room),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message',
|
||||
function() {
|
||||
let downloadPromise;
|
||||
let sendPromise;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
// this will block
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
|
||||
// so will this.
|
||||
sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
||||
.then(() => {
|
||||
throw new Error("sendTextMessage failed on an unknown device");
|
||||
}, (e) => {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return aliceTestClient.httpBackend.flushAllExpected();
|
||||
}).then(function() {
|
||||
return Promise.all([downloadPromise, sendPromise]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Alice exports megolm keys and imports them to a new device", function() {
|
||||
let messageEncrypted;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
|
||||
return aliceTestClient.client.exportRoomKeys();
|
||||
}).then(function(exported) {
|
||||
// start a new client
|
||||
aliceTestClient.stop();
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
return aliceTestClient.client.initCrypto().then(() => {
|
||||
aliceTestClient.client.importRoomKeys(exported);
|
||||
return aliceTestClient.start();
|
||||
});
|
||||
}).then(function() {
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,811 @@
|
||||
/*
|
||||
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";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
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 roomG = "!g_join_invite_counts: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,
|
||||
},
|
||||
[roomG]: {
|
||||
name: "G",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
],
|
||||
joined_count: 5,
|
||||
invited_count: 2,
|
||||
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();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
});
|
||||
|
||||
it("can be created with timeline only", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
});
|
||||
|
||||
it("can be created with a highlight_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
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();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(data[roomD].notification_count);
|
||||
});
|
||||
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
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();
|
||||
if (gotRoom == null) { return; }
|
||||
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();
|
||||
if (gotRoom == null) { return; }
|
||||
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).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||
],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
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();
|
||||
if (gotRoom == null) { return; }
|
||||
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();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
});
|
||||
|
||||
// Regression test for a bug which caused the timeline entries to be out-of-order
|
||||
// when the same room appears twice with different timeline limits. E.g appears in
|
||||
// the list with timeline_limit:1 then appears again as a room subscription with
|
||||
// timeline_limit:50
|
||||
it("can return history with a larger timeline_limit", async () => {
|
||||
const timeline = data[roomA].timeline;
|
||||
const oldTimeline = [
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "old event A" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "old event B" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
|
||||
...timeline,
|
||||
];
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: oldTimeline,
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
|
||||
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
|
||||
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
|
||||
(e) => (e.getType() + " : " + e.getContent().body)),
|
||||
);
|
||||
|
||||
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../src/logger';
|
||||
import { logger } from '../src/logger';
|
||||
import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
@@ -28,8 +29,9 @@ try {
|
||||
|
||||
// also try to set node crypto
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
console.log('nodejs was compiled without crypto support: some tests will fail');
|
||||
logger.log('nodejs was compiled without crypto support: some tests will fail');
|
||||
}
|
||||
@@ -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 in 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"].indexOf(opts.type) !== -1) {
|
||||
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) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
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, any[]>) =>
|
||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||
@@ -0,0 +1,384 @@
|
||||
// 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, IPusher, 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));
|
||||
|
||||
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
app_display_name: "app",
|
||||
app_id: "123",
|
||||
data: {},
|
||||
device_display_name: "name",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "pushpush",
|
||||
...extra,
|
||||
});
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { NamespacedValue, UnstableValue } from "../../src/NamespacedValue";
|
||||
|
||||
describe("NamespacedValue", () => {
|
||||
it("should prefer stable over unstable", () => {
|
||||
const ns = new NamespacedValue("stable", "unstable");
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBe(ns.unstable);
|
||||
expect(ns.names).toEqual([ns.stable, ns.unstable]);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new NamespacedValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.unstable]);
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.stable]);
|
||||
});
|
||||
|
||||
it("should match against either stable or unstable", () => {
|
||||
const ns = new NamespacedValue("stable", "unstable");
|
||||
expect(ns.matches("no")).toBe(false);
|
||||
expect(ns.matches(ns.stable)).toBe(true);
|
||||
expect(ns.matches(ns.unstable)).toBe(true);
|
||||
});
|
||||
|
||||
it("should not permit falsey values for both parts", () => {
|
||||
try {
|
||||
new UnstableValue(null, null);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("UnstableValue", () => {
|
||||
it("should prefer unstable over stable", () => {
|
||||
const ns = new UnstableValue("stable", "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBe(ns.stable);
|
||||
expect(ns.names).toEqual([ns.unstable, ns.stable]);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new UnstableValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.unstable]);
|
||||
});
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
try {
|
||||
new UnstableValue("stable", null);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2021 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 { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
const EVENTNAME = "UnknownEntry";
|
||||
|
||||
class EventSource extends EventEmitter {
|
||||
doTheThing() {
|
||||
this.emit(EVENTNAME, "foo", "bar");
|
||||
}
|
||||
|
||||
doAnError() {
|
||||
this.emit('error');
|
||||
}
|
||||
}
|
||||
|
||||
class EventTarget extends EventEmitter {
|
||||
|
||||
}
|
||||
|
||||
describe("ReEmitter", function() {
|
||||
it("Re-Emits events with the same args", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on(EVENTNAME, handler);
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, [EVENTNAME]);
|
||||
|
||||
src.doTheThing();
|
||||
|
||||
// Args should be the args passed to 'emit' after the event name, and
|
||||
// also the source object of the event which re-emitter adds
|
||||
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
|
||||
});
|
||||
|
||||
it("Doesn't throw if no handler for 'error' event", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, ['error']);
|
||||
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on('error', handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
// Now we've attached an error handler, it should be called
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 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.
|
||||
@@ -16,19 +16,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import * as sdk from "../../src";
|
||||
import {AutoDiscovery} from "../../src/autodiscovery";
|
||||
|
||||
import { request } from "../../src/matrix";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new MockHttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
const httpBackend = new MockHttpBackend();
|
||||
request(httpBackend.requestFn);
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
it("should throw an error when no domain is specified", function() {
|
||||
getHttpBackend();
|
||||
return Promise.all([
|
||||
// @ts-ignore testing no args
|
||||
AutoDiscovery.findClientConfig(/* no args */).then(() => {
|
||||
throw new Error("Expected a failure, not success with no args");
|
||||
}, () => {
|
||||
@@ -41,13 +43,13 @@ describe("AutoDiscovery", function() {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig(null).then(() => {
|
||||
AutoDiscovery.findClientConfig(null as any).then(() => {
|
||||
throw new Error("Expected a failure, not success with null");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig(true).then(() => {
|
||||
AutoDiscovery.findClientConfig(true as any).then(() => {
|
||||
throw new Error("Expected a failure, not success with a non-string");
|
||||
}, () => {
|
||||
return true;
|
||||
@@ -56,6 +58,7 @@ describe("AutoDiscovery", function() {
|
||||
});
|
||||
|
||||
it("should return PROMPT when .well-known 404s", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
@@ -79,6 +82,7 @@ describe("AutoDiscovery", function() {
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
@@ -102,6 +106,7 @@ describe("AutoDiscovery", function() {
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
@@ -125,6 +130,7 @@ describe("AutoDiscovery", function() {
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
@@ -147,31 +153,32 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
|
||||
it("should return FAIL_PROMPT when .well-known returns not-JSON", async () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc", true);
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
AutoDiscovery.findClientConfig("example.org").then(
|
||||
expect(expected).toEqual,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (empty string)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "",
|
||||
@@ -200,6 +207,7 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (no property)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {},
|
||||
});
|
||||
@@ -226,6 +234,7 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (disallowed scheme)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "mxc://example.org",
|
||||
@@ -254,6 +263,7 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 404)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -283,6 +293,7 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 500)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -312,6 +323,7 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 200 but wrong content)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["r0.0.1"],
|
||||
});
|
||||
@@ -343,8 +355,9 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
|
||||
"m.homeserver", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
|
||||
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
@@ -375,8 +388,9 @@ describe("AutoDiscovery", function() {
|
||||
});
|
||||
|
||||
it("should return SUCCESS with the right homeserver URL", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
@@ -410,8 +424,9 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (missing base_url)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
@@ -450,8 +465,9 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (empty base_url)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
@@ -490,8 +506,9 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 404)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
@@ -531,8 +548,9 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 500)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
@@ -572,14 +590,15 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return SUCCESS when the identity server configuration is " +
|
||||
"verifiably accurate", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -614,14 +633,15 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return SUCCESS and preserve non-standard keys from the " +
|
||||
".well-known response", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
expect(req.path)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
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,
|
||||
parseBeaconContent,
|
||||
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("parseBeaconContent()", () => {
|
||||
it("should not explode when parsing an invalid beacon", () => {
|
||||
// deliberate cast to simulate wire content being invalid
|
||||
const result = parseBeaconContent({} as any);
|
||||
expect(result).toEqual({
|
||||
description: undefined,
|
||||
uri: undefined,
|
||||
timestamp: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse unstable values", () => {
|
||||
const uri = "urigoeshere";
|
||||
const description = "descriptiongoeshere";
|
||||
const timestamp = 1234;
|
||||
const result = parseBeaconContent({
|
||||
"org.matrix.msc3488.location": {
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
"org.matrix.msc3488.ts": timestamp,
|
||||
|
||||
// relationship not used - just here to satisfy types
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$unused",
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
description,
|
||||
uri,
|
||||
timestamp,
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse stable values", () => {
|
||||
const uri = "urigoeshere";
|
||||
const description = "descriptiongoeshere";
|
||||
const timestamp = 1234;
|
||||
const result = parseBeaconContent({
|
||||
"m.location": {
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
"m.ts": timestamp,
|
||||
|
||||
// relationship not used - just here to satisfy types
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$unused",
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
description,
|
||||
uri,
|
||||
timestamp,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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>",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(
|
||||
getHttpUriForMxc(
|
||||
baseUrl, httpUrl, undefined, undefined, undefined, true,
|
||||
),
|
||||
).toEqual(httpUrl);
|
||||
});
|
||||
|
||||
it("should return the empty string HTTP URLs by default", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a download URL if no width/height/resize are specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function() {
|
||||
expect(getHttpUriForMxc(null)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a thumbnail URL if a width/height/resize is specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=96&height=96",
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to set custom w/h", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
|
||||
it("should URL encode the identicon string", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { getHttpUriForMxc } from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(
|
||||
getHttpUriForMxc(
|
||||
baseUrl, httpUrl, undefined, undefined, undefined, true,
|
||||
),
|
||||
).toEqual(httpUrl);
|
||||
});
|
||||
|
||||
it("should return the empty string HTTP URLs by default", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a download URL if no width/height/resize are specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function() {
|
||||
expect(getHttpUriForMxc(null as any, '')).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a thumbnail URL if a width/height/resize is specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,340 +0,0 @@
|
||||
import '../olm-loader';
|
||||
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";
|
||||
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";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||
});
|
||||
|
||||
|
||||
describe('Session management', function() {
|
||||
const otkResponse = {
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:FLIBBLE': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally a valid signature',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let crypto;
|
||||
let mockBaseApis;
|
||||
let mockRoomList;
|
||||
|
||||
let fakeEmitter;
|
||||
|
||||
beforeEach(async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
cryptoStore.storeEndToEndDeviceData({
|
||||
devices: {
|
||||
'@bob:home.server': {
|
||||
'BOBDEVICE': {
|
||||
keys: {
|
||||
'curve25519:BOBDEVICE': 'this is a key',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
trackingStatus: {},
|
||||
});
|
||||
|
||||
mockBaseApis = {
|
||||
sendToDevice: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
isGuest: jest.fn(),
|
||||
};
|
||||
mockRoomList = {};
|
||||
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
crypto = new Crypto(
|
||||
mockBaseApis,
|
||||
sessionStore,
|
||||
"@alice:home.server",
|
||||
"FLIBBLE",
|
||||
sessionStore,
|
||||
cryptoStore,
|
||||
mockRoomList,
|
||||
);
|
||||
crypto.registerEventHandlers(fakeEmitter);
|
||||
await crypto.init();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
await crypto.stop();
|
||||
});
|
||||
|
||||
it("restarts wedged Olm sessions", async function() {
|
||||
const prom = new Promise((resolve) => {
|
||||
mockBaseApis.claimOneTimeKeys = function() {
|
||||
resolve();
|
||||
return otkResponse;
|
||||
};
|
||||
});
|
||||
|
||||
fakeEmitter.emit('toDeviceEvent', {
|
||||
getId: jest.fn().mockReturnValue("$wedged"),
|
||||
getType: jest.fn().mockReturnValue('m.room.message'),
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
msgtype: 'm.bad.encrypted',
|
||||
}),
|
||||
getWireContent: jest.fn().mockReturnValue({
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
sender_key: 'this is a key',
|
||||
}),
|
||||
getSender: jest.fn().mockReturnValue('@bob:home.server'),
|
||||
});
|
||||
|
||||
await prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Key requests', function() {
|
||||
let aliceClient;
|
||||
let bobClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await aliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
aliceClient.stopClient();
|
||||
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 = {};
|
||||
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,
|
||||
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(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("creates a new keyshare request if we request a keyshare", async function() {
|
||||
// make sure that cancelAndResend... creates a new keyshare request
|
||||
// if there wasn't an already-existing one
|
||||
const event = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
const cryptoStore = aliceClient._cryptoStore;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: "!someroom",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
};
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toBeDefined();
|
||||
});
|
||||
|
||||
it("uses a new txnid for re-requesting keys", async function() {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const event = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
// replace Alice's sendToDevice function with a mock
|
||||
aliceClient.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
aliceClient.startClient();
|
||||
|
||||
// make a room key request, and record the transaction ID for the
|
||||
// sendToDevice call
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
// key requests get queued until the sync has finished, but we don't
|
||||
// let the client set up enough for that to happen, so gut-wrench a bit
|
||||
// to force it to send now.
|
||||
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
||||
const txnId = aliceClient.sendToDevice.mock.calls[0][2];
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
// of the request
|
||||
await Promise.resolve();
|
||||
|
||||
// cancel and resend the room key request
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
// cancelAndResend will call sendToDevice twice:
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
// the second call to sendToDevice will be the key request
|
||||
expect(aliceClient.sendToDevice).toBeCalledTimes(3);
|
||||
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+72
-62
@@ -22,10 +22,11 @@ import {
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
@@ -38,7 +39,7 @@ const testKey = new Uint8Array([
|
||||
]);
|
||||
|
||||
const types = [
|
||||
{ type: "master", shouldCache: false },
|
||||
{ type: "master", shouldCache: true },
|
||||
{ type: "self_signing", shouldCache: true },
|
||||
{ type: "user_signing", shouldCache: true },
|
||||
{ type: "invalid", shouldCache: false },
|
||||
@@ -51,7 +52,7 @@ const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,33 +66,40 @@ 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, ab] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(ab).toEqual({a: 106712, b: 106712});
|
||||
// check that the pkSigning object corresponds to the pubKey
|
||||
const signature = pkSigning.sign("message");
|
||||
const util = new global.Olm.Utility();
|
||||
try {
|
||||
util.ed25519_verify(pubKey, "message", signature);
|
||||
} finally {
|
||||
util.free();
|
||||
}
|
||||
});
|
||||
|
||||
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"));
|
||||
@@ -114,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 }) => {
|
||||
@@ -212,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.
|
||||
@@ -16,12 +16,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../../src/logger";
|
||||
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 { 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,7 +47,41 @@ const signedDeviceList = {
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {},
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const signedDeviceList2: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test2:sw1v.org": {
|
||||
"QJVRHWAKGH": {
|
||||
"signatures": {
|
||||
"@test2:sw1v.org": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
|
||||
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
|
||||
},
|
||||
},
|
||||
"user_id": "@test2:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH":
|
||||
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -69,14 +105,16 @@ describe('DeviceList', function() {
|
||||
}
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
};
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
} as unknown as MatrixClient;
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
|
||||
} as unknown as OlmDevice;
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
@@ -86,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();
|
||||
@@ -96,6 +134,7 @@ describe('DeviceList', function() {
|
||||
return prom1.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,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();
|
||||
@@ -122,6 +161,7 @@ describe('DeviceList', function() {
|
||||
dl.saveIfDirty().then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
failures: {},
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
@@ -133,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));
|
||||
|
||||
@@ -146,6 +187,34 @@ describe('DeviceList', function() {
|
||||
}).then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
it("should download device keys in batches", function() {
|
||||
const dl = createTestDeviceList(1);
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
const queryDefer2 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,694 +0,0 @@
|
||||
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 {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {TestClient} from "../../../TestClient";
|
||||
import {Room} from "../../../../src/models/room";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("MegolmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let megolmDecryption;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockBaseApis;
|
||||
|
||||
beforeEach(async function() {
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockBaseApis = {};
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
jest.fn().mockResolvedValue(undefined);
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
});
|
||||
|
||||
describe('receives some keys:', function() {
|
||||
let groupSession;
|
||||
beforeEach(async function() {
|
||||
groupSession = new global.Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
// 'crypto' implementation.
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: ROOM_ID,
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
},
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
|
||||
const mockCrypto = {
|
||||
decryptEvent: function() {
|
||||
return Promise.resolve(decryptedData);
|
||||
},
|
||||
};
|
||||
|
||||
await event.attemptDecryption(mockCrypto).then(() => {
|
||||
megolmDecryption.onRoomKeyEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
it('can decrypt an event', function() {
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: groupSession.session_id(),
|
||||
ciphertext: groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
|
||||
it('can respond to a key request event', function() {
|
||||
const keyRequest = {
|
||||
userId: '@alice:foo',
|
||||
deviceId: 'alidevice',
|
||||
requestBody: {
|
||||
room_id: ROOM_ID,
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: groupSession.session_id(),
|
||||
},
|
||||
};
|
||||
|
||||
return megolmDecryption.hasKeysForKeyRequest(
|
||||
keyRequest,
|
||||
).then((hasKeys) => {
|
||||
expect(hasKeys).toBe(true);
|
||||
|
||||
// set up some pre-conditions for the share call
|
||||
const deviceInfo = {};
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
'@alice:foo': {'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}},
|
||||
});
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
res();
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
mockBaseApis.sendToDevice = jest.fn();
|
||||
|
||||
// do the share
|
||||
megolmDecryption.shareKeysWithDevice(keyRequest);
|
||||
|
||||
// it's asynchronous, so we have to wait a bit
|
||||
return awaitEncryptForDevice;
|
||||
}).then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
||||
const payload = call[6];
|
||||
|
||||
expect(payload.type).toEqual("m.forwarded_room_key");
|
||||
expect(payload.content).toMatchObject({
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
sender_claimed_ed25519_key: "SENDER_ED25519",
|
||||
session_id: groupSession.session_id(),
|
||||
chain_index: 0,
|
||||
forwarding_curve25519_key_chain: [],
|
||||
});
|
||||
expect(payload.content.session_key).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("can detect replay attacks", function() {
|
||||
// trying to decrypt two different messages (marked by different
|
||||
// event IDs or timestamps) using the same (sender key, session id,
|
||||
// message index) triple should result in an exception being thrown
|
||||
// as it should be detected as a replay attack.
|
||||
const sessionId = groupSession.session_id();
|
||||
const cipherText = groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
}));
|
||||
const event1 = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const successHandler = jest.fn();
|
||||
const failureHandler = jest.fn((err) => {
|
||||
expect(err.toString()).toMatch(
|
||||
/Duplicate message index, possible replay attack/,
|
||||
);
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event1).then((res) => {
|
||||
const event2 = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event2",
|
||||
origin_server_ts: 1507754149000,
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event2);
|
||||
}).then(
|
||||
successHandler,
|
||||
failureHandler,
|
||||
).then(() => {
|
||||
expect(successHandler).not.toHaveBeenCalled();
|
||||
expect(failureHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("allows re-decryption of the same event", function() {
|
||||
// in contrast with the previous test, if the event ID and
|
||||
// timestamp are the same, then it should not be considered a
|
||||
// replay attack
|
||||
const sessionId = groupSession.session_id();
|
||||
const cipherText = groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
}));
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event).then((res) => {
|
||||
return megolmDecryption.decryptEvent(event);
|
||||
// test is successful if no exception is thrown
|
||||
});
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const 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);
|
||||
|
||||
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(''),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => 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();
|
||||
|
||||
// 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("notifies devices that have been blocked", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient1 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice1",
|
||||
)).client;
|
||||
const bobClient2 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice2",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice1 = bobClient1._crypto._olmDevice;
|
||||
const bobDevice2 = bobClient2._crypto._olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
room.getEncryptionTargetMembers = async function() {
|
||||
return [{userId: "@bob:example.com"}];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
|
||||
const BOB_DEVICES = {
|
||||
bobdevice1: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice1",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
|
||||
},
|
||||
verified: 0,
|
||||
},
|
||||
bobdevice2: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
|
||||
},
|
||||
verified: -1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
let run = false;
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
run = true;
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice1: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.unverified',
|
||||
reason:
|
||||
'The sender has disabled encrypting to unverified devices.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
bobdevice2: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.blacklisted',
|
||||
reason: 'The sender has blocked you.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, room);
|
||||
|
||||
expect(run).toBe(true);
|
||||
|
||||
aliceClient.stopClient();
|
||||
bobClient1.stopClient();
|
||||
bobClient2.stopClient();
|
||||
});
|
||||
|
||||
it("notifies devices when unable to create olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
|
||||
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);
|
||||
|
||||
aliceRoom.getEncryptionTargetMembers = async () => {
|
||||
return [
|
||||
{
|
||||
userId: "@alice:example.com",
|
||||
membership: "join",
|
||||
},
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
membership: "join",
|
||||
},
|
||||
];
|
||||
};
|
||||
const BOB_DEVICES = {
|
||||
bobdevice: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
|
||||
},
|
||||
known: true,
|
||||
verified: 1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
aliceClient.claimOneTimeKeys = async () => {
|
||||
// Bob has no one-time keys
|
||||
return {
|
||||
one_time_keys: {},
|
||||
};
|
||||
};
|
||||
|
||||
const sendPromise = new Promise((resolve, reject) => {
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
code: 'm.no_olm',
|
||||
reason: 'Unable to establish a secure channel.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
await sendPromise;
|
||||
});
|
||||
|
||||
it("throws an error describing why it doesn't have a key", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id",
|
||||
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_id",
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
});
|
||||
|
||||
it("throws an error describing the lack of an olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id",
|
||||
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_id",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
|
||||
});
|
||||
|
||||
it("throws an error to indicate a wedged olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// pretend we got an event that we can't decrypt
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "session_id",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
},
|
||||
}));
|
||||
|
||||
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_id",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,895 @@
|
||||
/*
|
||||
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 { mocked, MockedObject } from 'jest-mock';
|
||||
|
||||
import '../../../olm-loader';
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import * as testUtils from "../../../test-utils/test-utils";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import { Crypto, IncomingRoomKeyRequest } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
import { Room } from "../../../../src/models/room";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import { TypedEventEmitter } from '../../../../src/models/typed-event-emitter';
|
||||
import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
|
||||
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
||||
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("MegolmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let megolmDecryption: algorithms.DecryptionAlgorithm;
|
||||
let mockOlmLib: MockedObject<typeof olmlib>;
|
||||
let mockCrypto: MockedObject<Crypto>;
|
||||
let mockBaseApis: MockedObject<MatrixClient>;
|
||||
|
||||
beforeEach(async function() {
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto') as MockedObject<Crypto>;
|
||||
mockBaseApis = {
|
||||
claimOneTimeKeys: jest.fn(),
|
||||
sendToDevice: jest.fn(),
|
||||
queueToDevice: jest.fn(),
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {
|
||||
encryptMessageForDevice: jest.fn().mockResolvedValue(undefined),
|
||||
ensureOlmSessionsForDevices: jest.fn(),
|
||||
} as unknown as MockedObject<typeof olmlib>;
|
||||
|
||||
// @ts-ignore illegal assignment that makes these tests work :/
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('receives some keys:', function() {
|
||||
let groupSession;
|
||||
beforeEach(async function() {
|
||||
groupSession = new global.Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
// 'crypto' implementation.
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: ROOM_ID,
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
},
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
event.getWireType = () => "m.room.encrypted";
|
||||
event.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
|
||||
const mockCrypto = {
|
||||
decryptEvent: function() {
|
||||
return Promise.resolve(decryptedData);
|
||||
},
|
||||
} as unknown as Crypto;
|
||||
|
||||
await event.attemptDecryption(mockCrypto).then(() => {
|
||||
megolmDecryption.onRoomKeyEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
it('can decrypt an event', function() {
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: groupSession.session_id(),
|
||||
ciphertext: groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
|
||||
it('can respond to a key request event', function() {
|
||||
const keyRequest: IncomingRoomKeyRequest = {
|
||||
requestId: '123',
|
||||
share: jest.fn(),
|
||||
userId: '@alice:foo',
|
||||
deviceId: 'alidevice',
|
||||
requestBody: {
|
||||
algorithm: '',
|
||||
room_id: ROOM_ID,
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: groupSession.session_id(),
|
||||
},
|
||||
};
|
||||
|
||||
return megolmDecryption.hasKeysForKeyRequest(
|
||||
keyRequest,
|
||||
).then((hasKeys) => {
|
||||
expect(hasKeys).toBe(true);
|
||||
|
||||
// set up some pre-conditions for the share call
|
||||
const deviceInfo = {} as DeviceInfo;
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
'@alice:foo': { 'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
device: new DeviceInfo('alidevice'),
|
||||
} },
|
||||
});
|
||||
|
||||
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
res();
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
mockBaseApis.sendToDevice.mockReset();
|
||||
mockBaseApis.queueToDevice.mockReset();
|
||||
|
||||
// do the share
|
||||
megolmDecryption.shareKeysWithDevice(keyRequest);
|
||||
|
||||
// it's asynchronous, so we have to wait a bit
|
||||
return awaitEncryptForDevice;
|
||||
}).then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
||||
const payload = call[6];
|
||||
|
||||
expect(payload.type).toEqual("m.forwarded_room_key");
|
||||
expect(payload.content).toMatchObject({
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
sender_claimed_ed25519_key: "SENDER_ED25519",
|
||||
session_id: groupSession.session_id(),
|
||||
chain_index: 0,
|
||||
forwarding_curve25519_key_chain: [],
|
||||
});
|
||||
expect(payload.content.session_key).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("can detect replay attacks", function() {
|
||||
// trying to decrypt two different messages (marked by different
|
||||
// event IDs or timestamps) using the same (sender key, session id,
|
||||
// message index) triple should result in an exception being thrown
|
||||
// as it should be detected as a replay attack.
|
||||
const sessionId = groupSession.session_id();
|
||||
const cipherText = groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
}));
|
||||
const event1 = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const successHandler = jest.fn();
|
||||
const failureHandler = jest.fn((err) => {
|
||||
expect(err.toString()).toMatch(
|
||||
/Duplicate message index, possible replay attack/,
|
||||
);
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event1).then((res) => {
|
||||
const event2 = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event2",
|
||||
origin_server_ts: 1507754149000,
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event2);
|
||||
}).then(
|
||||
successHandler,
|
||||
failureHandler,
|
||||
).then(() => {
|
||||
expect(successHandler).not.toHaveBeenCalled();
|
||||
expect(failureHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("allows re-decryption of the same event", function() {
|
||||
// in contrast with the previous test, if the event ID and
|
||||
// timestamp are the same, then it should not be considered a
|
||||
// replay attack
|
||||
const sessionId = groupSession.session_id();
|
||||
const cipherText = groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
}));
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event).then((res) => {
|
||||
return megolmDecryption.decryptEvent(event);
|
||||
// test is successful if no exception is thrown
|
||||
});
|
||||
});
|
||||
|
||||
describe("session reuse and key reshares", () => {
|
||||
const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it
|
||||
|
||||
let megolmEncryption;
|
||||
let aliceDeviceInfo;
|
||||
let mockRoom;
|
||||
let olmDevice;
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-ignore assigning to readonly prop
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.mockResolvedValue({
|
||||
failures: {},
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
mockBaseApis.sendToDevice.mockResolvedValue(undefined);
|
||||
mockBaseApis.queueToDevice.mockResolvedValue(undefined);
|
||||
|
||||
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,
|
||||
} as DeviceTrustLevel);
|
||||
|
||||
megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
deviceId: '12345',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
rotation_period_ms: rotationPeriodMs,
|
||||
},
|
||||
});
|
||||
|
||||
// Splice the real method onto the mock object as megolm uses this method
|
||||
// on the crypto class in order to encrypt / start sessions
|
||||
// @ts-ignore Mock
|
||||
mockCrypto.encryptAndSendToDevices = Crypto.prototype.encryptAndSendToDevices;
|
||||
// @ts-ignore Mock
|
||||
mockCrypto.olmDevice = olmDevice;
|
||||
// @ts-ignore Mock
|
||||
mockCrypto.baseApis = mockBaseApis;
|
||||
|
||||
mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 10000,
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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.queueToDevice).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.queueToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
'@alice:home.server',
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.queueToDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies devices that have been blocked", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient1 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice1",
|
||||
)).client;
|
||||
const bobClient2 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice2",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice1 = bobClient1.crypto.olmDevice;
|
||||
const bobDevice2 = bobClient2.crypto.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
|
||||
const bobMember = new RoomMember(roomId, "@bob:example.com");
|
||||
room.getEncryptionTargetMembers = async function() {
|
||||
return [bobMember];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
|
||||
const BOB_DEVICES: Record<string, IDevice> = {
|
||||
bobdevice1: {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
|
||||
},
|
||||
verified: 0,
|
||||
known: false,
|
||||
},
|
||||
bobdevice2: {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
|
||||
},
|
||||
verified: -1,
|
||||
known: false,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, room);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice1: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.unverified',
|
||||
reason:
|
||||
'The sender has disabled encrypting to unverified devices.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
bobdevice2: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.blacklisted',
|
||||
reason: 'The sender has blocked you.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
aliceClient.stopClient();
|
||||
bobClient1.stopClient();
|
||||
bobClient2.stopClient();
|
||||
});
|
||||
|
||||
it("notifies devices when unable to create olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
|
||||
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);
|
||||
|
||||
aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([
|
||||
{
|
||||
userId: "@alice:example.com",
|
||||
membership: "join",
|
||||
},
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
membership: "join",
|
||||
},
|
||||
]);
|
||||
const BOB_DEVICES = {
|
||||
bobdevice: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
|
||||
},
|
||||
known: true,
|
||||
verified: 1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
|
||||
// Bob has no one-time keys
|
||||
one_time_keys: {},
|
||||
failures: {},
|
||||
});
|
||||
|
||||
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
code: 'm.no_olm',
|
||||
reason: 'Unable to establish a secure channel.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error describing why it doesn't have a key", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id1",
|
||||
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_id1",
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, 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() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
aliceClient.crypto.downloadKeys = jest.fn();
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id1",
|
||||
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_id1",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
|
||||
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, 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() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
aliceClient.crypto.downloadKeys = jest.fn();
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// pretend we got an event that we can't decrypt
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "session_id",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
},
|
||||
}));
|
||||
|
||||
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_id",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
});
|
||||
+101
-14
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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.
|
||||
@@ -15,17 +15,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockedObject } from 'jest-mock';
|
||||
|
||||
import '../../../olm-loader';
|
||||
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../../MockStorageApi";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { MatrixClient } from '../../../../src';
|
||||
|
||||
function makeOlmDevice() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
return olmDevice;
|
||||
}
|
||||
@@ -51,8 +52,8 @@ describe("OlmDevice", function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
let aliceOlmDevice;
|
||||
let bobOlmDevice;
|
||||
let aliceOlmDevice: OlmDevice;
|
||||
let bobOlmDevice: OlmDevice;
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceOlmDevice = makeOlmDevice();
|
||||
@@ -69,7 +70,7 @@ describe("OlmDevice", function() {
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sid,
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
);
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const result = await bobOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
@@ -96,7 +97,7 @@ describe("OlmDevice", function() {
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sessionId,
|
||||
MESSAGE,
|
||||
);
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const bobRecreatedOlmDevice = makeOlmDevice();
|
||||
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
|
||||
@@ -120,7 +121,7 @@ describe("OlmDevice", function() {
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sessionId,
|
||||
MESSAGE_2,
|
||||
);
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const bobRecreatedAgainOlmDevice = makeOlmDevice();
|
||||
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
|
||||
@@ -148,7 +149,7 @@ describe("OlmDevice", function() {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
};
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage({
|
||||
@@ -190,5 +191,91 @@ describe("OlmDevice", function() {
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
|
||||
// This test checks whether `ensureOlmSessionsForDevices` properly
|
||||
// handles multiple tasks in flight ensuring some set of devices in
|
||||
// common without deadlocks.
|
||||
|
||||
let claimRequestCount = 0;
|
||||
const baseApis = {
|
||||
claimOneTimeKeys: () => {
|
||||
// simulate a very slow server (.5 seconds to respond)
|
||||
claimRequestCount++;
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
|
||||
const deviceBobA = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-A": "akey",
|
||||
},
|
||||
}, "BOB-A");
|
||||
const deviceBobB = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-B": "bkey",
|
||||
},
|
||||
}, "BOB-B");
|
||||
|
||||
// There's no required ordering of devices per user, so here we
|
||||
// create two different orderings so that each task reserves a
|
||||
// device the other task needs before continuing.
|
||||
const devicesByUserAB = {
|
||||
"@bob:example.com": [
|
||||
deviceBobA,
|
||||
deviceBobB,
|
||||
],
|
||||
};
|
||||
const devicesByUserBA = {
|
||||
"@bob:example.com": [
|
||||
deviceBobB,
|
||||
deviceBobA,
|
||||
],
|
||||
};
|
||||
|
||||
function alwaysSucceed(promise) {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserAB,
|
||||
));
|
||||
|
||||
// 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);
|
||||
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||
));
|
||||
|
||||
// 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);
|
||||
|
||||
// Track the tasks, but don't await them yet.
|
||||
const promises = Promise.all([
|
||||
task1,
|
||||
task2,
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 200);
|
||||
});
|
||||
|
||||
// After .2s, the first task should have made an initial claim request.
|
||||
expect(claimRequestCount).toBe(1);
|
||||
|
||||
await promises;
|
||||
|
||||
// After waiting for both tasks to complete, the first task should
|
||||
// have failed, so the second task should have tried to create a
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(claimRequestCount).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,22 +15,26 @@ 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 { logger } from "../../../src/logger";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {MatrixClient} from "../../../src/client";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
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 {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../src/crypto";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
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;
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
@@ -50,7 +54,7 @@ const ENCRYPTED_EVENT = new MatrixEvent({
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const KEY_BACKUP_DATA = {
|
||||
const CURVE25519_KEY_BACKUP_DATA = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
@@ -71,14 +75,41 @@ const KEY_BACKUP_DATA = {
|
||||
},
|
||||
};
|
||||
|
||||
const BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
const AES256_KEY_BACKUP_DATA = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
|
||||
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
|
||||
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
|
||||
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
|
||||
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
|
||||
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
|
||||
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
|
||||
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
|
||||
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
|
||||
+ 'RgaDHkfzoA3g3aeQ',
|
||||
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
|
||||
},
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO = {
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
const AES256_BACKUP_INFO = {
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: '1',
|
||||
auth_data: {
|
||||
// FIXME: add iv and mac
|
||||
},
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
|
||||
function getCrossSigningKey(type) {
|
||||
@@ -89,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 },
|
||||
});
|
||||
@@ -131,21 +154,18 @@ describe("MegolmBackup", function() {
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockStorage;
|
||||
let sessionStore;
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
);
|
||||
mockCrypto.backupInfo = BACKUP_INFO;
|
||||
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
@@ -158,7 +178,6 @@ describe("MegolmBackup", function() {
|
||||
|
||||
describe("backup", function() {
|
||||
let mockBaseApis;
|
||||
let realSetTimeout;
|
||||
|
||||
beforeEach(function() {
|
||||
mockBaseApis = {};
|
||||
@@ -176,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() {
|
||||
@@ -195,6 +214,12 @@ describe("MegolmBackup", function() {
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
event.getWireType = () => "m.room.encrypted";
|
||||
event.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
@@ -214,22 +239,24 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
|
||||
mockCrypto.backupGroupSession = jest.fn();
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: jest.fn(),
|
||||
};
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
|
||||
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server', function() {
|
||||
it('sends backups to the server (Curve25519 version)', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
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',
|
||||
@@ -256,22 +283,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({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
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) => {
|
||||
client._http.authedRequest = function(
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
@@ -279,27 +306,108 @@ 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.backupGroupSession(
|
||||
"roomId",
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server (AES-256 version)', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
})
|
||||
.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(async () => {
|
||||
await client.enableKeyBackup({
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: '1',
|
||||
auth_data: {
|
||||
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
|
||||
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// 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(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -310,7 +418,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',
|
||||
@@ -323,53 +431,56 @@ 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]);
|
||||
});
|
||||
await client.resetCrossSigningKeys();
|
||||
client.uploadDeviceSigningKeys = async function(e) {return {};};
|
||||
client.uploadKeySignatures = async function(e) {return { failures: {} };};
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
data.auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
await Promise.all([
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let backupInfo;
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls === 1) {
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
data.auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
}
|
||||
backupInfo = data;
|
||||
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) 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({}) as IAbortablePromise<any>;
|
||||
}
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(numCalls).toBe(1);
|
||||
}),
|
||||
]);
|
||||
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();
|
||||
@@ -378,26 +489,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,
|
||||
});
|
||||
|
||||
@@ -411,73 +513,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: "foobar",
|
||||
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.backupGroupSession(
|
||||
"roomId",
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -485,7 +582,7 @@ describe("MegolmBackup", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = makeTestClient(sessionStore, cryptoStore);
|
||||
client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
@@ -504,29 +601,47 @@ describe("MegolmBackup", function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('can restore from backup', function() {
|
||||
client._http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
it('can restore from backup (Curve25519 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
CURVE25519_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore backup by room', function() {
|
||||
client._http.authedRequest = function() {
|
||||
it('can restore from backup (AES-256 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(AES256_KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
AES256_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore backup by room (Curve25519 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve({
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[SESSION_ID]: KEY_BACKUP_DATA,
|
||||
[SESSION_ID]: CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -534,7 +649,7 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
null, null, BACKUP_INFO,
|
||||
null, null, CURVE25519_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
@@ -544,28 +659,44 @@ describe("MegolmBackup", function() {
|
||||
|
||||
it('has working cache functions', async function() {
|
||||
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
await client._crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client._crypto.getSessionBackupPrivateKey();
|
||||
await client.crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(new Uint8Array(result)).toEqual(key);
|
||||
});
|
||||
|
||||
it('caches session backup keys as it encounters them', async function() {
|
||||
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
|
||||
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedNull).toBeNull();
|
||||
client._http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
CURVE25519_BACKUP_INFO,
|
||||
{ cacheCompleteCallback: resolve },
|
||||
);
|
||||
});
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedKey).not.toBeNull();
|
||||
});
|
||||
|
||||
it("fails if an known algorithm is used", async function() {
|
||||
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
|
||||
algorithm: "this.algorithm.does.not.exist",
|
||||
});
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
|
||||
await expect(client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BAD_BACKUP_INFO,
|
||||
)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,13 +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 { 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];
|
||||
}
|
||||
@@ -32,22 +64,22 @@ 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() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,31 +88,88 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
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.crypto.olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T>() => ({} as T);
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
const errorResponse = {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{
|
||||
stages: [
|
||||
"m.login.password",
|
||||
],
|
||||
},
|
||||
],
|
||||
params: {},
|
||||
};
|
||||
|
||||
// If we're not just polling for flows, add on error rejecting the
|
||||
// auth attempt.
|
||||
if (auth) {
|
||||
Object.assign(errorResponse, {
|
||||
completed: [],
|
||||
error: "Invalid password",
|
||||
errcode: "M_FORBIDDEN",
|
||||
});
|
||||
}
|
||||
|
||||
const error = new MatrixError(errorResponse);
|
||||
error.httpStatus == 401;
|
||||
throw error;
|
||||
};
|
||||
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
|
||||
// through failure, stopping before actually applying changes.
|
||||
let bootstrapDidThrow = false;
|
||||
try {
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_FORBIDDEN") {
|
||||
bootstrapDidThrow = true;
|
||||
}
|
||||
}
|
||||
expect(bootstrapDidThrow).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
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 alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -90,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,
|
||||
@@ -116,12 +209,12 @@ describe("Cross Signing", function() {
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
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 {
|
||||
@@ -133,45 +226,57 @@ 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();
|
||||
await alice.checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const uploadSigsPromise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = jest.fn(async (content) => {
|
||||
await olmlib.verifySignature(
|
||||
alice._crypto._olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"@alice:example.com",
|
||||
);
|
||||
resolve();
|
||||
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,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"@alice:example.com",
|
||||
);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
// @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");
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
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",
|
||||
@@ -182,7 +287,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpResponse.filterResponse("@alice:example.com"),
|
||||
filterResponse("@alice:example.com"),
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -246,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;
|
||||
@@ -264,16 +370,17 @@ 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(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
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 alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -281,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: {
|
||||
@@ -294,7 +401,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -305,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"],
|
||||
@@ -317,13 +424,18 @@ 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", {
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be TOFU
|
||||
@@ -336,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
|
||||
@@ -348,22 +460,23 @@ 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(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
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.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
@@ -372,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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
// @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);
|
||||
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],
|
||||
@@ -402,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: {
|
||||
@@ -418,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
|
||||
@@ -426,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",
|
||||
@@ -437,7 +561,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpResponse.filterResponse("@alice:example.com"),
|
||||
filterResponse("@alice:example.com"),
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -496,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
|
||||
@@ -511,16 +635,17 @@ 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(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
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 alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
// (NOTE: device key is not signed by ssk)
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
@@ -529,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: {
|
||||
@@ -542,7 +667,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -553,8 +678,8 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
const bobDevice = {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -565,8 +690,8 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice as unknown as IDevice,
|
||||
});
|
||||
// Bob's device key should be untrusted
|
||||
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
@@ -580,15 +705,16 @@ 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(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
await alice.resetCrossSigningKeys();
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -596,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: {
|
||||
@@ -609,7 +735,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -620,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"],
|
||||
@@ -632,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;
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
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
|
||||
@@ -656,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: {
|
||||
@@ -669,7 +802,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey2]: sskSig2,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -680,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");
|
||||
@@ -693,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
|
||||
@@ -706,7 +839,7 @@ describe("Cross Signing", function() {
|
||||
// Alice gets new signature for device
|
||||
const sig2 = bobSigning2.sign(bobDeviceString);
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
|
||||
@@ -716,49 +849,51 @@ 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(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
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(
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
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 bob.resetCrossSigningKeys();
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key,
|
||||
},
|
||||
verified: 1,
|
||||
known: true,
|
||||
},
|
||||
});
|
||||
alice._crypto._deviceList.storeCrossSigningForUser(
|
||||
alice.crypto.deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
bob._crypto._crossSigningInfo.toStorage(),
|
||||
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
|
||||
@@ -766,7 +901,7 @@ describe("Cross Signing", function() {
|
||||
let upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
await upgradePromise;
|
||||
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
@@ -774,7 +909,7 @@ describe("Cross Signing", function() {
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// "forget" that Bob is trusted
|
||||
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
||||
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures["@alice:example.com"];
|
||||
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
@@ -784,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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
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 }: { level?: CrossSigningLevel} = {},
|
||||
): Promise<void> {
|
||||
const crypto = client.crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
|
||||
try {
|
||||
await crypto.crossSigningInfo.resetKeys(level);
|
||||
await crypto.signObject(crypto.crossSigningInfo.keys.master);
|
||||
// write a copy locally so we know these are trusted keys
|
||||
await crypto.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto.cryptoStore.storeCrossSigningKeys(
|
||||
txn, crypto.crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If anything failed here, revert the keys so we know to try again from the start
|
||||
// next time.
|
||||
crypto.crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto.emit("crossSigning.keysChanged", {});
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
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, key: undefined },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
import {
|
||||
ROOM_KEY_REQUEST_STATES,
|
||||
} from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
requestBody: { session_id: "A", room_id: "A" },
|
||||
state: ROOM_KEY_REQUEST_STATES.SENT,
|
||||
},
|
||||
{
|
||||
requestId: "B",
|
||||
requestBody: { session_id: "B", room_id: "B" },
|
||||
state: ROOM_KEY_REQUEST_STATES.SENT,
|
||||
},
|
||||
{
|
||||
requestId: "C",
|
||||
requestBody: { session_id: "C", room_id: "C" },
|
||||
state: ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
},
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
||||
store._backend = new MemoryCryptoStore();
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
return store;
|
||||
}],
|
||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||
let store;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = dbFactory();
|
||||
await store.startup();
|
||||
await Promise.all(requests.map((request) =>
|
||||
store.getOrAddOutgoingRoomKeyRequest(request),
|
||||
));
|
||||
});
|
||||
|
||||
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
|
||||
async () => {
|
||||
const r = await
|
||||
store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT);
|
||||
expect(r).toHaveLength(2);
|
||||
requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => {
|
||||
expect(r).toContainEqual(e);
|
||||
});
|
||||
});
|
||||
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
async () => {
|
||||
const r =
|
||||
await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CryptoStore } from '../../../src/crypto/store/base';
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-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';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
|
||||
state: RoomKeyRequestState.Sent,
|
||||
recipients: [
|
||||
{ userId: "@alice:example.com", deviceId: "*" },
|
||||
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||
],
|
||||
},
|
||||
{
|
||||
requestId: "B",
|
||||
requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
|
||||
state: RoomKeyRequestState.Sent,
|
||||
recipients: [
|
||||
{ userId: "@alice:example.com", deviceId: "*" },
|
||||
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
|
||||
],
|
||||
},
|
||||
{
|
||||
requestId: "C",
|
||||
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
|
||||
state: RoomKeyRequestState.Unsent,
|
||||
recipients: [
|
||||
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||
let store: CryptoStore;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = dbFactory();
|
||||
await store.startup();
|
||||
await Promise.all(requests.map((request) =>
|
||||
store.getOrAddOutgoingRoomKeyRequest(request),
|
||||
));
|
||||
});
|
||||
|
||||
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
|
||||
async () => {
|
||||
const r = await store.getOutgoingRoomKeyRequestsByTarget(
|
||||
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
|
||||
);
|
||||
expect(r).toHaveLength(1);
|
||||
expect(r[0]).toEqual(requests[0]);
|
||||
});
|
||||
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
@@ -16,21 +16,27 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { makeTestClients } from './verification/util';
|
||||
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';
|
||||
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
console.log('nodejs was compiled without crypto support');
|
||||
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;
|
||||
@@ -44,7 +50,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;
|
||||
}
|
||||
@@ -52,13 +58,13 @@ 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;
|
||||
}
|
||||
|
||||
describe("Secrets", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,42 +88,44 @@ 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];
|
||||
});
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => signingKey,
|
||||
getCrossSigningKey: async t => signingKey,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
alice._crypto._crossSigningInfo.setKeys({
|
||||
alice.crypto.crossSigningInfo.setKeys({
|
||||
master: signingkeyInfo,
|
||||
});
|
||||
|
||||
const secretStorage = alice._crypto._secretStorage;
|
||||
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,
|
||||
};
|
||||
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
@@ -134,11 +142,12 @@ 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() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -148,11 +157,12 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -160,19 +170,20 @@ 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];
|
||||
});
|
||||
|
||||
let keys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
@@ -188,11 +199,12 @@ describe("Secrets", function() {
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
return {};
|
||||
};
|
||||
alice.resetCrossSigningKeys();
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(
|
||||
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
|
||||
@@ -201,11 +213,12 @@ 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() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -213,44 +226,45 @@ 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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@alice:example.com", deviceId: "VAX" },
|
||||
],
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
onSecretRequested: e => {
|
||||
expect(e.name).toBe("foo");
|
||||
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
|
||||
expect(secretName).toBe("foo");
|
||||
return "bar";
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const vaxDevice = vax.client._crypto._olmDevice;
|
||||
const osborne2Device = osborne2.client._crypto._olmDevice;
|
||||
const secretStorage = osborne2.client._crypto._secretStorage;
|
||||
const vaxDevice = vax.client.crypto.olmDevice;
|
||||
const osborne2Device = osborne2.client.crypto.olmDevice;
|
||||
const secretStorage = osborne2.client.crypto.secretStorage;
|
||||
|
||||
osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "VAX",
|
||||
known: false,
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
||||
},
|
||||
verified: DeviceInfo.DeviceVerification.VERIFIED,
|
||||
},
|
||||
});
|
||||
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"Osborne2": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
verified: 0,
|
||||
known: false,
|
||||
keys: {
|
||||
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
|
||||
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
|
||||
@@ -262,15 +276,20 @@ describe("Secrets", function() {
|
||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||
await osborne2Device.markKeysAsPublished();
|
||||
|
||||
await vax.client._crypto._olmDevice.createOutboundSession(
|
||||
await vax.client.crypto.olmDevice.createOutboundSession(
|
||||
osborne2Device.deviceCurve25519Key,
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
const secret = await request.promise;
|
||||
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
expect(secret).toBe("bar");
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
await request.promise; // return value not used
|
||||
|
||||
osborne2.stop();
|
||||
vax.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
describe("bootstrap", function() {
|
||||
@@ -296,7 +315,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];
|
||||
});
|
||||
|
||||
@@ -311,8 +330,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,
|
||||
@@ -322,17 +341,24 @@ describe("Secrets", function() {
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await bob.bootstrapSecretStorage();
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
const crossSigning = bob.crypto.crossSigningInfo;
|
||||
const secretStorage = bob.crypto.secretStorage;
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function() {
|
||||
@@ -368,12 +394,15 @@ describe("Secrets", function() {
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
bob._crypto.checkKeyBackup = async () => {};
|
||||
bob.crypto.backupManager.checkKeyBackup = async () => {};
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
const crossSigning = bob.crypto.crossSigningInfo;
|
||||
const secretStorage = bob.crypto.secretStorage;
|
||||
|
||||
// Set up cross-signing keys from scratch with specific storage key
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => ({
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
@@ -383,21 +412,24 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
bob._crypto._deviceList.storeCrossSigningForUser(
|
||||
bob.crypto.deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapSecretStorage();
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
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,
|
||||
@@ -406,12 +438,12 @@ describe("Secrets", function() {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ 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]];
|
||||
@@ -445,7 +477,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -453,7 +485,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -461,12 +493,14 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -507,14 +541,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"});
|
||||
.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({
|
||||
@@ -526,6 +561,141 @@ 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: Record<string, Uint8Array> = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
};
|
||||
const secretStorageKeys = {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: async t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: async ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "key_id",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.key.key_id",
|
||||
content: {
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
passphrase: {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.megolm_backup.v1",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: await encryptAES(
|
||||
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
|
||||
secretStorageKeys.key_id, "m.megolm_backup.v1",
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||
},
|
||||
},
|
||||
self_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
user_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
[`ed25519:${USPubKey}`]: USPubKey,
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
},
|
||||
});
|
||||
alice.getKeyBackupVersion = async () => {
|
||||
return {
|
||||
version: "1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: sign({
|
||||
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||
}, XSK, "@alice:example.com"),
|
||||
};
|
||||
};
|
||||
alice.setAccountData = async function(name, data) {
|
||||
const event = new MatrixEvent({
|
||||
type: name,
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
return {};
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -13,9 +13,9 @@ 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 {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
@@ -23,7 +23,7 @@ describe("InRoomChannel tests", function() {
|
||||
const MALORY = "@malory:hs.tld";
|
||||
const client = {
|
||||
getUserId() { return ALICE; },
|
||||
};
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
it("getEventType only returns .request for a message with a msgtype", function() {
|
||||
const invalidEvent = new MatrixEvent({
|
||||
+1
-1
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import { logger } from "../../../../src/logger";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
+22
-19
@@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -40,43 +40,46 @@ 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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
algorithms: [],
|
||||
verified: 0,
|
||||
known: false,
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.on("crypto.verification.request", (request) => {
|
||||
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
|
||||
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||
bobVerifier.verify();
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
bobVerifier._endTimer();
|
||||
// @ts-ignore Private function access (but it's a test, so we're okay)
|
||||
bobVerifier.endTimer();
|
||||
});
|
||||
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
const aliceVerifier = aliceRequest.verifier;
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier._endTimer();
|
||||
// @ts-ignore Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier.endTimer();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
});
|
||||
+127
-44
@@ -15,13 +15,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { resetCrossSigningKeys } from "../crypto-utils";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { MatrixClient } from "../../../../src";
|
||||
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -47,13 +52,15 @@ describe("SAS verification", function() {
|
||||
//channel, baseApis, userId, deviceId, startEvent, request
|
||||
const request = {
|
||||
onVerifierCancelled: function() {},
|
||||
};
|
||||
} as VerificationRequest;
|
||||
const channel = {
|
||||
send: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
|
||||
} as unknown as IVerificationChannel;
|
||||
const mockClient = {} as unknown as MatrixClient;
|
||||
const event = new MatrixEvent({ type: 'test' });
|
||||
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
|
||||
sas.handleEvent(new MatrixEvent({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
@@ -64,7 +71,7 @@ describe("SAS verification", function() {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
// Cancel the SAS for cleanup (we started a verification, so abort)
|
||||
sas.cancel();
|
||||
sas.cancel(new Error('error'));
|
||||
});
|
||||
|
||||
describe("verification", () => {
|
||||
@@ -74,20 +81,21 @@ 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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
const aliceDevice = alice.client._crypto._olmDevice;
|
||||
const bobDevice = bob.client._crypto._olmDevice;
|
||||
const aliceDevice = alice.client.crypto.olmDevice;
|
||||
const bobDevice = bob.client.crypto.olmDevice;
|
||||
|
||||
ALICE_DEVICES = {
|
||||
Osborne2: {
|
||||
@@ -113,14 +121,14 @@ describe("SAS verification", function() {
|
||||
},
|
||||
};
|
||||
|
||||
alice.client._crypto._deviceList.storeDevicesForUser(
|
||||
alice.client.crypto.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client._crypto._deviceList.storeDevicesForUser(
|
||||
bob.client.crypto.deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.downloadKeys = () => {
|
||||
@@ -177,6 +185,8 @@ describe("SAS verification", function() {
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
it("should verify a key", async () => {
|
||||
@@ -214,7 +224,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
|
||||
@@ -226,6 +236,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
|
||||
@@ -288,16 +354,16 @@ describe("SAS verification", function() {
|
||||
);
|
||||
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
alice.httpBackend.flush(undefined, 2);
|
||||
await alice.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice.client);
|
||||
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
|
||||
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 2);
|
||||
|
||||
await bob.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
||||
bob.client.crypto.deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
keys: alice.client._crypto._crossSigningInfo.keys,
|
||||
keys: alice.client.crypto.crossSigningInfo.keys,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -333,26 +399,22 @@ 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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.client.setDeviceVerified = jest.fn();
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.request", request => {
|
||||
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
@@ -361,7 +423,7 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
|
||||
verificationMethods.SAS, bob.client.getUserId()!, bob.client.deviceId!,
|
||||
);
|
||||
|
||||
const aliceSpy = jest.fn();
|
||||
@@ -376,6 +438,10 @@ describe("SAS verification", function() {
|
||||
.not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
describe("verification in DM", function() {
|
||||
@@ -385,19 +451,20 @@ 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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
alice.client.setDeviceVerified = jest.fn();
|
||||
alice.client.crypto.setDeviceVerification = jest.fn();
|
||||
alice.client.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
@@ -415,7 +482,7 @@ describe("SAS verification", function() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.crypto.setDeviceVerification = jest.fn();
|
||||
bob.client.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
@@ -436,7 +503,7 @@ describe("SAS verification", function() {
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bobPromise = new Promise<void>((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.request", async (request) => {
|
||||
const verifier = request.beginKeyVerification(SAS.NAME);
|
||||
verifier.on("show_sas", (e) => {
|
||||
@@ -487,6 +554,8 @@ describe("SAS verification", function() {
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
@@ -496,10 +565,24 @@ describe("SAS verification", function() {
|
||||
]);
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
|
||||
expect(alice.client.crypto.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
bob.client.getUserId(),
|
||||
bob.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
|
||||
);
|
||||
expect(bob.client.crypto.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
alice.client.getUserId(),
|
||||
alice.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+42
-24
@@ -14,10 +14,13 @@ 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 { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src';
|
||||
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
|
||||
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -48,17 +51,30 @@ describe("self-verifications", () => {
|
||||
storeCrossSigningKeyCache: jest.fn(),
|
||||
};
|
||||
|
||||
const _crossSigningInfo = new CrossSigningInfo(
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
userId,
|
||||
{},
|
||||
cacheCallbacks,
|
||||
);
|
||||
_crossSigningInfo.keys = {
|
||||
self_signing: { keys: { X: testKeyPub } },
|
||||
user_signing: { keys: { X: testKeyPub } },
|
||||
crossSigningInfo.keys = {
|
||||
master: {
|
||||
keys: { X: testKeyPub },
|
||||
usage: [],
|
||||
user_id: 'user-id',
|
||||
},
|
||||
self_signing: {
|
||||
keys: { X: testKeyPub },
|
||||
usage: [],
|
||||
user_id: 'user-id',
|
||||
},
|
||||
user_signing: {
|
||||
keys: { X: testKeyPub },
|
||||
usage: [],
|
||||
user_id: 'user-id',
|
||||
},
|
||||
};
|
||||
|
||||
const _secretStorage = {
|
||||
const secretStorage = {
|
||||
request: jest.fn().mockReturnValue({
|
||||
promise: Promise.resolve(encodeBase64(testKey)),
|
||||
}),
|
||||
@@ -68,45 +84,47 @@ describe("self-verifications", () => {
|
||||
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
|
||||
|
||||
const client = {
|
||||
_crypto: {
|
||||
_crossSigningInfo,
|
||||
_secretStorage,
|
||||
crypto: {
|
||||
crossSigningInfo,
|
||||
secretStorage,
|
||||
storeSessionBackupPrivateKey,
|
||||
getSessionBackupPrivateKey: () => null,
|
||||
},
|
||||
requestSecret: _secretStorage.request.bind(_secretStorage),
|
||||
requestSecret: secretStorage.request.bind(secretStorage),
|
||||
getUserId: () => userId,
|
||||
getKeyBackupVersion: () => Promise.resolve({}),
|
||||
restoreKeyBackupWithCache,
|
||||
};
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const request = {
|
||||
onVerifierFinished: () => undefined,
|
||||
};
|
||||
} as unknown as VerificationRequest;
|
||||
|
||||
const verification = new VerificationBase(
|
||||
undefined, // channel
|
||||
undefined as unknown as IVerificationChannel, // channel
|
||||
client, // baseApis
|
||||
userId,
|
||||
"ABC", // deviceId
|
||||
undefined, // startEvent
|
||||
undefined as unknown as MatrixEvent, // startEvent
|
||||
request,
|
||||
);
|
||||
verification._resolve = () => undefined;
|
||||
|
||||
// @ts-ignore set private property
|
||||
verification.resolve = () => undefined;
|
||||
|
||||
const result = await verification.done();
|
||||
|
||||
/* We should request, and store, two cross signing key and the key backup key */
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(2);
|
||||
expect(_secretStorage.request.mock.calls.length).toBe(3);
|
||||
/* We should request, and store, 3 cross signing keys and the key backup key */
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
|
||||
expect(secretStorage.request.mock.calls.length).toBe(4);
|
||||
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
|
||||
.toEqual(testKey);
|
||||
.toEqual(testKey);
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
|
||||
.toEqual(testKey);
|
||||
.toEqual(testKey);
|
||||
|
||||
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
|
||||
.toEqual(testKey);
|
||||
.toEqual(testKey);
|
||||
|
||||
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
|
||||
|
||||
@@ -15,42 +15,49 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../../../TestClient';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
// console.log(this.getUserId(), "sends", type, map);
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
|
||||
import { Room, RoomEvent } from "../../../../src/models/room";
|
||||
import { logger } from '../../../../src/logger';
|
||||
import { MatrixClient, ClientEvent } from '../../../../src/client';
|
||||
|
||||
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
|
||||
const clients: TestClient[] = [];
|
||||
const timeouts: ReturnType<typeof setTimeout>[] = [];
|
||||
const clientMap: Record<string, Record<string, MatrixClient>> = {};
|
||||
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client._crypto) :
|
||||
event.attemptDecryption(client.crypto) :
|
||||
Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
() => client.emit("toDeviceEvent", event),
|
||||
() => client.emit(ClientEvent.ToDeviceEvent, event),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const sendEvent = function(room, type, content) {
|
||||
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
|
||||
// make up a unique ID as the event ID
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
|
||||
const eventId = "$" + matrixClient.makeTxnId();
|
||||
const rawEvent = {
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
@@ -60,22 +67,26 @@ export async function makeTestClients(userInfos, options) {
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
|
||||
transaction_id: matrixClient.makeTxnId(),
|
||||
},
|
||||
}));
|
||||
|
||||
setImmediate(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
for (const tc of clients) {
|
||||
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
|
||||
console.log("sending remote echo!!");
|
||||
tc.client.emit("Room.timeline", remoteEcho);
|
||||
const room = new Room('test', tc.client, tc.client.getUserId()!);
|
||||
const roomTimelineData = {} as unknown as IRoomTimelineData;
|
||||
if (tc.client === matrixClient) {
|
||||
logger.log("sending remote echo!!");
|
||||
tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData);
|
||||
} else {
|
||||
tc.client.emit("Room.timeline", event);
|
||||
tc.client.emit(RoomEvent.Timeline, event, room, false, false, roomTimelineData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve({event_id: eventId});
|
||||
timeouts.push(timeout as unknown as ReturnType<typeof setTimeout>);
|
||||
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
@@ -94,24 +105,29 @@ export async function makeTestClients(userInfos, options) {
|
||||
clientMap[userInfo.userId] = {};
|
||||
}
|
||||
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
|
||||
testClient.client.sendToDevice = sendToDevice;
|
||||
testClient.client.sendEvent = sendEvent;
|
||||
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
|
||||
testClient.client.sendEvent = makeSendEvent(testClient.client);
|
||||
clients.push(testClient);
|
||||
}
|
||||
|
||||
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
|
||||
|
||||
return clients;
|
||||
const destroy = () => {
|
||||
timeouts.forEach((t) => clearTimeout(t));
|
||||
};
|
||||
|
||||
return [clients, destroy];
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf);
|
||||
return nodeCrypto.randomFillSync(buf as any);
|
||||
},
|
||||
};
|
||||
} as unknown as Crypto;
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
// @ts-ignore undefined != Crypto
|
||||
global.crypto = undefined;
|
||||
}
|
||||
+137
-37
@@ -13,17 +13,24 @@ 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 {VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE} from
|
||||
import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from
|
||||
"../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel} from
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {setupWebcrypto, teardownWebcrypto} from "./util";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
|
||||
function makeMockClient(userId, deviceId) {
|
||||
type MockClient = MatrixClient & {
|
||||
popEvents: () => MatrixEvent[];
|
||||
popDeviceEvents: (userId: string, deviceId: string) => MatrixEvent[];
|
||||
};
|
||||
function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
let counter = 1;
|
||||
let events = [];
|
||||
let events: MatrixEvent[] = [];
|
||||
const deviceEvents = {};
|
||||
return {
|
||||
getUserId() { return userId; },
|
||||
@@ -40,7 +47,7 @@ function makeMockClient(userId, deviceId) {
|
||||
content,
|
||||
origin_server_ts: Date.now(),
|
||||
}));
|
||||
return Promise.resolve({event_id: eventId});
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
},
|
||||
|
||||
sendToDevice(type, msgMap) {
|
||||
@@ -48,22 +55,24 @@ function makeMockClient(userId, deviceId) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
const event = new MatrixEvent({content, type});
|
||||
const event = new MatrixEvent({ content, type });
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({});
|
||||
},
|
||||
|
||||
popEvents() {
|
||||
// @ts-ignore special testing fn
|
||||
popEvents(): MatrixEvent[] {
|
||||
const e = events;
|
||||
events = [];
|
||||
return e;
|
||||
},
|
||||
|
||||
popDeviceEvents(userId, deviceId) {
|
||||
// @ts-ignore special testing fn
|
||||
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
|
||||
const forDevice = deviceEvents[userId];
|
||||
const events = forDevice && forDevice[deviceId];
|
||||
const result = events || [];
|
||||
@@ -72,12 +81,21 @@ function makeMockClient(userId, deviceId) {
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
} as unknown as MockClient;
|
||||
}
|
||||
|
||||
const MOCK_METHOD = "mock-verify";
|
||||
class MockVerifier {
|
||||
constructor(channel, client, userId, deviceId, startEvent) {
|
||||
class MockVerifier extends VerificationBase<'', any> {
|
||||
public _channel;
|
||||
public _startEvent;
|
||||
constructor(
|
||||
channel: IVerificationChannel,
|
||||
client: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
startEvent: MatrixEvent,
|
||||
) {
|
||||
super(channel, client, userId, deviceId, startEvent, {} as unknown as VerificationRequest);
|
||||
this._channel = channel;
|
||||
this._startEvent = startEvent;
|
||||
}
|
||||
@@ -90,7 +108,7 @@ class MockVerifier {
|
||||
if (this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
} else {
|
||||
await this._channel.send(START_TYPE, {method: MOCK_METHOD});
|
||||
await this._channel.send(START_TYPE, { method: MOCK_METHOD });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +133,15 @@ function makeRemoteEcho(event) {
|
||||
|
||||
async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
await ownRequest.channel.handleEvent(
|
||||
makeRemoteEcho(event), ownRequest, true);
|
||||
makeRemoteEcho(event),
|
||||
ownRequest,
|
||||
true,
|
||||
);
|
||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||
}
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
@@ -131,12 +154,19 @@ describe("verification request unit tests", function() {
|
||||
it("transition from UNSENT to DONE through happy path", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const verificationMethods = new Map(
|
||||
[[MOCK_METHOD, MockVerifier]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), alice);
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
verificationMethods,
|
||||
alice,
|
||||
);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob);
|
||||
verificationMethods,
|
||||
bob,
|
||||
);
|
||||
expect(aliceRequest.invalid).toBe(true);
|
||||
expect(bobRequest.invalid).toBe(true);
|
||||
|
||||
@@ -155,7 +185,7 @@ describe("verification request unit tests", function() {
|
||||
expect(aliceRequest.ready).toBe(true);
|
||||
|
||||
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
|
||||
await verifier.start();
|
||||
await (verifier as MockVerifier).start();
|
||||
const [startEvent] = alice.popEvents();
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
await distributeEvent(aliceRequest, bobRequest, startEvent);
|
||||
@@ -163,8 +193,7 @@ describe("verification request unit tests", function() {
|
||||
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
expect(bobRequest.started).toBe(true);
|
||||
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
|
||||
await bobRequest.verifier.start();
|
||||
await (bobRequest.verifier as MockVerifier).start();
|
||||
const [bobDoneEvent] = bob.popEvents();
|
||||
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
|
||||
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
|
||||
@@ -178,12 +207,20 @@ describe("verification request unit tests", function() {
|
||||
it("methods only contains common methods", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceVerificationMethods = new Map(
|
||||
[["c", function() {}], ["a", function() {}]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const bobVerificationMethods = new Map(
|
||||
[["c", function() {}], ["b", function() {}]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
||||
new Map([["c", function() {}], ["a", function() {}]]), alice);
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
aliceVerificationMethods, alice);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([["c", function() {}], ["b", function() {}]]), bob);
|
||||
bobVerificationMethods,
|
||||
bob,
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||
@@ -199,13 +236,22 @@ describe("verification request unit tests", function() {
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
|
||||
new InRoomChannel(alice, "!room", bob1.getUserId()!),
|
||||
new Map(),
|
||||
alice,
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bob1Request = new VerificationRequest(
|
||||
new InRoomChannel(bob1, "!room"), new Map(), bob1);
|
||||
new InRoomChannel(bob1, "!room"),
|
||||
new Map(),
|
||||
bob1,
|
||||
);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new InRoomChannel(bob2, "!room"), new Map(), bob2);
|
||||
new InRoomChannel(bob2, "!room"),
|
||||
new Map(),
|
||||
bob2,
|
||||
);
|
||||
|
||||
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
|
||||
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
|
||||
@@ -220,22 +266,34 @@ describe("verification request unit tests", function() {
|
||||
it("verify own device with to_device messages", async function() {
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
const verificationMethods = new Map(
|
||||
[[MOCK_METHOD, MockVerifier]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const bob1Request = new VerificationRequest(
|
||||
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(), "device2"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
|
||||
const to = {userId: "@bob:matrix.tld", deviceId: "device2"};
|
||||
new ToDeviceChannel(
|
||||
bob1,
|
||||
bob1.getUserId()!,
|
||||
["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(),
|
||||
"device2",
|
||||
),
|
||||
verificationMethods,
|
||||
bob1,
|
||||
);
|
||||
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
|
||||
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
||||
expect(verifier).toBeInstanceOf(MockVerifier);
|
||||
await verifier.start();
|
||||
await (verifier as MockVerifier).start();
|
||||
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
|
||||
new ToDeviceChannel(bob2, bob2.getUserId()!, ["device1"]),
|
||||
verificationMethods,
|
||||
bob2,
|
||||
);
|
||||
|
||||
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
|
||||
await bob2Request.verifier.start();
|
||||
await (bob2Request.verifier as MockVerifier).start();
|
||||
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
|
||||
expect(doneEvent1.getType()).toBe(DONE_TYPE);
|
||||
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
|
||||
@@ -246,4 +304,46 @@ describe("verification request unit tests", function() {
|
||||
expect(bob1Request.done).toBe(true);
|
||||
expect(bob2Request.done).toBe(true);
|
||||
});
|
||||
|
||||
it("request times out after 10 minutes", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
new Map(),
|
||||
alice,
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
|
||||
|
||||
expect(aliceRequest.cancelled).toBe(false);
|
||||
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId());
|
||||
});
|
||||
|
||||
it("request times out 2 minutes after receipt", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
new Map(),
|
||||
alice,
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map(),
|
||||
bob,
|
||||
);
|
||||
|
||||
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
||||
|
||||
expect(bobRequest.cancelled).toBe(false);
|
||||
expect(bobRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(2 * 60 * 1000);
|
||||
expect(bobRequest._cancellingUserId).toBe(bob.getUserId());
|
||||
});
|
||||
});
|
||||
@@ -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,325 @@
|
||||
/*
|
||||
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 {
|
||||
DuplicateStrategy,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
Room,
|
||||
} 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();
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleRemoteEcho", () => {
|
||||
it("should add to liveTimeline only if the event matches the filter", () => {
|
||||
const filter = new Filter(client.getUserId()!, "test_filter");
|
||||
filter.setDefinition({
|
||||
room: {
|
||||
timeline: {
|
||||
types: [EventType.RoomMessage],
|
||||
},
|
||||
},
|
||||
});
|
||||
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
|
||||
|
||||
const roomMessageEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: { body: "test" },
|
||||
event_id: "!test1:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
|
||||
|
||||
const roomFilteredEvent = new MatrixEvent({
|
||||
type: "other_event_type",
|
||||
content: { body: "test" },
|
||||
event_id: "!test2:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,36 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
function mockRoomStates(timeline) {
|
||||
timeline._startState = utils.mock(RoomState, "startState");
|
||||
timeline._endState = utils.mock(RoomState, "endState");
|
||||
}
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
|
||||
jest.mock("../../src/models/room-state");
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bertha:bar";
|
||||
let timeline;
|
||||
let timeline: EventTimeline;
|
||||
|
||||
const mockClient = {} as unknown as MatrixClient;
|
||||
|
||||
const getTimeline = (): EventTimeline => {
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
|
||||
return new EventTimeline(timelineSet);
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
const timelineSet = { room: { roomId: roomId }};
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
return timelineSet;
|
||||
};
|
||||
// reset any RoomState mocks
|
||||
jest.resetAllMocks();
|
||||
|
||||
timeline = new EventTimeline(timelineSet);
|
||||
timeline = getTimeline();
|
||||
});
|
||||
|
||||
describe("construction", function() {
|
||||
@@ -31,10 +41,6 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
describe("initialiseState", function() {
|
||||
beforeEach(function() {
|
||||
mockRoomStates(timeline);
|
||||
});
|
||||
|
||||
it("should copy state events to start and end state", function() {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
@@ -48,11 +54,17 @@ describe("EventTimeline", function() {
|
||||
}),
|
||||
];
|
||||
timeline.initialiseState(events);
|
||||
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -73,7 +85,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();
|
||||
@@ -94,7 +106,6 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("neighbouringTimelines", function() {
|
||||
it("neighbouring timelines should start null", function() {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
|
||||
@@ -102,8 +113,8 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should set neighbour", function() {
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
const prev = getTimeline();
|
||||
const next = getTimeline();
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
|
||||
@@ -111,8 +122,8 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should throw if called twice", function() {
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
const prev = getTimeline();
|
||||
const next = getTimeline();
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).not.toThrow();
|
||||
@@ -134,10 +145,6 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
describe("addEvent", function() {
|
||||
beforeEach(function() {
|
||||
mockRoomStates(timeline);
|
||||
});
|
||||
|
||||
const events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
@@ -150,9 +157,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]);
|
||||
@@ -160,9 +167,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]);
|
||||
@@ -170,24 +177,22 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("should set event.sender for new and old events", function() {
|
||||
const sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice",
|
||||
};
|
||||
const oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
@@ -204,50 +209,48 @@ 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);
|
||||
});
|
||||
|
||||
it("should set event.target for new and old m.room.member events",
|
||||
function() {
|
||||
const sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice",
|
||||
};
|
||||
const oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
function() {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
const newEv = utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const newEv = utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
});
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
|
||||
});
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
});
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
|
||||
});
|
||||
timeline.addEvent(newEv, false);
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, true);
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for new events", function() {
|
||||
@@ -263,13 +266,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);
|
||||
@@ -278,7 +281,6 @@ describe("EventTimeline", function() {
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
const events = [
|
||||
@@ -293,13 +295,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);
|
||||
@@ -307,6 +309,15 @@ 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();
|
||||
// @ts-ignore stateContext is not a valid param
|
||||
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
|
||||
expect(() => timeline.addEvent(events[0],
|
||||
{ toStartOfTimeline: false, roomState: new RoomState(roomId) },
|
||||
)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeEvent", function() {
|
||||
@@ -326,8 +337,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());
|
||||
@@ -340,9 +351,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);
|
||||
|
||||
@@ -359,14 +370,14 @@ describe("EventTimeline", function() {
|
||||
// - removing the last event got baseIndex into such a state that
|
||||
// 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.removeEvent(events[0].getId());
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[2], false);
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
});
|
||||
function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.removeEvent(events[0].getId());
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
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, 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,16 +15,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
const eventId = 'test_encrypted_event';
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
id: 'test_encrypted_event',
|
||||
event_id: eventId,
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
@@ -32,45 +32,34 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', () => {
|
||||
let callCount = 0;
|
||||
|
||||
let prom2;
|
||||
let prom2Fulfilled = false;
|
||||
it('should retry decryption if a retry is queued', async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: function() {
|
||||
++callCount;
|
||||
logger.log(`decrypt: ${callCount}`);
|
||||
if (callCount == 1) {
|
||||
decryptEvent: jest.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
prom2 = encryptedEvent.attemptDecryption(crypto);
|
||||
prom2.then(() => prom2Fulfilled = true);
|
||||
encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
expect(prom2Fulfilled).toBe(
|
||||
false, 'second attemptDecryption resolved too soon');
|
||||
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return encryptedEvent.attemptDecryption(crypto).then(() => {
|
||||
expect(callCount).toEqual(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
// make sure the second attemptDecryption resolves
|
||||
return prom2;
|
||||
});
|
||||
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Filter} from "../../src/filter";
|
||||
import { Filter, IFilterDefinition } from "../../src/filter";
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
const userId = "@sir_arthur_david:humming.tiger";
|
||||
let filter;
|
||||
let filter: Filter;
|
||||
|
||||
beforeEach(function() {
|
||||
filter = new Filter(userId);
|
||||
@@ -37,7 +37,7 @@ describe("Filter", function() {
|
||||
describe("setDefinition/getDefinition", function() {
|
||||
it("should set and get the filter body", function() {
|
||||
const definition = {
|
||||
event_format: "client",
|
||||
event_format: "client" as IFilterDefinition['event_format'],
|
||||
};
|
||||
filter.setDefinition(definition);
|
||||
expect(filter.getDefinition()).toEqual(definition);
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
import {logger} from "../../src/logger";
|
||||
import {InteractiveAuth} from "../../src/interactive-auth";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
class FakeClient {
|
||||
generateClientSecret() {
|
||||
return "testcl1Ent5EcreT";
|
||||
}
|
||||
}
|
||||
|
||||
describe("InteractiveAuth", function() {
|
||||
it("should start an auth stage and complete it", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: ["logintype"] },
|
||||
],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
logger.log('aaaa');
|
||||
expect(stage).toEqual("logintype");
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = {"a": "b"};
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return Promise.resolve(requestRes);
|
||||
});
|
||||
|
||||
return ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
stateUpdated: stateUpdated,
|
||||
doRequest: doRequest,
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toBe(undefined);
|
||||
expect(ia.getStageParams("logintype")).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual(null); // first request should be null
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: ["logintype"] },
|
||||
],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = {"a": "b"};
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// submitAuthDict should trigger another call to doRequest
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return Promise.resolve(requestRes);
|
||||
});
|
||||
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
return ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
});
|
||||
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual(null); // first request should be null
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return ia.attemptAuth().catch(function(error) {
|
||||
expect(error.message).toBe('No appropriate authentication flow found');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,552 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth, AuthType } 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)
|
||||
class FakeClient {
|
||||
generateClientSecret() {
|
||||
return "testcl1Ent5EcreT";
|
||||
}
|
||||
}
|
||||
|
||||
const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixClient;
|
||||
|
||||
describe("InteractiveAuth", () => {
|
||||
it("should start an auth stage and complete it", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: [AuthType.Password] },
|
||||
],
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams(AuthType.Password)).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.mockImplementation((stage) => {
|
||||
logger.log('aaaa');
|
||||
expect(stage).toEqual(AuthType.Password);
|
||||
ia.submitAuthDict({
|
||||
type: AuthType.Password,
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = { "a": "b" };
|
||||
doRequest.mockImplementation(async (authData) => {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: AuthType.Password,
|
||||
});
|
||||
return requestRes;
|
||||
});
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle auth errcode presence ", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: [AuthType.Password] },
|
||||
],
|
||||
errcode: "MockError0",
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams(AuthType.Password)).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.mockImplementation((stage) => {
|
||||
logger.log('aaaa');
|
||||
expect(stage).toEqual(AuthType.Password);
|
||||
ia.submitAuthDict({
|
||||
type: AuthType.Password,
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = { "a": "b" };
|
||||
doRequest.mockImplementation(async (authData) => {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: AuthType.Password,
|
||||
});
|
||||
return requestRes;
|
||||
});
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle set emailSid for email flow", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
matrixClient: getFakeClient(),
|
||||
emailSid: 'myEmailSid',
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: [AuthType.Email, AuthType.Password] },
|
||||
],
|
||||
params: {
|
||||
[AuthType.Email]: { param: "aa" },
|
||||
[AuthType.Password]: { param: "bb" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams(AuthType.Email)).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.mockImplementation((stage) => {
|
||||
logger.log('husky');
|
||||
expect(stage).toEqual(AuthType.Email);
|
||||
ia.submitAuthDict({
|
||||
type: AuthType.Email,
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = { "a": "b" };
|
||||
doRequest.mockImplementation(async (authData) => {
|
||||
logger.log('barfoo');
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: AuthType.Email,
|
||||
});
|
||||
return requestRes;
|
||||
});
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(requestEmailToken).toBeCalledTimes(0);
|
||||
expect(ia.getEmailSid()).toBe("myEmailSid");
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
stateUpdated,
|
||||
doRequest,
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toBe(undefined);
|
||||
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.mockImplementation((authData) => {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual(null); // first request should be null
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: [AuthType.Password] },
|
||||
],
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = { "a": "b" };
|
||||
stateUpdated.mockImplementation((stage) => {
|
||||
expect(stage).toEqual(AuthType.Password);
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams(AuthType.Password)).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// submitAuthDict should trigger another call to doRequest
|
||||
doRequest.mockImplementation(async (authData) => {
|
||||
logger.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: AuthType.Password,
|
||||
});
|
||||
return requestRes;
|
||||
});
|
||||
|
||||
ia.submitAuthDict({
|
||||
type: AuthType.Password,
|
||||
});
|
||||
});
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should make a request if authdata is null", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
authData: null,
|
||||
matrixClient: getFakeClient(),
|
||||
stateUpdated,
|
||||
doRequest,
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toBe(undefined);
|
||||
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.mockImplementation((authData) => {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual(null); // first request should be null
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: [AuthType.Password] },
|
||||
],
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = { "a": "b" };
|
||||
stateUpdated.mockImplementation((stage) => {
|
||||
expect(stage).toEqual(AuthType.Password);
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams(AuthType.Password)).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// submitAuthDict should trigger another call to doRequest
|
||||
doRequest.mockImplementation(async (authData) => {
|
||||
logger.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: AuthType.Password,
|
||||
});
|
||||
return requestRes;
|
||||
});
|
||||
|
||||
ia.submitAuthDict({
|
||||
type: AuthType.Password,
|
||||
});
|
||||
});
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
doRequest.mockImplementation((authData) => {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual(null); // first request should be null
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [],
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
|
||||
new Error('No appropriate authentication flow found'),
|
||||
);
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow but has session", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
authData: {
|
||||
},
|
||||
sessionId: "sessionId",
|
||||
});
|
||||
|
||||
doRequest.mockImplementation((authData) => {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [],
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
error: "Mock Error 1",
|
||||
errcode: "MOCKERR1",
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
|
||||
new Error('No appropriate authentication flow found'),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle unexpected error types without data propery set", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
},
|
||||
});
|
||||
|
||||
doRequest.mockImplementation((authData) => {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
|
||||
const err = new Error('myerror');
|
||||
(err as any).httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
|
||||
new Error("myerror"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow dummy auth", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
authData: {
|
||||
session: 'sessionId',
|
||||
flows: [
|
||||
{ stages: [AuthType.Dummy] },
|
||||
],
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
|
||||
const requestRes = { "a": "b" };
|
||||
doRequest.mockImplementation((authData) => {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: AuthType.Dummy,
|
||||
});
|
||||
return requestRes;
|
||||
});
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
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: getFakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("passes errors through", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => {
|
||||
throw new Error("unspecific network error");
|
||||
});
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await expect(ia.requestEmailToken.bind(ia)).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: getFakeClient(),
|
||||
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: getFakeClient(),
|
||||
doRequest, stateUpdated, requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(ia.getEmailSid()).toEqual(sid);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 { LocalNotificationSettings } from "../../src/@types/local_notifications";
|
||||
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
|
||||
import { TestClient } from '../TestClient';
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
describe("Local notification settings", () => {
|
||||
beforeEach(() => {
|
||||
client = (new TestClient(
|
||||
"@alice:matrix.org", "123", undefined, undefined, undefined,
|
||||
)).client;
|
||||
client.setAccountData = jest.fn();
|
||||
});
|
||||
|
||||
describe("Lets you set local notification settings", () => {
|
||||
it("stores settings in account data", () => {
|
||||
const deviceId = "device";
|
||||
const settings: LocalNotificationSettings = { is_silenced: true };
|
||||
client.setLocalNotificationSettings(deviceId, settings);
|
||||
|
||||
expect(client.setAccountData).toHaveBeenCalledWith(
|
||||
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
|
||||
settings,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,24 +0,0 @@
|
||||
import {TestClient} from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new TestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it('should store "access_token" and "user_id" if in response', async function() {
|
||||
const response = { user_id: 1, access_token: Date.now().toString(16) };
|
||||
|
||||
client.httpBackend.when('POST', '/login').respond(200, response);
|
||||
client.httpBackend.flush('/login', 1, 100);
|
||||
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
|
||||
|
||||
expect(client.client.getAccessToken()).toBe(response.access_token);
|
||||
expect(client.client.getUserId()).toBe(response.user_id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { SSOAction } from '../../src/@types/auth';
|
||||
import { TestClient } from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new TestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it('should store "access_token" and "user_id" if in response', async function() {
|
||||
const response = { user_id: 1, access_token: Date.now().toString(16) };
|
||||
|
||||
client.httpBackend.when('POST', '/login').respond(200, response);
|
||||
client.httpBackend.flush('/login', 1, 100);
|
||||
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
|
||||
|
||||
expect(client.client.getAccessToken()).toBe(response.access_token);
|
||||
expect(client.client.getUserId()).toBe(response.user_id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSO login URL', function() {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new TestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
});
|
||||
|
||||
describe('SSOAction', function() {
|
||||
const redirectUri = "https://test.com/foo";
|
||||
|
||||
it('No action', function() {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.has('org.matrix.msc3824.action')).toBe(false);
|
||||
});
|
||||
|
||||
it('register', function() {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('register');
|
||||
});
|
||||
|
||||
it('login', function() {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('login');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,524 +0,0 @@
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixClient} from "../../src/client";
|
||||
import {Filter} from "../../src/filter";
|
||||
|
||||
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 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) {
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user