Compare commits
1016 Commits
v16.0.2-rc.1
...
v22.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b6369cc2bd | |||
| e6e079f487 | |||
| 83a1e07380 | |||
| 7f3123ed65 | |||
| 5a88a6c62a | |||
| 12cecbdcf1 | |||
| c17deb0806 | |||
| 31c4f6c16b | |||
| 22271d22f8 | |||
| 9d3ac66cf8 | |||
| a4ad4ed2cf | |||
| 7fd55a61bf | |||
| 847766c114 | |||
| c8c39052a7 | |||
| 6592b2c205 | |||
| fc91153be4 | |||
| 5511a6ef8c | |||
| 19e02e894f | |||
| c54d61e158 | |||
| 44da9040f4 | |||
| 995f5bf7d7 | |||
| ad16b26247 | |||
| aaf3702c66 | |||
| 74147b9943 | |||
| 815370c5f9 | |||
| a01d8e3174 | |||
| 007b7dd242 | |||
| 77d6def1cc | |||
| b318a77ece | |||
| 1a90259326 | |||
| 3702ac56f4 | |||
| af4811b327 | |||
| f46ecf970c | |||
| dd98d7eb2c | |||
| f3dc1c4ca2 | |||
| 305b83f8ea | |||
| acc488da64 | |||
| 7217f83db9 | |||
| 37ea905faa | |||
| 78de55b835 | |||
| cb410f463a | |||
| 72f9d5e6f9 | |||
| c389de98f3 | |||
| 20745dc9ac | |||
| 9410902049 | |||
| a6badbb7fa | |||
| 0b65b199e3 | |||
| c1138bc085 | |||
| c0f7df8c3b | |||
| e085609572 | |||
| 0a4f86a79e | |||
| 5d6ff6c7f9 | |||
| ffcdfe166e | |||
| e1aa7d335b | |||
| 29643e745c | |||
| 54622ce424 | |||
| ca2ae24d46 | |||
| d4601d9910 | |||
| 2ced5e1aa4 | |||
| 45e19e51c1 | |||
| 3f1c3392d4 | |||
| f86f67f5a5 | |||
| 4dcf54f448 | |||
| d43e664594 | |||
| 0e322848f9 | |||
| b454318684 | |||
| 692f1d49b9 | |||
| b40cf75c9d | |||
| ba6a001d67 | |||
| d0c71ec516 | |||
| 67f343d6f0 | |||
| a7f0ba97cd | |||
| 54d11e1745 | |||
| 14744fd4dc | |||
| 9b0919350c | |||
| b53ad2c081 | |||
| 6222d238e4 | |||
| c6ee258789 | |||
| a584324a0d | |||
| 059b07cfa0 | |||
| b628cabe58 | |||
| b7d925f5ec | |||
| 47b729f085 | |||
| 05d980608a | |||
| 1c901e3137 | |||
| bd4589fcc4 | |||
| 0fbd0b3685 | |||
| 1646ea05bc | |||
| 885ec1fc73 | |||
| df2b65f111 | |||
| f09853ccb1 | |||
| 6c543382e6 | |||
| 52932f59ab | |||
| 4f63ff21ea | |||
| c8dc71eb69 | |||
| 2dda837db6 | |||
| fff4cdab7c | |||
| 4f00566b9f | |||
| c1a3b95073 | |||
| 38adbaf923 | |||
| 9459a95134 | |||
| 433b7afd71 | |||
| 777cf1f135 | |||
| 8235b65d71 | |||
| 4a33e584b0 | |||
| 6ee185e93f | |||
| 5df9705bae | |||
| 76458d3a40 | |||
| 27bb79a29a | |||
| 82d942dcc5 | |||
| ce6d0e2cb1 | |||
| db49cd8d13 | |||
| 42b08eca57 | |||
| a92c148f15 | |||
| 6cd60e32dc | |||
| b6633ad4b0 | |||
| e4dd7bcc87 | |||
| b6e97fcecb | |||
| dee2b60c3d | |||
| a07fe44565 | |||
| 0a35f2e2c7 | |||
| 32d535c2b1 | |||
| 7fb313c17c | |||
| d8f6449422 | |||
| c043e36f50 | |||
| e6524239bd | |||
| daed4b9dcc | |||
| 135d2da143 | |||
| fef53be5b4 | |||
| 7ec726e10b | |||
| 476f6f78b1 | |||
| cb8123dec7 | |||
| 6729c7d421 | |||
| 99af67a963 | |||
| b77f5a5598 | |||
| 76377c7cc4 | |||
| 7ddd198df8 | |||
| 81681f4090 | |||
| 52830a2a50 | |||
| 81c3668cb6 | |||
| 8f40dc6304 | |||
| fcdd8c93f4 | |||
| 7d7803380c | |||
| 9fa6616052 | |||
| 94072a096d | |||
| 1f3ae4bde2 | |||
| 545a74364d | |||
| 646b3a69fe | |||
| db33f396b8 | |||
| 6c475d9b54 | |||
| b9cccf9109 | |||
| f0d4ef7f99 | |||
| 068fbb7660 | |||
| 849e3d67c2 | |||
| 4c6e1e5c21 | |||
| 9ff6b357fc | |||
| 77ef8558bd | |||
| d979302e9b | |||
| dbdaa1540a | |||
| b44787192d | |||
| 6f2390a765 | |||
| dddc0aeccb | |||
| 9f6b42d3ae | |||
| 13c751c060 | |||
| 2e56c34df0 | |||
| 87115d181d | |||
| 5679c86ca6 | |||
| 4cd50e4871 | |||
| 0f1012278a | |||
| 0d211dfbad | |||
| 384116c8f5 | |||
| c374ba2367 | |||
| 9f2f08dfd3 | |||
| 4b3e6939d6 | |||
| f2ae3bc8ef | |||
| 1842004db2 | |||
| c8c7af0ae2 | |||
| 11cc30f345 | |||
| 24a9562b07 | |||
| 8f10c0d921 | |||
| 450ff00c3e | |||
| b4ab7fc0b3 | |||
| 35f697a04b | |||
| 193d8a429a | |||
| 8cd5aac128 | |||
| e7ce1fb9e8 | |||
| 7772f855e6 | |||
| 9bdeea0a8d | |||
| ade2e81d3d | |||
| 2fe434f3ae | |||
| eddd0cafe8 | |||
| 4ccc52da8e | |||
| 3a6561af36 | |||
| 403286cb81 | |||
| 219eab9139 | |||
| a12e6185f9 | |||
| d9eac57e9c | |||
| 9a9009d838 | |||
| 6f729ad7fd | |||
| cd33bafa04 | |||
| 867a0ca7ee | |||
| fdbbd9bca4 | |||
| bf1137fc58 | |||
| 5a0787349d | |||
| c57c8978cf | |||
| 508bb5841c | |||
| 35227e3a75 | |||
| 620a8d9c7f | |||
| 17e16b9b1a | |||
| 671dedca1c | |||
| 2464a691ef | |||
| d548b04d06 | |||
| 7ffdf17213 | |||
| 1c3dd0e51e | |||
| 63f4bf571e | |||
| 0231d40277 | |||
| fc1b03c0bf | |||
| e592f60240 | |||
| b1e9f39d65 | |||
| dfe535bc07 | |||
| 30570bcce6 | |||
| 6af3b114e1 | |||
| 041f9951c5 | |||
| be11fa6b5a | |||
| 6245661cd7 | |||
| 12a4d2a749 | |||
| f70f6db926 | |||
| 500601ea85 | |||
| 3c33c422e6 | |||
| d521f97411 | |||
| c0a5299704 | |||
| e32dfccbd9 | |||
| ed78737768 | |||
| ac561b743b | |||
| e8be7af751 | |||
| 400b457edf | |||
| 5ed4e9f535 | |||
| c81d759334 | |||
| 50dd79c595 | |||
| 2eb0afbad5 | |||
| 007ca97741 | |||
| f81e53c908 | |||
| 89d743ac48 | |||
| fd61a49157 | |||
| bb3d51652d | |||
| bbece73346 | |||
| cc025ea458 | |||
| d06a3a47c3 | |||
| 34c5598a3f | |||
| 41a973a3c6 | |||
| 913660c818 | |||
| 8eed354e17 | |||
| 7e12b62b7c | |||
| aa5a34948a | |||
| 6ba35e9fbc | |||
| fe2c35092e | |||
| e37aab2967 | |||
| 62007ec673 | |||
| 029280b9d9 | |||
| 6e5326f9c8 | |||
| a1b046b5d8 | |||
| 3a3dcfb254 | |||
| 121250a6fb | |||
| a7aa227f55 | |||
| 21a6f61b7b | |||
| ff720e3aa3 | |||
| 2935daeb3f | |||
| 04c1dfe43a | |||
| 890a840685 | |||
| b1ed972867 | |||
| 2f24e90e53 | |||
| 07476a0ae0 | |||
| 6348704bec | |||
| f84a33910c | |||
| 0ccf7c50f2 | |||
| ead33003b7 | |||
| 7d5360a00f | |||
| 4b283015ba | |||
| 887e15aac5 | |||
| a83d80f502 | |||
| 6166a8f7fd | |||
| 3efc18cfde | |||
| 5520aa3e2a | |||
| 5afe373446 | |||
| 9bb5afe5c0 | |||
| f349663329 | |||
| f398e3564d | |||
| ce3b72c850 | |||
| 935517746a | |||
| 91171afddd | |||
| b54c9d689a | |||
| 4e69d7c9ac | |||
| bf9f595984 | |||
| f410e71bfa | |||
| 83fca5b57d | |||
| 90052670e7 | |||
| db49a1a623 | |||
| 45330c6418 | |||
| 4ba083e6af | |||
| 0403e4bedc | |||
| 14aa7846a5 | |||
| 2d067ad957 | |||
| 418aa3ff6a | |||
| a587d7c360 | |||
| e48d919cd4 | |||
| 45348a354e | |||
| fa3339fc84 | |||
| b64a30f0ad | |||
| 5451f6139a | |||
| b332c6c4b9 | |||
| 209a101be7 | |||
| ab39ee37d6 | |||
| af6f9d49f4 | |||
| efbf5479d1 | |||
| dacef048be | |||
| a2981efac3 | |||
| 4625ed73cf | |||
| 6f7a72d69e | |||
| caadc6f95b | |||
| 39bc7e2bb3 | |||
| 040f012350 | |||
| 8b2b677f92 | |||
| 2a0ffe1223 | |||
| 72a6ec0dd3 | |||
| 2b1fab928b | |||
| 516f52c5a4 | |||
| 8599a98b47 | |||
| 7e24cb6cae | |||
| 38aa8d18c0 | |||
| 2967ee6309 | |||
| 2e10b6065c | |||
| 69057ee035 | |||
| 72b89fde6e | |||
| c400dd4ff8 | |||
| f34e568a98 | |||
| 6fd80ed3ed | |||
| 9ff11d1f32 | |||
| f41b7706e4 | |||
| de694459be | |||
| 41ab3337b5 | |||
| 6fc9827b10 | |||
| 1432e094c2 | |||
| e09936cc9b | |||
| f52c5eb667 | |||
| c05cb3ad2b | |||
| 586a313c8d | |||
| d32d190a8a | |||
| 53de8d5690 | |||
| 829110b580 | |||
| c605310b87 | |||
| 41cee6f1cc | |||
| 59c82cb679 | |||
| 6d3741d55c | |||
| 43213fec78 | |||
| c8438ff6da | |||
| a1d0f037e2 | |||
| fb565f301b | |||
| afa3b37ad5 | |||
| 3e1e99f8e5 | |||
| 276849f068 | |||
| 37118991f5 | |||
| a57c430b09 | |||
| 583e48086b | |||
| 00629e6dc9 | |||
| 02f6a09bcf | |||
| b22c671fee | |||
| 36a6117ee2 | |||
| aebe26db96 | |||
| 60e175a0e0 | |||
| 1b86acb2fb | |||
| d950cda05c | |||
| d2f7a2575e | |||
| 83c848093f | |||
| 8490f72488 | |||
| d87e53858b | |||
| 917e8c01d8 | |||
| eb3309db43 | |||
| 65741d7860 | |||
| fa6f70f708 | |||
| a7a264f4e7 | |||
| 0fa125b60a | |||
| 98d119d6e1 | |||
| 8aee884d03 | |||
| a8fd0f3d13 | |||
| 876491e38d | |||
| 71fcd9f35b | |||
| 193ff0b4d1 | |||
| f616022d07 | |||
| 3c206c0b85 | |||
| a8c4ff473a | |||
| 289a930cda | |||
| 8ba30bc4ef | |||
| 6e28634819 | |||
| b1e70c5404 | |||
| b11c502a40 | |||
| 167f51c8cd | |||
| 5d2753241e | |||
| 274fe447fd | |||
| c0f1849a83 | |||
| 7b2b618d26 | |||
| aca51fd8a3 | |||
| c78631bdee | |||
| 37187ef347 | |||
| 0d6a93b5f6 | |||
| 40ecfa7932 | |||
| e87ce873b0 | |||
| d656b848f8 | |||
| 0981652de4 | |||
| 207171efd6 | |||
| 8cc5efdf46 | |||
| cbcf47d5c0 | |||
| bbaa0e6536 | |||
| 1efeb1ec0e | |||
| 06e8d98911 | |||
| 8716c1ab9b | |||
| db32420d16 | |||
| d5b82e343a | |||
| ac7f505a2b | |||
| b5576758e4 | |||
| c32a83fdac | |||
| 2d9556c1bb | |||
| 818b70554a | |||
| 725336ffc6 | |||
| 1fbd8983ed | |||
| 1c77816dbd | |||
| 965f4fb13b | |||
| c1160f40c2 | |||
| 9e1b126854 | |||
| c527f85fb1 | |||
| 4a294c9dd3 | |||
| b789cc5933 | |||
| 5e4474b959 | |||
| 4059b5bfba | |||
| 8e646ea584 | |||
| 528e9343ae | |||
| 6571b6a1ab | |||
| 1df329df7c | |||
| 760eeaeed7 | |||
| 438fc70615 | |||
| be94f5ea93 | |||
| 5f9369abee | |||
| e7a7ec0673 | |||
| de3b3960d2 | |||
| b265d795a4 | |||
| eb79f6246d | |||
| 37f8f736e0 | |||
| 92cd84fc0c | |||
| 4b1a443f90 | |||
| 3a120f8fb8 | |||
| d2535b8516 | |||
| 2cda229bc4 | |||
| 45e56f8cc3 | |||
| e95947dc73 | |||
| 448a5c9a77 | |||
| 9589a97952 | |||
| 2566c40e96 | |||
| 099cac0162 | |||
| e4cf5b26ee | |||
| 3ae974e23e | |||
| c698317f3f | |||
| e8f682f452 | |||
| 566b4ba56c | |||
| 13291f33d2 | |||
| 8502759e3e | |||
| 24f9075a84 | |||
| d18aae09c8 | |||
| be3e731499 | |||
| a9f2ae6b55 | |||
| b254ca7fc8 | |||
| 3f6f5b69c7 | |||
| 0e8bd3f02d | |||
| 478270b225 | |||
| 9eb72908a7 | |||
| 2728d74771 | |||
| 020743141b | |||
| 5f5a9b1a43 | |||
| edcef9364c | |||
| 1635ac9971 | |||
| 8f13df2dd9 | |||
| fa9f078a75 | |||
| 055af933cc | |||
| 1ba2730e25 | |||
| 1c9d644a23 | |||
| e29e0d15a5 | |||
| 9ee94c9902 | |||
| 1645867ea6 | |||
| 24d4181a08 | |||
| f576a9f2e9 | |||
| fed121b0aa | |||
| 3334c01191 | |||
| 0b8de251bf | |||
| 88ce017333 | |||
| 3e37c74264 | |||
| 3762c20aad | |||
| 2596999cb8 | |||
| a3248c0aa1 | |||
| c96f1ba22b | |||
| 43c81358b2 | |||
| e05f9b5815 | |||
| 6316a6ae44 | |||
| 471f174889 | |||
| 575b416856 | |||
| 7b7f8c1592 | |||
| c0dacb5037 | |||
| 2cc51e0db7 | |||
| 3907d1c28f | |||
| c629d2f60e | |||
| 43b453804b | |||
| d867affc40 | |||
| c36bfc821c | |||
| 7e784da00a | |||
| b79f469008 | |||
| cf33569a21 | |||
| fb0a0c66c8 | |||
| aac0023338 | |||
| e3873ddef5 | |||
| f0991348e2 | |||
| 22c5999fed | |||
| fa6708c27e | |||
| 4427201326 | |||
| b711781f16 | |||
| 4a4241806e | |||
| 8ba2d257ae | |||
| 3824f65d15 | |||
| 9e2e144530 | |||
| 3c17e4a6d6 | |||
| 75513d08de | |||
| 7cb3b40493 | |||
| ab89804c55 | |||
| ab6cf93c2b | |||
| 4c80762e22 | |||
| 1f7e80c68d | |||
| e91b879a69 | |||
| 14885ba7a2 | |||
| 0dda187d96 | |||
| 680d8cac4d | |||
| a7fd7fd539 | |||
| 300d8b026a | |||
| d5a15ac275 | |||
| bbb5294b3b | |||
| 7731579796 | |||
| 55ab38a097 | |||
| 38a6949e5d | |||
| e876482e62 | |||
| 5367ee18fb | |||
| 45db39ec88 | |||
| 32f55de383 | |||
| 7e8dfa56d0 | |||
| 32bb4b1fc4 | |||
| ae9bb6f27f | |||
| 08ab51eeac | |||
| aa130c88da | |||
| 9523978861 | |||
| 5112340040 | |||
| 544b1c6742 | |||
| 6fb40d465e | |||
| 8d7eaa769a | |||
| 7a18991342 | |||
| f18c64db9e | |||
| 7c560b6daa | |||
| de2add5d5d | |||
| 24710ee2fc | |||
| 1fbfdaf221 | |||
| c4f7e4d5aa | |||
| 9a6dccb79b | |||
| 3935152d08 | |||
| 984dd26a13 | |||
| 72f9a51c27 | |||
| efdda8425d | |||
| 685cab38b9 | |||
| bdb91b3806 | |||
| 85a96c6467 | |||
| 2f832a9bfe | |||
| 1cb32c174b | |||
| b899fd6ccc | |||
| 9a15094374 | |||
| f4aecb317f | |||
| ee0264f77d | |||
| e980c88901 | |||
| 9bf8b936d4 | |||
| 6ea2885796 | |||
| ca5ac79927 | |||
| f9672cf307 | |||
| 9f01c8d1fb | |||
| df5ab4fa91 | |||
| e7493fd417 | |||
| f553854730 | |||
| 39465b50cb | |||
| c89bbf4bf5 | |||
| a745c67dec | |||
| 55bec4fbe9 | |||
| 3a40348860 | |||
| 98262853c7 | |||
| 4b3aac21db | |||
| 7521f82cac | |||
| 9b7628c103 | |||
| 7d2f4cfd42 | |||
| ebcb26f1b3 | |||
| 5822730797 | |||
| fc946ab0fa | |||
| 5b4263bf55 | |||
| df9ffdc408 | |||
| 70449ea003 | |||
| 9b843daf2f | |||
| ab588f0e51 | |||
| b43b4aa9f9 | |||
| d3ff7655f7 | |||
| a1ab0d42fe | |||
| b9ca3ceacd | |||
| eb8491c91b | |||
| 9192b876d2 | |||
| 78db74dad8 | |||
| 04d0d61a0e | |||
| 4897bccdc9 | |||
| aaf508e309 | |||
| 404f8e130e | |||
| 8eeefc72e9 | |||
| b97b862fb6 | |||
| 8e896c4da3 | |||
| 5e766978b8 | |||
| 2c2686c910 | |||
| b20063f8a8 | |||
| 11cc2aca9d | |||
| 8f5162c40d | |||
| 2982bd79f6 | |||
| 96c35e2dd3 | |||
| cb5b2e1470 | |||
| 37d5e4a5e9 | |||
| bf30c15d77 | |||
| 8acc92770d | |||
| bfed6edf41 | |||
| 07189f0637 | |||
| aa94d5d95c | |||
| d73126ecb2 | |||
| 76797704ea | |||
| e35ede0370 | |||
| 518e16e6d5 | |||
| 012f6c56e6 | |||
| 8711499121 | |||
| b64dbdce74 | |||
| 2e27a4134c | |||
| 8412ccfa9b | |||
| 34ef7bc64a | |||
| c707c8632b | |||
| 2700f1d053 | |||
| 142c285063 | |||
| c0ff9756a8 | |||
| 64c6c477dc | |||
| c28939c250 | |||
| 05dd3c4967 | |||
| 8c72c5d0e6 | |||
| 93293750ce | |||
| 220f709b3a | |||
| 6c336ae470 | |||
| a4a50a4a5c | |||
| 169e865bb6 | |||
| 8803d2b7e2 | |||
| ab16adfb7d | |||
| 18e2052af2 | |||
| f90adcab89 | |||
| 6090c7bbfc | |||
| 12253064d1 | |||
| b2120a0a13 | |||
| ad030bfc1f | |||
| 60d665e866 | |||
| 2fd48a607d | |||
| 2f540e31a5 | |||
| 7bd9626496 | |||
| 58a5742bd3 | |||
| d90f983438 | |||
| 81a6e48fc0 | |||
| db4a6fa9e1 | |||
| 457f063c67 | |||
| 01eee96b29 | |||
| 25afb7cb3c | |||
| c12932b2a6 | |||
| 7ac1d07cc4 | |||
| 66adc2d597 | |||
| a9516d047f | |||
| e81d84502b | |||
| 32b2c217c7 | |||
| 7da555c255 | |||
| 88348660eb | |||
| aa0d3bd1f5 | |||
| 81d884f899 | |||
| 942a28ddf6 | |||
| 2dccefb33a | |||
| af6915b4fc | |||
| 3786ea4ca2 | |||
| c7f6777e48 | |||
| ba371f7468 | |||
| 761facf98a | |||
| f78fed5ede | |||
| add3732450 | |||
| c6af997542 | |||
| e9e8e90a94 | |||
| f44510e65f | |||
| ba1f6ffc84 | |||
| 3e4f02b41e | |||
| af17fb27b8 | |||
| 87791cd391 | |||
| 72013341db | |||
| 6f445ca99a | |||
| e2af78d8d3 | |||
| 4721aa1d24 | |||
| 4d4d6e1411 | |||
| 67f5293d6c | |||
| 38e54ae7f2 | |||
| 923ff4b282 | |||
| 49dd76b91e | |||
| acef1d7dd0 | |||
| da615fd512 | |||
| f4f05550ef | |||
| 34cfa51104 | |||
| f475251ddd | |||
| 685276f056 | |||
| 83f61c96f3 | |||
| 85a6a552b5 | |||
| 03a79dc6dd | |||
| 83a4881498 | |||
| e29ee105aa | |||
| 454eb7e627 | |||
| 62d77231af | |||
| 9702e8a5fa | |||
| d82c041b99 | |||
| 706b4d6054 | |||
| da69ca215b | |||
| 95a94cdbe3 | |||
| 6b5f4aa0a9 | |||
| dea3f52fe9 | |||
| a388fde3e2 | |||
| 1cde686a13 | |||
| 592f2931dc | |||
| 030fcb57a5 | |||
| 8be30acb11 | |||
| b86630f0e3 | |||
| 81c89f69c5 | |||
| 9b633251d5 | |||
| 70c6a4b567 | |||
| 5cd615c495 | |||
| 88f5ea675d | |||
| ac5fee0a69 | |||
| 8d9cd0fcb3 | |||
| 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 | |||
| d9f0704048 | |||
| 96ba061732 | |||
| ee4cbd1ec9 | |||
| 2a0dc39eec | |||
| 6e25b13312 | |||
| 94c5e37570 | |||
| 09fee4a2d9 | |||
| 49994ac4fc | |||
| e68cabc70e | |||
| c819ac634f | |||
| 17f5ab4191 | |||
| e270f075a4 | |||
| 0ef6c2e35f | |||
| 7a249e3ef5 | |||
| 353d6bab47 | |||
| 7f21f569d5 | |||
| fa5eae70dd | |||
| 3db056ad3e | |||
| a2a127d9a4 | |||
| d12bccd211 | |||
| d8e597ccdf | |||
| c801690e28 | |||
| b4fe00a3a8 | |||
| d42e2fe2c0 | |||
| 4a4465b9fc | |||
| 1a78301adb | |||
| bbf7020755 | |||
| 592fb0cf10 | |||
| 015eb5d5c4 | |||
| 42fef0e7aa | |||
| 28f3169a28 | |||
| d8285aad00 | |||
| eeacf8c22c | |||
| ee995cb39b | |||
| 7529af43e4 | |||
| 3fac6d7180 | |||
| 487bfc88ef | |||
| c91617a799 | |||
| 87bf115967 | |||
| 18bb5c3079 | |||
| f3f9e41787 | |||
| 7993dd7630 | |||
| bef557976b | |||
| 549f9b7e29 | |||
| 06d9d6207c | |||
| e336aceaba | |||
| fcc4b71f06 | |||
| d1a62eddfc | |||
| ffbd10a7b8 | |||
| d0e37ee323 | |||
| 96ef535ebb | |||
| 0683133d5b | |||
| 64c3ac55a4 | |||
| 5f06df8a87 | |||
| 3291846714 | |||
| 139904f297 | |||
| c2fe2ab270 | |||
| 4e26f29032 | |||
| 31391121dc | |||
| 7d48a8394d | |||
| 28da62c01c | |||
| e880cece93 | |||
| 97e8fcea75 | |||
| f28cb48fe1 | |||
| a2e255c2c9 | |||
| 74c5a20371 | |||
| 4b87907b92 | |||
| f76f708c96 | |||
| 17f7dc5463 | |||
| b253ad9e81 | |||
| c1f56ba3c4 | |||
| 7998817f7e | |||
| bdc12a2544 | |||
| 5a92597abd | |||
| 6f695c1b82 | |||
| d99428f2c1 | |||
| 4c9648a23b | |||
| 8c5f88c4a7 | |||
| 923e9c4ada | |||
| 13d62e71b6 | |||
| 32aca09f47 | |||
| 067ac62271 | |||
| 841e6e999d | |||
| a48546f60d | |||
| 2f09e9641c | |||
| f46355e7c0 | |||
| 53397ee0d1 | |||
| 5a83635ef5 | |||
| 56c0c9be4d | |||
| 24406d2411 | |||
| aeeed6ecd7 | |||
| 9f3f9990ef | |||
| 119ce2e46f | |||
| fc8a867e8e | |||
| b4d8c0b603 | |||
| 3b0d1b2696 | |||
| 5110e0b91e | |||
| 305de54106 | |||
| 0555f9db1c | |||
| 159e825877 | |||
| 8131b3900d | |||
| 431d7a0933 | |||
| e9b52e23d2 | |||
| 0148ad0766 | |||
| 213f1134b6 | |||
| 50e6a8f6b1 | |||
| 4a82e1bf05 | |||
| 843973c4da | |||
| debeb66d6f | |||
| 015d0f9135 | |||
| 5c8e7f2be0 | |||
| 411b5f111c | |||
| 2d231c0ae2 | |||
| ec37eb8b6f | |||
| 1cdcebb5db | |||
| a0f6eea363 | |||
| 18b1a44df7 | |||
| 4b6b1599a2 | |||
| a582b19435 | |||
| 4a8c3d273f | |||
| 8dc608d917 | |||
| 7ef38ed1b2 | |||
| 593f62c1c4 | |||
| 04d674b8c7 | |||
| 27eb88f4a1 | |||
| 1409a4f814 | |||
| 8232896c85 | |||
| e2ed80ffa0 | |||
| 8ac3841a2f | |||
| ba57736bf6 | |||
| 8be4ca909e | |||
| 0d964523a9 | |||
| bb504bc001 | |||
| 326aec9f9e | |||
| 688327dab5 | |||
| 3f4522ba88 | |||
| 625983a2b2 | |||
| 137fd2bd40 | |||
| 1e65bfd316 | |||
| 5da072712d | |||
| 529d61b5f4 | |||
| 5111ca622a | |||
| f627507b86 | |||
| aee4459201 | |||
| 1a824750dd | |||
| 73cb5e1ee9 | |||
| 96bde1f706 | |||
| 5251dcf67f | |||
| ce0b0ea182 | |||
| 7a142e9102 | |||
| f85aa44f28 | |||
| efbf252e22 | |||
| d873f14b6d | |||
| cf1ba12232 | |||
| df208e4de8 | |||
| d8ef7f9f63 | |||
| 2515ba31a0 | |||
| 715c4577d0 | |||
| a2f23900c9 | |||
| e9e65cf484 | |||
| 205c80ea28 | |||
| 678023717b | |||
| b535969845 | |||
| 027bc6bfc9 | |||
| 71ca424712 | |||
| 3280394bf9 | |||
| fc07530434 | |||
| f592d4dbc5 | |||
| 96f48929ac | |||
| 454da84f6e | |||
| 89bda6c2e5 | |||
| ac70dcfc91 | |||
| 9c7cb3cbea | |||
| d8d7bd548f | |||
| 55ef57ead8 | |||
| 9996afed03 | |||
| 61a80a11c9 | |||
| 6a8e8ed0a6 | |||
| 5895ce32fa | |||
| fe0a268991 | |||
| 7f189b0abd | |||
| 6e07c9e900 | |||
| bbeea51a36 | |||
| 151b54ed65 | |||
| 18986cb33a | |||
| aef5d73de4 | |||
| e4fc1f3628 | |||
| 8b1c173659 | |||
| f0916f14d1 | |||
| a291f5ab05 | |||
| 2d7e07f4ed | |||
| 2427f75f98 | |||
| d25fb71eba | |||
| c81b9d2fd9 | |||
| fb3ca90bc9 | |||
| eb2a47623f | |||
| f18d8ead08 | |||
| 2da14bd6e9 | |||
| 1dbb776e12 | |||
| 07b2c57064 | |||
| 7021f70a66 | |||
| 503e954671 | |||
| 2add1fcbcb | |||
| 4fe115b2c4 | |||
| 60e168806d | |||
| 03dfab1282 | |||
| 19302ea4fb | |||
| d5aaed67ba | |||
| 8fe6afd9ab | |||
| 782fbb115f | |||
| 3971bf34ed | |||
| 6dac6e53f7 | |||
| 7ec84e92a0 | |||
| 154e5c45a6 | |||
| 2cd5c813ac | |||
| 1c5101aa1a | |||
| 76f11bee9e | |||
| 91f409e8f4 |
@@ -21,3 +21,6 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
+45
-8
@@ -1,22 +1,32 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
"import",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
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: {
|
||||
"no-var": ["warn"],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
"padded-blocks": ["warn"],
|
||||
"no-extend-native": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"no-var": ["error"],
|
||||
"prefer-rest-params": ["error"],
|
||||
"prefer-spread": ["error"],
|
||||
"one-var": ["error"],
|
||||
"padded-blocks": ["error"],
|
||||
"no-extend-native": ["error"],
|
||||
"camelcase": ["error"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
@@ -33,7 +43,19 @@ module.exports = {
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", "events"],
|
||||
"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: [
|
||||
@@ -52,10 +74,25 @@ module.exports = {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// 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",
|
||||
},
|
||||
}, {
|
||||
files: [
|
||||
"spec/**/*.ts",
|
||||
],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Minor white-space adjustments
|
||||
1d1d59c75744e1f6a2be1cb3e0d1bd9ded5f8025
|
||||
# Import ordering and spacing: eslint-plugin-import
|
||||
80aaa6c32b50601f82e0c991c24e5a4590f39463
|
||||
# Minor white-space adjustment
|
||||
8fb036ba2d01fab66dc4373802ccf19b5cac8541
|
||||
# Minor white-space adjustment
|
||||
b63de6a902a9e1f8ffd7697dea33820fc04f028e
|
||||
3ca84cfc491b0987eec1f13f13cae58d2032bf54
|
||||
# Conform to new typescript eslint rules
|
||||
a87858840b57514603f63e2abbbda4f107f05a77
|
||||
5cf6684129a921295f5593173f16f192336fe0a2
|
||||
# Comply with new member-delimiter-style rule
|
||||
b2ad957d298720d3e026b6bd91be0c403338361a
|
||||
# Fix semicolons in TS files
|
||||
e2ec8952e38b8fea3f0ccaa09ecb42feeba0d923
|
||||
# Migrate to `eslint-plugin-matrix-org`
|
||||
# and `babel/...` to `@babel/...` migration
|
||||
09fac77ce0d9bcf6637088c29afab84084f0e739
|
||||
102704e91a70643bcc09721e14b0d909f0ef55c6
|
||||
# Eslint formatting
|
||||
cec00cd303787fa9008b6c48826e75ed438036fa
|
||||
# Minor eslint changes
|
||||
68bb8182e4e62d8f450f80c408c4b231b8725f1b
|
||||
c979ff6696e30ab8983ac416a3590996d84d3560
|
||||
f4a7395e3a3751a1a8e92dd302c49175a3296ad2
|
||||
# eslint --fix for dangley commas on function calls
|
||||
423175f5397910b0afe3112d6fb18283fc7d27d4
|
||||
# eslint ---fix for prefer-const
|
||||
7bca05af644e8b997dae81e568a3913d8f18d7ca
|
||||
# Fix linting on tests
|
||||
cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||
# eslint --fix
|
||||
0fa9f7c6098822db1ae214f352fd1fe5c248b02c
|
||||
# eslint --fix for lots of white-space
|
||||
5abf6b9f208801c5022a47023150b5846cb0b309
|
||||
# eslint --fix
|
||||
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
* @matrix-org/element-web
|
||||
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
|
||||
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
|
||||
|
||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
|
||||
## Checklist
|
||||
|
||||
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
|
||||
Notes:
|
||||
* [ ] Tests written for new code (and old code if feasible)
|
||||
* [ ] Linter and other CI checks pass
|
||||
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
|
||||
<!--
|
||||
If you would like to specify text for the changelog entry other than your PR title, add the following:
|
||||
|
||||
Notes: Add super cool feature
|
||||
-->
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
codecov:
|
||||
allow_coverage_offsets: True
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
require_base: no
|
||||
require_head: no
|
||||
@@ -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,34 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Static Analysis" ]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v1
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Documentation preview
|
||||
deployment_env: PR Documentation Preview
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Notify Downstream Projects
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-js-sdk'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
event-type: ${{ matrix.event }}
|
||||
@@ -1,12 +0,0 @@
|
||||
name: Preview Changelog
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled ]
|
||||
jobs:
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Preview Changelog
|
||||
uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
workflow_call:
|
||||
inputs:
|
||||
labels:
|
||||
type: string
|
||||
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
required: false
|
||||
description: "No longer used, uses allchange logic now, will be removed at a later date"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v6
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
core.setFailed("Preventing merge whilst PR is marked blocked!");
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v1
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
organization: matrix-org
|
||||
team: Core Team
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Z-Community-PR']
|
||||
});
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
|
||||
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
|
||||
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
|
||||
});
|
||||
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
@@ -0,0 +1,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: |
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
VERSION="${tag#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/docs/ $VERSION
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
|
||||
- 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,50 @@
|
||||
# Must only be called from a workflow_run in the context of the upstream repo
|
||||
name: SonarCloud
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
extra_args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
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.3
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: 'cat package.json | jq -r .version'
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- 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,43 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
|
||||
prepare:
|
||||
name: Prepare
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
|
||||
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
|
||||
steps:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
|
||||
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
needs: prepare
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
|
||||
@@ -0,0 +1,109 @@
|
||||
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"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
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: '--noImplicitAny'
|
||||
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 }}
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Test coverage
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, main, master]
|
||||
jobs:
|
||||
test-coverage:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# This must be set for fetchdep.sh to get the right branch
|
||||
PR_NUMBER: ${{github.event.number}}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# If this is a pull request, make sure we check out its head rather than the
|
||||
# automatically generated merge commit, so that the coverage diff excludes
|
||||
# unrelated changes in the base branch
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: c-hive/gha-yarn-cache@v2
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn install && yarn build && yarn coverage"
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
jest:
|
||||
name: 'Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [browserify, integ, unit]
|
||||
node: [16, 18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build
|
||||
if: matrix.specs == 'browserify'
|
||||
run: "yarn build"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage and metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- name: Run tests with coverage
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- 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 }}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
/_docs
|
||||
.DS_Store
|
||||
|
||||
node_modules
|
||||
/.npmrc
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
instrumentation:
|
||||
compact: false
|
||||
+381
-2
@@ -1,3 +1,376 @@
|
||||
Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)).
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)).
|
||||
|
||||
## ✨ Features
|
||||
* sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)).
|
||||
* Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)).
|
||||
* Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)).
|
||||
* sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)).
|
||||
* Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)).
|
||||
* Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823.
|
||||
* Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)).
|
||||
* Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)).
|
||||
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
|
||||
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
|
||||
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
|
||||
|
||||
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Make calls go back to 'connecting' state when media lost ([\#2880](https://github.com/matrix-org/matrix-js-sdk/pull/2880)).
|
||||
* Add ability to send unthreaded receipt ([\#2878](https://github.com/matrix-org/matrix-js-sdk/pull/2878)).
|
||||
* Add way to abort search requests ([\#2877](https://github.com/matrix-org/matrix-js-sdk/pull/2877)).
|
||||
* sliding sync: add custom room subscriptions support ([\#2834](https://github.com/matrix-org/matrix-js-sdk/pull/2834)).
|
||||
* webrtc: add advanced audio settings ([\#2434](https://github.com/matrix-org/matrix-js-sdk/pull/2434)). Contributed by @MrAnno.
|
||||
* Add support for group calls using MSC3401 ([\#2553](https://github.com/matrix-org/matrix-js-sdk/pull/2553)).
|
||||
* Make the js-sdk conform to tsc --strict ([\#2835](https://github.com/matrix-org/matrix-js-sdk/pull/2835)). Fixes #2112 #2116 and #2124.
|
||||
* Let leave requests outlive the window ([\#2815](https://github.com/matrix-org/matrix-js-sdk/pull/2815)). Fixes vector-im/element-call#639.
|
||||
* Add event and message capabilities to RoomWidgetClient ([\#2797](https://github.com/matrix-org/matrix-js-sdk/pull/2797)).
|
||||
* Misc fixes for group call widgets ([\#2657](https://github.com/matrix-org/matrix-js-sdk/pull/2657)).
|
||||
* Support nested Matrix clients via the widget API ([\#2473](https://github.com/matrix-org/matrix-js-sdk/pull/2473)).
|
||||
* Set max average bitrate on PTT calls ([\#2499](https://github.com/matrix-org/matrix-js-sdk/pull/2499)). Fixes vector-im/element-call#440.
|
||||
* Add config option for e2e group call signalling ([\#2492](https://github.com/matrix-org/matrix-js-sdk/pull/2492)).
|
||||
* Enable DTX on audio tracks in calls ([\#2482](https://github.com/matrix-org/matrix-js-sdk/pull/2482)).
|
||||
* Don't ignore call member events with a distant future expiration date ([\#2466](https://github.com/matrix-org/matrix-js-sdk/pull/2466)).
|
||||
* Expire call member state events after 1 hour ([\#2446](https://github.com/matrix-org/matrix-js-sdk/pull/2446)).
|
||||
* Emit unknown device errors for group call participants without e2e ([\#2447](https://github.com/matrix-org/matrix-js-sdk/pull/2447)).
|
||||
* Mute disconnected peers in PTT mode ([\#2421](https://github.com/matrix-org/matrix-js-sdk/pull/2421)).
|
||||
* Add support for sending encrypted to-device events with OLM ([\#2322](https://github.com/matrix-org/matrix-js-sdk/pull/2322)). Contributed by @robertlong.
|
||||
* Support for PTT group call mode ([\#2338](https://github.com/matrix-org/matrix-js-sdk/pull/2338)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix registration add phone number not working ([\#2876](https://github.com/matrix-org/matrix-js-sdk/pull/2876)). Contributed by @bagvand.
|
||||
* Use an underride rule for Element Call notifications ([\#2873](https://github.com/matrix-org/matrix-js-sdk/pull/2873)). Fixes vector-im/element-web#23691.
|
||||
* Fixes unwanted highlight notifications with encrypted threads ([\#2862](https://github.com/matrix-org/matrix-js-sdk/pull/2862)).
|
||||
* Extra insurance that we don't mix events in the wrong timelines - v2 ([\#2856](https://github.com/matrix-org/matrix-js-sdk/pull/2856)). Contributed by @MadLittleMods.
|
||||
* Hide pending events in thread timelines ([\#2843](https://github.com/matrix-org/matrix-js-sdk/pull/2843)). Fixes vector-im/element-web#23684.
|
||||
* Fix pagination token tracking for mixed room timelines ([\#2855](https://github.com/matrix-org/matrix-js-sdk/pull/2855)). Fixes vector-im/element-web#23695.
|
||||
* Extra insurance that we don't mix events in the wrong timelines ([\#2848](https://github.com/matrix-org/matrix-js-sdk/pull/2848)). Contributed by @MadLittleMods.
|
||||
* Do not freeze state in `initialiseState()` ([\#2846](https://github.com/matrix-org/matrix-js-sdk/pull/2846)).
|
||||
* Don't remove our own member for a split second when entering a call ([\#2844](https://github.com/matrix-org/matrix-js-sdk/pull/2844)).
|
||||
* Resolve races between `initLocalCallFeed` and `leave` ([\#2826](https://github.com/matrix-org/matrix-js-sdk/pull/2826)).
|
||||
* Add throwOnFail to groupCall.setScreensharingEnabled ([\#2787](https://github.com/matrix-org/matrix-js-sdk/pull/2787)).
|
||||
* Fix connectivity regressions ([\#2780](https://github.com/matrix-org/matrix-js-sdk/pull/2780)).
|
||||
* Fix screenshare failing after several attempts ([\#2771](https://github.com/matrix-org/matrix-js-sdk/pull/2771)). Fixes vector-im/element-call#625.
|
||||
* Don't block muting/unmuting on network requests ([\#2754](https://github.com/matrix-org/matrix-js-sdk/pull/2754)). Fixes vector-im/element-call#592.
|
||||
* Fix ICE restarts ([\#2702](https://github.com/matrix-org/matrix-js-sdk/pull/2702)).
|
||||
* Target widget actions at a specific room ([\#2670](https://github.com/matrix-org/matrix-js-sdk/pull/2670)).
|
||||
* Add tests for ice candidate sending ([\#2674](https://github.com/matrix-org/matrix-js-sdk/pull/2674)).
|
||||
* Prevent exception when muting ([\#2667](https://github.com/matrix-org/matrix-js-sdk/pull/2667)). Fixes vector-im/element-call#578.
|
||||
* Fix race in creating calls ([\#2662](https://github.com/matrix-org/matrix-js-sdk/pull/2662)).
|
||||
* Add client.waitUntilRoomReadyForGroupCalls() ([\#2641](https://github.com/matrix-org/matrix-js-sdk/pull/2641)).
|
||||
* Wait for client to start syncing before making group calls ([\#2632](https://github.com/matrix-org/matrix-js-sdk/pull/2632)). Fixes #2589.
|
||||
* Add GroupCallEventHandlerEvent.Room ([\#2631](https://github.com/matrix-org/matrix-js-sdk/pull/2631)).
|
||||
* Add missing events from reemitter to GroupCall ([\#2527](https://github.com/matrix-org/matrix-js-sdk/pull/2527)). Contributed by @toger5.
|
||||
* Prevent double mute status changed events ([\#2502](https://github.com/matrix-org/matrix-js-sdk/pull/2502)).
|
||||
* Don't mute the remote side immediately in PTT calls ([\#2487](https://github.com/matrix-org/matrix-js-sdk/pull/2487)). Fixes vector-im/element-call#425.
|
||||
* Fix some MatrixCall leaks and use a shared AudioContext ([\#2484](https://github.com/matrix-org/matrix-js-sdk/pull/2484)). Fixes vector-im/element-call#412.
|
||||
* Don't block muting on determining whether the device exists ([\#2461](https://github.com/matrix-org/matrix-js-sdk/pull/2461)).
|
||||
* Only clone streams on Safari ([\#2450](https://github.com/matrix-org/matrix-js-sdk/pull/2450)). Fixes vector-im/element-call#267.
|
||||
* Set PTT mode on call correctly ([\#2445](https://github.com/matrix-org/matrix-js-sdk/pull/2445)). Fixes vector-im/element-call#382.
|
||||
* Wait for mute event to send in PTT mode ([\#2401](https://github.com/matrix-org/matrix-js-sdk/pull/2401)).
|
||||
* Handle other members having no e2e keys ([\#2383](https://github.com/matrix-org/matrix-js-sdk/pull/2383)). Fixes vector-im/element-call#338.
|
||||
* Fix races when muting/unmuting ([\#2370](https://github.com/matrix-org/matrix-js-sdk/pull/2370)).
|
||||
|
||||
Changes in [21.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.1.0) (2022-11-08)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Loading threads with server-side assistance ([\#2735](https://github.com/matrix-org/matrix-js-sdk/pull/2735)). Contributed by @justjanne.
|
||||
* Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 ([\#2747](https://github.com/matrix-org/matrix-js-sdk/pull/2747)). Contributed by @hughns.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Replace `instanceof Array` with `Array.isArray` ([\#2812](https://github.com/matrix-org/matrix-js-sdk/pull/2812)). Fixes #2811.
|
||||
* Emit UnreadNotification event on notifications reset ([\#2804](https://github.com/matrix-org/matrix-js-sdk/pull/2804)). Fixes vector-im/element-web#23590.
|
||||
* Fix incorrect prevEv being sent in ClientEvent.AccountData events ([\#2794](https://github.com/matrix-org/matrix-js-sdk/pull/2794)).
|
||||
* Fix build error caused by wrong ts-strict improvements ([\#2783](https://github.com/matrix-org/matrix-js-sdk/pull/2783)). Contributed by @justjanne.
|
||||
* Encryption should not hinder verification ([\#2734](https://github.com/matrix-org/matrix-js-sdk/pull/2734)).
|
||||
|
||||
Changes in [21.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.1) (2022-11-01)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix default behavior of Room.getBlacklistUnverifiedDevices ([\#2830](https://github.com/matrix-org/matrix-js-sdk/pull/2830)). Contributed by @duxovni.
|
||||
* Catch server versions API call exception when starting the client ([\#2828](https://github.com/matrix-org/matrix-js-sdk/pull/2828)). Fixes vector-im/element-web#23634.
|
||||
* Fix authedRequest including `Authorization: Bearer undefined` for password resets ([\#2822](https://github.com/matrix-org/matrix-js-sdk/pull/2822)). Fixes vector-im/element-web#23655.
|
||||
|
||||
Changes in [21.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.0) (2022-10-25)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Changes the `uploadContent` API, kills off `request` and `browser-request` in favour of `fetch`, removed callback support on a lot of the methods, adds a lot of tests. ([\#2719](https://github.com/matrix-org/matrix-js-sdk/pull/2719)). Fixes #2415 and #801.
|
||||
* Remove deprecated `m.room.aliases` references ([\#2759](https://github.com/matrix-org/matrix-js-sdk/pull/2759)). Fixes vector-im/element-web#12680.
|
||||
|
||||
## ✨ Features
|
||||
* Remove node-specific crypto bits, use Node 16's WebCrypto ([\#2762](https://github.com/matrix-org/matrix-js-sdk/pull/2762)). Fixes #2760.
|
||||
* Export types for MatrixEvent and Room emitted events, and make event handler map types stricter ([\#2750](https://github.com/matrix-org/matrix-js-sdk/pull/2750)). Contributed by @stas-demydiuk.
|
||||
* Use even more stable calls to `/room_keys` ([\#2746](https://github.com/matrix-org/matrix-js-sdk/pull/2746)).
|
||||
* Upgrade to Olm 3.2.13 which has been repackaged to support Node 18 ([\#2744](https://github.com/matrix-org/matrix-js-sdk/pull/2744)).
|
||||
* Fix `power_level_content_override` type ([\#2741](https://github.com/matrix-org/matrix-js-sdk/pull/2741)).
|
||||
* Add custom notification handling for MSC3401 call events ([\#2720](https://github.com/matrix-org/matrix-js-sdk/pull/2720)).
|
||||
* Add support for unread thread notifications ([\#2726](https://github.com/matrix-org/matrix-js-sdk/pull/2726)).
|
||||
* Load Thread List with server-side assistance (MSC3856) ([\#2602](https://github.com/matrix-org/matrix-js-sdk/pull/2602)).
|
||||
* Use stable calls to `/room_keys` ([\#2729](https://github.com/matrix-org/matrix-js-sdk/pull/2729)). Fixes vector-im/element-web#22839.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix POST data not being passed for registerWithIdentityServer ([\#2769](https://github.com/matrix-org/matrix-js-sdk/pull/2769)). Fixes matrix-org/element-web-rageshakes#16206.
|
||||
* Fix IdentityPrefix.V2 containing spurious `/api` ([\#2761](https://github.com/matrix-org/matrix-js-sdk/pull/2761)). Fixes vector-im/element-web#23505.
|
||||
* Always send back an httpStatus property if one is known ([\#2753](https://github.com/matrix-org/matrix-js-sdk/pull/2753)).
|
||||
* Check for AbortError, not any generic connection error, to avoid tightlooping ([\#2752](https://github.com/matrix-org/matrix-js-sdk/pull/2752)).
|
||||
* Correct the dir parameter of MSC3715 ([\#2745](https://github.com/matrix-org/matrix-js-sdk/pull/2745)). Contributed by @dhenneke.
|
||||
* Fix sync init when thread unread notif is not supported ([\#2739](https://github.com/matrix-org/matrix-js-sdk/pull/2739)). Fixes vector-im/element-web#23435.
|
||||
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
|
||||
|
||||
Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
|
||||
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
|
||||
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
|
||||
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
|
||||
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
|
||||
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
|
||||
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
|
||||
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
|
||||
|
||||
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue in sync when crypto is not supported by client ([\#2715](https://github.com/matrix-org/matrix-js-sdk/pull/2715)). Contributed by @stas-demydiuk.
|
||||
|
||||
Changes in [20.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.1) (2022-09-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix missing return when receiving an invitation without shared history ([\#2710](https://github.com/matrix-org/matrix-js-sdk/pull/2710)).
|
||||
|
||||
Changes in [20.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.0) (2022-09-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)).
|
||||
|
||||
Changes in [19.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.7.0) (2022-09-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🔒 Security
|
||||
* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249)
|
||||
* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250)
|
||||
* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251)
|
||||
* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236)
|
||||
|
||||
Changes in [19.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.6.0) (2022-09-27)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add a property aggregating all names of a NamespacedValue ([\#2656](https://github.com/matrix-org/matrix-js-sdk/pull/2656)).
|
||||
* Implementation of MSC3824 to add action= param on SSO login ([\#2398](https://github.com/matrix-org/matrix-js-sdk/pull/2398)). Contributed by @hughns.
|
||||
* Add invited_count and joined_count to sliding sync room responses. ([\#2628](https://github.com/matrix-org/matrix-js-sdk/pull/2628)).
|
||||
* Base support for MSC3847: Ignore invites with policy rooms ([\#2626](https://github.com/matrix-org/matrix-js-sdk/pull/2626)). Contributed by @Yoric.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix handling of remote echoes doubling up ([\#2639](https://github.com/matrix-org/matrix-js-sdk/pull/2639)). Fixes #2618.
|
||||
|
||||
Changes in [19.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.5.0) (2022-09-13)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal ([\#2586](https://github.com/matrix-org/matrix-js-sdk/pull/2586)). Contributed by @3nprob.
|
||||
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
|
||||
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
|
||||
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
|
||||
|
||||
Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31)
|
||||
==================================================================================================
|
||||
|
||||
## 🔒 Security
|
||||
* Fix for [CVE-2022-36059](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D36059)
|
||||
|
||||
Find more details at https://matrix.org/blog/2022/08/31/security-releases-matrix-js-sdk-19-4-0-and-matrix-react-sdk-3-53-0
|
||||
|
||||
## ✨ Features
|
||||
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
|
||||
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
|
||||
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
|
||||
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
|
||||
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
|
||||
* fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
|
||||
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald.
|
||||
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
|
||||
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
|
||||
|
||||
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
|
||||
* Emit an event when the client receives TURN servers ([\#2529](https://github.com/matrix-org/matrix-js-sdk/pull/2529)).
|
||||
* Add support for stable prefixes for MSC2285 ([\#2524](https://github.com/matrix-org/matrix-js-sdk/pull/2524)).
|
||||
* Remove stream-replacement ([\#2551](https://github.com/matrix-org/matrix-js-sdk/pull/2551)).
|
||||
* Add support for sending user-defined encrypted to-device messages ([\#2528](https://github.com/matrix-org/matrix-js-sdk/pull/2528)).
|
||||
* Retry to-device messages ([\#2549](https://github.com/matrix-org/matrix-js-sdk/pull/2549)). Fixes vector-im/element-web#12851.
|
||||
* Sliding sync: add missing filters from latest MSC ([\#2555](https://github.com/matrix-org/matrix-js-sdk/pull/2555)).
|
||||
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078.
|
||||
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
|
||||
* Fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
|
||||
* Don't load the sync accumulator if there's already a sync persist in flight ([\#2569](https://github.com/matrix-org/matrix-js-sdk/pull/2569)).
|
||||
|
||||
Changes in [19.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.2.0) (2022-08-02)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Remove unstable support for `m.room_key.withheld` ([\#2512](https://github.com/matrix-org/matrix-js-sdk/pull/2512)). Fixes #2233.
|
||||
|
||||
## ✨ Features
|
||||
* Sliding sync: add missing filters from latest MSC ([\#2555](https://github.com/matrix-org/matrix-js-sdk/pull/2555)).
|
||||
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
|
||||
* Add support for MSC3575: Sliding Sync ([\#2242](https://github.com/matrix-org/matrix-js-sdk/pull/2242)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Correct the units in TURN servers expiry documentation ([\#2520](https://github.com/matrix-org/matrix-js-sdk/pull/2520)).
|
||||
* Re-insert room IDs when decrypting bundled redaction events returned by `/sync` ([\#2531](https://github.com/matrix-org/matrix-js-sdk/pull/2531)). Contributed by @duxovni.
|
||||
|
||||
Changes in [19.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.1.0) (2022-07-26)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Remove MSC3244 support ([\#2504](https://github.com/matrix-org/matrix-js-sdk/pull/2504)).
|
||||
|
||||
## ✨ Features
|
||||
* `room` now exports `KNOWN_SAFE_ROOM_VERSION` ([\#2474](https://github.com/matrix-org/matrix-js-sdk/pull/2474)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't crash with undefined room in `processBeaconEvents()` ([\#2500](https://github.com/matrix-org/matrix-js-sdk/pull/2500)). Fixes #2494.
|
||||
* Properly re-insert room ID in bundled thread relation messages from sync ([\#2505](https://github.com/matrix-org/matrix-js-sdk/pull/2505)). Fixes vector-im/element-web#22094. Contributed by @duxovni.
|
||||
* Actually store the identity server in the client when given as an option ([\#2503](https://github.com/matrix-org/matrix-js-sdk/pull/2503)). Fixes vector-im/element-web#22757.
|
||||
* Fix call.collectCallStats() ([\#2480](https://github.com/matrix-org/matrix-js-sdk/pull/2480)).
|
||||
|
||||
Changes in [19.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.0.0) (2022-07-05)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Remove unused sessionStore ([\#2455](https://github.com/matrix-org/matrix-js-sdk/pull/2455)).
|
||||
|
||||
## ✨ Features
|
||||
* Implement MSC3827: Filtering of `/publicRooms` by room type ([\#2469](https://github.com/matrix-org/matrix-js-sdk/pull/2469)).
|
||||
* expose latestLocationEvent on beacon model ([\#2467](https://github.com/matrix-org/matrix-js-sdk/pull/2467)). Contributed by @kerryarchibald.
|
||||
* Live location share - add start time leniency ([\#2465](https://github.com/matrix-org/matrix-js-sdk/pull/2465)). Contributed by @kerryarchibald.
|
||||
* Log real errors and not just their messages, traces are useful ([\#2464](https://github.com/matrix-org/matrix-js-sdk/pull/2464)).
|
||||
* Various changes to `src/crypto` files for correctness ([\#2137](https://github.com/matrix-org/matrix-js-sdk/pull/2137)). Contributed by @ShadowJonathan.
|
||||
* Update MSC3786 implementation: Check the `state_key` ([\#2429](https://github.com/matrix-org/matrix-js-sdk/pull/2429)).
|
||||
* Timeline needs to refresh when we see a MSC2716 marker event ([\#2299](https://github.com/matrix-org/matrix-js-sdk/pull/2299)). Contributed by @MadLittleMods.
|
||||
* Try to load keys from key backup when a message fails to decrypt ([\#2373](https://github.com/matrix-org/matrix-js-sdk/pull/2373)). Fixes vector-im/element-web#21026. Contributed by @duxovni.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Send call version `1` as a string ([\#2471](https://github.com/matrix-org/matrix-js-sdk/pull/2471)). Fixes vector-im/element-web#22629.
|
||||
* Fix issue with `getEventTimeline` returning undefined for thread roots in main timeline ([\#2454](https://github.com/matrix-org/matrix-js-sdk/pull/2454)). Fixes vector-im/element-web#22539.
|
||||
* Add missing `type` property on `IAuthData` ([\#2463](https://github.com/matrix-org/matrix-js-sdk/pull/2463)).
|
||||
* Clearly indicate that `lastReply` on a Thread can return falsy ([\#2462](https://github.com/matrix-org/matrix-js-sdk/pull/2462)).
|
||||
* Fix issues with getEventTimeline and thread roots ([\#2444](https://github.com/matrix-org/matrix-js-sdk/pull/2444)). Fixes vector-im/element-web#21613.
|
||||
* Live location sharing - monitor liveness of beacons yet to start ([\#2437](https://github.com/matrix-org/matrix-js-sdk/pull/2437)). Contributed by @kerryarchibald.
|
||||
* Refactor Relations to not be per-EventTimelineSet ([\#2412](https://github.com/matrix-org/matrix-js-sdk/pull/2412)). Fixes #2399 and vector-im/element-web#22298.
|
||||
* Add tests for sendEvent threadId handling ([\#2435](https://github.com/matrix-org/matrix-js-sdk/pull/2435)). Fixes vector-im/element-web#22433.
|
||||
* Make sure `encryptAndSendKeysToDevices` assumes devices are unique per-user. ([\#2136](https://github.com/matrix-org/matrix-js-sdk/pull/2136)). Fixes #2135. Contributed by @ShadowJonathan.
|
||||
* Don't bug the user while re-checking key backups after decryption failures ([\#2430](https://github.com/matrix-org/matrix-js-sdk/pull/2430)). Fixes vector-im/element-web#22416. Contributed by @duxovni.
|
||||
|
||||
Changes in [18.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.1.0) (2022-06-07)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Convert `getLocalAliases` to a stable API call ([\#2402](https://github.com/matrix-org/matrix-js-sdk/pull/2402)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix request, crypto, and bs58 imports ([\#2414](https://github.com/matrix-org/matrix-js-sdk/pull/2414)). Fixes #2415.
|
||||
* Update relations after every decryption attempt ([\#2387](https://github.com/matrix-org/matrix-js-sdk/pull/2387)). Fixes vector-im/element-web#22258. Contributed by @weeman1337.
|
||||
* Fix degraded mode for the IDBStore and test it ([\#2400](https://github.com/matrix-org/matrix-js-sdk/pull/2400)). Fixes matrix-org/element-web-rageshakes#13170.
|
||||
* Don't cancel SAS verifications if `ready` is received after `start` ([\#2250](https://github.com/matrix-org/matrix-js-sdk/pull/2250)).
|
||||
* Prevent overlapping sync accumulator persists ([\#2392](https://github.com/matrix-org/matrix-js-sdk/pull/2392)). Fixes vector-im/element-web#21541.
|
||||
* Fix behaviour of isRelation with relation m.replace for state events ([\#2389](https://github.com/matrix-org/matrix-js-sdk/pull/2389)). Fixes vector-im/element-web#22280.
|
||||
* Fixes #2384 ([\#2385](https://github.com/matrix-org/matrix-js-sdk/pull/2385)). Fixes undefined/matrix-js-sdk#2384. Contributed by @schmop.
|
||||
* Ensure rooms are recalculated on re-invites ([\#2374](https://github.com/matrix-org/matrix-js-sdk/pull/2374)). Fixes vector-im/element-web#22106.
|
||||
|
||||
Changes in [18.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0) (2022-05-24)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES (to experimental methods)
|
||||
* Implement changes to MSC2285 (private read receipts) ([\#2221](https://github.com/matrix-org/matrix-js-sdk/pull/2221)).
|
||||
|
||||
## ✨ Features
|
||||
* Add support for HTML renderings of room topics ([\#2272](https://github.com/matrix-org/matrix-js-sdk/pull/2272)).
|
||||
* Add stopClient parameter to MatrixClient::logout ([\#2367](https://github.com/matrix-org/matrix-js-sdk/pull/2367)).
|
||||
* registration: add function to re-request email token ([\#2357](https://github.com/matrix-org/matrix-js-sdk/pull/2357)).
|
||||
* Remove hacky custom status feature ([\#2350](https://github.com/matrix-org/matrix-js-sdk/pull/2350)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Remove default push rule override for MSC1930 ([\#2376](https://github.com/matrix-org/matrix-js-sdk/pull/2376)). Fixes vector-im/element-web#15439.
|
||||
* Tweak thread creation & event adding to fix bugs around relations ([\#2369](https://github.com/matrix-org/matrix-js-sdk/pull/2369)). Fixes vector-im/element-web#22162 and vector-im/element-web#22180.
|
||||
* Prune both clear & wire content on redaction ([\#2346](https://github.com/matrix-org/matrix-js-sdk/pull/2346)). Fixes vector-im/element-web#21929.
|
||||
* MSC3786: Add a default push rule to ignore `m.room.server_acl` events ([\#2333](https://github.com/matrix-org/matrix-js-sdk/pull/2333)). Fixes vector-im/element-web#20788.
|
||||
|
||||
Changes in [17.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.2.0) (2022-05-10)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Live location sharing: handle encrypted messages in processBeaconEvents ([\#2327](https://github.com/matrix-org/matrix-js-sdk/pull/2327)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race conditions around threads ([\#2331](https://github.com/matrix-org/matrix-js-sdk/pull/2331)). Fixes vector-im/element-web#21627.
|
||||
* Ignore m.replace relations on state events, they're invalid ([\#2306](https://github.com/matrix-org/matrix-js-sdk/pull/2306)). Fixes vector-im/element-web#21851.
|
||||
* fix example in readme ([\#2315](https://github.com/matrix-org/matrix-js-sdk/pull/2315)).
|
||||
* Don't decrement the length count of a thread when root redacted ([\#2314](https://github.com/matrix-org/matrix-js-sdk/pull/2314)).
|
||||
* Prevent attempt to create thread with id "undefined" ([\#2308](https://github.com/matrix-org/matrix-js-sdk/pull/2308)).
|
||||
* Update threads handling for replies-to-thread-responses as per MSC update ([\#2305](https://github.com/matrix-org/matrix-js-sdk/pull/2305)). Fixes vector-im/element-web#19678.
|
||||
|
||||
Changes in [17.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.1.0) (2022-04-26)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add MatrixClient.doesServerSupportLogoutDevices() for MSC2457 ([\#2297](https://github.com/matrix-org/matrix-js-sdk/pull/2297)).
|
||||
* Live location sharing - expose room liveBeaconIds ([\#2296](https://github.com/matrix-org/matrix-js-sdk/pull/2296)).
|
||||
* Support for MSC2457 logout_devices param for setPassword() ([\#2285](https://github.com/matrix-org/matrix-js-sdk/pull/2285)).
|
||||
* Stabilise token authenticated registration support ([\#2181](https://github.com/matrix-org/matrix-js-sdk/pull/2181)). Contributed by @govynnus.
|
||||
* Live location sharing - Aggregate beacon locations on beacons ([\#2268](https://github.com/matrix-org/matrix-js-sdk/pull/2268)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Prevent duplicated re-emitter setups in event-mapper ([\#2293](https://github.com/matrix-org/matrix-js-sdk/pull/2293)).
|
||||
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
|
||||
|
||||
Changes in [17.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0) (2022-04-11)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
|
||||
|
||||
## ✨ Features
|
||||
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
|
||||
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
|
||||
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
|
||||
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
|
||||
|
||||
Changes in [16.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.2-rc.1) (2022-04-05)
|
||||
============================================================================================================
|
||||
|
||||
@@ -655,7 +1028,7 @@ Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
|
||||
|
||||
All changes
|
||||
@@ -1328,7 +1701,7 @@ BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
|
||||
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
@@ -2003,6 +2376,12 @@ All Changes
|
||||
* [BREAKING] Refactor the entire build process
|
||||
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
|
||||
|
||||
Changes in [3.42.2-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.42.2-rc.3) (2022-04-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
|
||||
|
||||
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
|
||||
|
||||
+1
-241
@@ -1,245 +1,5 @@
|
||||
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](LICENSE)).
|
||||
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
||||
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/)
|
||||
|
||||
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.
|
||||
|
||||
Things that should go into your PR description:
|
||||
* A changelog entry in the `Notes` section (see below)
|
||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
* Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch.
|
||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
||||
what's changing.
|
||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
* Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
Things that should *not* go into your PR description:
|
||||
* Any information on how the code works or why you chose to do it the way
|
||||
you did. If this isn't obvious from your code, you haven't written enough
|
||||
comments.
|
||||
|
||||
We rely on information in pull request to populate the information that goes
|
||||
into the changelogs our users see, both for the JS SDK itself and also for some
|
||||
projects based on it. This is picked up from both labels on the pull request and
|
||||
the `Notes:` annotation in the description. By default, the PR title will be
|
||||
used for the changelog entry, but you can specify more options, as follows.
|
||||
|
||||
To add a longer, more detailed description of the change for the changelog:
|
||||
|
||||
|
||||
*Fix llama herding bug*
|
||||
|
||||
```
|
||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
||||
```
|
||||
|
||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
||||
the default for PRs labelled with `T-Task`):
|
||||
|
||||
*Remove outdated comment from `Ungulates.ts`*
|
||||
```
|
||||
Notes: none
|
||||
```
|
||||
|
||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
||||
an entry in that project's changelog. You can do that too:
|
||||
|
||||
*Fix another herding bug*
|
||||
```
|
||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
||||
```
|
||||
|
||||
This example is for Element Web. You can specify:
|
||||
* matrix-react-sdk
|
||||
* element-web
|
||||
* element-desktop
|
||||
|
||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
||||
to specify in the notes that it's a breaking change - this will be added
|
||||
automatically based on the label - but remember to tell the developer how to
|
||||
migrate:
|
||||
|
||||
*Remove legacy class*
|
||||
|
||||
```
|
||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
||||
```
|
||||
|
||||
Other metadata can be added using labels.
|
||||
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
|
||||
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
|
||||
* `T-Defect`: A bug fix (in either code or docs).
|
||||
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
||||
|
||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
||||
to add them: ask in the PR description or comments.
|
||||
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Tests
|
||||
-----
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-react-sdk` or `element-web`, you
|
||||
must include:
|
||||
|
||||
1. Comprehensive unit tests written in Jest. These are located in `/test`.
|
||||
2. "happy path" end-to-end tests.
|
||||
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
|
||||
are run using `element-web`. Ideally, you would also include tests for edge
|
||||
and error cases.
|
||||
|
||||
Unit tests are expected even when the feature is in labs. It's good practice
|
||||
to write tests alongside the code as it ensures the code is testable from
|
||||
the start, and gives you a fast feedback loop while you're developing the
|
||||
functionality. End-to-end tests should be added prior to the feature
|
||||
leaving labs, but don't have to be present from the start (although it might
|
||||
be beneficial to have some running early, so you can test things faster).
|
||||
|
||||
For bugs in those repos, your change must include at least one unit test or
|
||||
end-to-end test; which is best depends on what sort of test most concisely
|
||||
exercises the area.
|
||||
|
||||
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
|
||||
These are located in `/spec/`.
|
||||
|
||||
When writing unit tests, please aim for a high level of test coverage
|
||||
for new code - 80% or greater. If you cannot achieve that, please document
|
||||
why it's not possible in your PR.
|
||||
|
||||
Tests validate that your change works as intended and also document
|
||||
concisely what is being changed. Ideally, your new tests fail
|
||||
prior to your change, and succeed once it has been applied. You may
|
||||
find this simpler to achieve if you write the tests first.
|
||||
|
||||
If you're spiking some code that's experimental and not being used to support
|
||||
production features, exceptions can be made to requirements for tests.
|
||||
Note that tests will still be required in order to ship the feature, and it's
|
||||
strongly encouraged to think about tests early in the process, as adding
|
||||
tests later will become progressively more difficult.
|
||||
|
||||
If you're not sure how to approach writing tests for your change, ask for help
|
||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
||||
|
||||
Code style
|
||||
----------
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
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,8 +1,22 @@
|
||||
[](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
|
||||
=====================
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
|
||||
JavaScript. This SDK can be run in a browser or in Node.js.
|
||||
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
|
||||
browser or in Node.js.
|
||||
|
||||
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
|
||||
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
|
||||
is removed in v1.4 then the feature is *eligible* for removal from the SDK when v1.8 is released. This SDK has no
|
||||
guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call
|
||||
endpoints from before Matrix 1.1, for example.
|
||||
|
||||
Quickstart
|
||||
==========
|
||||
@@ -16,7 +30,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.
|
||||
@@ -25,12 +39,10 @@ In Node.js
|
||||
----------
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
|
||||
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
|
||||
|
||||
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
@@ -289,13 +301,13 @@ API Reference
|
||||
A hosted reference can be found at
|
||||
http://matrix-org.github.io/matrix-js-sdk/index.html
|
||||
|
||||
This SDK uses JSDoc3 style comments. You can manually build and
|
||||
This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. You can manually build and
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
$ cd _docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
@@ -307,7 +319,7 @@ The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
|
||||
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ function printLine(event) {
|
||||
|
||||
var maxNameWidth = 15;
|
||||
if (name.length > maxNameWidth) {
|
||||
name = name.substr(0, maxNameWidth-1) + "\u2026";
|
||||
name = name.slice(0, maxNameWidth-1) + "\u2026";
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.message") {
|
||||
@@ -398,7 +398,7 @@ function print(str, formatter) {
|
||||
|
||||
function fixWidth(str, len) {
|
||||
if (str.length > len) {
|
||||
return str.substr(0, len-2) + "\u2026";
|
||||
return str.substring(0, len-2) + "\u2026";
|
||||
}
|
||||
else if (str.length < len) {
|
||||
return str + new Array(len - str.length).join(" ");
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"tags": {
|
||||
"allowUnknownTags": true
|
||||
},
|
||||
"plugins": [
|
||||
"node_modules/better-docs/category",
|
||||
"node_modules/better-docs/typescript"
|
||||
],
|
||||
"source": {
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"includePattern": ".(ts|js)$"
|
||||
},
|
||||
"opts": {
|
||||
"encoding": "utf8",
|
||||
"destination": ".jsdoc",
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"verbose": true,
|
||||
"template": "node_modules/docdash"
|
||||
},
|
||||
"docdash": {
|
||||
"static": true,
|
||||
"private": false,
|
||||
"search": true,
|
||||
"collapse": true,
|
||||
"typedefs": true
|
||||
}
|
||||
}
|
||||
+44
-24
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "16.0.2-rc.1",
|
||||
"version": "22.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
@@ -13,7 +16,7 @@
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
@@ -51,15 +54,16 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"p-retry": "^4.5.0",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -75,33 +79,40 @@
|
||||
"@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.8.tgz",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.9.0",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.4.0",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^0.8.0",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
@@ -109,13 +120,22 @@
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/spec/setupTests.ts"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text",
|
||||
"json"
|
||||
]
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
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
|
||||
+82
-116
@@ -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 element-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,24 +60,70 @@ 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
|
||||
|
||||
function check_dependency {
|
||||
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
|
||||
if [ "$depver" == "null" ]; then return 0; fi
|
||||
|
||||
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
|
||||
@@ -102,20 +132,9 @@ yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
|
||||
# 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
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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
|
||||
@@ -126,20 +145,11 @@ echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
else
|
||||
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
|
||||
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
|
||||
fi
|
||||
rel_branch=$(git symbolic-ref --short HEAD)
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
@@ -151,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
|
||||
|
||||
@@ -179,7 +189,7 @@ do
|
||||
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
|
||||
@@ -191,7 +201,7 @@ git commit package.json $pkglock -m "$tag"
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
|
||||
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
|
||||
@@ -209,8 +219,8 @@ 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" .
|
||||
@@ -235,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
|
||||
@@ -301,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}"
|
||||
@@ -313,35 +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"
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
@@ -358,34 +339,19 @@ git merge "$rel_branch" --no-edit
|
||||
git push origin master
|
||||
|
||||
# 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 --no-edit
|
||||
|
||||
# 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
|
||||
|
||||
[ -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/jest-sonar-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,238 +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/test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} userId
|
||||
* @param {string} deviceId
|
||||
* @param {string} accessToken
|
||||
*
|
||||
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
|
||||
* session store. If undefined, we will create a MockStorageApi.
|
||||
* @param {object} options additional options to pass to the client
|
||||
*/
|
||||
export function TestClient(
|
||||
userId, deviceId, accessToken, sessionStoreBackend, options,
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new MockStorageApi();
|
||||
}
|
||||
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
options = Object.assign({
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: sessionStore,
|
||||
request: this.httpBackend.requestFn,
|
||||
}, options);
|
||||
if (!options.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
options.cryptoStore = this.cryptoStore;
|
||||
}
|
||||
this.client = createClient(options);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
this.callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
return 'TestClient[' + this.userId + ']';
|
||||
};
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/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: 'detached',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
this.httpBackend.flushAllExpected(),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(this + ': started');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
TestClient.prototype.stop = function() {
|
||||
this.client.stopClient();
|
||||
return this.httpBackend.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
*/
|
||||
TestClient.prototype.expectDeviceKeyUpload = function() {
|
||||
const self = this;
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(self + ': received device keys');
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
|
||||
|
||||
self.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns {Promise} for the one-time keys
|
||||
*/
|
||||
TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
} };
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
logger.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
} };
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query device keys.
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} response response to the query.
|
||||
*/
|
||||
TestClient.prototype.expectKeyQuery = function(response) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, (path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual(
|
||||
[],
|
||||
"Expected key query for " + userId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getDeviceKey = function() {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getSigningKey = function() {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @returns {Promise} promise which completes once the sync has been flushed
|
||||
*/
|
||||
TestClient.prototype.flushSync = function() {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
|
||||
TestClient.prototype.isFallbackICEServerAllowed = function() {
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
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 { 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 | null;
|
||||
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?.slice(1).replace(":", ".") + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
|
||||
...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!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import type { MatrixClient, ClientEvent } from "../../src";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
|
||||
afterAll(() => {
|
||||
// clean up XMLHttpRequest mock
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
||||
|
||||
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
|
||||
global.matrixcs = {
|
||||
...global.matrixcs,
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
};
|
||||
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import "./setupTests";// uses browser-matrix instead of the src
|
||||
import type { MatrixClient } from "../../src";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
@@ -26,34 +25,42 @@ const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
let client: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
|
||||
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend = new HttpBackend();
|
||||
client = new global.matrixcs.MatrixClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: USER_ID,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
deviceId: DEVICE_ID,
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
httpBackend.stop();
|
||||
client.http.abort();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
await httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", async function() {
|
||||
const event = utils.mkMembership({
|
||||
room: ROOM_ID,
|
||||
mship: "join",
|
||||
user: "@other_user:server.test",
|
||||
name: "Displayname",
|
||||
});
|
||||
it("Sync", async () => {
|
||||
const event = {
|
||||
type: "m.room.member",
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
membership: "join",
|
||||
name: "Displayname",
|
||||
},
|
||||
event_id: "$foobar",
|
||||
};
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
@@ -71,11 +78,16 @@ describe("Browserify Test", function() {
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return await Promise.race([
|
||||
httpBackend.flushAllExpected(),
|
||||
new Promise((_, reject) => {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
]);
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
|
||||
const unexpectedErrorFn = jest.fn();
|
||||
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
|
||||
|
||||
client.startClient();
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await syncPromise;
|
||||
expect(unexpectedErrorFn).not.toHaveBeenCalled();
|
||||
}, 20000); // additional timeout as this test can take quite a while
|
||||
});
|
||||
@@ -122,7 +122,7 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
@@ -136,138 +136,137 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
it.skip("We should not get confused by out-of-order device query responses", () => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
}).timeout(3000);
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
@@ -288,11 +287,12 @@ describe("DeviceList management:", function() {
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should be tracking bob's device list
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
0, "Alice should be tracking bob's device list",
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -324,11 +324,12 @@ describe("DeviceList management:", function() {
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -360,11 +361,12 @@ describe("DeviceList management:", function() {
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -379,13 +381,15 @@ 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) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
// @ts-ignore accessing private property
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
@@ -1,758 +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/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
|
||||
let aliTestClient;
|
||||
const roomId = "!room:localhost";
|
||||
const aliUserId = "@ali:localhost";
|
||||
const aliDeviceId = "zxcvb";
|
||||
const aliAccessToken = "aseukfgwef";
|
||||
let bobTestClient;
|
||||
const bobUserId = "@bob:localhost";
|
||||
const bobDeviceId = "bvcxz";
|
||||
const bobAccessToken = "fewgfkuesa";
|
||||
let aliMessages;
|
||||
let bobMessages;
|
||||
|
||||
function bobUploadsDeviceKeys() {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
return Promise.all([
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flush(),
|
||||
]).then(() => {
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will query bobs keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliQueryKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.deviceKeys).toBeTruthy();
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual(
|
||||
[],
|
||||
"Expected Alice to key query for " + bobUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return aliTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that bob will query alis keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} which resolves once the http request has completed.
|
||||
*/
|
||||
function expectBobQueryKeys() {
|
||||
// can't query keys before ali has uploaded them
|
||||
expect(aliTestClient.deviceKeys).toBeTruthy();
|
||||
|
||||
const aliKeys = {};
|
||||
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
|
||||
logger.log("query result will be", aliKeys);
|
||||
|
||||
bobTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual(
|
||||
[],
|
||||
"Expected Bob to key query for " + aliUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return bobTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliClaimKeys() {
|
||||
return bobTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return { one_time_keys: result };
|
||||
});
|
||||
}).then(() => {
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
// time, and make sure the claim actually happens rather than ploughing on
|
||||
// confusingly.
|
||||
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
|
||||
expect(r).toEqual(1, "Ali did not claim Bob's keys");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
|
||||
const p1 = aliTestClient.client.downloadKeys([bobUserId]).then(function() {
|
||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
}).then((devices) => {
|
||||
expect(devices.length).toEqual(1);
|
||||
expect(devices[0].deviceId).toEqual("bvcxz");
|
||||
});
|
||||
const p2 = expectAliQueryKeys();
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
return Promise.all([p1, p2]).then(() => {
|
||||
return aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function aliEnablesEncryption() {
|
||||
return aliTestClient.client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(aliTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
function bobEnablesEncryption() {
|
||||
return bobTestClient.client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(bobTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsFirstMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
function bobSendsReplyMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectAliSendMessageRequest() {
|
||||
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectBobSendMessageRequest() {
|
||||
return expectSendMessageRequest(bobTestClient.httpBackend).then(function(content) {
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(client) {
|
||||
return client.sendMessage(
|
||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||
);
|
||||
}
|
||||
|
||||
function expectSendMessageRequest(httpBackend) {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
return httpBackend.flush(path, 1).then(() => prom);
|
||||
}
|
||||
|
||||
function aliRecvMessage() {
|
||||
const message = bobMessages.shift();
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage() {
|
||||
const message = aliMessages.shift();
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function recvMessage(httpBackend, client, sender, message) {
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
logger.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
resolve(event);
|
||||
};
|
||||
client.on("event", onEvent);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
|
||||
return eventPromise.then((event) => {
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
// it may still be being decrypted
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).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) {
|
||||
// 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", "/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);
|
||||
|
||||
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, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from '../../src/crypto/deviceinfo';
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
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: IUploadKeysRequest) {
|
||||
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: IUploadKeysRequest) {
|
||||
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = '';
|
||||
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(DeviceInfo.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<IContent>((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: IUploadKeysRequest) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
});
|
||||
});
|
||||
+130
-126
@@ -1,25 +1,59 @@
|
||||
/*
|
||||
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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
import {
|
||||
ClientEvent,
|
||||
HttpApiEvent,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
RoomEvent,
|
||||
RoomMemberEvent,
|
||||
RoomStateEvent,
|
||||
UserEvent,
|
||||
} from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const client = testClient.client;
|
||||
const 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" });
|
||||
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(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" });
|
||||
[client!, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend?.verifyNoOutstandingExpectation();
|
||||
client?.stopClient();
|
||||
return httpBackend?.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
@@ -92,53 +126,49 @@ describe("MatrixClient events", function() {
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let expectedEvents = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client.on("event", function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
client!.on(ClientEvent.Event, function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(found).toBe(
|
||||
true, "Unexpected 'event' emitted: " + event.getType(),
|
||||
);
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client).then(() => {
|
||||
return utils.syncPromise(client);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(
|
||||
0, "Failed to see all events from /sync calls",
|
||||
);
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
client.on("User.presence", function(event, user) {
|
||||
client!.on(UserEvent.Presence, function(event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
@@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0].content.presence,
|
||||
SYNC_DATA.presence.events[0]?.content?.presence,
|
||||
);
|
||||
});
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
httpBackend.flushAllExpected().then(function() {
|
||||
expect(fired).toBe(true, "User.presence didn't fire.");
|
||||
httpBackend!.flushAllExpected().then(function() {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client.on("Room", function(room) {
|
||||
client!.on(ClientEvent.Room, function(room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.timeline", function(event, room) {
|
||||
client!.on(RoomEvent.Timeline, function(event, room) {
|
||||
timelineFireCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
expect(room?.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.name", function(room) {
|
||||
client!.on(RoomEvent.Name, function(room) {
|
||||
roomNameInvokeCount++;
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(roomInvokeCount).toEqual(
|
||||
1, "Room fired wrong number of times.",
|
||||
);
|
||||
expect(roomNameInvokeCount).toEqual(
|
||||
1, "Room.name fired wrong number of times.",
|
||||
);
|
||||
expect(timelineFireCount).toEqual(
|
||||
3, "Room.timeline fired the wrong number of times",
|
||||
);
|
||||
expect(roomInvokeCount).toEqual(1);
|
||||
expect(roomNameInvokeCount).toEqual(1);
|
||||
expect(timelineFireCount).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
@@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
client!.on(RoomStateEvent.Events, function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
expect(index).not.toEqual(-1);
|
||||
if (index >= 0) {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
client.on("RoomState.members", function(event, state, member) {
|
||||
client!.on(RoomStateEvent.Members, function(event, state, member) {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
client.on("RoomState.newMember", function(event, state, member) {
|
||||
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toBeFalsy();
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(membersInvokeCount).toEqual(
|
||||
1, "RoomState.members fired wrong number of times",
|
||||
);
|
||||
expect(newMemberInvokeCount).toEqual(
|
||||
1, "RoomState.newMember fired wrong number of times",
|
||||
);
|
||||
expect(eventsInvokeCount).toEqual(
|
||||
2, "RoomState.events fired wrong number of times",
|
||||
);
|
||||
expect(membersInvokeCount).toEqual(1);
|
||||
expect(newMemberInvokeCount).toEqual(1);
|
||||
expect(eventsInvokeCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let typingInvokeCount = 0;
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client.on("RoomMember.name", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Name, function(event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Typing, function(event, member) {
|
||||
typingInvokeCount++;
|
||||
expect(member.typing).toBe(true);
|
||||
});
|
||||
client.on("RoomMember.powerLevel", function(event, member) {
|
||||
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
|
||||
powerLevelInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.membership", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Membership, function(event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(typingInvokeCount).toEqual(
|
||||
1, "RoomMember.typing fired wrong number of times",
|
||||
);
|
||||
expect(powerLevelInvokeCount).toEqual(
|
||||
0, "RoomMember.powerLevel fired wrong number of times",
|
||||
);
|
||||
expect(nameInvokeCount).toEqual(
|
||||
0, "RoomMember.name fired wrong number of times",
|
||||
);
|
||||
expect(membershipInvokeCount).toEqual(
|
||||
1, "RoomMember.membership fired wrong number of times",
|
||||
);
|
||||
expect(typingInvokeCount).toEqual(1);
|
||||
expect(powerLevelInvokeCount).toEqual(0);
|
||||
expect(nameInvokeCount).toEqual(0);
|
||||
expect(membershipInvokeCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,812 +0,0 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventTimeline } 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";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
|
||||
const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
});
|
||||
|
||||
const INITIAL_SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob",
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const EVENTS = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "we",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "could",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "be",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "heroes",
|
||||
}),
|
||||
];
|
||||
|
||||
const THREAD_ROOT = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
msg: "thread root",
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
function startClient(httpBackend, client) {
|
||||
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);
|
||||
|
||||
client.startClient();
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
const prom = new Promise((resolve) => {
|
||||
client.on("sync", function(state) {
|
||||
logger.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
prom,
|
||||
]);
|
||||
}
|
||||
|
||||
describe("getEventTimeline support", function() {
|
||||
let httpBackend;
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
const testClient = new TestClient(userId, "DEVICE", accessToken);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function() {
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function() {
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
return startClient(httpBackend, client).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[1],
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[1]);
|
||||
|
||||
httpBackend.when("GET", "/messages").respond(200, {
|
||||
chunk: [EVENTS[0]],
|
||||
start: "pagin_start",
|
||||
end: "pagin_end",
|
||||
});
|
||||
httpBackend.flush("/messages", 1);
|
||||
return client.scrollback(room);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[0]);
|
||||
expect(room.timeline[1].event).toEqual(EVENTS[1]);
|
||||
expect(room.oldState.paginationToken).toEqual("pagin_end");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
return startClient(httpBackend, client);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
Thread.setServerSideSupport(false);
|
||||
});
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
||||
.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.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
|
||||
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 return existing timeline for known events", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync"),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].sender.name).toEqual(userName);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("f_1_1");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
});
|
||||
});
|
||||
|
||||
it("should update timelines where they overlap a previous /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[3],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
const prom = new Promise((resolve, reject) => {
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).then(resolve, reject);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
prom,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should join timelines where they overlap a previous /context", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token2",
|
||||
events_before: [],
|
||||
event: EVENTS[2],
|
||||
events_after: [],
|
||||
end: "end_token2",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[3].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token3",
|
||||
events_before: [],
|
||||
event: EVENTS[3],
|
||||
events_after: [],
|
||||
end: "end_token3",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[1].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token4",
|
||||
events_before: [EVENTS[0]],
|
||||
event: EVENTS[1],
|
||||
events_after: [EVENTS[2], EVENTS[3]],
|
||||
end: "end_token4",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
let tl0;
|
||||
let tl3;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl0 = tl;
|
||||
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl3 = tl;
|
||||
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
|
||||
}).then(function(tl) {
|
||||
// we expect it to get merged in with event 2
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(tl0);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(tl3);
|
||||
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token3");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should fail gracefully if there is no event field", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, "event1",
|
||||
).then(function(tl) {
|
||||
// could do with a fail()
|
||||
expect(true).toBeFalsy();
|
||||
}, function(e) {
|
||||
expect(String(e)).toMatch(/'event'/);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||
Thread.setServerSideSupport(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];
|
||||
|
||||
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],
|
||||
next_batch: "next_batch_token0",
|
||||
prev_batch: "prev_batch_token0",
|
||||
};
|
||||
});
|
||||
|
||||
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));
|
||||
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id));
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline", function() {
|
||||
it("should allow you to paginate backwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual("30");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "start_token1",
|
||||
};
|
||||
});
|
||||
|
||||
let tl;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, { backwards: true });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token1");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token0");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should allow you to paginate forwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual("20");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "end_token1",
|
||||
};
|
||||
});
|
||||
|
||||
let tl;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
tl, { backwards: false, limit: 20 });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token1");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event timeline for sent events", function() {
|
||||
const TXN_ID = "txn1";
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body",
|
||||
});
|
||||
event.unsigned = { transaction_id: TXN_ID };
|
||||
|
||||
beforeEach(function() {
|
||||
// set up handlers for both the message send, and the
|
||||
// /sync
|
||||
httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
|
||||
.respond(200, {
|
||||
event_id: event.event_id,
|
||||
});
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should work when /send returns before /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
return Promise.all([
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
// 2 because the initial sync contained an event
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
|
||||
// now let the sync complete, and check it again
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
}),
|
||||
|
||||
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should work when /send returns after /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
return Promise.all([
|
||||
// initiate the send, and set up checks to be done when it completes
|
||||
// - but note that it won't complete until after the /sync does, below.
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
logger.log("sendTextMessage completed");
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
logger.log("getEventTimeline completed (2)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
}),
|
||||
|
||||
Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
logger.log("getEventTimeline completed (1)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
|
||||
// now let the send complete.
|
||||
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
const redaction = utils.mkEvent({
|
||||
type: "m.room.redaction",
|
||||
room_id: roomId,
|
||||
sender: otherUserId,
|
||||
content: {},
|
||||
});
|
||||
redaction.redacts = event.event_id;
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
redaction,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[1].isRedacted()).toBe(true);
|
||||
|
||||
const sync2 = {
|
||||
next_batch: "batch2",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
sync2.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, sync2);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+725
-183
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,11 @@ import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { IStore } from "../../src/store";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let httpBackend = new HttpBackend();
|
||||
const userId = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
@@ -65,9 +65,10 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
describe("without opts.store", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: undefined,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@@ -99,7 +100,7 @@ describe("MatrixClient opts", function() {
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1, "Recv unexpected event type: " + event.getType(),
|
||||
-1,
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
@@ -118,16 +119,17 @@ describe("MatrixClient opts", function() {
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes,
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without opts.scheduler", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
store: new MemoryStore(),
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
@@ -135,13 +137,17 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
|
||||
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
error: "Ruh roh",
|
||||
}));
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
|
||||
expect(false).toBe(true);
|
||||
}, function(err) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
done();
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2022 Dominik Henneke
|
||||
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient relations", () => {
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
it("should read related events with the default options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type and event type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with custom options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null, {
|
||||
dir: Direction.Forward,
|
||||
from: 'FROM',
|
||||
limit: 10,
|
||||
to: 'TO',
|
||||
});
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||
const response = client!.fetchRelations(roomId, '$event-0', null, null);
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,35 @@
|
||||
import { EventStatus, RoomEvent } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
/*
|
||||
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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client: TestClient = null;
|
||||
let httpBackend: TestClient["httpBackend"] = null;
|
||||
let scheduler;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let room: Room;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
let room: Room | undefined;
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = new MatrixScheduler();
|
||||
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
@@ -21,15 +37,21 @@ describe("MatrixClient retrying", function() {
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
room = new Room(roomId, client, userId);
|
||||
client.store.storeRoom(room);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
const room = new Room(roomId, client, userId);
|
||||
client!.store.storeRoom(room);
|
||||
|
||||
return [client, httpBackend, room];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
[client, httpBackend, room] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function() {
|
||||
@@ -50,7 +72,7 @@ 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, {
|
||||
const p1 = client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m1",
|
||||
}).then(function() {
|
||||
@@ -63,13 +85,13 @@ describe("MatrixClient retrying", function() {
|
||||
// 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, {
|
||||
client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
const tl = room.getLiveTimeline().getEvents();
|
||||
const tl = room!.getLiveTimeline().getEvents();
|
||||
expect(tl.length).toEqual(2);
|
||||
const ev1 = tl[0];
|
||||
const ev2 = tl[1];
|
||||
@@ -78,24 +100,24 @@ 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() {
|
||||
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client.cancelPendingEvent(ev2);
|
||||
client!.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() {
|
||||
client.cancelPendingEvent(ev1);
|
||||
client!.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
@@ -105,7 +127,7 @@ describe("MatrixClient retrying", function() {
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// cancel the first message
|
||||
client.cancelPendingEvent(ev1);
|
||||
client!.cancelPendingEvent(ev1);
|
||||
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(0);
|
||||
});
|
||||
@@ -113,7 +135,7 @@ describe("MatrixClient retrying", function() {
|
||||
return Promise.all([
|
||||
p1,
|
||||
p3,
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventStatus } from "../../src/models/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
});
|
||||
let NEXT_SYNC_DATA;
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob",
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function setNextSyncData(events) {
|
||||
events = events || [];
|
||||
NEXT_SYNC_DATA = {
|
||||
next_batch: "n",
|
||||
presence: { events: [] },
|
||||
rooms: {
|
||||
invite: {},
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: { events: [] },
|
||||
state: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
},
|
||||
},
|
||||
leave: {},
|
||||
},
|
||||
};
|
||||
events.forEach(function(e) {
|
||||
if (e.room_id !== roomId) {
|
||||
throw new Error("setNextSyncData only works with one room id");
|
||||
}
|
||||
if (e.state_key) {
|
||||
if (e.__prev_event === undefined) {
|
||||
throw new Error(
|
||||
"setNextSyncData needs the prev state set to '__prev_event' " +
|
||||
"for " + e.type,
|
||||
);
|
||||
}
|
||||
if (e.__prev_event !== null) {
|
||||
// push the previous state for this event type
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
|
||||
}
|
||||
// push the current
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
|
||||
} else {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ 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);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
|
||||
await httpBackend.flush("/versions");
|
||||
await httpBackend.flush("/pushrules");
|
||||
await httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("local echo events", function() {
|
||||
it("should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member.userId).toEqual(userId);
|
||||
expect(member.name).toEqual(userName);
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"AFTER the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
promise.then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginated events", function() {
|
||||
let sbEvents;
|
||||
const sbEndTok = "pagin_end";
|
||||
|
||||
beforeEach(function() {
|
||||
sbEvents = [];
|
||||
httpBackend.when("GET", "/messages").respond(200, function() {
|
||||
return {
|
||||
chunk: sbEvents,
|
||||
start: "pagin_start",
|
||||
end: sbEndTok,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
// We're aiming for an eventual timeline of:
|
||||
//
|
||||
// 'Old Alice' joined the room
|
||||
// <Old Alice> I'm old alice
|
||||
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
|
||||
// <Alice> I'm alice
|
||||
// ------^ /messages results above this point, /sync result below
|
||||
// <Bob> hello
|
||||
|
||||
// make an m.room.member event for alice's join
|
||||
const joinMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
||||
url: null,
|
||||
});
|
||||
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
// change
|
||||
const oldMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: userName,
|
||||
url: "mxc://some/url",
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: null,
|
||||
membership: "join",
|
||||
};
|
||||
|
||||
// set the list of events to return on scrollback (/messages)
|
||||
// N.B. synapse returns /messages in reverse chronological order
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm alice",
|
||||
}),
|
||||
oldMshipEvent,
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm old alice",
|
||||
}),
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||
const oldMsg = room.timeline[1];
|
||||
expect(oldMsg.sender.name).toEqual("Old Alice");
|
||||
const newMsg = room.timeline[3];
|
||||
expect(newMsg.sender.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function(done) {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am new",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am old",
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function(done) {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am new",
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client.scrollback(room, 1).then(function() {
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1).then(function() {
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
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 }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
let index = 0;
|
||||
client.on("Room.timeline", function(event, rm, toStart) {
|
||||
expect(toStart).toBe(false);
|
||||
expect(rm).toEqual(room);
|
||||
expect(event.event).toEqual(eventData[index]);
|
||||
index += 1;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(index).toEqual(2);
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[2].event).toEqual(
|
||||
eventData[1],
|
||||
);
|
||||
expect(room.timeline[1].event).toEqual(
|
||||
eventData[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
const preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
const postNameEvent = room.timeline[room.timeline.length - 1];
|
||||
expect(preNameEvent.sender.name).toEqual(userName);
|
||||
expect(postNameEvent.sender.name).toEqual("New Name");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right room.name", function() {
|
||||
const secondRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 2",
|
||||
},
|
||||
});
|
||||
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
|
||||
setNextSyncData([secondRoomNameEvent]);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
let nameEmitCount = 0;
|
||||
client.on("Room.name", function(rm) {
|
||||
nameEmitCount += 1;
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(nameEmitCount).toEqual(1);
|
||||
expect(room.name).toEqual("Room 2");
|
||||
// do another round
|
||||
const thirdRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 3",
|
||||
},
|
||||
});
|
||||
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
|
||||
setNextSyncData([thirdRoomNameEvent]);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(nameEmitCount).toEqual(2);
|
||||
expect(room.name).toEqual("Room 3");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right room members", function() {
|
||||
const userC = "@cee:bar";
|
||||
const userD = "@dee:bar";
|
||||
const eventData = [
|
||||
utils.mkMembership({
|
||||
user: userC, room: roomId, mship: "join", name: "C",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, room: roomId, mship: "invite", skey: userD,
|
||||
}),
|
||||
];
|
||||
eventData[0].__prev_event = null;
|
||||
eventData[1].__prev_event = null;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC).name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC).membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(userD).name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD).membership).toEqual(
|
||||
"invite",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("gappy sync", function() {
|
||||
it("should copy the last known state to the new timeline", function() {
|
||||
const eventData = [
|
||||
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(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||
expect(room.currentState.getMembers().length).toEqual(2);
|
||||
expect(room.currentState.getMember(userId).name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId).membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId).membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit a 'Room.timelineReset' event", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
let emitCount = 0;
|
||||
client.on("Room.timelineReset", function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,884 @@
|
||||
/*
|
||||
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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventStatus } from "../../src/models/event";
|
||||
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
});
|
||||
let NEXT_SYNC_DATA;
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob",
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function setNextSyncData(events: Partial<IEvent>[] = []) {
|
||||
NEXT_SYNC_DATA = {
|
||||
next_batch: "n",
|
||||
presence: { events: [] },
|
||||
rooms: {
|
||||
invite: {},
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: { events: [] },
|
||||
state: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
},
|
||||
},
|
||||
leave: {},
|
||||
},
|
||||
};
|
||||
events.forEach(function(e) {
|
||||
if (e.room_id !== roomId) {
|
||||
throw new Error("setNextSyncData only works with one room id");
|
||||
}
|
||||
if (e.state_key) {
|
||||
// push the current
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
|
||||
} else {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const 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);
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client!.startClient();
|
||||
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(async function() {
|
||||
[client!, httpBackend] = setupTestClient();
|
||||
await httpBackend.flush("/versions");
|
||||
await httpBackend.flush("/pushrules");
|
||||
await httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
describe("local echo events", function() {
|
||||
it("should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender", function(done) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
msg: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"AFTER the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
msg: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
promise.then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginated events", function() {
|
||||
let sbEvents;
|
||||
const sbEndTok = "pagin_end";
|
||||
|
||||
beforeEach(function() {
|
||||
sbEvents = [];
|
||||
httpBackend!.when("GET", "/messages").respond(200, function() {
|
||||
return {
|
||||
chunk: sbEvents,
|
||||
start: "pagin_start",
|
||||
end: sbEndTok,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
// We're aiming for an eventual timeline of:
|
||||
//
|
||||
// 'Old Alice' joined the room
|
||||
// <Old Alice> I'm old alice
|
||||
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
|
||||
// <Alice> I'm alice
|
||||
// ------^ /messages results above this point, /sync result below
|
||||
// <Bob> hello
|
||||
|
||||
// make an m.room.member event for alice's join
|
||||
const joinMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
// change
|
||||
const oldMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: userName,
|
||||
url: "mxc://some/url",
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: undefined,
|
||||
membership: "join",
|
||||
};
|
||||
|
||||
// set the list of events to return on scrollback (/messages)
|
||||
// N.B. synapse returns /messages in reverse chronological order
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm alice",
|
||||
}),
|
||||
oldMshipEvent,
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm old alice",
|
||||
}),
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender?.name).toEqual("Old Alice");
|
||||
const oldMsg = room.timeline[1];
|
||||
expect(oldMsg.sender?.name).toEqual("Old Alice");
|
||||
const newMsg = room.timeline[3];
|
||||
expect(newMsg.sender?.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function(done) {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am new",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am old",
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function(done) {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am new",
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client!.scrollback(room, 1).then(function() {
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1).then(function() {
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
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 }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let index = 0;
|
||||
client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
|
||||
expect(toStart).toBe(false);
|
||||
expect(rm).toEqual(room);
|
||||
expect(event.event).toEqual(eventData[index]);
|
||||
index += 1;
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(index).toEqual(2);
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[2].event).toEqual(
|
||||
eventData[1],
|
||||
);
|
||||
expect(room.timeline[1].event).toEqual(
|
||||
eventData[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
const preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
const postNameEvent = room.timeline[room.timeline.length - 1];
|
||||
expect(preNameEvent.sender?.name).toEqual(userName);
|
||||
expect(postNameEvent.sender?.name).toEqual("New Name");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right room.name", function() {
|
||||
const secondRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 2",
|
||||
},
|
||||
});
|
||||
setNextSyncData([secondRoomNameEvent]);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
let nameEmitCount = 0;
|
||||
client!.on(RoomEvent.Name, function(rm) {
|
||||
nameEmitCount += 1;
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(nameEmitCount).toEqual(1);
|
||||
expect(room.name).toEqual("Room 2");
|
||||
// do another round
|
||||
const thirdRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 3",
|
||||
},
|
||||
});
|
||||
setNextSyncData([thirdRoomNameEvent]);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(nameEmitCount).toEqual(2);
|
||||
expect(room.name).toEqual("Room 3");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right room members", function() {
|
||||
const userC = "@cee:bar";
|
||||
const userD = "@dee:bar";
|
||||
const eventData = [
|
||||
utils.mkMembership({
|
||||
user: userC, room: roomId, mship: "join", name: "C",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, room: roomId, mship: "invite", skey: userD,
|
||||
}),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC)!.name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual(
|
||||
"invite",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("gappy sync", function() {
|
||||
it("should copy the last known state to the new timeline", function() {
|
||||
const eventData = [
|
||||
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(() => {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||
expect(room.currentState.getMembers().length).toEqual(2);
|
||||
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit a `RoomEvent.TimelineReset` event when the sync response is `limited`", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let emitCount = 0;
|
||||
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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<void>((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).check(() => {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
}).respond(500, new MatrixError({
|
||||
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,755 +0,0 @@
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import * as utils from "../test-utils/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", "/versions").respond(200, {});
|
||||
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,170 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Account } from "@matrix-org/olm";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { decodeRecoveryKey } from "../../src/crypto/recoverykey";
|
||||
import { IKeyBackupInfo, IKeyBackupSession } from "../../src/crypto/keybackup";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { IEvent } from "../../src";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
|
||||
|
||||
const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: 'SENDER_CURVE25519',
|
||||
session_id: SESSION_ID,
|
||||
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
|
||||
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
|
||||
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
|
||||
},
|
||||
room_id: '!ROOM:ID',
|
||||
event_id: '$event1',
|
||||
origin_server_ts: 1507753886000,
|
||||
};
|
||||
|
||||
const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
|
||||
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
|
||||
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
|
||||
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
|
||||
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
|
||||
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
|
||||
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
|
||||
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
|
||||
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
|
||||
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
|
||||
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
|
||||
mac: '5lxYBHQU80M',
|
||||
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
|
||||
},
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO: IKeyBackupInfo = {
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: "1",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/**
|
||||
* start an Olm session with a given recipient
|
||||
*/
|
||||
function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClient): Promise<Olm.Session> {
|
||||
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
session.create_outbound(
|
||||
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
|
||||
);
|
||||
return session;
|
||||
});
|
||||
}
|
||||
|
||||
describe("megolm key backups", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running megolm tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
let testOlmAccount: Olm.Account;
|
||||
let aliceTestClient: TestClient;
|
||||
|
||||
const setupTestClient = (): [Account, TestClient] => {
|
||||
const aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount!.create();
|
||||
|
||||
return [testOlmAccount, aliceTestClient];
|
||||
};
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
[testOlmAccount, aliceTestClient] = setupTestClient();
|
||||
await aliceTestClient!.client.initCrypto();
|
||||
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient!.stop();
|
||||
});
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", function() {
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [ENCRYPTED_EVENT],
|
||||
},
|
||||
};
|
||||
|
||||
return aliceTestClient!.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then(() => {
|
||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||
}).then(() => {
|
||||
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient!.expectKeyBackupQuery(
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
200,
|
||||
CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||
}).then(function(): Promise<MatrixEvent> {
|
||||
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}).then((event) => {
|
||||
expect(event.getContent()).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,20 +16,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../src/logger';
|
||||
import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
}
|
||||
|
||||
// also try to set node crypto
|
||||
try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
logger.log('nodejs was compiled without crypto support: some tests will fail');
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
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 DOMException from "domexception";
|
||||
|
||||
global.DOMException = DOMException;
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
}));
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
class JestSlowTestReporter {
|
||||
constructor(globalConfig, options) {
|
||||
this._globalConfig = globalConfig;
|
||||
this._options = options;
|
||||
this._slowTests = [];
|
||||
this._slowTestSuites = [];
|
||||
}
|
||||
|
||||
onRunComplete() {
|
||||
const displayResult = (result, isTestSuite) => {
|
||||
if (!isTestSuite) console.log();
|
||||
|
||||
result.sort((a, b) => b.duration - a.duration);
|
||||
const rootPathRegex = new RegExp(`^${process.cwd()}`);
|
||||
const slowestTests = result.slice(0, this._options.numTests || 10);
|
||||
const slowTestTime = this._slowTestTime(slowestTests);
|
||||
const allTestTime = this._allTestTime(result);
|
||||
const percentTime = (slowTestTime / allTestTime) * 100;
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
const duration = slowestTests[i].duration;
|
||||
const filePath = slowestTests[i].filePath.replace(rootPathRegex, '.');
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
} else {
|
||||
const fullName = slowestTests[i].fullName;
|
||||
console.log(` ${fullName}`);
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
};
|
||||
|
||||
displayResult(this._slowTests);
|
||||
displayResult(this._slowTestSuites, true);
|
||||
}
|
||||
|
||||
onTestResult(test, testResult) {
|
||||
this._slowTestSuites.push({
|
||||
duration: testResult.perfStats.runtime,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
for (let i = 0; i < testResult.testResults.length; i++) {
|
||||
this._slowTests.push({
|
||||
duration: testResult.testResults[i].duration,
|
||||
fullName: testResult.testResults[i].fullName,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_slowTestTime(slowestTests) {
|
||||
let slowTestTime = 0;
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
slowTestTime += slowestTests[i].duration;
|
||||
}
|
||||
return slowTestTime;
|
||||
}
|
||||
|
||||
_allTestTime(result) {
|
||||
let allTestTime = 0;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
allTestTime += result[i].duration;
|
||||
}
|
||||
return allTestTime;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JestSlowTestReporter;
|
||||
@@ -27,6 +27,7 @@ type InfoContentProps = {
|
||||
isLive?: boolean;
|
||||
assetType?: LocationAssetType;
|
||||
description?: string;
|
||||
timestamp?: number;
|
||||
};
|
||||
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
|
||||
timeout: 3600000,
|
||||
@@ -42,21 +43,26 @@ export const makeBeaconInfoEvent = (
|
||||
roomId: string,
|
||||
contentProps: Partial<InfoContentProps> = {},
|
||||
eventId?: string,
|
||||
eventTypeSuffix?: string,
|
||||
): MatrixEvent => {
|
||||
const {
|
||||
timeout, isLive, description, assetType,
|
||||
timeout,
|
||||
isLive,
|
||||
description,
|
||||
assetType,
|
||||
timestamp,
|
||||
} = {
|
||||
...DEFAULT_INFO_CONTENT_PROPS,
|
||||
...contentProps,
|
||||
};
|
||||
const event = new MatrixEvent({
|
||||
type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || Date.now()}`,
|
||||
type: M_BEACON_INFO.name,
|
||||
room_id: roomId,
|
||||
state_key: sender,
|
||||
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
|
||||
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()}`);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
|
||||
|
||||
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { User } from "../../src/models/user";
|
||||
|
||||
/**
|
||||
* Mock client with real event emitter
|
||||
* useful for testing code that listens
|
||||
* to MatrixClient events
|
||||
*/
|
||||
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
|
||||
constructor(mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> = {}) {
|
||||
super();
|
||||
Object.assign(this, mockProperties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* - make a mock client
|
||||
* - cast the type to mocked(MatrixClient)
|
||||
* - spy on MatrixClientPeg.get to return the mock
|
||||
* eg
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const getMockClientWithEventEmitter = (
|
||||
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
|
||||
): MockedObject<MatrixClient> => {
|
||||
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
return mock;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to the current user
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser('@mytestuser:domain'),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
|
||||
credentials: { userId },
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: jest.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to rendering events
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser('@mytestuser:domain'),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsEvents = () => ({
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
doesServerSupportSeparateAddAndBind: jest.fn(),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCapabilities: jest.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
@@ -24,5 +24,5 @@ limitations under the License.
|
||||
* expect(beaconLivenessEmits.length).toBe(1);
|
||||
* ```
|
||||
*/
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
|
||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
+129
-17
@@ -5,9 +5,10 @@ import EventEmitter from "events";
|
||||
import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, MatrixClient } from "../../src";
|
||||
import { IContent, IEvent, IEventRelation, 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
|
||||
@@ -69,16 +70,18 @@ export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
|
||||
|
||||
interface IEventOpts {
|
||||
type: EventType | string;
|
||||
room: string;
|
||||
room?: string;
|
||||
sender?: string;
|
||||
skey?: string;
|
||||
content: IContent;
|
||||
event?: boolean;
|
||||
prev_content?: IContent;
|
||||
user?: string;
|
||||
unsigned?: IUnsigned;
|
||||
redacts?: string;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
|
||||
/**
|
||||
* Create an Event.
|
||||
* @param {Object} opts Values for the event.
|
||||
@@ -88,9 +91,12 @@ interface IEventOpts {
|
||||
* @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): object | MatrixEvent {
|
||||
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));
|
||||
}
|
||||
@@ -99,9 +105,12 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
prev_content: opts.prev_content,
|
||||
unsigned: opts.unsigned || {},
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
redacts: opts.redacts,
|
||||
origin_server_ts: opts.ts ?? 0,
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
@@ -116,15 +125,35 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
|
||||
].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;
|
||||
url?: string;
|
||||
name?: string;
|
||||
ago?: number;
|
||||
presence?: string;
|
||||
event?: boolean;
|
||||
}
|
||||
@@ -134,7 +163,9 @@ interface IPresenceOpts {
|
||||
* @param {Object} opts Values for the presence.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
|
||||
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",
|
||||
@@ -150,7 +181,7 @@ export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
|
||||
}
|
||||
|
||||
interface IMembershipOpts {
|
||||
room: string;
|
||||
room?: string;
|
||||
mship: string;
|
||||
sender?: string;
|
||||
user?: string;
|
||||
@@ -173,7 +204,9 @@ interface IMembershipOpts {
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
|
||||
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,
|
||||
@@ -194,11 +227,25 @@ export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
|
||||
return mkEvent(eventOpts);
|
||||
}
|
||||
|
||||
interface IMessageOpts {
|
||||
room: string;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
export interface IMessageOpts {
|
||||
room?: string;
|
||||
user: string;
|
||||
msg?: string;
|
||||
event?: boolean;
|
||||
relatesTo?: IEventRelation;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,22 +255,76 @@ interface IMessageOpts {
|
||||
* @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): object | MatrixEvent {
|
||||
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: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: opts.msg,
|
||||
},
|
||||
};
|
||||
|
||||
if (opts.relatesTo) {
|
||||
eventOpts.content["m.relates_to"] = opts.relatesTo;
|
||||
}
|
||||
|
||||
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);
|
||||
return mkEvent(eventOpts, client);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,3 +381,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
|
||||
}
|
||||
|
||||
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,134 @@
|
||||
/*
|
||||
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 { RelationType } from "../../src/@types/event";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { mkMessage } from "./test-utils";
|
||||
|
||||
export const makeThreadEvent = ({ rootEventId, replyToEventId, ...props }: any & {
|
||||
rootEventId: string; replyToEventId: string; event?: boolean;
|
||||
}): MatrixEvent => mkMessage({
|
||||
...props,
|
||||
relatesTo: {
|
||||
event_id: rootEventId,
|
||||
rel_type: "m.thread",
|
||||
['m.in_reply_to']: {
|
||||
event_id: replyToEventId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type MakeThreadEventsProps = {
|
||||
roomId: Room["roomId"];
|
||||
// root message user id
|
||||
authorId: string;
|
||||
// user ids of thread replies
|
||||
// cycled through until thread length is fulfilled
|
||||
participantUserIds: string[];
|
||||
// number of messages in the thread, root message included
|
||||
// optional, default 2
|
||||
length?: number;
|
||||
ts?: number;
|
||||
// provide to set current_user_participated accurately
|
||||
currentUserId?: string;
|
||||
};
|
||||
|
||||
export const makeThreadEvents = ({
|
||||
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
|
||||
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
const rootEvent = mkMessage({
|
||||
user: authorId,
|
||||
room: roomId,
|
||||
msg: 'root event message ' + Math.random(),
|
||||
ts,
|
||||
event: true,
|
||||
});
|
||||
|
||||
const rootEventId = rootEvent.getId();
|
||||
const events = [rootEvent];
|
||||
|
||||
for (let i = 1; i < length; i++) {
|
||||
const prevEvent = events[i - 1];
|
||||
const replyToEventId = prevEvent.getId();
|
||||
const user = participantUserIds[i % participantUserIds.length];
|
||||
events.push(makeThreadEvent({
|
||||
user,
|
||||
room: roomId,
|
||||
event: true,
|
||||
msg: `reply ${i} by ${user}`,
|
||||
rootEventId,
|
||||
replyToEventId,
|
||||
// replies are 1ms after each other
|
||||
ts: ts + i,
|
||||
}));
|
||||
}
|
||||
|
||||
rootEvent.setUnsigned({
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
latest_event: events[events.length - 1],
|
||||
count: length,
|
||||
current_user_participated: [...participantUserIds, authorId].includes(currentUserId ?? ""),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { rootEvent, events };
|
||||
};
|
||||
|
||||
type MakeThreadProps = {
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
authorId: string;
|
||||
participantUserIds: string[];
|
||||
length?: number;
|
||||
ts?: number;
|
||||
};
|
||||
|
||||
export const mkThread = ({
|
||||
room,
|
||||
client,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
const { rootEvent, events } = makeThreadEvents({
|
||||
roomId: room.roomId,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length,
|
||||
ts,
|
||||
currentUserId: client.getUserId() ?? "",
|
||||
});
|
||||
expect(rootEvent).toBeTruthy();
|
||||
|
||||
for (const evt of events) {
|
||||
room?.reEmitter.reEmit(evt, [
|
||||
MatrixEventEvent.BeforeRedaction,
|
||||
]);
|
||||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
|
||||
// So that we do not have to mock the thread loading
|
||||
thread.initialEventsFetched = true;
|
||||
thread.addEvents(events, true);
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
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 {
|
||||
ClientEvent,
|
||||
ClientEventHandlerMap,
|
||||
EventType,
|
||||
GroupCall,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
IContent,
|
||||
ISendEventResponse,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call";
|
||||
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
|
||||
import { CallFeed } from "../../src/webrtc/callFeed";
|
||||
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
|
||||
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
|
||||
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
|
||||
|
||||
export const DUMMY_SDP = (
|
||||
"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 const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
|
||||
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
|
||||
|
||||
class MockMediaStreamAudioSourceNode {
|
||||
public connect() {}
|
||||
}
|
||||
|
||||
class MockAnalyser {
|
||||
public getFloatFrequencyData() { return 0.0; }
|
||||
}
|
||||
|
||||
export class MockAudioContext {
|
||||
constructor() {}
|
||||
public createAnalyser() { return new MockAnalyser(); }
|
||||
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
|
||||
public close() {}
|
||||
}
|
||||
|
||||
export class MockRTCPeerConnection {
|
||||
private static instances: MockRTCPeerConnection[] = [];
|
||||
|
||||
private negotiationNeededListener?: () => void;
|
||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||
public needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate?: () => void;
|
||||
public localDescription: RTCSessionDescription;
|
||||
public signalingState: RTCSignalingState = "stable";
|
||||
public transceivers: MockRTCRtpTransceiver[] = [];
|
||||
|
||||
public static triggerAllNegotiations(): void {
|
||||
for (const inst of this.instances) {
|
||||
inst.doNegotiation();
|
||||
}
|
||||
}
|
||||
|
||||
public static hasAnyPendingNegotiations(): boolean {
|
||||
return this.instances.some(i => i.needsNegotiation);
|
||||
}
|
||||
|
||||
public static resetInstances() {
|
||||
this.instances = [];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>(resolve => {
|
||||
this.onReadyToNegotiate = resolve;
|
||||
});
|
||||
|
||||
MockRTCPeerConnection.instances.push(this);
|
||||
}
|
||||
|
||||
public addEventListener(type: string, listener: () => void) {
|
||||
if (type === 'negotiationneeded') {
|
||||
this.negotiationNeededListener = listener;
|
||||
} else if (type == 'icecandidate') {
|
||||
this.iceCandidateListener = listener;
|
||||
} else if (type == 'track') {
|
||||
this.onTrackListener = listener;
|
||||
}
|
||||
}
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
public createOffer() {
|
||||
return Promise.resolve({
|
||||
type: 'offer',
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public createAnswer() {
|
||||
return Promise.resolve({
|
||||
type: 'answer',
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
public setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
public close() { }
|
||||
public getStats() { return []; }
|
||||
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
|
||||
const newSender = new MockRTCRtpSender(track);
|
||||
const newReceiver = new MockRTCRtpReceiver(track);
|
||||
|
||||
const newTransceiver = new MockRTCRtpTransceiver(this);
|
||||
newTransceiver.sender = newSender as unknown as RTCRtpSender;
|
||||
newTransceiver.receiver = newReceiver as unknown as RTCRtpReceiver;
|
||||
|
||||
this.transceivers.push(newTransceiver);
|
||||
|
||||
return newTransceiver;
|
||||
}
|
||||
public addTrack(track: MockMediaStreamTrack): MockRTCRtpSender {
|
||||
return this.addTransceiver(track).sender as unknown as MockRTCRtpSender;
|
||||
}
|
||||
|
||||
public removeTrack() {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
}
|
||||
|
||||
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
|
||||
public getSenders(): MockRTCRtpSender[] {
|
||||
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
|
||||
}
|
||||
|
||||
public doNegotiation() {
|
||||
if (this.needsNegotiation && this.negotiationNeededListener) {
|
||||
this.needsNegotiation = false;
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
|
||||
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
}
|
||||
|
||||
export class MockRTCRtpReceiver {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
}
|
||||
|
||||
export class MockRTCRtpTransceiver {
|
||||
constructor(private peerConn: MockRTCPeerConnection) {}
|
||||
|
||||
public sender?: RTCRtpSender;
|
||||
public receiver?: RTCRtpReceiver;
|
||||
|
||||
public set direction(_: string) {
|
||||
this.peerConn.needsNegotiation = true;
|
||||
}
|
||||
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
|
||||
|
||||
public stop = jest.fn<void, []>();
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
public settings?: MediaTrackSettings;
|
||||
|
||||
public getSettings(): MediaTrackSettings { return this.settings!; }
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
|
||||
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
export class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public getTracks() { return this.tracks; }
|
||||
public getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
public getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
public addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
|
||||
public clone(): MediaStream {
|
||||
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
|
||||
}
|
||||
|
||||
public isCloneOf(stream: MediaStream) {
|
||||
return this.id === stream.id + ".clone";
|
||||
}
|
||||
|
||||
// syntactic sugar for typing
|
||||
public typed(): MediaStream {
|
||||
return this as unknown as MediaStream;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audioinput" | "videoinput" | "audiooutput",
|
||||
) { }
|
||||
|
||||
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
public userMediaStreams: MockMediaStream[] = [];
|
||||
public screensharingStreams: MockMediaStream[] = [];
|
||||
|
||||
public getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks: MockMediaStreamTrack[] = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("usermedia_audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("usermedia_video_track", "video"));
|
||||
|
||||
const stream = new MockMediaStream(USERMEDIA_STREAM_ID, tracks);
|
||||
this.userMediaStreams.push(stream);
|
||||
return stream;
|
||||
}
|
||||
public stopUserMediaStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
|
||||
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
|
||||
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
|
||||
|
||||
const stream = new MockMediaStream(SCREENSHARE_STREAM_ID, tracks);
|
||||
this.screensharingStreams.push(stream);
|
||||
return stream;
|
||||
});
|
||||
public stopScreensharingStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public hasAudioDevice() { return true; }
|
||||
public hasVideoDevice() { return true; }
|
||||
public stopAllStreams() {}
|
||||
|
||||
public typed(): MediaHandler { return this as unknown as MediaHandler; }
|
||||
}
|
||||
|
||||
export class MockMediaDevices {
|
||||
public enumerateDevices = jest.fn<Promise<MediaDeviceInfo[]>, []>().mockResolvedValue([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]);
|
||||
|
||||
public getUserMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_stream").typed()),
|
||||
);
|
||||
|
||||
public getDisplayMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_display_stream").typed()),
|
||||
);
|
||||
|
||||
public typed(): MediaDevices { return this as unknown as MediaDevices; }
|
||||
}
|
||||
|
||||
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
|
||||
type EmittedEventMap = CallEventHandlerEventHandlerMap &
|
||||
CallEventHandlerMap &
|
||||
ClientEventHandlerMap &
|
||||
RoomStateEventHandlerMap &
|
||||
GroupCallEventHandlerMap;
|
||||
|
||||
export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, EmittedEventMap> {
|
||||
public mediaHandler = new MockMediaHandler();
|
||||
|
||||
constructor(public userId: string, public deviceId: string, public sessionId: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public groupCallEventHandler = {
|
||||
groupCalls: new Map<string, GroupCall>(),
|
||||
};
|
||||
|
||||
public callEventHandler = {
|
||||
calls: new Map<string, MatrixCall>(),
|
||||
};
|
||||
|
||||
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
|
||||
roomId: string, eventType: EventType, content: any, statekey: string,
|
||||
]>();
|
||||
public sendToDevice = jest.fn<Promise<{}>, [
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]>();
|
||||
|
||||
public getMediaHandler(): MediaHandler { return this.mediaHandler.typed(); }
|
||||
|
||||
public getUserId(): string { return this.userId; }
|
||||
|
||||
public getDeviceId(): string { return this.deviceId; }
|
||||
public getSessionId(): string { return this.sessionId; }
|
||||
|
||||
public getTurnServers = () => [];
|
||||
public isFallbackICEServerAllowed = () => false;
|
||||
public reEmitter = new ReEmitter(new TypedEventEmitter());
|
||||
public getUseE2eForGroupCall = () => false;
|
||||
public checkTurnServers = () => null;
|
||||
|
||||
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
|
||||
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public supportsExperimentalThreads(): boolean { return true; }
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
|
||||
public typed(): MatrixClient { return this as unknown as MatrixClient; }
|
||||
|
||||
public emitRoomState(event: MatrixEvent, state: RoomState): void {
|
||||
this.emit(
|
||||
RoomStateEvent.Events,
|
||||
event,
|
||||
state,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCallFeed {
|
||||
constructor(
|
||||
public userId: string,
|
||||
public deviceId: string | undefined,
|
||||
public stream: MockMediaStream,
|
||||
) {}
|
||||
|
||||
public measureVolumeActivity(val: boolean) {}
|
||||
public dispose() {}
|
||||
|
||||
public typed(): CallFeed {
|
||||
return this as unknown as CallFeed;
|
||||
}
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
global.navigator = {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
}): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
@@ -21,34 +21,37 @@ describe("NamespacedValue", () => {
|
||||
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);
|
||||
const ns = new NamespacedValue("stable");
|
||||
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);
|
||||
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);
|
||||
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");
|
||||
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -58,21 +61,23 @@ describe("UnstableValue", () => {
|
||||
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");
|
||||
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);
|
||||
new UnstableValue("stable", null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Unstable value must be supplied");
|
||||
expect((<Error>e).message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
@@ -17,19 +17,19 @@ limitations under the License.
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as sdk from "../../src";
|
||||
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();
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
|
||||
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");
|
||||
}, () => {
|
||||
@@ -42,13 +42,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;
|
||||
@@ -57,6 +57,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(),
|
||||
@@ -80,6 +81,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(),
|
||||
@@ -103,6 +105,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(),
|
||||
@@ -126,6 +129,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(),
|
||||
@@ -148,31 +152,31 @@ 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() {
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (empty string)", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "",
|
||||
@@ -199,8 +203,8 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (no property)", function() {
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (no property)", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {},
|
||||
});
|
||||
@@ -225,8 +229,8 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (disallowed scheme)", function() {
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for m.homeserver (disallowed scheme)", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "mxc://example.org",
|
||||
@@ -255,6 +259,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": {
|
||||
@@ -284,6 +289,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": {
|
||||
@@ -313,6 +319,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"],
|
||||
});
|
||||
@@ -344,8 +351,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"],
|
||||
});
|
||||
@@ -376,8 +384,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"],
|
||||
@@ -411,8 +420,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"],
|
||||
@@ -451,8 +461,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"],
|
||||
@@ -491,8 +502,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"],
|
||||
@@ -532,8 +544,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"],
|
||||
@@ -573,14 +586,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, {
|
||||
@@ -615,14 +629,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, {
|
||||
@@ -660,4 +675,76 @@ describe("AutoDiscovery", function() {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT for connection errors", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined!);
|
||||
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);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT for fetch errors", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, new Error("CORS or something"));
|
||||
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);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT for invalid JSON", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "<html>", true);
|
||||
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);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,15 @@ limitations under the License.
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
|
||||
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
import { M_TOPIC } from "../../src/@types/topic";
|
||||
import {
|
||||
makeBeaconContent,
|
||||
makeBeaconInfoContent,
|
||||
makeTopicContent,
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
|
||||
describe('Beacon content helpers', () => {
|
||||
describe('makeBeaconInfoContent()', () => {
|
||||
@@ -36,11 +42,9 @@ describe('Beacon content helpers', () => {
|
||||
'nice beacon_info',
|
||||
LocationAssetType.Pin,
|
||||
)).toEqual({
|
||||
[M_BEACON_INFO.name]: {
|
||||
description: 'nice beacon_info',
|
||||
timeout: 1234,
|
||||
live: true,
|
||||
},
|
||||
description: 'nice beacon_info',
|
||||
timeout: 1234,
|
||||
live: true,
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
[M_ASSET.name]: {
|
||||
type: LocationAssetType.Pin,
|
||||
@@ -124,4 +128,129 @@ describe('Beacon content helpers', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,59 +0,0 @@
|
||||
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)).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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
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 { 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,428 +0,0 @@
|
||||
import '../olm-loader';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { TestClient } from "../TestClient";
|
||||
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 { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { logger } from '../../src/logger';
|
||||
|
||||
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("encrypted events", function() {
|
||||
it("provides encryption information", async function() {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "deviceid",
|
||||
)).client;
|
||||
await client.initCrypto();
|
||||
|
||||
// unencrypted event
|
||||
const event = {
|
||||
getId: () => "$event_id",
|
||||
getSenderKey: () => null,
|
||||
getWireContent: () => {return {};},
|
||||
};
|
||||
|
||||
let encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeFalsy();
|
||||
|
||||
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.getClaimedEd25519Key =
|
||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeFalsy();
|
||||
|
||||
// known sender, megolm key from backup
|
||||
event.getForwardingCurve25519KeyChain = () => [];
|
||||
event.isKeySourceUntrusted = () => true;
|
||||
const device = new DeviceInfo("FLIBBLE");
|
||||
device.keys["curve25519:FLIBBLE"] =
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeFalsy();
|
||||
|
||||
// known sender, trusted megolm key, but bad ed25519key
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeTruthy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeTruthy();
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
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 = undefined;
|
||||
event.senderCurve25519Key = null;
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
let eventPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the second message, so
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(events[1], 1);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Secret storage', function() {
|
||||
it("creates secret storage even if there is no keyInfo", async function() {
|
||||
jest.spyOn(logger, 'log').mockImplementation(() => {});
|
||||
jest.setTimeout(10000);
|
||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||
await client.initCrypto();
|
||||
client.crypto.getSecretStorageKey = async () => null;
|
||||
client.crypto.isCrossSigningReady = async () => false;
|
||||
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
|
||||
client.crypto.baseApis.setAccountData = () => null;
|
||||
client.crypto.baseApis.uploadKeySignatures = () => null;
|
||||
client.crypto.baseApis.http.authedRequest = () => null;
|
||||
const createSecretStorageKey = async () => {
|
||||
return {
|
||||
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||
privateKey: Uint8Array.of(32, 33),
|
||||
};
|
||||
};
|
||||
await client.crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+63
-61
@@ -66,23 +66,23 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
});
|
||||
|
||||
it.each(types)("should throw if the callback returns falsey",
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => false,
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: async () => false as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
});
|
||||
|
||||
it("should throw if the expected key doesn't come back", async () => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => masterKeyPub,
|
||||
getCrossSigningKey: async () => masterKeyPub as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should return a key from its callback", async () => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => testKey,
|
||||
getCrossSigningKey: async () => testKey,
|
||||
});
|
||||
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
@@ -99,7 +99,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
it.each(types)("should request a key from the cache callback (if set)" +
|
||||
" and does not call app if one is found" +
|
||||
" %o",
|
||||
async ({ type, shouldCache }) => {
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
@@ -122,58 +122,58 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
});
|
||||
|
||||
it.each(types)("should store a key with the cache callback (if set)",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
}
|
||||
});
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("does not store a bad key to the cache",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
|
||||
});
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it.each(types)("does not store a value to the cache if it came from the cache",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
|
||||
new Error("Tried to store a value from cache"),
|
||||
);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
|
||||
new Error("Tried to store a value from cache"),
|
||||
);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
});
|
||||
|
||||
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
|
||||
" if one is not found", async ({ type, shouldCache }) => {
|
||||
@@ -220,12 +220,14 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
*/
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
() => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
||||
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;
|
||||
}],
|
||||
@@ -245,14 +247,14 @@ describe.each([
|
||||
const olmDevice = new OlmDevice(store);
|
||||
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
|
||||
createCryptoStoreCacheCallbacks(store, olmDevice);
|
||||
await storeCrossSigningKeyCache("self_signing", testKey);
|
||||
await storeCrossSigningKeyCache!("self_signing", testKey);
|
||||
|
||||
// If we've not saved anything, don't expect anything
|
||||
// Definitely don't accidentally return the wrong key for the type
|
||||
const nokey = await getCrossSigningKeyCache("self", "");
|
||||
const nokey = await getCrossSigningKeyCache!("self", "");
|
||||
expect(nokey).toBeNull();
|
||||
|
||||
const key = await getCrossSigningKeyCache("self_signing", "");
|
||||
expect(new Uint8Array(key)).toEqual(testKey);
|
||||
const key = await getCrossSigningKeyCache!("self_signing", "");
|
||||
expect(new Uint8Array(key!)).toEqual(testKey);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -20,8 +20,10 @@ import { logger } from "../../../src/logger";
|
||||
import * as utils from "../../../src/utils";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { DeviceList } from "../../../src/crypto/DeviceList";
|
||||
import { IDownloadKeyResult, MatrixClient } from "../../../src";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
|
||||
const signedDeviceList = {
|
||||
const signedDeviceList: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test1:sw1v.org": {
|
||||
@@ -45,13 +47,15 @@ const signedDeviceList = {
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {},
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const signedDeviceList2 = {
|
||||
const signedDeviceList2: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test2:sw1v.org": {
|
||||
@@ -75,7 +79,9 @@ const signedDeviceList2 = {
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {},
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -84,7 +90,7 @@ const signedDeviceList2 = {
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
let deviceLists = [];
|
||||
let deviceLists: DeviceList[] = [];
|
||||
|
||||
beforeEach(function() {
|
||||
deviceLists = [];
|
||||
@@ -104,10 +110,10 @@ describe('DeviceList', function() {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
};
|
||||
} as unknown as MatrixClient;
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
} as unknown as OlmDevice;
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
@@ -118,7 +124,7 @@ describe('DeviceList', function() {
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
@@ -128,6 +134,7 @@ describe('DeviceList', function() {
|
||||
return prom1.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,7 +144,7 @@ describe('DeviceList', function() {
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
@@ -154,6 +161,7 @@ describe('DeviceList', function() {
|
||||
dl.saveIfDirty().then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
failures: {},
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
@@ -165,11 +173,12 @@ describe('DeviceList', function() {
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.mockReset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = utils.defer();
|
||||
const queryDefer3 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
dl2.stop();
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
@@ -178,6 +187,7 @@ describe('DeviceList', function() {
|
||||
}).then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,9 +197,9 @@ describe('DeviceList', function() {
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
const queryDefer2 = utils.defer();
|
||||
const queryDefer2 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
@@ -204,6 +214,7 @@ describe('DeviceList', function() {
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
+320
-132
@@ -1,18 +1,39 @@
|
||||
/*
|
||||
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 { MockStorageApi } from "../../../MockStorageApi";
|
||||
import * as testUtils from "../../../test-utils/test-utils";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import { Crypto } from "../../../../src/crypto";
|
||||
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['m.megolm.v1.aes-sha2'];
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
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';
|
||||
|
||||
@@ -28,17 +49,20 @@ describe("MegolmDecryption", function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let megolmDecryption;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockBaseApis;
|
||||
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');
|
||||
mockBaseApis = {};
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto') as MockedObject<Crypto>;
|
||||
mockBaseApis = {
|
||||
claimOneTimeKeys: jest.fn(),
|
||||
sendToDevice: jest.fn(),
|
||||
queueToDevice: jest.fn(),
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
@@ -51,11 +75,15 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
jest.fn().mockResolvedValue(undefined);
|
||||
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() {
|
||||
@@ -82,12 +110,18 @@ describe("MegolmDecryption", function() {
|
||||
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);
|
||||
@@ -115,10 +149,13 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
|
||||
it('can respond to a key request event', function() {
|
||||
const keyRequest = {
|
||||
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(),
|
||||
@@ -131,23 +168,25 @@ describe("MegolmDecryption", function() {
|
||||
expect(hasKeys).toBe(true);
|
||||
|
||||
// set up some pre-conditions for the share call
|
||||
const deviceInfo = {};
|
||||
const deviceInfo = {} as DeviceInfo;
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
'@alice:foo': { 'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
device: new DeviceInfo('alidevice'),
|
||||
} },
|
||||
});
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
res();
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
mockBaseApis.sendToDevice = jest.fn();
|
||||
mockBaseApis.sendToDevice.mockReset();
|
||||
mockBaseApis.queueToDevice.mockReset();
|
||||
|
||||
// do the share
|
||||
megolmDecryption.shareKeysWithDevice(keyRequest);
|
||||
@@ -257,23 +296,26 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
|
||||
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 mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
mockBaseApis.claimOneTimeKeys.mockResolvedValue({
|
||||
failures: {},
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
@@ -288,8 +330,9 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
});
|
||||
mockBaseApis.sendToDevice.mockResolvedValue({});
|
||||
mockBaseApis.queueToDevice.mockResolvedValue(undefined);
|
||||
|
||||
aliceDeviceInfo = {
|
||||
deviceId: 'aliceDevice',
|
||||
@@ -309,18 +352,30 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
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: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
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" }],
|
||||
@@ -329,6 +384,31 @@ describe("MegolmDecryption", function() {
|
||||
};
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -342,7 +422,7 @@ describe("MegolmDecryption", function() {
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.queueToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
@@ -385,7 +465,7 @@ describe("MegolmDecryption", function() {
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
|
||||
);
|
||||
|
||||
mockBaseApis.sendToDevice.mockClear();
|
||||
mockBaseApis.queueToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
@@ -393,7 +473,7 @@ describe("MegolmDecryption", function() {
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
|
||||
expect(mockBaseApis.queueToDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -413,78 +493,54 @@ describe("MegolmDecryption", function() {
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice1 = bobClient1.crypto.olmDevice;
|
||||
const bobDevice2 = bobClient2.crypto.olmDevice;
|
||||
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 [{ userId: "@bob:example.com" }];
|
||||
return [bobMember];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
|
||||
const BOB_DEVICES = {
|
||||
const BOB_DEVICES: Record<string, IDevice> = {
|
||||
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,
|
||||
"ed25519:Dynabook": bobDevice1.deviceEd25519Key!,
|
||||
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key!,
|
||||
},
|
||||
verified: 0,
|
||||
known: false,
|
||||
},
|
||||
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,
|
||||
"ed25519:Dynabook": bobDevice2.deviceEd25519Key!,
|
||||
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key!,
|
||||
},
|
||||
verified: -1,
|
||||
known: false,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
|
||||
// @ts-ignore short-circuiting private method
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
let run = false;
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
run = true;
|
||||
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.sendToDevice = jest.fn().mockResolvedValue({});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
@@ -496,15 +552,132 @@ describe("MegolmDecryption", function() {
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, room);
|
||||
await aliceClient.crypto!.encryptEvent(event, room);
|
||||
|
||||
expect(run).toBe(true);
|
||||
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("does not block unverified devices when sending verification events", 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 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> = {
|
||||
bobdevice: {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
|
||||
},
|
||||
verified: 0,
|
||||
known: true,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
|
||||
// @ts-ignore private
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
await bobDevice.generateOneTimeKeys(1);
|
||||
const oneTimeKeys = await bobDevice.getOneTimeKeys();
|
||||
const signedOneTimeKeys: Record<string, { key: string, signatures: object }> = {};
|
||||
for (const keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
const k = {
|
||||
key: oneTimeKeys.curve25519[keyId],
|
||||
signatures: {},
|
||||
};
|
||||
signedOneTimeKeys["signed_curve25519:" + keyId] = k;
|
||||
await bobClient.crypto!.signObject(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
|
||||
one_time_keys: {
|
||||
'@bob:example.com': {
|
||||
bobdevice: signedOneTimeKeys,
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
|
||||
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.key.verification.start",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {
|
||||
from_device: "alicedevice",
|
||||
method: "m.sas.v1",
|
||||
transaction_id: "transactionid",
|
||||
},
|
||||
});
|
||||
await aliceClient.crypto!.encryptEvent(event, room);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toEqual("m.room.encrypted");
|
||||
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("notifies devices when unable to create olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
@@ -516,8 +689,8 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
const aliceDevice = aliceClient.crypto!.olmDevice;
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -530,63 +703,46 @@ describe("MegolmDecryption", function() {
|
||||
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",
|
||||
},
|
||||
];
|
||||
};
|
||||
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,
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
|
||||
},
|
||||
known: true,
|
||||
verified: 1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
|
||||
// @ts-ignore private
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
aliceClient.claimOneTimeKeys = async () => {
|
||||
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
|
||||
// Bob has no one-time keys
|
||||
return {
|
||||
one_time_keys: {},
|
||||
};
|
||||
};
|
||||
|
||||
const sendPromise = new Promise((resolve, reject) => {
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
one_time_keys: {},
|
||||
failures: {},
|
||||
});
|
||||
|
||||
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
@@ -594,8 +750,24 @@ describe("MegolmDecryption", function() {
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await sendPromise;
|
||||
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() {
|
||||
@@ -609,12 +781,15 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
@@ -626,7 +801,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -640,7 +815,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -653,7 +828,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -666,6 +841,8 @@ describe("MegolmDecryption", function() {
|
||||
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() {
|
||||
@@ -679,15 +856,19 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
aliceClient.crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
|
||||
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();
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
@@ -703,7 +884,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -718,7 +899,7 @@ describe("MegolmDecryption", function() {
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
|
||||
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -735,7 +916,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -749,6 +930,8 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
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() {
|
||||
@@ -762,15 +945,18 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
aliceClient.crypto.downloadKeys = async () => {};
|
||||
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
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -785,7 +971,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -799,5 +985,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
});
|
||||
@@ -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 * as olmlib from "../../../../src/crypto/olmlib";
|
||||
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();
|
||||
@@ -66,13 +67,13 @@ describe("OlmDevice", function() {
|
||||
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
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,
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
@@ -93,16 +94,16 @@ describe("OlmDevice", function() {
|
||||
+ " in the family Proteidae"
|
||||
);
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sessionId,
|
||||
MESSAGE,
|
||||
);
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const bobRecreatedOlmDevice = makeOlmDevice();
|
||||
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
|
||||
|
||||
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
@@ -117,17 +118,17 @@ describe("OlmDevice", function() {
|
||||
+ " the olm is entirely aquatic"
|
||||
);
|
||||
const ciphertext2 = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sessionId,
|
||||
MESSAGE_2,
|
||||
);
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const bobRecreatedAgainOlmDevice = makeOlmDevice();
|
||||
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
|
||||
|
||||
// Note: "decrypted_2" does not have the same structure as "decrypted"
|
||||
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
decrypted.session_id,
|
||||
ciphertext2.type,
|
||||
ciphertext2.body,
|
||||
@@ -148,7 +149,7 @@ describe("OlmDevice", function() {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
};
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage({
|
||||
@@ -205,7 +206,7 @@ describe("OlmDevice", function() {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
};
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
|
||||
const deviceBobA = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
@@ -15,24 +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 * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import * as algorithms from "../../../src/crypto/algorithms";
|
||||
import { WebStorageSessionStore } from "../../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../MockStorageApi";
|
||||
import * as testUtils from "../../test-utils/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 { 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';
|
||||
|
||||
@@ -93,8 +95,8 @@ const AES256_KEY_BACKUP_DATA = {
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
@@ -102,7 +104,7 @@ const CURVE25519_BACKUP_INFO = {
|
||||
|
||||
const AES256_BACKUP_INFO = {
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: 1,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
// FIXME: add iv and mac
|
||||
},
|
||||
@@ -118,30 +120,22 @@ function saveCrossSigningKeys(k) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
function makeTestClient(sessionStore, cryptoStore) {
|
||||
function makeTestClient(cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
|
||||
const store = new StubStore();
|
||||
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
|
||||
});
|
||||
@@ -160,8 +154,6 @@ describe("MegolmBackup", function() {
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockStorage;
|
||||
let sessionStore;
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
@@ -173,9 +165,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
@@ -188,7 +178,6 @@ describe("MegolmBackup", function() {
|
||||
|
||||
describe("backup", function() {
|
||||
let mockBaseApis;
|
||||
let realSetTimeout;
|
||||
|
||||
beforeEach(function() {
|
||||
mockBaseApis = {};
|
||||
@@ -206,14 +195,14 @@ describe("MegolmBackup", function() {
|
||||
// clobber the setTimeout function to run 100x faster.
|
||||
// ideally we would use lolex, but we have no oportunity
|
||||
// to tick the clock between the first try and the retry.
|
||||
realSetTimeout = global.setTimeout;
|
||||
global.setTimeout = function(f, n) {
|
||||
return realSetTimeout(f, n/100);
|
||||
};
|
||||
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() {
|
||||
@@ -225,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',
|
||||
@@ -261,7 +256,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',
|
||||
@@ -293,42 +288,43 @@ describe("MegolmBackup", function() {
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
.then(async () => {
|
||||
await client.enableKeyBackup({
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
++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({});
|
||||
return Promise.resolve({} as T);
|
||||
}
|
||||
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(
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -339,7 +335,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',
|
||||
@@ -353,7 +349,7 @@ describe("MegolmBackup", function() {
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
})
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
@@ -374,43 +370,44 @@ describe("MegolmBackup", function() {
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
.then(async () => {
|
||||
await client.enableKeyBackup({
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: 1,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
|
||||
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
++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({});
|
||||
return Promise.resolve({} as T);
|
||||
}
|
||||
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(
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -421,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',
|
||||
@@ -434,22 +431,15 @@ describe("MegolmBackup", function() {
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
await client.initCrypto();
|
||||
let privateKeys;
|
||||
client.uploadDeviceSigningKeys = async function(e) {return;};
|
||||
client.uploadKeySignatures = async function(e) {return;};
|
||||
client.on("crossSigning.saveCrossSigningKeys", function(e) {
|
||||
privateKeys = e;
|
||||
});
|
||||
client.on("crossSigning.getKey", function(e) {
|
||||
e.done(privateKeys[e.type]);
|
||||
});
|
||||
client.uploadDeviceSigningKeys = async function(e) {return {};};
|
||||
client.uploadKeySignatures = async function(e) {return { failures: {} };};
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let backupInfo;
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
@@ -459,7 +449,7 @@ describe("MegolmBackup", function() {
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
data.auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
(data as Record<string, any>).auth_data, client.getCrossSigningId()!, "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
@@ -480,16 +470,17 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(numCalls).toBe(2);
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
it('retries when a backup fails', async function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
@@ -498,26 +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
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
});
|
||||
|
||||
@@ -531,70 +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: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
await client.initCrypto();
|
||||
await cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
);
|
||||
}
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(2);
|
||||
});
|
||||
txn);
|
||||
});
|
||||
|
||||
await client.enableKeyBackup({
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
++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 T);
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({} as T);
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
);
|
||||
}
|
||||
};
|
||||
return client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
});
|
||||
expect(numCalls).toBe(2);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -602,7 +582,7 @@ describe("MegolmBackup", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = makeTestClient(sessionStore, cryptoStore);
|
||||
client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
@@ -719,4 +699,30 @@ describe("MegolmBackup", function() {
|
||||
)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("flagAllGroupSessionsForBackup", () => {
|
||||
it("should return number of sesions needing backup", async () => {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].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",
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store,
|
||||
scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
cryptoStore,
|
||||
});
|
||||
await client.initCrypto();
|
||||
|
||||
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
|
||||
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,12 +17,16 @@ 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 { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
import { logger } from '../../../src/logger';
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client';
|
||||
import { CryptoEvent } from '../../../src/crypto';
|
||||
import { IDevice } from '../../../src/crypto/deviceinfo';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
|
||||
const PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
@@ -47,9 +51,11 @@ function setHttpResponses(httpBackend, responses) {
|
||||
});
|
||||
}
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string, deviceId: string},
|
||||
options: Partial<ICreateClientOpts> = {},
|
||||
keys = {},
|
||||
) {
|
||||
function getCrossSigningKey(type) {
|
||||
return keys[type];
|
||||
}
|
||||
@@ -58,7 +64,6 @@ async function makeTestClient(userInfo, options, keys) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
if (!options) options = {};
|
||||
options.cryptoCallbacks = Object.assign(
|
||||
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
|
||||
);
|
||||
@@ -86,20 +91,21 @@ describe("Cross Signing", function() {
|
||||
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.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => {};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T>() => ({} as T);
|
||||
// set Alice's cross-signing key
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
@@ -133,9 +139,9 @@ describe("Cross Signing", function() {
|
||||
error.httpStatus == 401;
|
||||
throw error;
|
||||
};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => { };
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
|
||||
const authUploadDeviceSigningKeys = async func => await func({});
|
||||
|
||||
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
|
||||
@@ -146,23 +152,24 @@ describe("Cross Signing", function() {
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_FORBIDDEN") {
|
||||
if ((<MatrixError>e).errcode === "M_FORBIDDEN") {
|
||||
bootstrapDidThrow = true;
|
||||
}
|
||||
}
|
||||
expect(bootstrapDidThrow).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -172,16 +179,20 @@ 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.skip("should get cross-signing keys from sync", async function() {
|
||||
@@ -203,7 +214,7 @@ describe("Cross Signing", function() {
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
// will be called to sign our own device
|
||||
getCrossSigningKey: type => {
|
||||
getCrossSigningKey: async type => {
|
||||
if (type === 'master') {
|
||||
return masterKey;
|
||||
} else {
|
||||
@@ -215,7 +226,7 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.once("crossSigning.keysChanged", async (e) => {
|
||||
alice.once(CryptoEvent.KeysChanged, async (e) => {
|
||||
resolve(e);
|
||||
await alice.checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests: true,
|
||||
@@ -223,16 +234,16 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
const uploadSigsPromise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = jest.fn(async (content) => {
|
||||
const uploadSigsPromise = new Promise<void>((resolve, reject) => {
|
||||
alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => {
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
alice.crypto.olmDevice,
|
||||
alice.crypto!.olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
|
||||
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
@@ -246,16 +257,22 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
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 = [
|
||||
@@ -353,14 +370,15 @@ describe("Cross Signing", function() {
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isTofu()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isVerified()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
@@ -370,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: {
|
||||
@@ -383,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",
|
||||
@@ -394,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"],
|
||||
@@ -406,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
|
||||
@@ -425,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
|
||||
@@ -437,19 +460,20 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it.skip("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys = {};
|
||||
const aliceKeys: Record<string, PkSigning> = {};
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
null,
|
||||
undefined,
|
||||
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 resetCrossSigningKeys(alice);
|
||||
@@ -461,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],
|
||||
@@ -491,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: {
|
||||
@@ -507,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
|
||||
@@ -600,14 +635,15 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
@@ -618,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: {
|
||||
@@ -631,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",
|
||||
@@ -642,8 +678,8 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
const bobDevice = {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -654,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");
|
||||
@@ -669,14 +705,15 @@ describe("Cross Signing", function() {
|
||||
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
@@ -685,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: {
|
||||
@@ -698,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",
|
||||
@@ -709,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"],
|
||||
@@ -721,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
|
||||
@@ -745,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: {
|
||||
@@ -758,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",
|
||||
@@ -769,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");
|
||||
@@ -782,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
|
||||
@@ -794,8 +838,8 @@ 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", {
|
||||
bobDevice.signatures!["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
|
||||
@@ -805,6 +849,7 @@ 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() {
|
||||
@@ -814,40 +859,41 @@ describe("Cross Signing", function() {
|
||||
{ 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 { client: bob } = await makeTestClient(
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
);
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Bob's cross-signing key
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
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
|
||||
@@ -863,8 +909,8 @@ describe("Cross Signing", function() {
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// "forget" that Bob is trusted
|
||||
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures["@alice:example.com"];
|
||||
delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures!["@alice:example.com"];
|
||||
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
|
||||
@@ -873,14 +919,213 @@ 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();
|
||||
});
|
||||
|
||||
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
|
||||
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,
|
||||
});
|
||||
|
||||
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("checkIfOwnDeviceCrossSigned should sanely handle unknown users", async () => {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import { IRecoveryKey } from '../../../src/crypto/api';
|
||||
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning';
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
// needs to be phased out and replaced with bootstrapSecretStorage,
|
||||
// but that is doing too much extra stuff for it to be an easy transition.
|
||||
export async function resetCrossSigningKeys(client, {
|
||||
level,
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
export async function resetCrossSigningKeys(
|
||||
client,
|
||||
{ level }: { level?: CrossSigningLevel} = {},
|
||||
): Promise<void> {
|
||||
const crypto = client.crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
|
||||
@@ -30,14 +32,14 @@ export async function resetCrossSigningKeys(client, {
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey() {
|
||||
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
keyInfo: { pubkey: storagePublicKey, key: undefined! },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { logger } from '../../../src/logger';
|
||||
import { DEHYDRATION_ALGORITHM } from '../../../src/crypto/dehydration';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Dehydration", () => {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running dehydration unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should rehydrate a dehydrated device", async () => {
|
||||
const key = new Uint8Array([1, 2, 3]);
|
||||
const alice = new TestClient(
|
||||
"@alice:example.com", "Osborne2", undefined, undefined,
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async t => key,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const dehydratedDevice = new Olm.Account();
|
||||
dehydratedDevice.create();
|
||||
|
||||
alice.httpBackend.when("GET", "/dehydrated_device").respond(200, {
|
||||
device_id: "ABCDEFG",
|
||||
device_data: {
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: dehydratedDevice.pickle(new Uint8Array(key)),
|
||||
},
|
||||
});
|
||||
alice.httpBackend.when("POST", "/dehydrated_device/claim").respond(200, {
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect((await Promise.all([
|
||||
alice.client.rehydrateDevice(),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0])
|
||||
.toEqual("ABCDEFG");
|
||||
|
||||
expect(alice.client.getDeviceId()).toEqual("ABCDEFG");
|
||||
});
|
||||
|
||||
it("should dehydrate a device", async () => {
|
||||
const key = new Uint8Array([1, 2, 3]);
|
||||
const alice = new TestClient(
|
||||
"@alice:example.com", "Osborne2", undefined, undefined,
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async t => key,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await alice.client.initCrypto();
|
||||
|
||||
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
|
||||
errcode: "M_NOT_FOUND",
|
||||
});
|
||||
|
||||
let pickledAccount = "";
|
||||
|
||||
alice.httpBackend.when("PUT", "/dehydrated_device")
|
||||
.check((req) => {
|
||||
expect(req.data.device_data).toMatchObject({
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: expect.any(String),
|
||||
});
|
||||
pickledAccount = req.data.device_data.account;
|
||||
})
|
||||
.respond(200, {
|
||||
device_id: "ABCDEFG",
|
||||
});
|
||||
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG")
|
||||
.check((req) => {
|
||||
expect(req.data).toMatchObject({
|
||||
"device_keys": expect.objectContaining({
|
||||
algorithms: expect.any(Array),
|
||||
device_id: "ABCDEFG",
|
||||
user_id: "@alice:example.com",
|
||||
keys: expect.objectContaining({
|
||||
"ed25519:ABCDEFG": expect.any(String),
|
||||
"curve25519:ABCDEFG": expect.any(String),
|
||||
}),
|
||||
signatures: expect.objectContaining({
|
||||
"@alice:example.com": expect.objectContaining({
|
||||
"ed25519:ABCDEFG": expect.any(String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
"one_time_keys": expect.any(Object),
|
||||
"org.matrix.msc2732.fallback_keys": expect.any(Object),
|
||||
});
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
try {
|
||||
const deviceId =
|
||||
(await Promise.all([
|
||||
alice.client.createDehydratedDevice(new Uint8Array(key), {}),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0];
|
||||
|
||||
expect(deviceId).toEqual("ABCDEFG");
|
||||
expect(deviceId).not.toEqual("");
|
||||
|
||||
// try to rehydrate the dehydrated device
|
||||
const rehydrated = new Olm.Account();
|
||||
try {
|
||||
rehydrated.unpickle(new Uint8Array(key), pickledAccount);
|
||||
} finally {
|
||||
rehydrated.free();
|
||||
}
|
||||
} finally {
|
||||
alice.client?.crypto?.dehydrationManager?.stop();
|
||||
alice.client?.crypto?.deviceList.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,85 +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 { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
requestBody: { session_id: "A", room_id: "A" },
|
||||
state: RoomKeyRequestState.Sent,
|
||||
},
|
||||
{
|
||||
requestId: "B",
|
||||
requestBody: { session_id: "B", room_id: "B" },
|
||||
state: RoomKeyRequestState.Sent,
|
||||
},
|
||||
{
|
||||
requestId: "C",
|
||||
requestBody: { session_id: "C", room_id: "C" },
|
||||
state: RoomKeyRequestState.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(RoomKeyRequestState.Sent);
|
||||
expect(r).toHaveLength(2);
|
||||
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
|
||||
expect(r).toContainEqual(e);
|
||||
});
|
||||
});
|
||||
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
async () => {
|
||||
const r =
|
||||
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(RoomKeyRequestState.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.
|
||||
@@ -21,18 +21,13 @@ 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 { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
import * as utils from "../../../src/utils";
|
||||
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
|
||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||
|
||||
try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
logger.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
|
||||
async function makeTestClient(userInfo, options) {
|
||||
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
@@ -46,7 +41,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;
|
||||
}
|
||||
@@ -54,7 +49,7 @@ async function makeTestClient(userInfo, options) {
|
||||
// Wrapper around pkSign to return a signed object. pkSign returns the
|
||||
// signature, rather than the signed object.
|
||||
function sign(obj, key, userId) {
|
||||
olmlib.pkSign(obj, key, userId);
|
||||
olmlib.pkSign(obj, key, userId, '');
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -84,7 +79,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
};
|
||||
|
||||
const getKey = jest.fn(e => {
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
||||
return ['abc', key];
|
||||
});
|
||||
@@ -93,33 +88,32 @@ describe("Secrets", function() {
|
||||
{ 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) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
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({
|
||||
@@ -136,6 +130,7 @@ describe("Secrets", function() {
|
||||
expect(await secretStorage.get("foo")).toBe("bar");
|
||||
|
||||
expect(getKey).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should throw if given a key that doesn't exist", async function() {
|
||||
@@ -150,6 +145,7 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
@@ -162,12 +158,13 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should encrypt with default key if keys is null", async function() {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn(e => {
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
expect(Object.keys(e.keys)).toEqual([newKeyId]);
|
||||
return [newKeyId, key];
|
||||
});
|
||||
@@ -183,18 +180,19 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.setAccountData = async function(eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
return {};
|
||||
};
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, { pubkey: undefined, key: undefined },
|
||||
);
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
// which won't happen in the test setup
|
||||
@@ -202,7 +200,8 @@ describe("Secrets", function() {
|
||||
await alice.storeSecret("foo", "bar");
|
||||
|
||||
const accountData = alice.getAccountData('foo');
|
||||
expect(accountData.getContent().encrypted).toBeTruthy();
|
||||
expect(accountData!.getContent().encrypted).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt if no keys given and no default key", async function() {
|
||||
@@ -215,10 +214,11 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should request secrets from other clients", async function() {
|
||||
const [osborne2, vax] = await makeTestClients(
|
||||
const [[osborne2, vax], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@alice:example.com", deviceId: "VAX" },
|
||||
@@ -233,29 +233,29 @@ describe("Secrets", function() {
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
"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,
|
||||
"ed25519:Osborne2": osborne2Device.deviceEd25519Key!,
|
||||
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key!,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -264,15 +264,20 @@ describe("Secrets", function() {
|
||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||
await osborne2Device.markKeysAsPublished();
|
||||
|
||||
await vax.client.crypto.olmDevice.createOutboundSession(
|
||||
osborne2Device.deviceCurve25519Key,
|
||||
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() {
|
||||
@@ -298,7 +303,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];
|
||||
});
|
||||
|
||||
@@ -313,9 +318,9 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
|
||||
bob.setAccountData = async function(eventType, contents) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
@@ -323,23 +328,25 @@ describe("Secrets", function() {
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
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() {
|
||||
@@ -406,10 +413,11 @@ describe("Secrets", function() {
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("adds passphrase checking if it's lacking", async function() {
|
||||
let crossSigningKeys = {
|
||||
let crossSigningKeys: Record<string, Uint8Array> = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
@@ -421,14 +429,15 @@ describe("Secrets", function() {
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
getCrossSigningKey: async t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
getSecretStorageKey: async ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -478,7 +487,9 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
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",
|
||||
@@ -518,15 +529,15 @@ describe("Secrets", function() {
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent())
|
||||
.toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
|
||||
.getContent();
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
|
||||
expect(keyInfo.algorithm)
|
||||
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
@@ -538,9 +549,10 @@ describe("Secrets", function() {
|
||||
expect(keyInfo).toHaveProperty("mac");
|
||||
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
|
||||
.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
it("fixes backup keys in the wrong format", async function() {
|
||||
let crossSigningKeys = {
|
||||
let crossSigningKeys: Record<string, Uint8Array> = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
@@ -552,14 +564,15 @@ describe("Secrets", function() {
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
getCrossSigningKey: async t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
getSecretStorageKey: async ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -618,7 +631,9 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
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",
|
||||
@@ -658,16 +673,17 @@ describe("Secrets", function() {
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -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 { MatrixClient } from "../../../../src/client";
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
|
||||
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({
|
||||
+16
-18
@@ -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 { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { makeTestClients } from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -31,16 +31,11 @@ describe("verification request integration tests with crypto layer", function()
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
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" },
|
||||
@@ -49,26 +44,25 @@ describe("verification request integration tests with crypto layer", function()
|
||||
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)
|
||||
// @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");
|
||||
@@ -76,7 +70,11 @@ describe("verification request integration tests with crypto layer", function()
|
||||
const aliceVerifier = aliceRequest.verifier;
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
// @ts-ignore Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier.endTimer();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
});
|
||||
+150
-70
@@ -15,14 +15,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { makeTestClients } from './util';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { verificationMethods } from "../../../../src/crypto";
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { resetCrossSigningKeys } from "../crypto-utils";
|
||||
import { VerificationBase as Verification, 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";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -36,25 +41,22 @@ describe("SAS verification", function() {
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async 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",
|
||||
@@ -65,19 +67,20 @@ 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", () => {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
let alice: TestClient;
|
||||
let bob: TestClient;
|
||||
let aliceSasEvent: ISasEvent | null;
|
||||
let bobSasEvent: ISasEvent | null;
|
||||
let aliceVerifier: Verification<any, any>;
|
||||
let bobPromise: Promise<VerificationBase<any, any>>;
|
||||
let clearTestClientTimeouts: () => void;
|
||||
|
||||
beforeEach(async () => {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
@@ -87,8 +90,8 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
|
||||
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: {
|
||||
@@ -114,26 +117,26 @@ 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();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
bob.client.crypto.deviceList.storeDevicesForUser(
|
||||
bob.client.crypto!.deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.request", request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier!.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
@@ -149,14 +152,14 @@ describe("SAS verification", function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(request.verifier);
|
||||
resolve(request.verifier!);
|
||||
});
|
||||
});
|
||||
|
||||
aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.getUserId(), bob.deviceId,
|
||||
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
aliceVerifier.on(SasEvent.ShowSas, (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
@@ -178,6 +181,8 @@ describe("SAS verification", function() {
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
it("should verify a key", async () => {
|
||||
@@ -186,9 +191,9 @@ describe("SAS verification", function() {
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
@@ -210,26 +215,26 @@ describe("SAS verification", function() {
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
alice.httpBackend.flush(undefined),
|
||||
bob.httpBackend.flush(undefined),
|
||||
]);
|
||||
|
||||
// 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
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
expect(bobDevice?.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
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
|
||||
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) => {
|
||||
@@ -239,15 +244,15 @@ describe("SAS verification", function() {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()][bob.client.deviceId]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
map[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]
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
@@ -269,18 +274,74 @@ describe("SAS verification", function() {
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
alice.httpBackend.flush(undefined),
|
||||
bob.httpBackend.flush(undefined),
|
||||
]);
|
||||
|
||||
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
|
||||
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 = ['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(undefined),
|
||||
bob.httpBackend.flush(undefined),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hmac-sha256");
|
||||
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
expect(bobDevice?.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
expect(aliceDevice?.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should verify a cross-signing key", async () => {
|
||||
@@ -296,9 +357,11 @@ describe("SAS verification", function() {
|
||||
|
||||
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,
|
||||
crossSigningVerifiedBefore: false,
|
||||
firstUse: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -334,7 +397,7 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
@@ -344,25 +407,21 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
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 => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier!.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(request.verifier);
|
||||
resolve(request.verifier!);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -377,6 +436,10 @@ describe("SAS verification", function() {
|
||||
.not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
describe("verification in DM", function() {
|
||||
@@ -386,9 +449,10 @@ describe("SAS verification", function() {
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
let clearTestClientTimeouts;
|
||||
|
||||
beforeEach(async function() {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
@@ -398,7 +462,7 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
|
||||
alice.client.setDeviceVerified = jest.fn();
|
||||
alice.client.crypto!.setDeviceVerification = jest.fn();
|
||||
alice.client.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
@@ -416,7 +480,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(
|
||||
{
|
||||
@@ -437,7 +501,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) => {
|
||||
@@ -488,6 +552,8 @@ describe("SAS verification", function() {
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
@@ -497,10 +563,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" },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+30
-17
@@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import '../../../olm-loader';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src/matrix';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import "../../../../src/crypto"; // import this to cycle-break
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
|
||||
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
|
||||
jest.useFakeTimers();
|
||||
@@ -32,14 +36,9 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("self-verifications", () => {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("triggers a request for key sharing upon completion", async () => {
|
||||
const userId = "@test:localhost";
|
||||
|
||||
@@ -54,9 +53,21 @@ describe("self-verifications", () => {
|
||||
cacheCallbacks,
|
||||
);
|
||||
crossSigningInfo.keys = {
|
||||
master: { keys: { X: testKeyPub } },
|
||||
self_signing: { keys: { X: testKeyPub } },
|
||||
user_signing: { keys: { X: testKeyPub } },
|
||||
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 = {
|
||||
@@ -79,20 +90,22 @@ describe("self-verifications", () => {
|
||||
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,
|
||||
);
|
||||
|
||||
// @ts-ignore set private property
|
||||
verification.resolve = () => undefined;
|
||||
|
||||
const result = await verification.done();
|
||||
@@ -102,12 +115,12 @@ describe("self-verifications", () => {
|
||||
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,44 +15,47 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { 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) {
|
||||
const clients = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
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,
|
||||
@@ -62,21 +65,25 @@ 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
|
||||
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("Room.timeline", remoteEcho);
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
timeouts.push(timeout as unknown as ReturnType<typeof setTimeout>);
|
||||
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
};
|
||||
|
||||
@@ -96,24 +103,16 @@ 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;
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf);
|
||||
},
|
||||
const destroy = () => {
|
||||
timeouts.forEach((t) => clearTimeout(t));
|
||||
};
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
global.crypto = undefined;
|
||||
return [clients, destroy];
|
||||
}
|
||||
+102
-43
@@ -19,11 +19,17 @@ import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoo
|
||||
import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
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; },
|
||||
@@ -54,16 +60,18 @@ function makeMockClient(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 +80,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;
|
||||
}
|
||||
@@ -113,32 +130,38 @@ function makeRemoteEcho(event) {
|
||||
}));
|
||||
}
|
||||
|
||||
async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
async function distributeEvent(
|
||||
ownRequest: VerificationRequest,
|
||||
theirRequest: VerificationRequest,
|
||||
event: MatrixEvent,
|
||||
): Promise<void> {
|
||||
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();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -157,7 +180,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);
|
||||
@@ -165,8 +188,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);
|
||||
@@ -180,12 +202,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);
|
||||
@@ -201,13 +231,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);
|
||||
@@ -222,22 +261,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);
|
||||
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);
|
||||
@@ -253,11 +304,13 @@ describe("verification request unit tests", 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);
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
new Map(),
|
||||
alice,
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
|
||||
true, true);
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
|
||||
|
||||
expect(aliceRequest.cancelled).toBe(false);
|
||||
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
||||
@@ -269,11 +322,17 @@ describe("verification request unit tests", 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);
|
||||
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);
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map(),
|
||||
bob,
|
||||
);
|
||||
|
||||
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// We have to use EventEmitter here to mock part of the matrix-widget-api
|
||||
// project, which doesn't know about our TypeEventEmitter implementation at all
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
import { MockedObject } from "jest-mock";
|
||||
import {
|
||||
WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
MatrixCapabilities,
|
||||
ITurnServer,
|
||||
IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { ICapabilities } from "../../src/embedded";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = jest.fn();
|
||||
public requestCapability = jest.fn();
|
||||
public requestCapabilities = jest.fn();
|
||||
public requestCapabilityForRoomTimeline = jest.fn();
|
||||
public requestCapabilityToSendEvent = jest.fn();
|
||||
public requestCapabilityToReceiveEvent = jest.fn();
|
||||
public requestCapabilityToSendMessage = jest.fn();
|
||||
public requestCapabilityToReceiveMessage = jest.fn();
|
||||
public requestCapabilityToSendState = jest.fn();
|
||||
public requestCapabilityToReceiveState = jest.fn();
|
||||
public requestCapabilityToSendToDevice = jest.fn();
|
||||
public requestCapabilityToReceiveToDevice = jest.fn();
|
||||
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
|
||||
public sendStateEvent = jest.fn();
|
||||
public sendToDevice = jest.fn();
|
||||
public readStateEvents = jest.fn(() => []);
|
||||
public getTurnServers = jest.fn(() => []);
|
||||
|
||||
public transport = { reply: jest.fn() };
|
||||
}
|
||||
|
||||
describe("RoomWidgetClient", () => {
|
||||
let widgetApi: MockedObject<WidgetApi>;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
|
||||
const baseUrl = "https://example.org";
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
|
||||
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
|
||||
widgetApi.emit("ready");
|
||||
await client.startClient();
|
||||
};
|
||||
|
||||
describe("events", () => {
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
"org.matrix.rageshake_request", { request_id: 123 }, "!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "org.matrix.rageshake_request",
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
content: { request_id: 123 },
|
||||
}).getEffectiveEvent();
|
||||
|
||||
await makeClient({ receiveEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
// It should've also inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.getLiveTimeline().getEvents().map(e => e.getEffectiveEvent())).toEqual([event]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("messages", () => {
|
||||
it("requests permissions for specific message types", async () => {
|
||||
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith(MsgType.Text);
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith(MsgType.Text);
|
||||
});
|
||||
|
||||
it("requests permissions for all message types", async () => {
|
||||
await makeClient({ sendMessage: true, receiveMessage: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith();
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
// No point in testing sending and receiving since it's done exactly the
|
||||
// same way as non-message events
|
||||
});
|
||||
|
||||
describe("state events", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "org.example.foo",
|
||||
event_id: "$sfkjfsksdkfsd",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
state_key: "bar",
|
||||
content: { hello: "world" },
|
||||
}).getEffectiveEvent();
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar");
|
||||
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
|
||||
"org.example.foo", "bar", { hello: "world" }, "!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
// It should've also inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
|
||||
it("backfills", async () => {
|
||||
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
|
||||
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
|
||||
? [event as IRoomEvent]
|
||||
: [],
|
||||
);
|
||||
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = {
|
||||
"@alice:example.org": { "*": { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await client.sendToDevice("org.example.foo", unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
});
|
||||
|
||||
it("sends unencrypted (queueToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const batch: ToDeviceBatch = {
|
||||
eventType: "org.example.foo",
|
||||
batch: [
|
||||
{ userId: "@alice:example.org", deviceId: "*", payload: { hello: "alice!" } },
|
||||
{ userId: "@bob:example.org", deviceId: "bobDesktop", payload: { hello: "bob!" } },
|
||||
],
|
||||
};
|
||||
await client.queueToDevice(batch);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
});
|
||||
|
||||
it("sends encrypted (encryptAndSendToDevices)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const payload = { type: "org.example.foo", hello: "world" };
|
||||
await client.encryptAndSendToDevices(
|
||||
[
|
||||
{ userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") },
|
||||
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
|
||||
"@alice:example.org": { aliceWeb: payload },
|
||||
"@bob:example.org": { bobDesktop: payload },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ encrypted: false, title: "unencrypted" },
|
||||
{ encrypted: true, title: "encrypted" },
|
||||
])("receives $title", async ({ encrypted }) => {
|
||||
await makeClient({ receiveToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const event = {
|
||||
type: "org.example.foo",
|
||||
sender: "@alice:example.org",
|
||||
encrypted,
|
||||
content: { hello: "world" },
|
||||
};
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.ToDeviceEvent, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendToDevice}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual({
|
||||
type: event.type,
|
||||
sender: event.sender,
|
||||
content: event.content,
|
||||
});
|
||||
expect((await emittedEvent).isEncrypted()).toEqual(encrypted);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets TURN servers", async () => {
|
||||
const server1: ITurnServer = {
|
||||
uris: [
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:10.20.30.40:3478?transport=tcp",
|
||||
"turns:10.20.30.40:443?transport=tcp",
|
||||
],
|
||||
username: "1443779631:@user:example.com",
|
||||
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
|
||||
};
|
||||
const server2: ITurnServer = {
|
||||
uris: [
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:10.20.30.40:3478?transport=tcp",
|
||||
"turns:10.20.30.40:443?transport=tcp",
|
||||
],
|
||||
username: "1448999322:@user:example.com",
|
||||
password: "hunter2",
|
||||
};
|
||||
const clientServer1: IClientTurnServer = {
|
||||
urls: server1.uris,
|
||||
username: server1.username,
|
||||
credential: server1.password,
|
||||
};
|
||||
const clientServer2: IClientTurnServer = {
|
||||
urls: server2.uris,
|
||||
username: server2.username,
|
||||
credential: server2.password,
|
||||
};
|
||||
|
||||
let emitServer2: () => void;
|
||||
const getServer2 = new Promise<ITurnServer>(resolve => emitServer2 = () => resolve(server2));
|
||||
widgetApi.getTurnServers.mockImplementation(async function* () {
|
||||
yield server1;
|
||||
yield await getServer2;
|
||||
});
|
||||
|
||||
await makeClient({ turnServers: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC3846TurnServers);
|
||||
|
||||
// The first server should've arrived immediately
|
||||
expect(client.getTurnServers()).toEqual([clientServer1]);
|
||||
|
||||
// Subsequent servers arrive asynchronously and should emit an event
|
||||
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
|
||||
client.once(ClientEvent.TurnServers, resolve),
|
||||
);
|
||||
emitServer2!();
|
||||
expect(await emittedServer).toEqual([clientServer2]);
|
||||
expect(client.getTurnServers()).toEqual([clientServer2]);
|
||||
});
|
||||
});
|
||||
@@ -29,10 +29,10 @@ describe("eventMapperFor", function() {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {} as any, // NOP
|
||||
fetchFn: function() {} as any, // NOP
|
||||
store: {
|
||||
getRoom(roomId: string): Room | null {
|
||||
return rooms.find(r => r.roomId === roomId);
|
||||
return rooms.find(r => r.roomId === roomId) ?? null;
|
||||
},
|
||||
} as IStore,
|
||||
scheduler: {
|
||||
@@ -44,6 +44,10 @@ describe("eventMapperFor", function() {
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
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());
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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', () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("Adds event to timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
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();
|
||||
});
|
||||
|
||||
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
|
||||
const eventTimelineSet2 = new EventTimelineSet(room);
|
||||
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
|
||||
expect(liveTimeline2.getEvents().length).toStrictEqual(0);
|
||||
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it("should not add a threaded reply to the main room timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
const threadedReplyEvent = mkThreadResponse(messageEvent);
|
||||
|
||||
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it("should not add a normal message to the timelineSet representing a thread", () => {
|
||||
const eventTimelineSetForThread = new EventTimelineSet(room, {}, client, thread);
|
||||
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
describe('non-room timeline', () => {
|
||||
it('Adds event to timeline', () => {
|
||||
const nonRoomEventTimelineSet = new EventTimelineSet(
|
||||
// This is what we're specifically testing against, a timeline
|
||||
// without a `room` defined
|
||||
undefined,
|
||||
);
|
||||
const nonRoomEventTimeline = new EventTimeline(nonRoomEventTimelineSet);
|
||||
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
|
||||
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,41 @@
|
||||
import * as utils from "../test-utils/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 { Direction, 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";
|
||||
|
||||
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(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
||||
// otherwise the default member property values (e.g. paginationToken) will be incorrect
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
|
||||
return timeline;
|
||||
};
|
||||
|
||||
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 +46,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 +59,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 +90,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();
|
||||
@@ -86,7 +103,17 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
|
||||
});
|
||||
|
||||
it("setPaginationToken should set token", function() {
|
||||
it("setPaginationToken should set token", function() {
|
||||
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
|
||||
});
|
||||
|
||||
it("should be able to store pagination tokens for mixed room timelines", () => {
|
||||
const timelineSet = new EventTimelineSet(undefined);
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
|
||||
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
||||
@@ -101,8 +128,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);
|
||||
@@ -110,8 +137,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();
|
||||
@@ -133,10 +160,6 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
describe("addEvent", function() {
|
||||
beforeEach(function() {
|
||||
mockRoomStates(timeline);
|
||||
});
|
||||
|
||||
const events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
@@ -149,9 +172,9 @@ describe("EventTimeline", function() {
|
||||
];
|
||||
|
||||
it("should be able to add events to the end", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[0]);
|
||||
@@ -159,9 +182,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]);
|
||||
@@ -169,24 +192,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;
|
||||
@@ -203,50 +224,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() {
|
||||
@@ -262,18 +281,18 @@ 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]]);
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -291,20 +310,29 @@ 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]]);
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
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() {
|
||||
@@ -324,31 +352,31 @@ 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());
|
||||
let ev = timeline.removeEvent(events[0].getId()!);
|
||||
expect(ev).toBe(events[0]);
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
|
||||
ev = timeline.removeEvent(events[1].getId());
|
||||
ev = timeline.removeEvent(events[1].getId()!);
|
||||
expect(ev).toBe(events[1]);
|
||||
expect(timeline.getEvents().length).toEqual(0);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
timeline.removeEvent(events[2].getId());
|
||||
timeline.removeEvent(events[2].getId()!);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
timeline.removeEvent(events[1].getId());
|
||||
timeline.removeEvent(events[1].getId()!);
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
expect(timeline.getBaseIndex()).toEqual(0);
|
||||
});
|
||||
@@ -357,14 +385,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,76 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
id: 'test_encrypted_event',
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', () => {
|
||||
let callCount = 0;
|
||||
|
||||
let prom2;
|
||||
let prom2Fulfilled = false;
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: function() {
|
||||
++callCount;
|
||||
logger.log(`decrypt: ${callCount}`);
|
||||
if (callCount == 1) {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
prom2 = encryptedEvent.attemptDecryption(crypto);
|
||||
prom2.then(() => prom2Fulfilled = true);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
expect(prom2Fulfilled).toBe(
|
||||
false, 'second attemptDecryption resolved too soon');
|
||||
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return encryptedEvent.attemptDecryption(crypto).then(() => {
|
||||
expect(callCount).toEqual(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
|
||||
// make sure the second attemptDecryption resolves
|
||||
return prom2;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature";
|
||||
|
||||
describe("Feature detection", () => {
|
||||
it("checks the matrix version", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.3"],
|
||||
unstable_features: {},
|
||||
});
|
||||
|
||||
expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable);
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
|
||||
});
|
||||
|
||||
it("checks the matrix msc number", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.2"],
|
||||
unstable_features: {
|
||||
"org.matrix.msc3771": true,
|
||||
"org.matrix.msc3773": true,
|
||||
},
|
||||
});
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable);
|
||||
});
|
||||
|
||||
it("requires two MSCs to pass", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.2"],
|
||||
unstable_features: {
|
||||
"org.matrix.msc3771": false,
|
||||
"org.matrix.msc3773": true,
|
||||
},
|
||||
});
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
|
||||
});
|
||||
|
||||
it("requires two MSCs OR matrix versions to pass", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.4"],
|
||||
unstable_features: {
|
||||
"org.matrix.msc3771": false,
|
||||
"org.matrix.msc3773": true,
|
||||
},
|
||||
});
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
MatrixEvent,
|
||||
RelationType,
|
||||
} from "../../src";
|
||||
import { RelationType } from "../../src";
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils/test-utils';
|
||||
|
||||
@@ -14,7 +11,7 @@ describe("Filter Component", function() {
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
@@ -28,7 +25,7 @@ describe("Filter Component", function() {
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
@@ -55,7 +52,7 @@ describe("Filter Component", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||
});
|
||||
@@ -80,7 +77,7 @@ describe("Filter Component", function() {
|
||||
user: '@someone-else:server.org',
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||
});
|
||||
@@ -100,7 +97,7 @@ describe("Filter Component", function() {
|
||||
[RelationType.Reference]: {},
|
||||
},
|
||||
},
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||
});
|
||||
@@ -123,7 +120,7 @@ describe("Filter Component", function() {
|
||||
},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
const eventWithMultipleRelations = mkEvent({
|
||||
"type": "m.room.message",
|
||||
@@ -148,7 +145,7 @@ describe("Filter Component", function() {
|
||||
},
|
||||
"room": 'roomId',
|
||||
"event": true,
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
const noMatchEvent = mkEvent({
|
||||
"type": "m.room.message",
|
||||
@@ -160,7 +157,7 @@ describe("Filter Component", function() {
|
||||
},
|
||||
"room": 'roomId',
|
||||
"event": true,
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootEvent)).toBe(true);
|
||||
expect(filter.check(eventWithMultipleRelations)).toBe(true);
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Filter } from "../../src/filter";
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
const userId = "@sir_arthur_david:humming.tiger";
|
||||
let filter;
|
||||
|
||||
beforeEach(function() {
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
describe("fromJson", function() {
|
||||
it("create a new Filter from the provided values", function() {
|
||||
const definition = {
|
||||
event_fields: ["type", "content"],
|
||||
};
|
||||
const f = Filter.fromJson(userId, filterId, definition);
|
||||
expect(f.getDefinition()).toEqual(definition);
|
||||
expect(f.userId).toEqual(userId);
|
||||
expect(f.filterId).toEqual(filterId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTimelineLimit", function() {
|
||||
it("should set room.timeline.limit of the filter definition", function() {
|
||||
filter.setTimelineLimit(10);
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
timeline: {
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDefinition/getDefinition", function() {
|
||||
it("should set and get the filter body", function() {
|
||||
const definition = {
|
||||
event_format: "client",
|
||||
};
|
||||
filter.setDefinition(definition);
|
||||
expect(filter.getDefinition()).toEqual(definition);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
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 { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
import { Filter, IFilterDefinition } from "../../src/filter";
|
||||
import { mkEvent } from "../test-utils/test-utils";
|
||||
import { EventType } from "../../src";
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
const userId = "@sir_arthur_david:humming.tiger";
|
||||
let filter: Filter;
|
||||
|
||||
beforeEach(function() {
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
describe("fromJson", function() {
|
||||
it("create a new Filter from the provided values", function() {
|
||||
const definition = {
|
||||
event_fields: ["type", "content"],
|
||||
};
|
||||
const f = Filter.fromJson(userId, filterId, definition);
|
||||
expect(f.getDefinition()).toEqual(definition);
|
||||
expect(f.userId).toEqual(userId);
|
||||
expect(f.filterId).toEqual(filterId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTimelineLimit", function() {
|
||||
it("should set room.timeline.limit of the filter definition", function() {
|
||||
filter.setTimelineLimit(10);
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
timeline: {
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDefinition/getDefinition", function() {
|
||||
it("should set and get the filter body", function() {
|
||||
const definition = {
|
||||
event_format: "client" as IFilterDefinition['event_format'],
|
||||
};
|
||||
filter.setDefinition(definition);
|
||||
expect(filter.getDefinition()).toEqual(definition);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUnreadThreadNotifications", function() {
|
||||
it("setUnreadThreadNotifications", function() {
|
||||
filter.setUnreadThreadNotifications(true);
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
timeline: {
|
||||
[UNREAD_THREAD_NOTIFICATIONS.name]: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterRoomTimeline", () => {
|
||||
it("should return input if no roomTimelineFilter and roomFilter", () => {
|
||||
const events = [mkEvent({ type: EventType.Sticker, content: {}, event: true })];
|
||||
expect(new Filter(undefined).filterRoomTimeline(events)).toStrictEqual(events);
|
||||
});
|
||||
|
||||
it("should filter using components when present", () => {
|
||||
const definition: IFilterDefinition = {
|
||||
room: {
|
||||
timeline: {
|
||||
types: [EventType.Sticker],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filter = Filter.fromJson(userId, filterId, definition);
|
||||
const events = [
|
||||
mkEvent({ type: EventType.Sticker, content: {}, event: true }),
|
||||
mkEvent({ type: EventType.RoomMessage, content: {}, event: true }),
|
||||
];
|
||||
expect(filter.filterRoomTimeline(events)).toStrictEqual([events[0]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user