Compare commits
592 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3702ac56f4 | |||
| af4811b327 | |||
| 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 | |||
| 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 | |||
| f41b7706e4 | |||
| de694459be | |||
| 6fc9827b10 | |||
| f52c5eb667 | |||
| c05cb3ad2b | |||
| 586a313c8d | |||
| c605310b87 | |||
| 41cee6f1cc | |||
| 3e1e99f8e5 | |||
| 276849f068 | |||
| 37118991f5 | |||
| 00629e6dc9 | |||
| 02f6a09bcf | |||
| 36a6117ee2 | |||
| aebe26db96 | |||
| 60e175a0e0 | |||
| d950cda05c | |||
| 83c848093f | |||
| fa6f70f708 | |||
| 98d119d6e1 | |||
| aca51fd8a3 | |||
| c78631bdee | |||
| 0d6a93b5f6 | |||
| 40ecfa7932 | |||
| d656b848f8 | |||
| 0981652de4 | |||
| db32420d16 | |||
| d5b82e343a | |||
| 965f4fb13b | |||
| 9e1b126854 | |||
| c527f85fb1 | |||
| 4a294c9dd3 | |||
| 4059b5bfba | |||
| be94f5ea93 | |||
| 5f9369abee | |||
| e7a7ec0673 | |||
| 92cd84fc0c | |||
| 3a120f8fb8 | |||
| d2535b8516 | |||
| 2cda229bc4 | |||
| 45e56f8cc3 | |||
| e95947dc73 | |||
| 448a5c9a77 | |||
| 9589a97952 | |||
| 2566c40e96 | |||
| 099cac0162 | |||
| e4cf5b26ee | |||
| c698317f3f | |||
| e8f682f452 | |||
| 020743141b | |||
| 5f5a9b1a43 | |||
| 3334c01191 | |||
| 0b8de251bf | |||
| 88ce017333 | |||
| 471f174889 | |||
| c0dacb5037 | |||
| 2cc51e0db7 | |||
| 22c5999fed | |||
| b711781f16 | |||
| 8ba2d257ae | |||
| 9e2e144530 | |||
| 38a6949e5d | |||
| e876482e62 | |||
| 544b1c6742 | |||
| 984dd26a13 | |||
| bdb91b3806 | |||
| 9a15094374 | |||
| e980c88901 | |||
| 6ea2885796 | |||
| ca5ac79927 | |||
| f9672cf307 | |||
| e7493fd417 | |||
| f553854730 | |||
| c89bbf4bf5 | |||
| ebcb26f1b3 | |||
| 5b4263bf55 | |||
| df9ffdc408 | |||
| 70449ea003 | |||
| 9192b876d2 | |||
| 04d0d61a0e | |||
| 404f8e130e | |||
| b97b862fb6 | |||
| 5e766978b8 | |||
| 34ef7bc64a | |||
| 18e2052af2 | |||
| aa0d3bd1f5 | |||
| 942a28ddf6 | |||
| 87791cd391 | |||
| 38e54ae7f2 | |||
| acef1d7dd0 | |||
| da615fd512 | |||
| f4f05550ef | |||
| f475251ddd | |||
| 83f61c96f3 | |||
| 85a6a552b5 | |||
| 9702e8a5fa | |||
| d82c041b99 | |||
| 8d9cd0fcb3 | |||
| 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 |
+28
-8
@@ -1,24 +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",
|
||||
@@ -35,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: [
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
* @matrix-org/element-web
|
||||
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -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@b12b127cf24433d14b4f93cee62f5465076ba82a # v2.24.1
|
||||
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
|
||||
@@ -36,5 +36,6 @@ jobs:
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
|
||||
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.version }}
|
||||
|
||||
@@ -24,10 +24,7 @@ jobs:
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
@@ -36,7 +33,10 @@ jobs:
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
cp -a "$RUNNER_TEMP/$VERSION" .
|
||||
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
|
||||
|
||||
@@ -5,6 +5,11 @@ on:
|
||||
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
|
||||
@@ -22,7 +27,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
||||
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' }}
|
||||
@@ -33,8 +38,8 @@ jobs:
|
||||
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:
|
||||
|
||||
@@ -8,8 +8,36 @@ 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 }}
|
||||
|
||||
@@ -23,6 +23,16 @@ jobs:
|
||||
- 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
|
||||
@@ -54,38 +64,11 @@ jobs:
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
|
||||
tsc-strict:
|
||||
name: Typescript Strict Error Checker
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
checks: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get diff lines
|
||||
id: diff
|
||||
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
include: '["\\.tsx?$"]'
|
||||
|
||||
- name: Detecting files changed
|
||||
id: files
|
||||
uses: futuratrepadeira/changed-files@v4.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: '^.*\.tsx?$'
|
||||
|
||||
- uses: t3chguy/typescript-check-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-check: false
|
||||
check-fail-mode: added
|
||||
output-behaviour: annotate
|
||||
ts-extra-args: '--strict'
|
||||
files-changed: ${{ steps.files.outputs.files_updated }}
|
||||
files-added: ${{ steps.files.outputs.files_created }}
|
||||
files-deleted: ${{ steps.files.outputs.files_deleted }}
|
||||
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
@@ -8,25 +8,40 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
jest:
|
||||
name: 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: Yarn cache
|
||||
- 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
|
||||
run: "yarn coverage --ci --reporters github-actions"
|
||||
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
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
/_docs
|
||||
.DS_Store
|
||||
|
||||
node_modules
|
||||
/.npmrc
|
||||
|
||||
+151
@@ -1,3 +1,154 @@
|
||||
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)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
@@ -33,10 +33,8 @@ In Node.js
|
||||
----------
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -297,13 +295,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.
|
||||
|
||||
-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
|
||||
}
|
||||
}
|
||||
+29
-18
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "19.6.0",
|
||||
"version": "21.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -16,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",
|
||||
@@ -54,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": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"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": {
|
||||
@@ -78,12 +79,13 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
@@ -92,20 +94,25 @@
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.23.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.6.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^0.7.0",
|
||||
"eslint-plugin-unicorn": "^44.0.2",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^2.1.2",
|
||||
"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": {
|
||||
@@ -113,6 +120,9 @@
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/spec/setupTests.ts"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
@@ -120,11 +130,12 @@
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "jest-sonar-reporter"
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"jestSonar": {
|
||||
"reportPath": "coverage",
|
||||
"sonar56x": true
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+1
-13
@@ -135,7 +135,6 @@ yarn install --ignore-scripts --pure-lockfile
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
@@ -150,18 +149,7 @@ 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', or the staging branch
|
||||
# we don't create a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; 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"
|
||||
|
||||
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();
|
||||
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/test-report.xml
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
|
||||
+15
-16
@@ -30,7 +30,6 @@ import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
|
||||
import { IKeyBackupSession } from "../src/crypto/keybackup";
|
||||
import { IHttpOpts } from "../src/http-api";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
||||
|
||||
/**
|
||||
@@ -39,8 +38,8 @@ import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
||||
export class TestClient {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys: IDeviceKeys;
|
||||
public oneTimeKeys: Record<string, IOneTimeKey>;
|
||||
public deviceKeys?: IDeviceKeys | null;
|
||||
public oneTimeKeys?: Record<string, IOneTimeKey>;
|
||||
|
||||
constructor(
|
||||
public readonly userId?: string,
|
||||
@@ -56,11 +55,11 @@ export class TestClient {
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
const fullOptions: ICreateClientOpts = {
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
request: this.httpBackend.requestFn as IHttpOpts["request"],
|
||||
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
|
||||
...options,
|
||||
};
|
||||
if (!fullOptions.cryptoStore) {
|
||||
@@ -124,7 +123,7 @@ export class TestClient {
|
||||
|
||||
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);
|
||||
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
|
||||
|
||||
this.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
@@ -139,9 +138,9 @@ export class TestClient {
|
||||
* @returns {Promise} for the one-time keys
|
||||
*/
|
||||
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
if (Object.keys(this.oneTimeKeys!).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
return Promise.resolve(this.oneTimeKeys!);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
@@ -149,7 +148,7 @@ export class TestClient {
|
||||
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,
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
} };
|
||||
});
|
||||
|
||||
@@ -159,17 +158,17 @@ export class TestClient {
|
||||
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);
|
||||
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,
|
||||
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;
|
||||
return this.oneTimeKeys!;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,7 +183,7 @@ export class TestClient {
|
||||
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
|
||||
200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual([]);
|
||||
expect(content.device_keys![userId]).toEqual([]);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
@@ -207,7 +206,7 @@ export class TestClient {
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +216,7 @@ export class TestClient {
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,6 +237,6 @@ export class TestClient {
|
||||
}
|
||||
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
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", 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 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([
|
||||
@@ -290,8 +290,9 @@ describe("DeviceList management:", function() {
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -326,8 +327,9 @@ describe("DeviceList management:", function() {
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -362,8 +364,9 @@ describe("DeviceList management:", function() {
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -378,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();
|
||||
|
||||
// @ts-ignore accessing private property
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const bobStat = data!.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
@@ -31,8 +31,9 @@ import '../olm-loader';
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { 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";
|
||||
@@ -58,7 +59,7 @@ async function bobUploadsDeviceKeys(): Promise<void> {
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,12 +72,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
|
||||
expect(uploader.deviceKeys).toBeTruthy();
|
||||
|
||||
const uploaderKeys = {};
|
||||
uploaderKeys[uploader.deviceId] = uploader.deviceKeys;
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
|
||||
querier.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(_path, content) {
|
||||
expect(content.device_keys[uploader.userId]).toEqual([]);
|
||||
.respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result = {};
|
||||
result[uploader.userId] = uploaderKeys;
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
@@ -93,12 +94,12 @@ async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(_path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
).respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
let keyId = '';
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
@@ -132,13 +133,13 @@ async function aliDownloadsKeys(): Promise<void> {
|
||||
// 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();
|
||||
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);
|
||||
const devices = data!.devices[bobUserId]!;
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,7 +223,7 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId];
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
@@ -237,7 +238,7 @@ function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
|
||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise((resolve) => {
|
||||
const prom = new Promise<IContent>((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
@@ -252,14 +253,14 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
|
||||
}
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift();
|
||||
const message = bobMessages.shift()!;
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift();
|
||||
const message = aliMessages.shift()!;
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
@@ -392,7 +393,7 @@ describe("MatrixClient crypto", () => {
|
||||
it("Ali gets keys with an invalid signature", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
// tamper bob's keys
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys;
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys!;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
await Promise.all([
|
||||
@@ -478,7 +479,7 @@ describe("MatrixClient crypto", () => {
|
||||
await bobTestClient.start();
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
});
|
||||
|
||||
it("Ali sends a message", async () => {
|
||||
@@ -494,6 +495,7 @@ describe("MatrixClient crypto", () => {
|
||||
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();
|
||||
@@ -504,10 +506,11 @@ describe("MatrixClient crypto", () => {
|
||||
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 message = aliMessages.shift()!;
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
@@ -567,6 +570,7 @@ describe("MatrixClient crypto", () => {
|
||||
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();
|
||||
@@ -584,6 +588,9 @@ describe("MatrixClient crypto", () => {
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {},
|
||||
);
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
@@ -658,11 +665,10 @@ describe("MatrixClient crypto", () => {
|
||||
]);
|
||||
logger.log(aliTestClient + ': started');
|
||||
httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (_path, content) => {
|
||||
.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);
|
||||
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
|
||||
+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);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+502
-218
File diff suppressed because it is too large
Load Diff
@@ -5,10 +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 httpBackend = null;
|
||||
let httpBackend = new HttpBackend();
|
||||
const userId = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
@@ -67,7 +68,7 @@ describe("MatrixClient opts", 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,7 +119,7 @@ describe("MatrixClient opts", function() {
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes,
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -127,8 +128,8 @@ describe("MatrixClient opts", 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,
|
||||
@@ -141,12 +142,12 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
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" });
|
||||
});
|
||||
});
|
||||
@@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import 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: MatrixClient = 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",
|
||||
@@ -37,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() {
|
||||
@@ -66,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() {
|
||||
@@ -79,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];
|
||||
@@ -94,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();
|
||||
}
|
||||
@@ -121,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);
|
||||
});
|
||||
@@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() {
|
||||
return Promise.all([
|
||||
p1,
|
||||
p3,
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
+178
-173
@@ -1,16 +1,35 @@
|
||||
/*
|
||||
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 { RoomEvent } from "../../src";
|
||||
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
|
||||
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";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
@@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() {
|
||||
},
|
||||
};
|
||||
|
||||
function setNextSyncData(events) {
|
||||
events = events || [];
|
||||
function setNextSyncData(events: Partial<IEvent>[] = []) {
|
||||
NEXT_SYNC_DATA = {
|
||||
next_batch: "n",
|
||||
presence: { events: [] },
|
||||
@@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() {
|
||||
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) {
|
||||
} 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);
|
||||
@@ -97,7 +105,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
@@ -106,112 +114,117 @@ describe("MatrixClient room timelines", function() {
|
||||
undefined,
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
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() {
|
||||
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();
|
||||
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();
|
||||
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) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
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);
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
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, {
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
msg: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 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, {
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
msg: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
const 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);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
promise.then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
@@ -219,7 +232,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,7 +242,7 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
sbEvents = [];
|
||||
httpBackend.when("GET", "/messages").respond(200, function() {
|
||||
httpBackend!.when("GET", "/messages").respond(200, function() {
|
||||
return {
|
||||
chunk: sbEvents,
|
||||
start: "pagin_start",
|
||||
@@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
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(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
@@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// 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,
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
@@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: null,
|
||||
avatar_url: undefined,
|
||||
membership: "join",
|
||||
};
|
||||
|
||||
@@ -303,32 +316,32 @@ describe("MatrixClient room timelines", function() {
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||
expect(joinMsg.sender?.name).toEqual("Old Alice");
|
||||
const oldMsg = room.timeline[1];
|
||||
expect(oldMsg.sender.name).toEqual("Old Alice");
|
||||
expect(oldMsg.sender?.name).toEqual("Old Alice");
|
||||
const newMsg = room.timeline[3];
|
||||
expect(newMsg.sender.name).toEqual(userName);
|
||||
expect(newMsg.sender?.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function(done) {
|
||||
@@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
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(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function(done) {
|
||||
@@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client.scrollback(room, 1).then(function() {
|
||||
client!.scrollback(room, 1).then(function() {
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1).then(function() {
|
||||
httpBackend!.flush("/messages", 1).then(function() {
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() {
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let index = 0;
|
||||
client.on("Room.timeline", function(event, rm, toStart) {
|
||||
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);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(index).toEqual(2);
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
@@ -442,22 +455,21 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
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),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
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");
|
||||
expect(preNameEvent.sender?.name).toEqual(userName);
|
||||
expect(postNameEvent.sender?.name).toEqual("New Name");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() {
|
||||
name: "Room 2",
|
||||
},
|
||||
});
|
||||
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
|
||||
setNextSyncData([secondRoomNameEvent]);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
let nameEmitCount = 0;
|
||||
client.on("Room.name", function(rm) {
|
||||
client!.on(RoomEvent.Name, function(rm) {
|
||||
nameEmitCount += 1;
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(nameEmitCount).toEqual(1);
|
||||
expect(room.name).toEqual("Room 2");
|
||||
@@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() {
|
||||
name: "Room 3",
|
||||
},
|
||||
});
|
||||
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
|
||||
setNextSyncData([thirdRoomNameEvent]);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(nameEmitCount).toEqual(2);
|
||||
@@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() {
|
||||
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),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
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(
|
||||
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(
|
||||
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual(
|
||||
"invite",
|
||||
);
|
||||
});
|
||||
@@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/versions", 1),
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/versions", 1),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
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(
|
||||
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(
|
||||
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
});
|
||||
@@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let emitCount = 0;
|
||||
client.on("Room.timelineReset", function(emitRoom) {
|
||||
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
@@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() {
|
||||
];
|
||||
|
||||
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
|
||||
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
|
||||
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
|
||||
const contextResponse = {
|
||||
start: "start_token",
|
||||
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
|
||||
@@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
// Create a room from the sync
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
room = client.getRoom(roomId);
|
||||
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)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
@@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// Refresh the timeline.
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure the message are visible
|
||||
@@ -681,7 +689,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// 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)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, () => {
|
||||
// Now finally return and make the `/context` request respond
|
||||
return contextResponse;
|
||||
@@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() {
|
||||
const racingSyncEventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
|
||||
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(() => {
|
||||
@@ -726,12 +734,12 @@ describe("MatrixClient room timelines", function() {
|
||||
// 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() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
// Make sure the timeline has the racey sync data
|
||||
const afterRaceySyncTimelineEvents = room
|
||||
@@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() {
|
||||
await Promise.all([
|
||||
refreshLiveTimelinePromise,
|
||||
// Then flush the remaining `/context` to left the refresh logic complete
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure sync pagination still works by seeing a new message show up
|
||||
@@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() {
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// Make sure the timeline includes the the events from the `/sync`
|
||||
@@ -794,22 +802,19 @@ describe("MatrixClient room timelines", function() {
|
||||
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
|
||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||
// to construct a new timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
.respond(500, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
|
||||
return {
|
||||
errcode: 'TEST_FAKE_ERROR',
|
||||
error: 'We purposely intercepted this /context request to make it fail ' +
|
||||
'in order to test whether the refresh timeline code is resilient',
|
||||
};
|
||||
});
|
||||
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(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
// We only expect `TEST_FAKE_ERROR` here. Anything else is
|
||||
// unexpected and should fail the test.
|
||||
@@ -825,7 +830,7 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
// `/messages` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` to construct a new timeline from.
|
||||
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||
.respond(200, function() {
|
||||
return {
|
||||
chunk: [{
|
||||
@@ -837,7 +842,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// `/context` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
|
||||
// timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
@@ -848,7 +853,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// Refresh the timeline again but this time it should pass
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure sync pagination still works by seeing a new message show up
|
||||
@@ -857,12 +862,12 @@ describe("MatrixClient room timelines", function() {
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// Make sure the message are visible
|
||||
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk/lib/types";
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import {
|
||||
@@ -25,13 +26,20 @@ import {
|
||||
RoomMemberEvent,
|
||||
UNSTABLE_MSC2716_MARKER,
|
||||
MatrixClient,
|
||||
ClientEvent,
|
||||
IndexedDBCryptoStore,
|
||||
ISyncResponse,
|
||||
IRoomEvent,
|
||||
IJoinedRoom,
|
||||
IStateEvent,
|
||||
IMinimalEvent,
|
||||
NotificationCountType,
|
||||
} from "../../src";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync';
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
let client: Optional<MatrixClient> = null;
|
||||
let httpBackend: Optional<HttpBackend> = null;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const otherUserId = "@bob:localhost";
|
||||
@@ -40,14 +48,21 @@ describe("MatrixClient syncing", () => {
|
||||
const userC = "@claire:bar";
|
||||
const roomOne = "!foo:localhost";
|
||||
const roomTwo = "!bar:localhost";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTestClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -76,7 +91,7 @@ describe("MatrixClient syncing", () => {
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.since).toEqual(syncData.next_batch);
|
||||
expect(req.queryParams!.since).toEqual(syncData.next_batch);
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
@@ -87,6 +102,8 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
@@ -196,7 +213,7 @@ describe("MatrixClient syncing", () => {
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(JSON.parse(req.queryParams.filter).room.state.lazy_load_members).toBeTruthy();
|
||||
expect(JSON.parse(req.queryParams!.filter).room.state.lazy_load_members).toBeTruthy();
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.setGuest(false);
|
||||
@@ -211,7 +228,7 @@ describe("MatrixClient syncing", () => {
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(JSON.parse(req.queryParams.filter).room?.state?.lazy_load_members).toBeFalsy();
|
||||
expect(JSON.parse(req.queryParams!.filter).room?.state?.lazy_load_members).toBeFalsy();
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.setGuest(true);
|
||||
@@ -219,6 +236,54 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
return httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should emit ClientEvent.Room when invited while crypto is disabled", async () => {
|
||||
const roomId = "!invite:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
const inviteSyncRoomSection = {
|
||||
invite: {
|
||||
[roomId]: {
|
||||
invite_state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial invite
|
||||
let fires = 0;
|
||||
client!.once(ClientEvent.Room, (room) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
client!.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(1);
|
||||
});
|
||||
|
||||
it("should work when all network calls fail", async () => {
|
||||
httpBackend!.expectedRequests = [];
|
||||
httpBackend!.when("GET", "").fail(0, new Error("CORS or something"));
|
||||
const prom = client!.startClient();
|
||||
await Promise.all([
|
||||
expect(prom).resolves.toBeUndefined(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initial sync", () => {
|
||||
@@ -231,11 +296,11 @@ describe("MatrixClient syncing", () => {
|
||||
it("should only apply initialSyncLimit to the initial sync", () => {
|
||||
// 1st request
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(JSON.parse(req.queryParams.filter).room.timeline.limit).toEqual(1);
|
||||
expect(JSON.parse(req.queryParams!.filter).room.timeline.limit).toEqual(1);
|
||||
}).respond(200, syncData);
|
||||
// 2nd request
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.filter).toEqual("a filter id");
|
||||
expect(req.queryParams!.filter).toEqual("a filter id");
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
@@ -246,7 +311,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
it("should not apply initialSyncLimit to a first sync if we have a stored token", () => {
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.filter).toEqual("a filter id");
|
||||
expect(req.queryParams!.filter).toEqual("a filter id");
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
@@ -257,26 +322,29 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
describe("resolving invites to profile info", () => {
|
||||
const syncData = {
|
||||
const syncData: ISyncResponse = {
|
||||
account_data: {
|
||||
events: [],
|
||||
},
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
join: {},
|
||||
invite: {},
|
||||
leave: {},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
syncData.presence.events = [];
|
||||
syncData.presence!.events = [];
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
}) as IRoomEvent,
|
||||
],
|
||||
},
|
||||
state: {
|
||||
@@ -295,14 +363,14 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
} as unknown as IJoinedRoom;
|
||||
});
|
||||
|
||||
it("should resolve incoming invites from /sync", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
@@ -321,26 +389,26 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const member = client!.getRoom(roomOne).getMember(userC);
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false, false),
|
||||
member.getAvatarUrl("home.server.url", 1, 1, '', false, false),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use cached values from m.presence wherever possible", () => {
|
||||
syncData.presence.events = [
|
||||
syncData.presence!.events = [
|
||||
utils.mkPresence({
|
||||
user: userC,
|
||||
presence: "online",
|
||||
name: "The Ghost",
|
||||
}),
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
@@ -353,28 +421,28 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const member = client!.getRoom(roomOne).getMember(userC);
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual("The Ghost");
|
||||
});
|
||||
});
|
||||
|
||||
it("should result in events on the room member firing", () => {
|
||||
syncData.presence.events = [
|
||||
syncData.presence!.events = [
|
||||
utils.mkPresence({
|
||||
user: userC,
|
||||
presence: "online",
|
||||
name: "The Ghost",
|
||||
}),
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
let latestFiredName = null;
|
||||
let latestFiredName: string;
|
||||
client!.on(RoomMemberEvent.Name, (event, m) => {
|
||||
if (m.userId === userC && m.roomId === roomOne) {
|
||||
latestFiredName = m.name;
|
||||
@@ -397,7 +465,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
@@ -408,10 +476,10 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const member = client!.getRoom(roomOne).getMember(userC);
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual(userC);
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false, false),
|
||||
member.getAvatarUrl("home.server.url", 1, 1, '', false, false),
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -443,8 +511,8 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
expect(client!.getUser(userA).presence).toEqual("online");
|
||||
expect(client!.getUser(userB).presence).toEqual("unavailable");
|
||||
expect(client!.getUser(userA)!.presence).toEqual("online");
|
||||
expect(client!.getUser(userB)!.presence).toEqual("unavailable");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -565,7 +633,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
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,
|
||||
@@ -583,7 +651,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
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);
|
||||
@@ -599,7 +667,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
// should use the display name of the other person.
|
||||
expect(room.name).toEqual(otherDisplayName);
|
||||
});
|
||||
@@ -615,11 +683,11 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
let member = room.getMember(otherUserId);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
let member = room.getMember(otherUserId)!;
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(true);
|
||||
member = room.getMember(selfUserId);
|
||||
member = room.getMember(selfUserId)!;
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(false);
|
||||
});
|
||||
@@ -638,16 +706,12 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
const stateAtStart = room.getLiveTimeline().getState(
|
||||
EventTimeline.BACKWARDS,
|
||||
);
|
||||
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 stateAtEnd = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
|
||||
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
|
||||
});
|
||||
@@ -736,7 +800,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(2),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -806,7 +870,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -836,7 +900,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -869,7 +933,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -903,7 +967,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
|
||||
let emitCount = 0;
|
||||
room.on(RoomEvent.HistoryImportedWithinTimeline, (markerEvent, room) => {
|
||||
@@ -959,7 +1023,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(2),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1014,7 +1078,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room).toBeTruthy();
|
||||
|
||||
let stateEventEmitCount = 0;
|
||||
@@ -1088,7 +1152,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room).toBeTruthy();
|
||||
|
||||
let stateEventEmitCount = 0;
|
||||
@@ -1185,7 +1249,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
expect(room).toBeTruthy();
|
||||
const tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
@@ -1218,9 +1282,9 @@ describe("MatrixClient syncing", () => {
|
||||
client!.on(RoomEvent.TimelineReset, (room) => {
|
||||
resetCallCount++;
|
||||
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(0);
|
||||
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
const tl = room?.getLiveTimeline();
|
||||
expect(tl?.getEvents().length).toEqual(0);
|
||||
const tok = tl?.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("newerTok");
|
||||
});
|
||||
|
||||
@@ -1228,7 +1292,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(resetCallCount).toEqual(1);
|
||||
@@ -1307,7 +1371,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
|
||||
type: "m.read",
|
||||
userId: userC,
|
||||
@@ -1319,6 +1383,73 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("unread notifications", () => {
|
||||
const THREAD_ID = "$ThisIsARandomEventId";
|
||||
|
||||
const 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: "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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
it("should sync unread notifications.", () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
"highlight_count": 2,
|
||||
"notification_count": 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", () => {
|
||||
xit("should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)", () => {
|
||||
@@ -1356,7 +1487,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const prom = new Promise<void>((resolve) => {
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
expect(req.queryParams!.filter).toEqual("another_id");
|
||||
resolve();
|
||||
}).respond(200, {});
|
||||
});
|
||||
@@ -1401,7 +1532,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
return Promise.all([
|
||||
client!.syncLeftRooms().then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
const tok = room.getLiveTimeline().getPaginationToken(
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
@@ -1416,6 +1547,107 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("peek", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend!.expectedRequests = [];
|
||||
});
|
||||
|
||||
it("should return a room based on the room initialSync API", async () => {
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
|
||||
room_id: roomOne,
|
||||
membership: "leave",
|
||||
messages: {
|
||||
start: "start",
|
||||
end: "end",
|
||||
chunk: [{
|
||||
content: { body: "Message 1" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId1",
|
||||
sender: userA,
|
||||
origin_server_ts: 12313525,
|
||||
room_id: roomOne,
|
||||
}, {
|
||||
content: { body: "Message 2" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId2",
|
||||
sender: userB,
|
||||
origin_server_ts: 12315625,
|
||||
room_id: roomOne,
|
||||
}],
|
||||
},
|
||||
state: [{
|
||||
content: { name: "Room Name" },
|
||||
type: "m.room.name",
|
||||
event_id: "$eventId",
|
||||
sender: userA,
|
||||
origin_server_ts: 12314525,
|
||||
state_key: "",
|
||||
room_id: roomOne,
|
||||
}],
|
||||
presence: [{
|
||||
content: {},
|
||||
type: "m.presence",
|
||||
sender: userA,
|
||||
}],
|
||||
});
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
|
||||
const prom = client!.peekInRoom(roomOne);
|
||||
await httpBackend!.flushAllExpected();
|
||||
const room = await prom;
|
||||
|
||||
expect(room.roomId).toBe(roomOne);
|
||||
expect(room.getMyMembership()).toBe("leave");
|
||||
expect(room.name).toBe("Room Name");
|
||||
expect(room.currentState.getStateEvents("m.room.name", "").getId()).toBe("$eventId");
|
||||
expect(room.timeline[0].getContent().body).toBe("Message 1");
|
||||
expect(room.timeline[1].getContent().body).toBe("Message 2");
|
||||
client?.stopPeeking();
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
});
|
||||
|
||||
describe("user account data", () => {
|
||||
it("should include correct prevEv in the ClientEvent.AccountData emit", async () => {
|
||||
const eventA1 = new MatrixEvent({ type: "a", content: { body: "1" } });
|
||||
const eventA2 = new MatrixEvent({ type: "a", content: { body: "2" } });
|
||||
const eventB1 = new MatrixEvent({ type: "b", content: { body: "1" } });
|
||||
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
|
||||
|
||||
client!.store.storeAccountDataEvents([eventA1, eventB1]);
|
||||
const fn = jest.fn();
|
||||
client!.on(ClientEvent.AccountData, fn);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
next_batch: "batch_token",
|
||||
rooms: {},
|
||||
presence: {},
|
||||
account_data: {
|
||||
events: [eventA2.event, eventB2.event],
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
client!.startClient(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
const eventA = client?.getAccountData("a");
|
||||
expect(eventA).not.toBe(eventA1);
|
||||
const eventB = client?.getAccountData("b");
|
||||
expect(eventB).not.toBe(eventB1);
|
||||
|
||||
expect(fn).toHaveBeenCalledWith(eventA, eventA1);
|
||||
expect(fn).toHaveBeenCalledWith(eventB, eventB1);
|
||||
|
||||
expect(eventA?.getContent().body).toBe("2");
|
||||
expect(eventB?.getContent().body).toBe("2");
|
||||
|
||||
client!.off(ClientEvent.AccountData, fn);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* waits for the MatrixClient to emit one or more 'sync' events.
|
||||
*
|
||||
@@ -1423,6 +1655,73 @@ describe("MatrixClient syncing", () => {
|
||||
* @returns {Promise} promise which resolves after the sync events have happened
|
||||
*/
|
||||
function awaitSyncEvent(numSyncs?: number) {
|
||||
return utils.syncPromise(client, numSyncs);
|
||||
return utils.syncPromise(client!, numSyncs);
|
||||
}
|
||||
});
|
||||
|
||||
describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {},
|
||||
presence: {},
|
||||
};
|
||||
|
||||
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
|
||||
const idbTestClient = new TestClient(
|
||||
selfUserId,
|
||||
"DEVICE",
|
||||
selfAccessToken,
|
||||
undefined,
|
||||
{ cryptoStore: new IndexedDBCryptoStore(global.indexedDB, "tests") },
|
||||
);
|
||||
const idbHttpBackend = idbTestClient.httpBackend;
|
||||
const idbClient = idbTestClient.client;
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, {});
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
await idbClient.initCrypto();
|
||||
|
||||
const roomId = "!invite:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
const inviteSyncRoomSection = {
|
||||
invite: {
|
||||
[roomId]: {
|
||||
invite_state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
idbHttpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial invite
|
||||
let fires = 0;
|
||||
idbClient.once(ClientEvent.Room, (room) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
idbClient.startClient();
|
||||
await idbHttpBackend.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(1);
|
||||
|
||||
idbHttpBackend.verifyNoOutstandingExpectation();
|
||||
idbClient.stopClient();
|
||||
idbHttpBackend.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,26 +95,31 @@ describe("megolm key backups", function() {
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount: Account;
|
||||
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() {
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
await aliceTestClient.client.initCrypto();
|
||||
aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
[testOlmAccount, aliceTestClient] = setupTestClient();
|
||||
await aliceTestClient!.client.initCrypto();
|
||||
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
return aliceTestClient!.stop();
|
||||
});
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", function() {
|
||||
@@ -130,22 +135,22 @@ describe("megolm key backups", function() {
|
||||
},
|
||||
};
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
return aliceTestClient!.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then(() => {
|
||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||
return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey);
|
||||
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||
}).then(() => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient.expectKeyBackupQuery(
|
||||
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient!.expectKeyBackupQuery(
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
200,
|
||||
CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
return aliceTestClient.httpBackend.flushAllExpected();
|
||||
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||
}).then(function(): Promise<MatrixEvent> {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent()) {
|
||||
|
||||
+349
-30
@@ -29,8 +29,11 @@ import {
|
||||
IDownloadKeyResult,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
IndexedDBCryptoStore,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { IDeviceKeys } from "../../src/crypto/dehydration";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -204,9 +207,11 @@ describe("megolm", () => {
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount: Olm.Account;
|
||||
let testSenderKey: string;
|
||||
let aliceTestClient: TestClient;
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = '';
|
||||
let aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
@@ -280,10 +285,13 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice receives a megolm message", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -316,7 +324,7 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event);
|
||||
@@ -326,10 +334,13 @@ describe("megolm", () => {
|
||||
it("Alice receives a megolm message before the session keys", async () => {
|
||||
// https://github.com/vector-im/element-web/issues/2273
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -353,7 +364,7 @@ describe("megolm", () => {
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted');
|
||||
|
||||
// now she gets the room_key event
|
||||
@@ -383,10 +394,13 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice gets a second room_key message", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -439,7 +453,7 @@ describe("megolm", () => {
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
@@ -468,6 +482,9 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||
@@ -484,7 +501,7 @@ describe("megolm", () => {
|
||||
let inboundGroupSession: Olm.InboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(_path, content) {
|
||||
).respond(200, function(_path, content: any) {
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
@@ -510,7 +527,7 @@ describe("megolm", () => {
|
||||
return { event_id: '$event_id' };
|
||||
});
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const pendingMsg = room.getPendingEvents()[0];
|
||||
|
||||
await Promise.all([
|
||||
@@ -541,13 +558,16 @@ describe("megolm", () => {
|
||||
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 2),
|
||||
]);
|
||||
|
||||
logger.log('Telling alice to block our device');
|
||||
@@ -592,6 +612,9 @@ describe("megolm", () => {
|
||||
|
||||
logger.log("Fetching bob's devices and marking known");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
@@ -607,7 +630,7 @@ describe("megolm", () => {
|
||||
let megolmSessionId: string;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(_path, content) {
|
||||
).respond(200, function(_path, content: any) {
|
||||
logger.log('sendToDevice: ', content);
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
@@ -685,7 +708,7 @@ describe("megolm", () => {
|
||||
// invalidate the device cache for all members in e2e rooms (ie,
|
||||
// herself), and do a key query.
|
||||
aliceTestClient.expectKeyQuery(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId),
|
||||
getTestKeysQueryResponse(aliceTestClient.userId!),
|
||||
);
|
||||
|
||||
await aliceTestClient.httpBackend.flushAllExpected();
|
||||
@@ -695,28 +718,30 @@ describe("megolm", () => {
|
||||
await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
||||
throw new Error("sendTextMessage succeeded on an unknown device");
|
||||
} catch (e) {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
|
||||
expect(Object.keys(e.devices[aliceTestClient.userId])).
|
||||
expect((e as any).name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys((e as any).devices)).toEqual([aliceTestClient.userId!]);
|
||||
expect(Object.keys((e as any)?.devices[aliceTestClient.userId!])).
|
||||
toEqual(['DEVICE_ID']);
|
||||
}
|
||||
|
||||
// mark the device as known, and resend.
|
||||
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
|
||||
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId!, 'DEVICE_ID');
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
||||
200, function(_path, content) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
||||
200, function(_path, content: IClaimOTKsResult) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId!].DEVICE_ID)
|
||||
.toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId!);
|
||||
});
|
||||
|
||||
let p2pSession: Olm.Session;
|
||||
let inboundGroupSession: Olm.InboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(_path, content) {
|
||||
).respond(200, function(_path, content: {
|
||||
messages: { [userId: string]: { [deviceId: string]: Record<string, any> }};
|
||||
}) {
|
||||
logger.log("sendToDevice: ", content);
|
||||
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
const m = content.messages[aliceTestClient.userId!].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
@@ -730,7 +755,7 @@ describe("megolm", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
let decrypted: IEvent;
|
||||
let decrypted: Partial<IEvent> = {};
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(_path, content: IContent) {
|
||||
@@ -745,7 +770,7 @@ describe("megolm", () => {
|
||||
});
|
||||
|
||||
// Grab the event that we'll need to resend
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const pendingEvents = room.getPendingEvents();
|
||||
expect(pendingEvents.length).toEqual(1);
|
||||
const unsentEvent = pendingEvents[0];
|
||||
@@ -760,7 +785,7 @@ describe("megolm", () => {
|
||||
]);
|
||||
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
expect(decrypted.content?.body).toEqual('test');
|
||||
});
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message', async () => {
|
||||
@@ -786,6 +811,10 @@ describe("megolm", () => {
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
// so will this.
|
||||
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
||||
.then(() => {
|
||||
@@ -805,9 +834,12 @@ describe("megolm", () => {
|
||||
it("Alice exports megolm keys and imports them to a new device", async () => {
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
// establish an olm session with alice
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
@@ -839,7 +871,7 @@ describe("megolm", () => {
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
await room.decryptCriticalEvents();
|
||||
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
|
||||
|
||||
@@ -855,6 +887,8 @@ describe("megolm", () => {
|
||||
await aliceTestClient.client.importRoomKeys(exported);
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
@@ -897,7 +931,7 @@ describe("megolm", () => {
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
await event1.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
||||
await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||
expect(event1.isKeySourceUntrusted()).toBeTruthy();
|
||||
|
||||
const event2 = testUtils.mkEvent({
|
||||
@@ -913,24 +947,27 @@ describe("megolm", () => {
|
||||
// @ts-ignore - private
|
||||
event2.senderCurve25519Key = testSenderKey;
|
||||
// @ts-ignore - private
|
||||
testClient.client.crypto.onRoomKeyEvent(event2);
|
||||
testClient.client.crypto!.onRoomKeyEvent(event2);
|
||||
|
||||
const event3 = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
await event3.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
||||
await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||
expect(event3.isKeySourceUntrusted()).toBeFalsy();
|
||||
testClient.stop();
|
||||
});
|
||||
|
||||
it("Alice can decrypt a message with falsey content", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -972,7 +1009,7 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event);
|
||||
@@ -985,10 +1022,13 @@ describe("megolm", () => {
|
||||
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
|
||||
async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -1036,13 +1076,292 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
await event.attemptDecryption(aliceTestClient.client.crypto);
|
||||
await event.attemptDecryption(aliceTestClient.client.crypto!);
|
||||
expect(event.getContent()).toEqual({});
|
||||
const redactionEvent: any = event.getRedactionEvent();
|
||||
expect(redactionEvent.content.reason).toEqual("redaction test");
|
||||
},
|
||||
);
|
||||
|
||||
it("Alice receives shared history before being invited to a room by the sharer", async () => {
|
||||
const beccaTestClient = new TestClient(
|
||||
"@becca:localhost", "foobar", "bazquux",
|
||||
);
|
||||
await beccaTestClient.client.initCrypto();
|
||||
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await beccaTestClient.start();
|
||||
|
||||
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
|
||||
beccaTestClient.client.store.storeRoom(beccaRoom);
|
||||
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@becca:localhost",
|
||||
room_id: ROOM_ID,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "test message",
|
||||
},
|
||||
});
|
||||
|
||||
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
|
||||
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
|
||||
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!;
|
||||
|
||||
// Create an olm session for Becca and Alice's devices
|
||||
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
|
||||
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const content = event.getWireContent();
|
||||
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
ROOM_ID,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
const encryptedForwardedKey = encryptOlmEvent({
|
||||
sender: "@becca:localhost",
|
||||
senderKey: beccaTestClient.getDeviceKey(),
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
plaincontent: {
|
||||
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||
"room_id": ROOM_ID,
|
||||
"sender_key": content.sender_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
|
||||
"session_id": content.session_id,
|
||||
"session_key": groupSessionKey!.key,
|
||||
"chain_index": groupSessionKey!.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
plaintype: 'm.forwarded_room_key',
|
||||
});
|
||||
|
||||
// Alice receives shared history
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 1,
|
||||
to_device: { events: [encryptedForwardedKey] },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice is invited to the room by Becca
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
|
||||
{
|
||||
sender: '@becca:localhost',
|
||||
type: 'm.room.encryption',
|
||||
state_key: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: '@becca:localhost',
|
||||
type: 'm.room.member',
|
||||
state_key: '@alice:localhost',
|
||||
content: {
|
||||
membership: 'invite',
|
||||
},
|
||||
},
|
||||
] } } } },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice has joined the room
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(
|
||||
200, getSyncResponse(["@alice:localhost", "@becca:localhost"]),
|
||||
);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 4,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [event.event] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(roomEvent.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||
expect(decryptedEvent.getContent().body).toEqual('test message');
|
||||
|
||||
await beccaTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives shared history before being invited to a room by someone else", async () => {
|
||||
const beccaTestClient = new TestClient(
|
||||
"@becca:localhost", "foobar", "bazquux",
|
||||
);
|
||||
await beccaTestClient.client.initCrypto();
|
||||
|
||||
await aliceTestClient.start();
|
||||
await beccaTestClient.start();
|
||||
|
||||
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
|
||||
beccaTestClient.client.store.storeRoom(beccaRoom);
|
||||
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@becca:localhost",
|
||||
room_id: ROOM_ID,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "test message",
|
||||
},
|
||||
});
|
||||
|
||||
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
|
||||
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
|
||||
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
// Create an olm session for Becca and Alice's devices
|
||||
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
|
||||
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const content = event.getWireContent();
|
||||
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
ROOM_ID,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
const encryptedForwardedKey = encryptOlmEvent({
|
||||
sender: "@becca:localhost",
|
||||
senderKey: beccaTestClient.getDeviceKey(),
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
plaincontent: {
|
||||
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||
"room_id": ROOM_ID,
|
||||
"sender_key": content.sender_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
|
||||
"session_id": content.session_id,
|
||||
"session_key": groupSessionKey!.key,
|
||||
"chain_index": groupSessionKey!.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
plaintype: 'm.forwarded_room_key',
|
||||
});
|
||||
|
||||
// Alice receives forwarded history from Becca
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 1,
|
||||
to_device: { events: [encryptedForwardedKey] },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice is invited to the room by Charlie
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
|
||||
{
|
||||
sender: '@becca:localhost',
|
||||
type: 'm.room.encryption',
|
||||
state_key: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: '@charlie:localhost',
|
||||
type: 'm.room.member',
|
||||
state_key: '@alice:localhost',
|
||||
content: {
|
||||
membership: 'invite',
|
||||
},
|
||||
},
|
||||
] } } } },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice has joined the room
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(
|
||||
200, getSyncResponse(["@alice:localhost", "@becca:localhost", "@charlie:localhost"]),
|
||||
);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 4,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [event.event] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Decryption should fail, because Alice hasn't received any keys she can trust
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(roomEvent.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
|
||||
|
||||
await beccaTestClient.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,18 +23,19 @@ import { TestClient } from "../TestClient";
|
||||
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
|
||||
import {
|
||||
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
|
||||
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent,
|
||||
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
let sdk: SlidingSyncSdk = null;
|
||||
let mockSlidingSync: SlidingSync = null;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
let sdk: SlidingSyncSdk | undefined;
|
||||
let mockSlidingSync: SlidingSync | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
@@ -66,7 +67,7 @@ describe("SlidingSyncSdk", () => {
|
||||
event_id: "$" + eventIdCounter,
|
||||
};
|
||||
};
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => {
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
|
||||
eventIdCounter++;
|
||||
return {
|
||||
type: evType,
|
||||
@@ -103,24 +104,24 @@ describe("SlidingSyncSdk", () => {
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client.initCrypto();
|
||||
testOpts.crypto = client.crypto;
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
testOpts.crypto = client!.crypto;
|
||||
}
|
||||
httpBackend.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
};
|
||||
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension => {
|
||||
expect(mockSlidingSync.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync.registerExtension as jest.Mock;
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension;
|
||||
@@ -137,14 +138,14 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
afterAll(teardownClient);
|
||||
it("can sync()", async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
expect(mockSlidingSync.start).toBeCalled();
|
||||
expect(mockSlidingSync!.start).toBeCalled();
|
||||
});
|
||||
it("can stop()", async () => {
|
||||
sdk.stop();
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
sdk!.stop();
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,8 +157,8 @@ describe("SlidingSyncSdk", () => {
|
||||
|
||||
describe("initial", () => {
|
||||
beforeAll(async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
// inject some rooms with different fields set.
|
||||
@@ -277,8 +278,8 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
|
||||
it("can be created with required_state and timeline", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
@@ -287,8 +288,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with timeline only", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client.getRoom(roomB);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
@@ -297,8 +298,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with a highlight_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@@ -307,8 +308,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with a notification_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@@ -317,8 +318,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
@@ -326,8 +327,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client.getRoom(roomE);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
@@ -335,8 +336,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("uses the 'name' field to caluclate the room name", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client.getRoom(roomF);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client!.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@@ -347,12 +348,12 @@ describe("SlidingSyncSdk", () => {
|
||||
describe("updating", () => {
|
||||
it("can update with a new timeline event", async () => {
|
||||
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: [newEvent],
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
const newTimeline = data[roomA].timeline;
|
||||
@@ -361,31 +362,31 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update with a new required_state event", async () => {
|
||||
let gotRoom = client.getRoom(roomB);
|
||||
let gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||
],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client.getRoom(roomB);
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||
name: data[roomC].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@@ -394,13 +395,13 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@@ -409,13 +410,13 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
@@ -433,13 +434,13 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
|
||||
...timeline,
|
||||
];
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: oldTimeline,
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
|
||||
@@ -458,50 +459,49 @@ describe("SlidingSyncSdk", () => {
|
||||
describe("lifecycle", () => {
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
|
||||
|
||||
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
|
||||
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
|
||||
{ pos: "h", lists: [], rooms: {}, extensions: {} },
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
|
||||
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
);
|
||||
}
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
});
|
||||
|
||||
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.Complete,
|
||||
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
||||
null,
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||
expect(mockSlidingSync.stop).not.toBeCalled();
|
||||
mockSlidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
message: "Oh no your access token is no longer valid",
|
||||
}));
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -517,8 +517,8 @@ describe("SlidingSyncSdk", () => {
|
||||
avatar_url: "mxc://foobar",
|
||||
displayname: "The Invitee",
|
||||
};
|
||||
httpBackend.when("GET", "/profile").respond(200, inviteeProfile);
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
initial: true,
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
@@ -529,10 +529,11 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
],
|
||||
});
|
||||
await httpBackend.flush("/profile", 1, 1000);
|
||||
const room = client.getRoom(roomId);
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
await emitPromise(client!, RoomMemberEvent.Name);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
const inviteeMember = room.getMember(invitee);
|
||||
const inviteeMember = room.getMember(invitee)!;
|
||||
expect(inviteeMember).toBeDefined();
|
||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||
@@ -545,8 +546,8 @@ describe("SlidingSyncSdk", () => {
|
||||
await setupClient({
|
||||
withCrypto: true,
|
||||
});
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
@@ -554,7 +555,7 @@ describe("SlidingSyncSdk", () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client.crypto.stop();
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
@@ -572,38 +573,38 @@ describe("SlidingSyncSdk", () => {
|
||||
// TODO: more assertions?
|
||||
});
|
||||
it("can update OTK counts", () => {
|
||||
client.crypto.updateOneTimeKeyCount = jest.fn();
|
||||
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
});
|
||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
not_signed_curve25519: 42,
|
||||
// missing field -> default to 0
|
||||
},
|
||||
});
|
||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
});
|
||||
it("can update fallback keys", () => {
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client.crypto.getNeedsNewFallback()).toEqual(false);
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["not_signed_curve25519"],
|
||||
});
|
||||
expect(client.crypto.getNeedsNewFallback()).toEqual(true);
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe("ExtensionAccountData", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("account_data");
|
||||
});
|
||||
@@ -618,7 +619,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const globalContent = {
|
||||
info: "here",
|
||||
};
|
||||
let globalData = client.getAccountData(globalType);
|
||||
let globalData = client!.getAccountData(globalType);
|
||||
expect(globalData).toBeUndefined();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
@@ -628,13 +629,13 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
globalData = client.getAccountData(globalType);
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
it("processes rooms account data", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with account data",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
@@ -660,9 +661,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
const event = room.getAccountData(roomType);
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeDefined();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
@@ -681,9 +682,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const room = client.getRoom(unknownRoomId);
|
||||
const room = client!.getRoom(unknownRoomId);
|
||||
expect(room).toBeNull();
|
||||
expect(client.getAccountData(roomType)).toBeUndefined();
|
||||
expect(client!.getAccountData(roomType)).toBeUndefined();
|
||||
});
|
||||
it("can update push rules via account data", async () => {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -703,7 +704,7 @@ describe("SlidingSyncSdk", () => {
|
||||
}],
|
||||
},
|
||||
};
|
||||
let pushRule = client.getRoomPushRule("global", roomId);
|
||||
let pushRule = client!.getRoomPushRule("global", roomId);
|
||||
expect(pushRule).toBeUndefined();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
@@ -713,16 +714,16 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
pushRule = client.getRoomPushRule("global", roomId);
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]);
|
||||
pushRule = client!.getRoomPushRule("global", roomId)!;
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
|
||||
});
|
||||
});
|
||||
describe("ExtensionToDevice", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("to_device");
|
||||
});
|
||||
@@ -753,7 +754,7 @@ describe("SlidingSyncSdk", () => {
|
||||
foo: "bar",
|
||||
};
|
||||
let called = false;
|
||||
client.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||
expect(ev.getType()).toEqual(toDeviceType);
|
||||
called = true;
|
||||
@@ -771,7 +772,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
it("can cancel key verification requests", async () => {
|
||||
const seen: Record<string, boolean> = {};
|
||||
client.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
const evType = ev.getType();
|
||||
expect(seen[evType]).toBeFalsy();
|
||||
seen[evType] = true;
|
||||
|
||||
+435
-87
@@ -22,7 +22,6 @@ import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src";
|
||||
import { sleep } from "../../src/utils";
|
||||
|
||||
/**
|
||||
* Tests for sliding sync. These tests are broken down into sub-tests which are reliant upon one another.
|
||||
@@ -30,8 +29,8 @@ import { sleep } from "../../src/utils";
|
||||
* Each test will call different functions on SlidingSync which may depend on state from previous tests.
|
||||
*/
|
||||
describe("SlidingSync", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const proxyBaseUrl = "http://localhost:8008";
|
||||
@@ -46,9 +45,9 @@ describe("SlidingSync", () => {
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
};
|
||||
|
||||
describe("start/stop", () => {
|
||||
@@ -57,14 +56,14 @@ describe("SlidingSync", () => {
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
it("should start the sync loop upon calling start()", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
const fakeResp = {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).respond(200, fakeResp);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, fakeResp);
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
expect(state).toEqual(SlidingSyncState.RequestFinished);
|
||||
expect(resp).toEqual(fakeResp);
|
||||
@@ -72,13 +71,113 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should stop the sync loop upon calling stop()", () => {
|
||||
slidingSync.stop();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it("should reset the connection on HTTP 400 and send everything again", async () => {
|
||||
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
const roomId = "!sub:localhost";
|
||||
const subInfo = {
|
||||
timeline_limit: 42,
|
||||
required_state: [["m.room.create", ""]],
|
||||
};
|
||||
const listInfo = {
|
||||
ranges: [[0, 10]],
|
||||
filters: {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
const ext = {
|
||||
name: () => "custom_extension",
|
||||
onRequest: (initial) => { return { initial: initial }; },
|
||||
onResponse: (res) => { return {}; },
|
||||
when: () => ExtensionState.PreProcess,
|
||||
};
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
slidingSync.modifyRoomSubscriptionInfo(subInfo);
|
||||
slidingSync.setList(0, listInfo);
|
||||
slidingSync.registerExtension(ext);
|
||||
slidingSync.start();
|
||||
|
||||
// expect everything to be sent
|
||||
let txnId;
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
txnId = body.txn_id;
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "11",
|
||||
lists: [{ count: 5 }],
|
||||
extensions: {},
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// expect nothing but ranges and non-initial extensions to be sent
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: false });
|
||||
expect(req.queryParams!["pos"]).toEqual("11");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "12",
|
||||
lists: [{ count: 5 }],
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now we expire the session
|
||||
httpBackend!.when("POST", syncUrl).respond(400, function() {
|
||||
logger.debug("sending session expired 400");
|
||||
return {
|
||||
error: "HTTP 400 : session expired",
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// ...and everything should be sent again
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "1",
|
||||
lists: [{ count: 6 }],
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,9 +202,9 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should be able to subscribe to a room", async () => {
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("room sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -125,7 +224,7 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
@@ -137,7 +236,7 @@ describe("SlidingSync", () => {
|
||||
["m.room.member", "*"],
|
||||
],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("adjusted sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -158,7 +257,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
// need to set what the new subscription info is for subsequent tests
|
||||
roomSubInfo = newSubInfo;
|
||||
@@ -179,7 +278,7 @@ describe("SlidingSync", () => {
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("new subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -204,12 +303,12 @@ describe("SlidingSync", () => {
|
||||
const subs = slidingSync.getRoomSubscriptions();
|
||||
subs.add(anotherRoomID);
|
||||
slidingSync.modifyRoomSubscriptions(subs);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should be able to unsubscribe from a room", async () => {
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("unsub request", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -226,7 +325,7 @@ describe("SlidingSync", () => {
|
||||
// remove the subscription for the first room
|
||||
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
|
||||
slidingSync.stop();
|
||||
@@ -273,8 +372,8 @@ describe("SlidingSync", () => {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1);
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@@ -301,7 +400,7 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
|
||||
expect(listenerData[roomA]).toEqual(rooms[roomA]);
|
||||
@@ -316,7 +415,7 @@ describe("SlidingSync", () => {
|
||||
expect(slidingSync.getList(0)).toBeDefined();
|
||||
expect(slidingSync.getList(5)).toBeNull();
|
||||
expect(slidingSync.getListData(5)).toBeNull();
|
||||
const syncData = slidingSync.getListData(0);
|
||||
const syncData = slidingSync.getListData(0)!;
|
||||
expect(syncData.joinedCount).toEqual(500); // from previous test
|
||||
expect(syncData.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -327,7 +426,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should be possible to adjust list ranges", async () => {
|
||||
// modify the list ranges
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("next ranges", body.lists[0].ranges);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@@ -351,7 +450,7 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.RequestFinished;
|
||||
});
|
||||
slidingSync.setListRanges(0, newRanges);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
@@ -364,7 +463,7 @@ describe("SlidingSync", () => {
|
||||
"is_dm": true,
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("extra list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@@ -403,13 +502,13 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.setList(1, extraListReq);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
it("should be possible to get list DELETE/INSERTs", async () => {
|
||||
// move C (2) to A (0)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -440,12 +539,12 @@ describe("SlidingSync", () => {
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
// move C (0) back to A (2)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -476,13 +575,13 @@ describe("SlidingSync", () => {
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should ignore invalid list indexes", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -509,13 +608,13 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should be possible to update a list", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [{
|
||||
count: 42,
|
||||
@@ -555,7 +654,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
@@ -566,8 +665,8 @@ describe("SlidingSync", () => {
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
};
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||
lists: [{
|
||||
@@ -598,17 +697,17 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should handle deletions correctly", async () => {
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [{
|
||||
count: 499,
|
||||
@@ -634,16 +733,16 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should handle insertions correctly", async () => {
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -670,11 +769,11 @@ describe("SlidingSync", () => {
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [{
|
||||
count: 501,
|
||||
@@ -702,11 +801,96 @@ describe("SlidingSync", () => {
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
// Regression test to make sure things like DELETE 0 INSERT 0 work correctly and we don't
|
||||
// end up losing room IDs.
|
||||
it("should handle insertions with a spurious DELETE correctly", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [
|
||||
{
|
||||
ranges: [[0, 20]],
|
||||
},
|
||||
], {}, client!, 1);
|
||||
// initially start with nothing
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "a",
|
||||
lists: [{
|
||||
count: 0,
|
||||
ops: [],
|
||||
}],
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({});
|
||||
|
||||
// insert a room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "b",
|
||||
lists: [{
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE", index: 0,
|
||||
},
|
||||
{
|
||||
op: "INSERT", index: 0, room_id: roomA,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
});
|
||||
|
||||
// insert another room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [{
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE", index: 1,
|
||||
},
|
||||
{
|
||||
op: "INSERT", index: 0, room_id: roomB,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomA,
|
||||
});
|
||||
|
||||
// insert a final room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [{
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE", index: 2,
|
||||
},
|
||||
{
|
||||
op: "INSERT", index: 0, room_id: roomC,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomB,
|
||||
2: roomA,
|
||||
});
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transaction IDs", () => {
|
||||
@@ -725,11 +909,11 @@ describe("SlidingSync", () => {
|
||||
],
|
||||
};
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
// modification before SlidingSync.start()
|
||||
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -752,7 +936,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await subscribePromise;
|
||||
});
|
||||
it("should resolve setList during a connection", async () => {
|
||||
@@ -761,7 +945,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
const promise = slidingSync.setList(0, newList);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -776,14 +960,14 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should resolve setListRanges during a connection", async () => {
|
||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -800,7 +984,7 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
@@ -809,7 +993,7 @@ describe("SlidingSync", () => {
|
||||
timeline_limit: 99,
|
||||
});
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -825,22 +1009,22 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
|
||||
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
|
||||
const gotTxnIds = [];
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function(req) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
};
|
||||
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
@@ -849,7 +1033,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check((req) => {
|
||||
httpBackend!.when("POST", syncUrl).check((req) => {
|
||||
txnId = req.data.txn_id;
|
||||
}).respond(200, () => {
|
||||
// include the txn_id, earlier requests should now be reject()ed.
|
||||
@@ -858,23 +1042,23 @@ describe("SlidingSync", () => {
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await okPromise;
|
||||
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
|
||||
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
|
||||
const gotTxnIds = [];
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function(req) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
gotTxnIds.push(req.data?.txn_id);
|
||||
};
|
||||
const A = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend!.flushAllExpected();
|
||||
const B = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
@@ -885,14 +1069,14 @@ describe("SlidingSync", () => {
|
||||
C.finally(() => {
|
||||
pendingC = false;
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, () => {
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, () => {
|
||||
// include the txn_id for B, so C's promise is outstanding
|
||||
return {
|
||||
pos: "C",
|
||||
txn_id: gotTxnIds[1],
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
// A is rejected, see above
|
||||
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
|
||||
expect(pendingC).toBe(true); // C is pending still
|
||||
@@ -904,7 +1088,7 @@ describe("SlidingSync", () => {
|
||||
pending = false;
|
||||
});
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -921,13 +1105,163 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(txnId).toBeDefined();
|
||||
expect(pending).toBe(true);
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom room subscriptions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
|
||||
const roomA = "!a";
|
||||
const roomB = "!b";
|
||||
const roomC = "!c";
|
||||
const roomD = "!d";
|
||||
|
||||
const defaultSub = {
|
||||
timeline_limit: 1,
|
||||
required_state: [["m.room.create", ""]],
|
||||
};
|
||||
|
||||
const customSubName1 = "sub1";
|
||||
const customSub1 = {
|
||||
timeline_limit: 2,
|
||||
required_state: [["*", "*"]],
|
||||
};
|
||||
|
||||
const customSubName2 = "sub2";
|
||||
const customSub2 = {
|
||||
timeline_limit: 3,
|
||||
required_state: [["*", "*"]],
|
||||
};
|
||||
|
||||
it("should be possible to use custom subscriptions on startup", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
// then call these depending on the kind of room / context
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.useCustomSubscription(roomB, customSubName1);
|
||||
slidingSync.useCustomSubscription(roomC, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA, roomB, roomC, roomD]));
|
||||
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
|
||||
expect(body.room_subscriptions[roomC]).toEqual(customSub2);
|
||||
expect(body.room_subscriptions[roomD]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("should be possible to use custom subscriptions mid-connection", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
// initially no subs
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user clicks on a room which uses the default sub
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user clicks on a room which uses a custom sub
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
|
||||
expect(body.unsubscribe_rooms).toEqual([roomA]);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.useCustomSubscription(roomB, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user uses a different sub for the same room: we don't unsub but just resend
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub2);
|
||||
expect(body.unsubscribe_rooms).toBeFalsy();
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.useCustomSubscription(roomB, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("uses the default subscription for unknown subscription names", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.useCustomSubscription(roomA, "unknown name");
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extensions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
@@ -963,10 +1297,10 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
|
||||
it("should be able to register an extension", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
|
||||
const callbackOrder = [];
|
||||
const callbackOrder: string[] = [];
|
||||
let extensionOnResponseCalled = false;
|
||||
onPreExtensionRequest = () => {
|
||||
return extReq;
|
||||
@@ -977,7 +1311,7 @@ describe("SlidingSync", () => {
|
||||
expect(resp).toEqual(extResp);
|
||||
};
|
||||
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -998,7 +1332,7 @@ describe("SlidingSync", () => {
|
||||
}
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(extensionOnResponseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
|
||||
@@ -1012,7 +1346,7 @@ describe("SlidingSync", () => {
|
||||
onPreExtensionResponse = (resp) => {
|
||||
responseCalled = true;
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req nothing", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -1030,7 +1364,7 @@ describe("SlidingSync", () => {
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(false);
|
||||
});
|
||||
@@ -1041,13 +1375,13 @@ describe("SlidingSync", () => {
|
||||
return extReq;
|
||||
};
|
||||
let responseCalled = false;
|
||||
const callbackOrder = [];
|
||||
const callbackOrder: string[] = [];
|
||||
onPostExtensionResponse = (resp) => {
|
||||
expect(resp).toEqual(extResp);
|
||||
responseCalled = true;
|
||||
callbackOrder.push("onPostExtensionResponse");
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req after start", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -1071,7 +1405,7 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
|
||||
@@ -1079,16 +1413,27 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("is not possible to register the same extension name twice", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function timeout(delayMs: number, reason: string): Promise<never> {
|
||||
await sleep(delayMs);
|
||||
throw new Error(`timeout: ${delayMs}ms - ${reason}`);
|
||||
function timeout(delayMs: number, reason: string): { promise: Promise<never>, cancel: () => void } {
|
||||
let timeoutId;
|
||||
return {
|
||||
promise: new Promise((resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`timeout: ${delayMs}ms - ${reason}`));
|
||||
}, delayMs);
|
||||
}),
|
||||
cancel: () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1106,19 +1451,22 @@ function listenUntil<T>(
|
||||
callback: (...args: any[]) => T,
|
||||
timeoutMs = 500,
|
||||
): Promise<T> {
|
||||
const trace = new Error().stack.split(`\n`)[2];
|
||||
const trace = new Error().stack?.split(`\n`)[2];
|
||||
const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace);
|
||||
return Promise.race([new Promise<T>((resolve, reject) => {
|
||||
const wrapper = (...args) => {
|
||||
try {
|
||||
const data = callback(...args);
|
||||
if (data) {
|
||||
emitter.off(eventName, wrapper);
|
||||
t.cancel();
|
||||
resolve(data);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
t.cancel();
|
||||
}
|
||||
};
|
||||
emitter.on(eventName, wrapper);
|
||||
}), timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace)]);
|
||||
}), t.promise]);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../src/logger';
|
||||
import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
@@ -26,12 +25,3 @@ try {
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
}
|
||||
|
||||
// also try to set node crypto
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
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,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),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -5,8 +5,8 @@ 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, MsgType } 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";
|
||||
|
||||
@@ -74,9 +74,11 @@ interface IEventOpts {
|
||||
sender?: string;
|
||||
skey?: string;
|
||||
content: IContent;
|
||||
prev_content?: IContent;
|
||||
user?: string;
|
||||
unsigned?: IUnsigned;
|
||||
redacts?: string;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
|
||||
@@ -103,10 +105,12 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
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: "$" + 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;
|
||||
@@ -235,11 +239,13 @@ export function mkMembershipCustom<T>(
|
||||
});
|
||||
}
|
||||
|
||||
interface IMessageOpts {
|
||||
export interface IMessageOpts {
|
||||
room?: string;
|
||||
user: string;
|
||||
msg?: string;
|
||||
event?: boolean;
|
||||
relatesTo?: IEventRelation;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,6 +273,10 @@ export function mkMessage(
|
||||
},
|
||||
};
|
||||
|
||||
if (opts.relatesTo) {
|
||||
eventOpts.content["m.relates_to"] = opts.relatesTo;
|
||||
}
|
||||
|
||||
if (!eventOpts.content.body) {
|
||||
eventOpts.content.body = "Random->" + Math.random();
|
||||
}
|
||||
@@ -305,7 +315,7 @@ export function mkReplyMessage(
|
||||
"rel_type": "m.in_reply_to",
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
"event_id": opts.replyToMessage.getId()!,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -371,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 };
|
||||
};
|
||||
+386
-29
@@ -14,6 +14,32 @@ 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" +
|
||||
@@ -54,8 +80,50 @@ export const DUMMY_SDP = (
|
||||
"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 {
|
||||
localDescription: RTCSessionDescription;
|
||||
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 = {
|
||||
@@ -63,34 +131,133 @@ export class MockRTCPeerConnection {
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>(resolve => {
|
||||
this.onReadyToNegotiate = resolve;
|
||||
});
|
||||
|
||||
MockRTCPeerConnection.instances.push(this);
|
||||
}
|
||||
|
||||
addEventListener() { }
|
||||
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
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;
|
||||
}
|
||||
}
|
||||
setRemoteDescription() {
|
||||
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();
|
||||
}
|
||||
setLocalDescription() {
|
||||
public setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() { }
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
|
||||
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) { }
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
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) { }
|
||||
|
||||
stop() { }
|
||||
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
|
||||
@@ -101,46 +268,236 @@ export class MockMediaStream {
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
listeners: [string, (...args: any[]) => any][] = [];
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
|
||||
dispatchEvent(eventType: string) {
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
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]);
|
||||
}
|
||||
removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
addTrack(track: MockMediaStreamTrack) {
|
||||
public addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
|
||||
public clone(): MediaStream {
|
||||
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
|
||||
}
|
||||
|
||||
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: "audio" | "video",
|
||||
public kind: "audioinput" | "videoinput" | "audiooutput",
|
||||
) { }
|
||||
|
||||
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
public userMediaStreams: MockMediaStream[] = [];
|
||||
public screensharingStreams: MockMediaStream[] = [];
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
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;
|
||||
}
|
||||
stopUserMediaStream() { }
|
||||
hasAudioDevice() { return true; }
|
||||
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>, [DisplayMediaStreamConstraints]>().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 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 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;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
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]);
|
||||
@@ -41,17 +41,17 @@ describe("NamespacedValue", () => {
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -65,7 +65,7 @@ describe("UnstableValue", () => {
|
||||
});
|
||||
|
||||
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]);
|
||||
@@ -73,11 +73,11 @@ describe("UnstableValue", () => {
|
||||
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,13 +17,12 @@ limitations under the License.
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { request } from "../../src/matrix";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
const httpBackend = new MockHttpBackend();
|
||||
request(httpBackend.requestFn);
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
@@ -176,8 +175,7 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
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": {
|
||||
@@ -205,8 +203,7 @@ 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": {},
|
||||
@@ -232,8 +229,7 @@ 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": {
|
||||
@@ -679,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);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
makeBeaconContent,
|
||||
makeBeaconInfoContent,
|
||||
makeTopicContent,
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
|
||||
@@ -127,6 +128,66 @@ 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', () => {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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() {
|
||||
|
||||
+694
-143
@@ -2,6 +2,7 @@ import '../olm-loader';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
@@ -15,6 +16,8 @@ import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { logger } from '../../src/logger';
|
||||
import { MemoryStore } from "../../src";
|
||||
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
import { RoomMember } from '../../src/models/room-member';
|
||||
import { IStore } from '../../src/store';
|
||||
|
||||
const Olm = global.Olm;
|
||||
@@ -30,7 +33,7 @@ function awaitEvent(emitter, event) {
|
||||
async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent> {
|
||||
const roomId = event.getRoomId();
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
eventContent.sender_key,
|
||||
eventContent.session_id,
|
||||
@@ -40,20 +43,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
|
||||
type: "m.forwarded_room_key",
|
||||
sender: client.getUserId(),
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: eventContent.sender_key,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: eventContent.session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain:
|
||||
key.forwarding_curve_key_chain,
|
||||
"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,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
// @ts-ignore private property
|
||||
ksEvent.senderCurve25519Key = "akey";
|
||||
ksEvent.getWireType = () => "m.room.encrypted";
|
||||
ksEvent.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
|
||||
const roomId = event.getRoomId();
|
||||
const eventContent = event.getWireContent();
|
||||
const key = client.crypto!.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
|
||||
const ksEvent = new MatrixEvent({
|
||||
type: "m.room_key",
|
||||
sender: client.getUserId()!,
|
||||
content: {
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"session_id": eventContent.session_id,
|
||||
"session_key": key.key,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
// @ts-ignore private property
|
||||
ksEvent.senderCurve25519Key = event.getSenderKey();
|
||||
ksEvent.getWireType = () => "m.room.encrypted";
|
||||
ksEvent.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
@@ -95,7 +130,7 @@ describe("Crypto", function() {
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.isKeySourceUntrusted = () => true;
|
||||
event.getClaimedEd25519Key =
|
||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
@@ -112,7 +147,7 @@ describe("Crypto", function() {
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
@@ -233,6 +268,7 @@ describe("Crypto", function() {
|
||||
describe('Key requests', function() {
|
||||
let aliceClient: MatrixClient;
|
||||
let bobClient: MatrixClient;
|
||||
let claraClient: MatrixClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceClient = (new TestClient(
|
||||
@@ -241,16 +277,266 @@ describe("Crypto", function() {
|
||||
bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
claraClient = (new TestClient(
|
||||
"@clara:example.com", "claradevice",
|
||||
)).client;
|
||||
await aliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
await claraClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
claraClient.stopClient();
|
||||
});
|
||||
|
||||
it("does not cancel keyshare requests if some messages are not decrypted", async function() {
|
||||
it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
// Make Bob invited by Alice so Bob will accept Alice's forwarded keys
|
||||
bobRoom.currentState.setStateEvents([new MatrixEvent({
|
||||
type: "m.room.member",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
state_key: "@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);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the second message, so
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
// 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
|
||||
const decryptEventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[0].isKeySourceUntrusted()).toBeTruthy();
|
||||
await sleep(1);
|
||||
// the room key request should still be there, since we've
|
||||
// decrypted everything with an untrusted key
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
|
||||
|
||||
// Now share a trusted room key event so Bob will re-decrypt the messages.
|
||||
// Bob will backfill trust when they receive a trusted session with a higher
|
||||
// index that connects to an untrusted session with a lower index.
|
||||
const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]);
|
||||
const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
await bobDecryptor.onRoomKeyEvent(roomKeyEvent);
|
||||
await trustedDecryptEventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[0].isKeySourceUntrusted()).toBeFalsy();
|
||||
await sleep(1);
|
||||
// now the room key request should be gone, since there's
|
||||
// no better key to wait for
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should error if a forwarded room key lacks a content.sender_key", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
});
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private property
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private property
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private property
|
||||
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 device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
|
||||
ksEvent.getContent().sender_key = undefined; // test
|
||||
bobClient.crypto!.olmDevice.addInboundGroupSession = jest.fn();
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a new keyshare request if we request a keyshare", async function() {
|
||||
// make sure that cancelAndResend... creates a new keyshare request
|
||||
// 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.crypto!.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
|
||||
const aliceSendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
aliceClient.sendToDevice = aliceSendToDevice;
|
||||
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.
|
||||
// @ts-ignore
|
||||
aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceSendToDevice).toBeCalledTimes(1);
|
||||
const txnId = aliceSendToDevice.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(aliceSendToDevice).toBeCalledTimes(3);
|
||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
});
|
||||
|
||||
it("should accept forwarded keys which it requested", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
@@ -286,7 +572,7 @@ describe("Crypto", function() {
|
||||
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);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@@ -295,31 +581,18 @@ describe("Crypto", function() {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the second message, so
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient.crypto.cryptoStore;
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
@@ -329,23 +602,123 @@ describe("Crypto", function() {
|
||||
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();
|
||||
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
|
||||
expect(outgoingReq).toBeDefined();
|
||||
await cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
outgoingReq!.requestId, RoomKeyRequestState.Unsent,
|
||||
{ state: RoomKeyRequestState.Sent },
|
||||
);
|
||||
|
||||
// keyshare the session key starting at the first message, so
|
||||
// that it can now be decrypted
|
||||
const decryptEventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventPromise;
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
await sleep(1);
|
||||
// the room key request should be gone since we've now decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("should error if a forwarded room key lacks a content.sender_key", async function() {
|
||||
it("should accept forwarded keys from the user who invited it to the room", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
|
||||
// Make Bob invited by Clara
|
||||
bobRoom.currentState.setStateEvents([new MatrixEvent({
|
||||
type: "m.room.member",
|
||||
sender: "@clara:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
state_key: "@bob:example.com",
|
||||
})]);
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
claraClient.store.storeRoom(claraRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await claraClient.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);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = claraClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("should accept forwarded keys from one of its own user's other devices", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
@@ -356,111 +729,225 @@ describe("Crypto", function() {
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
});
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private property
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private property
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private property
|
||||
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 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);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
|
||||
ksEvent.getContent().sender_key = undefined; // test
|
||||
bobClient.crypto.olmDevice.addInboundGroupSession = jest.fn();
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = bobClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
expect(bobClient.crypto.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
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.crypto.cryptoStore;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: "!someroom",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
it("should not accept unexpected forwarded keys for a room it's in", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toBeDefined();
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
claraClient.store.storeRoom(claraRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await claraClient.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);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = claraClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).toBeNull();
|
||||
});
|
||||
|
||||
it("uses a new txnid for re-requesting keys", async function() {
|
||||
jest.useFakeTimers();
|
||||
it("should park forwarded keys for a room it's not in", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
await aliceClient.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);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
}));
|
||||
|
||||
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
|
||||
const aliceSendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
aliceClient.sendToDevice = aliceSendToDevice;
|
||||
aliceClient.startClient();
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
// 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.
|
||||
// @ts-ignore
|
||||
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceSendToDevice).toBeCalledTimes(1);
|
||||
const txnId = aliceSendToDevice.mock.calls[0][2];
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
// of the request
|
||||
await Promise.resolve();
|
||||
const content = events[0].getWireContent();
|
||||
|
||||
// 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(aliceSendToDevice).toBeCalledTimes(3);
|
||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const bobKey = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
expect(bobKey).toBeNull();
|
||||
|
||||
const aliceKey = await aliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId);
|
||||
expect(parked).toEqual([{
|
||||
senderId: aliceClient.getUserId(),
|
||||
senderKey: content.sender_key,
|
||||
sessionId: content.session_id,
|
||||
sessionKey: aliceKey!.key,
|
||||
keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key },
|
||||
forwardingCurve25519KeyChain: ["akey"],
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -470,19 +957,19 @@ describe("Crypto", function() {
|
||||
jest.setTimeout(10000);
|
||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||
await client.initCrypto();
|
||||
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.isCrossSigningReady = async () => false;
|
||||
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.uploadKeySignatures = jest.fn();
|
||||
client.crypto.baseApis.http.authedRequest = jest.fn();
|
||||
client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.isCrossSigningReady = async () => false;
|
||||
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.baseApis.uploadKeySignatures = jest.fn();
|
||||
client.crypto!.baseApis.http.authedRequest = jest.fn();
|
||||
const createSecretStorageKey = async () => {
|
||||
return {
|
||||
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||
privateKey: Uint8Array.of(32, 33),
|
||||
};
|
||||
};
|
||||
await client.crypto.bootstrapSecretStorage({
|
||||
await client.crypto!.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
client.stopClient();
|
||||
@@ -509,7 +996,7 @@ describe("Crypto", function() {
|
||||
|
||||
encryptedPayload = {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
sender_key: client.client.crypto.olmDevice.deviceCurve25519Key,
|
||||
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: { plaintext: JSON.stringify(payload) },
|
||||
};
|
||||
});
|
||||
@@ -589,4 +1076,68 @@ describe("Crypto", function() {
|
||||
client.httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkSecretStoragePrivateKey", () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await client.stop();
|
||||
});
|
||||
|
||||
it("should free PkDecryption", () => {
|
||||
const free = jest.fn();
|
||||
jest.spyOn(Olm, "PkDecryption").mockImplementation(() => ({
|
||||
init_with_private_key: jest.fn(),
|
||||
free,
|
||||
}) as unknown as PkDecryption);
|
||||
client.client.checkSecretStoragePrivateKey(new Uint8Array(), "");
|
||||
expect(free).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkCrossSigningPrivateKey", () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await client.stop();
|
||||
});
|
||||
|
||||
it("should free PkSigning", () => {
|
||||
const free = jest.fn();
|
||||
jest.spyOn(Olm, "PkSigning").mockImplementation(() => ({
|
||||
init_with_seed: jest.fn(),
|
||||
free,
|
||||
}) as unknown as PkSigning);
|
||||
client.client.checkCrossSigningPrivateKey(new Uint8Array(), "");
|
||||
expect(free).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
await client!.stop();
|
||||
});
|
||||
|
||||
// start() is a no-op nowadays, so there's not much to test here.
|
||||
it("should complete successfully", async () => {
|
||||
await client!.client.crypto!.start();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,9 +222,9 @@ describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => 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
|
||||
@@ -247,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ const signedDeviceList2: IDownloadKeyResult = {
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
let deviceLists = [];
|
||||
let deviceLists: DeviceList[] = [];
|
||||
|
||||
beforeEach(function() {
|
||||
deviceLists = [];
|
||||
|
||||
@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
|
||||
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
||||
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const 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';
|
||||
|
||||
@@ -110,6 +110,12 @@ 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() {
|
||||
@@ -325,7 +331,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
},
|
||||
});
|
||||
mockBaseApis.sendToDevice.mockResolvedValue(undefined);
|
||||
mockBaseApis.sendToDevice.mockResolvedValue({});
|
||||
mockBaseApis.queueToDevice.mockResolvedValue(undefined);
|
||||
|
||||
aliceDeviceInfo = {
|
||||
@@ -487,9 +493,9 @@ 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",
|
||||
@@ -509,8 +515,8 @@ describe("MegolmDecryption", function() {
|
||||
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,
|
||||
@@ -518,18 +524,19 @@ describe("MegolmDecryption", function() {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -545,7 +552,7 @@ describe("MegolmDecryption", function() {
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, room);
|
||||
await aliceClient.crypto!.encryptEvent(event, room);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
@@ -577,6 +584,100 @@ describe("MegolmDecryption", function() {
|
||||
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",
|
||||
@@ -588,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",
|
||||
@@ -618,18 +719,19 @@ describe("MegolmDecryption", function() {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -648,7 +750,7 @@ describe("MegolmDecryption", function() {
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
@@ -679,10 +781,10 @@ 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);
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
@@ -699,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",
|
||||
@@ -726,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",
|
||||
@@ -756,10 +858,10 @@ describe("MegolmDecryption", function() {
|
||||
]);
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
aliceClient.crypto.downloadKeys = jest.fn();
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
aliceClient.crypto!.downloadKeys = jest.fn();
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
@@ -782,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",
|
||||
@@ -814,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",
|
||||
@@ -844,10 +946,10 @@ describe("MegolmDecryption", function() {
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
aliceClient.crypto.downloadKeys = jest.fn();
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
aliceClient.crypto!.downloadKeys = jest.fn();
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
@@ -869,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",
|
||||
|
||||
@@ -67,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,
|
||||
);
|
||||
@@ -94,7 +94,7 @@ 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
|
||||
@@ -103,7 +103,7 @@ describe("OlmDevice", function() {
|
||||
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
|
||||
|
||||
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
@@ -118,7 +118,7 @@ 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
|
||||
@@ -128,7 +128,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -30,11 +30,11 @@ import { Crypto } from "../../../src/crypto";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BackupManager } from "../../../src/crypto/backup";
|
||||
import { StubStore } from "../../../src/store/stub";
|
||||
import { IAbortablePromise, MatrixScheduler } from '../../../src';
|
||||
import { MatrixScheduler } from '../../../src';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
@@ -131,7 +131,7 @@ function makeTestClient(cryptoStore) {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: jest.fn(), // NOP
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
@@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
|
||||
// to tick the clock between the first try and the retry.
|
||||
const realSetTimeout = global.setTimeout;
|
||||
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
|
||||
return realSetTimeout(f, n/100);
|
||||
return realSetTimeout(f!, n!/100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,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',
|
||||
@@ -292,27 +298,27 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
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({}) as IAbortablePromise<any>;
|
||||
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((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({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
@@ -343,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(
|
||||
@@ -375,27 +381,27 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
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({}) as IAbortablePromise<any>;
|
||||
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((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({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
@@ -433,7 +439,7 @@ describe("MegolmBackup", function() {
|
||||
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);
|
||||
@@ -443,23 +449,23 @@ 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);
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({});
|
||||
}
|
||||
backupInfo = data;
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({});
|
||||
} else if (numCalls === 2) {
|
||||
expect(method).toBe("GET");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
resolve();
|
||||
return Promise.resolve(backupInfo) as IAbortablePromise<any>;
|
||||
return Promise.resolve(backupInfo);
|
||||
} else {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({});
|
||||
}
|
||||
};
|
||||
}),
|
||||
@@ -489,7 +495,7 @@ describe("MegolmBackup", function() {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: jest.fn(), // NOP
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
@@ -536,33 +542,33 @@ describe("MegolmBackup", function() {
|
||||
let numCalls = 0;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
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 IAbortablePromise<any>;
|
||||
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((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 IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
) as IAbortablePromise<any>;
|
||||
);
|
||||
}
|
||||
};
|
||||
return client.crypto.backupManager.backupGroupSession(
|
||||
return client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
@@ -693,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,8 +93,8 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
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 () => ({ failures: {} });
|
||||
@@ -141,7 +141,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T> => ({} as T);
|
||||
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
|
||||
@@ -152,7 +152,7 @@ describe("Cross Signing", function() {
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_FORBIDDEN") {
|
||||
if ((<MatrixError>e).errcode === "M_FORBIDDEN") {
|
||||
bootstrapDidThrow = true;
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ describe("Cross Signing", function() {
|
||||
// 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",
|
||||
@@ -238,12 +238,12 @@ describe("Cross Signing", function() {
|
||||
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"],
|
||||
@@ -258,7 +258,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -266,7 +266,7 @@ describe("Cross Signing", function() {
|
||||
keys: deviceInfo.keys,
|
||||
algorithms: deviceInfo.algorithms,
|
||||
};
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
await alice.crypto!.signObject(aliceDevice);
|
||||
olmlib.pkSign(
|
||||
aliceDevice as ISignedKey,
|
||||
selfSigningKey as unknown as PkSigning,
|
||||
@@ -401,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",
|
||||
@@ -435,7 +435,7 @@ describe("Cross Signing", function() {
|
||||
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
|
||||
@@ -467,11 +467,11 @@ describe("Cross Signing", function() {
|
||||
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.crypto!.deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice.crypto!.deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
@@ -486,7 +486,7 @@ describe("Cross Signing", function() {
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise<void>((resolve, reject) => {
|
||||
alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
|
||||
alice.crypto!.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
|
||||
if (userId === "@bob:example.com") {
|
||||
resolve();
|
||||
}
|
||||
@@ -494,7 +494,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -502,7 +502,7 @@ describe("Cross Signing", function() {
|
||||
keys: deviceInfo.keys,
|
||||
algorithms: deviceInfo.algorithms,
|
||||
};
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
await alice.crypto!.signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
@@ -667,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",
|
||||
@@ -690,7 +690,7 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice as unknown as IDevice,
|
||||
});
|
||||
// Bob's device key should be untrusted
|
||||
@@ -735,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",
|
||||
@@ -770,7 +770,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Alice verifies Bob's SSK
|
||||
@@ -802,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",
|
||||
@@ -838,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,
|
||||
});
|
||||
|
||||
@@ -876,20 +876,20 @@ describe("Cross Signing", function() {
|
||||
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 () => ({});
|
||||
@@ -909,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();
|
||||
@@ -919,9 +919,9 @@ describe("Cross Signing", function() {
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
|
||||
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
|
||||
await new Promise((resolve) => {
|
||||
alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve);
|
||||
alice.crypto!.on(CryptoEvent.UserTrustStatusChanged, resolve);
|
||||
});
|
||||
await upgradePromise;
|
||||
|
||||
@@ -963,7 +963,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys, but doesn't trust them yet
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -999,7 +999,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + alicePubkey]: sig,
|
||||
},
|
||||
} };
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
[aliceDeviceId]: aliceCrossSignedDevice,
|
||||
});
|
||||
|
||||
@@ -1042,7 +1042,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -1067,11 +1067,65 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function createSecretStorageKey(): Promise<IRecoveryKey> {
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey, key: undefined },
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -93,7 +93,7 @@ describe.each([
|
||||
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(r!.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,19 +21,11 @@ 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 { ICreateClientOpts } from '../../../src/client';
|
||||
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
|
||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
logger.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||
|
||||
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
|
||||
const client = (new TestClient(
|
||||
@@ -49,7 +41,7 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({});
|
||||
jest.spyOn(client.crypto!, 'downloadKeys').mockResolvedValue({});
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -101,30 +93,27 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.crypto.crossSigningInfo.setKeys({
|
||||
alice.crypto!.crossSigningInfo.setKeys({
|
||||
master: signingkeyInfo,
|
||||
});
|
||||
|
||||
const secretStorage = alice.crypto.secretStorage;
|
||||
const secretStorage = alice.crypto!.secretStorage;
|
||||
|
||||
jest.spyOn(alice, 'setAccountData').mockImplementation(
|
||||
async function(eventType, contents, callback) {
|
||||
async function(eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
if (callback) {
|
||||
callback(undefined, undefined);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
};
|
||||
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
await alice.crypto!.crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
@@ -191,7 +180,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.setAccountData = async function(eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
@@ -211,7 +200,7 @@ 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();
|
||||
});
|
||||
|
||||
@@ -244,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": {
|
||||
verified: 0,
|
||||
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": {
|
||||
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!,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -275,15 +264,17 @@ 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";
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
await request.promise; // return value not used
|
||||
|
||||
expect(secret).toBe("bar");
|
||||
osborne2.stop();
|
||||
vax.stop();
|
||||
clearTestClientTimeouts();
|
||||
@@ -329,7 +320,7 @@ describe("Secrets", function() {
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
bob.setAccountData = async function(eventType, contents) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
@@ -337,7 +328,7 @@ describe("Secrets", function() {
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -348,8 +339,8 @@ describe("Secrets", function() {
|
||||
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))
|
||||
@@ -446,6 +437,7 @@ describe("Secrets", function() {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -495,7 +487,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
@@ -537,16 +529,15 @@ describe("Secrets", function() {
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
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() as ISecretStorageKeyInfo;
|
||||
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({
|
||||
@@ -581,6 +572,7 @@ describe("Secrets", function() {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -639,7 +631,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
@@ -681,14 +673,13 @@ describe("Secrets", function() {
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
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==");
|
||||
|
||||
@@ -18,7 +18,7 @@ import "../../../olm-loader";
|
||||
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,14 +31,9 @@ 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], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
@@ -49,7 +44,7 @@ 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: [],
|
||||
|
||||
@@ -15,18 +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 { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { resetCrossSigningKeys } from "../crypto-utils";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
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;
|
||||
|
||||
@@ -40,14 +41,9 @@ 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 = {
|
||||
@@ -75,13 +71,13 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
describe("verification", () => {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
let clearTestClientTimeouts;
|
||||
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], clearTestClientTimeouts] = await makeTestClients(
|
||||
@@ -94,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: {
|
||||
@@ -121,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) {
|
||||
@@ -156,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) {
|
||||
@@ -195,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);
|
||||
@@ -219,8 +215,8 @@ 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
|
||||
@@ -230,10 +226,10 @@ describe("SAS verification", function() {
|
||||
// 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 base64", async () => {
|
||||
@@ -248,7 +244,7 @@ describe("SAS verification", function() {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()][bob.client.deviceId]
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!]
|
||||
.message_authentication_codes = ['hkdf-hmac-sha256'];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
@@ -256,7 +252,7 @@ describe("SAS verification", function() {
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
@@ -278,18 +274,18 @@ 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();
|
||||
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 () => {
|
||||
@@ -304,7 +300,7 @@ describe("SAS verification", function() {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()][bob.client.deviceId]
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
@@ -312,7 +308,7 @@ describe("SAS verification", function() {
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
@@ -334,18 +330,18 @@ 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("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 () => {
|
||||
@@ -361,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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -415,10 +413,10 @@ describe("SAS verification", function() {
|
||||
|
||||
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
request.verifier!.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(request.verifier);
|
||||
resolve(request.verifier!);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,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";
|
||||
};
|
||||
@@ -482,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(
|
||||
{
|
||||
@@ -565,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" },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,13 +14,14 @@ 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 { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src';
|
||||
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();
|
||||
|
||||
@@ -35,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";
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ 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";
|
||||
@@ -41,7 +39,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client.crypto) :
|
||||
event.attemptDecryption(client.crypto!) :
|
||||
Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
@@ -118,16 +116,3 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
|
||||
|
||||
return [clients, destroy];
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf as any);
|
||||
},
|
||||
} as unknown as Crypto;
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
// @ts-ignore undefined != Crypto
|
||||
global.crypto = undefined;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
|
||||
@@ -131,7 +130,11 @@ 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,
|
||||
@@ -143,14 +146,6 @@ async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
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");
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -45,16 +45,33 @@ describe('EventTimelineSet', () => {
|
||||
it('should return the related events', () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId(),
|
||||
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());
|
||||
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');
|
||||
@@ -117,6 +134,13 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
|
||||
describe('addEventToTimeline', () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("Adds event to timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -144,6 +168,58 @@ describe('EventTimelineSet', () => {
|
||||
);
|
||||
}).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', () => {
|
||||
@@ -193,7 +269,7 @@ describe('EventTimelineSet', () => {
|
||||
it('should not return the related events', () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId(),
|
||||
messageEvent.getId()!,
|
||||
"m.in_reply_to",
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
@@ -236,7 +312,7 @@ describe('EventTimelineSet', () => {
|
||||
"m.relates_to": {
|
||||
"event_id": root.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": root.getId(),
|
||||
"event_id": root.getId()!,
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
@@ -278,14 +354,14 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
|
||||
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 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 thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
|
||||
messageEvent.setThread(thread);
|
||||
const event = mkThreadResponse(messageEvent);
|
||||
@@ -310,7 +386,7 @@ describe('EventTimelineSet', () => {
|
||||
content: { body: "test" },
|
||||
event_id: "!test1:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
|
||||
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId()!);
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
|
||||
|
||||
const roomFilteredEvent = new MatrixEvent({
|
||||
@@ -318,7 +394,7 @@ describe('EventTimelineSet', () => {
|
||||
content: { body: "test" },
|
||||
event_id: "!test2:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
|
||||
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId()!);
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
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";
|
||||
|
||||
jest.mock("../../src/models/room-state");
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -21,9 +19,16 @@ describe("EventTimeline", function() {
|
||||
const getTimeline = (): EventTimeline => {
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
|
||||
return new EventTimeline(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() {
|
||||
@@ -55,13 +60,13 @@ describe("EventTimeline", function() {
|
||||
];
|
||||
timeline.initialiseState(events);
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState;
|
||||
const timelineStartState = timeline.startState!;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState;
|
||||
const timelineEndState = timeline.endState!;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
@@ -98,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");
|
||||
@@ -185,14 +200,14 @@ describe("EventTimeline", function() {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
@@ -225,14 +240,14 @@ describe("EventTimeline", function() {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
@@ -269,15 +284,15 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -298,15 +313,15 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -341,11 +356,11 @@ describe("EventTimeline", function() {
|
||||
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);
|
||||
});
|
||||
@@ -357,11 +372,11 @@ describe("EventTimeline", function() {
|
||||
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);
|
||||
});
|
||||
@@ -372,7 +387,7 @@ describe("EventTimeline", function() {
|
||||
it("should not make baseIndex assplode when removing the last event",
|
||||
function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.removeEvent(events[0].getId());
|
||||
timeline.removeEvent(events[0].getId()!);
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
const eventId = 'test_encrypted_event';
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,4 +1,23 @@
|
||||
/*
|
||||
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";
|
||||
@@ -43,4 +62,40 @@ describe("Filter", function() {
|
||||
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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
|
||||
{
|
||||
"base": "http://baseUrl",
|
||||
"params": {
|
||||
"access_token": "token",
|
||||
},
|
||||
"path": "/_matrix/media/r0/upload",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
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 { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
|
||||
describe("FetchHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
const idBaseUrl = "http://idBaseUrl";
|
||||
const prefix = ClientPrefix.V3;
|
||||
|
||||
it("should support aborting multiple times", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
|
||||
api.request(Method.Get, "/foo");
|
||||
api.request(Method.Get, "/baz");
|
||||
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
|
||||
|
||||
api.request(Method.Get, "/bar");
|
||||
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||
global.fetch = jest.fn();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
api.fetch("test");
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update identity server base url", () => {
|
||||
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
expect(api.opts.idBaseUrl).toBeUndefined();
|
||||
api.setIdBaseUrl("https://id.foo.bar");
|
||||
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
|
||||
});
|
||||
|
||||
describe("idServerRequest", () => {
|
||||
it("should throw if no idBaseUrl", () => {
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2))
|
||||
.toThrow("No identity server base URL set");
|
||||
});
|
||||
|
||||
it("should send params as query string for GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should send params as body for non-GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const params = { foo: "bar", via: ["a", "b"] };
|
||||
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
|
||||
});
|
||||
|
||||
it("should add Authorization header if token provided", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the Response object if onlyData=false", async () => {
|
||||
const res = { ok: true };
|
||||
const fetchFn = jest.fn().mockResolvedValue(res);
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
|
||||
});
|
||||
|
||||
it("should return text if json=false", async () => {
|
||||
const text = "418 I'm a teapot";
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||
json: false,
|
||||
})).resolves.toBe(text);
|
||||
});
|
||||
|
||||
it("should send token via query params if useAuthorizationHeader=false", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
|
||||
});
|
||||
|
||||
it("should send token via headers by default", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not send a token if not calling `authedRequest`", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
});
|
||||
api.request(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should ensure no token is leaked out via query params if sending via headers", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path", { access_token: "123" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via query params", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via header", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Authorization: "Bearer RealToken" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
|
||||
});
|
||||
|
||||
it("should not override Accept header", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Accept: "text/html" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
|
||||
});
|
||||
|
||||
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(JSON.stringify({
|
||||
errcode: "M_CONSENT_NOT_GIVEN",
|
||||
error: "Ye shall ask for consent",
|
||||
})),
|
||||
});
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
|
||||
|
||||
await Promise.all([
|
||||
emitPromise(emitter, HttpApiEvent.NoConsent),
|
||||
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("authedRequest", () => {
|
||||
it("should not include token if unset", () => {
|
||||
const fetchFn = jest.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
|
||||
api.authedRequest(Method.Post, "/account/password");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
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";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
|
||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("MatrixHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
const prefix = ClientPrefix.V3;
|
||||
|
||||
let xhr: Writeable<XMLHttpRequest>;
|
||||
let upload: Promise<UploadResponse>;
|
||||
|
||||
const DONE = 0;
|
||||
|
||||
global.DOMException = DOMException;
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = {
|
||||
upload: {} as XMLHttpRequestUpload,
|
||||
open: jest.fn(),
|
||||
send: jest.fn(),
|
||||
abort: jest.fn(),
|
||||
setRequestHeader: jest.fn(),
|
||||
onreadystatechange: undefined,
|
||||
getResponseHeader: jest.fn(),
|
||||
} as unknown as XMLHttpRequest;
|
||||
// We stub out XHR here as it is not available in JSDOM
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest.DONE = DONE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
upload?.catch(() => {});
|
||||
// Abort any remaining requests
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 0;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
});
|
||||
|
||||
it("should fall back to `fetch` where xhr is unavailable", () => {
|
||||
global.XMLHttpRequest = undefined!;
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(fetchFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should prefer xhr where available", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(fetchFn).not.toHaveBeenCalled();
|
||||
expect(xhr.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send access token in query params if header disabled", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open)
|
||||
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token");
|
||||
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
});
|
||||
|
||||
it("should send access token in header by default", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
});
|
||||
|
||||
it("should include filename by default", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File, { name: "name" });
|
||||
expect(xhr.open)
|
||||
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name");
|
||||
});
|
||||
|
||||
it("should allow not sending the filename", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
});
|
||||
|
||||
it("should abort xhr when the upload is aborted", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
api.cancelUpload(upload);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
return expect(upload).rejects.toThrow("Aborted");
|
||||
});
|
||||
|
||||
it("should timeout if no progress in 30s", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
jest.advanceTimersByTime(25000);
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
||||
jest.advanceTimersByTime(25000);
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call progressHandler", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const progressHandler = jest.fn();
|
||||
upload = api.uploadContent({} as File, { progressHandler });
|
||||
const progressEvent = new Event("progress") as ProgressEvent;
|
||||
Object.assign(progressEvent, { loaded: 1, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
|
||||
|
||||
Object.assign(progressEvent, { loaded: 95, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
|
||||
});
|
||||
|
||||
it("should error when no response body", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = "";
|
||||
xhr.status = 200;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("No response body.");
|
||||
});
|
||||
|
||||
it("should error on a 400-code", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
xhr.status = 404;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("Not found");
|
||||
});
|
||||
|
||||
it("should return response on successful upload", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
|
||||
xhr.status = 200;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
|
||||
});
|
||||
|
||||
it("should abort xhr when calling `cancelUpload`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.cancelUpload(upload)).toBeTruthy();
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 500;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
await upload.catch(() => {});
|
||||
|
||||
expect(api.cancelUpload(upload)).toBeFalsy();
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return active uploads in `getCurrentUploads`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy();
|
||||
api.cancelUpload(upload);
|
||||
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return expected object from `getContentUri`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
|
||||
expect(api.getContentUri()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
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 } from "jest-mock";
|
||||
|
||||
import {
|
||||
anySignal,
|
||||
ConnectionError,
|
||||
HTTPError,
|
||||
MatrixError,
|
||||
parseErrorResponse,
|
||||
retryNetworkOperation,
|
||||
timeoutSignal,
|
||||
} from "../../../src";
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
jest.mock("../../../src/utils");
|
||||
// setupTests mocks `timeoutSignal` due to hanging timers
|
||||
jest.unmock("../../../src/http-api/utils");
|
||||
|
||||
describe("timeoutSignal", () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
it("should fire abort signal after specified timeout", () => {
|
||||
const signal = timeoutSignal(3000);
|
||||
const onabort = jest.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("anySignal", () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
it("should fire when any signal fires", () => {
|
||||
const { signal } = anySignal([
|
||||
timeoutSignal(3000),
|
||||
timeoutSignal(2000),
|
||||
]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should cleanup when instructed", () => {
|
||||
const { signal, cleanup } = anySignal([
|
||||
timeoutSignal(3000),
|
||||
timeoutSignal(2000),
|
||||
]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
cleanup();
|
||||
jest.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should abort immediately if passed an aborted signal", () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const { signal } = anySignal([controller.signal]);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseErrorResponse", () => {
|
||||
it("should resolve Matrix Errors from XHR", () => {
|
||||
expect(parseErrorResponse({
|
||||
getResponseHeader(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
status: 500,
|
||||
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
|
||||
errcode: "TEST",
|
||||
}, 500));
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
|
||||
errcode: "TEST",
|
||||
}, 500));
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from XHR with urls", () => {
|
||||
expect(parseErrorResponse({
|
||||
responseURL: "https://example.com",
|
||||
getResponseHeader(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
status: 500,
|
||||
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
|
||||
errcode: "TEST",
|
||||
}, 500, "https://example.com"));
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch with urls", () => {
|
||||
expect(parseErrorResponse({
|
||||
url: "https://example.com",
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
|
||||
errcode: "TEST",
|
||||
}, 500, "https://example.com"));
|
||||
});
|
||||
|
||||
it("should set a sensible default error message on MatrixError", () => {
|
||||
let err = new MatrixError();
|
||||
expect(err.message).toEqual("MatrixError: Unknown message");
|
||||
err = new MatrixError({
|
||||
error: "Oh no",
|
||||
});
|
||||
expect(err.message).toEqual("MatrixError: Oh no");
|
||||
});
|
||||
|
||||
it("should handle no type gracefully", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new HTTPError("Server returned 500 error", 500));
|
||||
});
|
||||
|
||||
it("should handle invalid type gracefully", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? " " : null;
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
} as Response, '{"errcode": "TEST"}'))
|
||||
.toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
|
||||
});
|
||||
|
||||
it("should handle plaintext errors", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "text/plain" : null;
|
||||
},
|
||||
},
|
||||
status: 418,
|
||||
} as Response, "I'm a teapot")).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryNetworkOperation", () => {
|
||||
it("should retry given number of times with exponential sleeps", async () => {
|
||||
const err = new ConnectionError("test");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
|
||||
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
|
||||
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
|
||||
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
|
||||
});
|
||||
|
||||
it("should bail out on errors other than ConnectionError", async () => {
|
||||
const err = new TypeError("invalid JSON");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should return newest ConnectionError when giving up", async () => {
|
||||
const err1 = new ConnectionError("test1");
|
||||
const err2 = new ConnectionError("test2");
|
||||
const err3 = new ConnectionError("test3");
|
||||
const errors = [err1, err2, err3];
|
||||
const fn = jest.fn().mockImplementation(() => {
|
||||
throw errors.shift();
|
||||
});
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,7 @@ limitations under the License.
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { HTTPError, MatrixError } from "../../src/http-api";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
|
||||
@@ -219,8 +219,7 @@ describe("InteractiveAuth", () => {
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
}, 401);
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -260,7 +259,6 @@ describe("InteractiveAuth", () => {
|
||||
const requestEmailToken = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
authData: null,
|
||||
matrixClient: getFakeClient(),
|
||||
stateUpdated,
|
||||
doRequest,
|
||||
@@ -282,8 +280,7 @@ describe("InteractiveAuth", () => {
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
}, 401);
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -338,8 +335,7 @@ describe("InteractiveAuth", () => {
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
}, 401);
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -374,8 +370,7 @@ describe("InteractiveAuth", () => {
|
||||
},
|
||||
error: "Mock Error 1",
|
||||
errcode: "MOCKERR1",
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
}, 401);
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -402,8 +397,7 @@ describe("InteractiveAuth", () => {
|
||||
doRequest.mockImplementation((authData) => {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
|
||||
const err = new Error('myerror');
|
||||
(err as any).httpStatus = 401;
|
||||
const err = new HTTPError('myerror', 401);
|
||||
throw err;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { LocalNotificationSettings } from "../../src/@types/local_notifications";
|
||||
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
|
||||
import { TestClient } from '../TestClient';
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
describe("Local notification settings", () => {
|
||||
beforeEach(() => {
|
||||
client = (new TestClient(
|
||||
"@alice:matrix.org", "123", undefined, undefined, undefined,
|
||||
)).client;
|
||||
client.setAccountData = jest.fn();
|
||||
});
|
||||
|
||||
describe("Lets you set local notification settings", () => {
|
||||
it("stores settings in account data", () => {
|
||||
const deviceId = "device";
|
||||
const settings: LocalNotificationSettings = { is_silenced: true };
|
||||
client.setLocalNotificationSettings(deviceId, settings);
|
||||
|
||||
expect(client.setAccountData).toHaveBeenCalledWith(
|
||||
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
|
||||
settings,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+249
-225
@@ -36,7 +36,7 @@ import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { ContentHelpers, EventTimeline, Room } from "../../src";
|
||||
import { ContentHelpers, EventTimeline, MatrixError, Room } from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
import {
|
||||
@@ -88,22 +88,23 @@ describe("MatrixClient", function() {
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
let httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
// method: "GET",
|
||||
// path: "/initialSync",
|
||||
// data: {},
|
||||
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
|
||||
// expectBody: {} // additional expects on the body
|
||||
// expectQueryParams: {} // additional expects on query params
|
||||
// thenCall: function(){} // function to call *AFTER* returning response.
|
||||
// }
|
||||
// items are popped off when processed and block if no items left.
|
||||
];
|
||||
// items are popped off when processed and block if no items left.
|
||||
let httpLookups: {
|
||||
method: string;
|
||||
path: string;
|
||||
data?: object;
|
||||
error?: object;
|
||||
expectBody?: object;
|
||||
expectQueryParams?: object;
|
||||
thenCall?: Function;
|
||||
}[] = [];
|
||||
let acceptKeepalives: boolean;
|
||||
let pendingLookup = null;
|
||||
function httpReq(cb, method, path, qp, data, prefix) {
|
||||
let pendingLookup: {
|
||||
promise: Promise<any>;
|
||||
method: string;
|
||||
path: string;
|
||||
} | null = null;
|
||||
function httpReq(method, path, qp, data, prefix) {
|
||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||
return Promise.resolve({
|
||||
unstable_features: {
|
||||
@@ -132,7 +133,6 @@ describe("MatrixClient", function() {
|
||||
method: method,
|
||||
path: path,
|
||||
};
|
||||
pendingLookup.promise.abort = () => {}; // to make it a valid IAbortablePromise
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
@@ -145,7 +145,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
if (next.expectQueryParams) {
|
||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
||||
expect(qp[k]).toEqual(next.expectQueryParams![k]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,9 +156,9 @@ describe("MatrixClient", function() {
|
||||
if (next.error) {
|
||||
// eslint-disable-next-line
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
errcode: (<MatrixError>next.error).errcode,
|
||||
httpStatus: (<MatrixError>next.error).httpStatus,
|
||||
name: (<MatrixError>next.error).errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
});
|
||||
@@ -178,7 +178,7 @@ describe("MatrixClient", function() {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {} as any, // NOP
|
||||
fetchFn: function() {} as any, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
@@ -231,6 +231,130 @@ describe("MatrixClient", function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const threadId = "$threadId:server";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: threadId,
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": false,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
const userId = "@test:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
@@ -255,7 +379,7 @@ describe("MatrixClient", function() {
|
||||
type: UNSTABLE_MSC3088_PURPOSE.unstable,
|
||||
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -300,7 +424,7 @@ describe("MatrixClient", function() {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -360,7 +484,7 @@ describe("MatrixClient", function() {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -394,7 +518,7 @@ describe("MatrixClient", function() {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -600,14 +724,14 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
it("should transition null -> PREPARED after the first /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition null -> ERROR after a failed /filter", function(done) {
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
@@ -621,36 +745,35 @@ describe("MatrixClient", function() {
|
||||
// Disabled because now `startClient` makes a legit call to `/versions`
|
||||
// And those tests are really unhappy about it... Not possible to figure
|
||||
// out what a good resolution would look like
|
||||
xit("should transition ERROR -> CATCHUP after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
xit("should transition ERROR -> CATCHUP after /sync if prev failed", function(done) {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
@@ -659,7 +782,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
xit("should transition SYNCING -> ERROR after a failed /sync", function(done) {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
@@ -676,37 +799,35 @@ describe("MatrixClient", function() {
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed", function(done) {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes", function(done) {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
@@ -780,130 +901,6 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const threadId = "$threadId:server";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: threadId,
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": false,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
@@ -1153,8 +1150,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
// event type combined
|
||||
const expectedEventType = M_BEACON_INFO.name;
|
||||
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(callback).toBeFalsy();
|
||||
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(method).toBe('PUT');
|
||||
expect(path).toEqual(
|
||||
`/rooms/${encodeURIComponent(roomId)}/state/` +
|
||||
@@ -1168,7 +1164,7 @@ describe("MatrixClient", function() {
|
||||
await client.unstable_setLiveBeacon(roomId, content);
|
||||
|
||||
// event type combined
|
||||
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
const [, path, , requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(path).toEqual(
|
||||
`/rooms/${encodeURIComponent(roomId)}/state/` +
|
||||
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
|
||||
@@ -1229,7 +1225,7 @@ describe("MatrixClient", function() {
|
||||
it("is called with plain text topic and callback and sends state event", async () => {
|
||||
const sendStateEvent = createSendStateEventMock("pizza");
|
||||
client.sendStateEvent = sendStateEvent;
|
||||
await client.setRoomTopic(roomId, "pizza", () => {});
|
||||
await client.setRoomTopic(roomId, "pizza");
|
||||
expect(sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -1244,15 +1240,9 @@ describe("MatrixClient", function() {
|
||||
describe("setPassword", () => {
|
||||
const auth = { session: 'abcdef', type: 'foo' };
|
||||
const newPassword = 'newpassword';
|
||||
const callback = () => {};
|
||||
|
||||
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
|
||||
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
if (expectedCallback) {
|
||||
expect(callback).toBe(expectedCallback);
|
||||
} else {
|
||||
expect(callback).toBeFalsy();
|
||||
}
|
||||
const passwordTest = (expectedRequestContent: any) => {
|
||||
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(method).toBe('POST');
|
||||
expect(path).toEqual('/account/password');
|
||||
expect(queryParams).toBeFalsy();
|
||||
@@ -1269,8 +1259,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("no logout_devices specified + callback", async () => {
|
||||
await client.setPassword(auth, newPassword, callback);
|
||||
passwordTest({ auth, new_password: newPassword }, callback);
|
||||
await client.setPassword(auth, newPassword);
|
||||
passwordTest({ auth, new_password: newPassword });
|
||||
});
|
||||
|
||||
it("overload logoutDevices=true", async () => {
|
||||
@@ -1279,8 +1269,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("overload logoutDevices=true + callback", async () => {
|
||||
await client.setPassword(auth, newPassword, true, callback);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
|
||||
await client.setPassword(auth, newPassword, true);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: true });
|
||||
});
|
||||
|
||||
it("overload logoutDevices=false", async () => {
|
||||
@@ -1289,8 +1279,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("overload logoutDevices=false + callback", async () => {
|
||||
await client.setPassword(auth, newPassword, false, callback);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
|
||||
await client.setPassword(auth, newPassword, false);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1305,8 +1295,7 @@ describe("MatrixClient", function() {
|
||||
const result = await client.getLocalAliases(roomId);
|
||||
|
||||
// Current version of the endpoint we support is v3
|
||||
const [callback, method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
|
||||
expect(callback).toBeFalsy();
|
||||
const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
|
||||
expect(data).toBeFalsy();
|
||||
expect(method).toBe('GET');
|
||||
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
|
||||
@@ -1714,4 +1703,39 @@ describe("MatrixClient", function() {
|
||||
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("using E2EE in group calls", () => {
|
||||
const opts = {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
it("enables E2EE by default", () => {
|
||||
const client = new MatrixClient(opts);
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(true);
|
||||
});
|
||||
|
||||
it("enables E2EE when enabled explicitly", () => {
|
||||
const client = new MatrixClient({
|
||||
useE2eForGroupCall: true,
|
||||
...opts,
|
||||
});
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(true);
|
||||
});
|
||||
|
||||
it("disables E2EE if disabled explicitly", () => {
|
||||
const client = new MatrixClient({
|
||||
useE2eForGroupCall: false,
|
||||
...opts,
|
||||
});
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,7 +312,7 @@ describe("MSC3089Branch", () => {
|
||||
} as MatrixEvent);
|
||||
|
||||
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
|
||||
replacingEventId: (): string => null,
|
||||
replacingEventId: (): string | undefined => undefined,
|
||||
getId: () => "$unknown",
|
||||
}];
|
||||
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.errcode).toEqual("M_FORBIDDEN");
|
||||
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
|
||||
}
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
@@ -513,7 +513,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
function expectOrder(childRoomId: string, order: number) {
|
||||
const child = childTrees.find(c => c.roomId === childRoomId);
|
||||
expect(child).toBeDefined();
|
||||
expect(child.getOrder()).toEqual(order);
|
||||
expect(child!.getOrder()).toEqual(order);
|
||||
}
|
||||
|
||||
function makeMockChildRoom(roomId: string): Room {
|
||||
@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
rooms = {};
|
||||
rooms[tree.roomId] = parentRoom;
|
||||
(<any>tree).room = parentRoom; // override readonly
|
||||
client.getRoom = (r) => rooms[r];
|
||||
client.getRoom = (r) => rooms[r ?? ""];
|
||||
|
||||
clientSendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
@@ -608,7 +608,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual("Cannot set order of top level spaces currently");
|
||||
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -706,7 +706,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
await treeA!.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -743,7 +743,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
await treeA!.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -771,7 +771,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(2);
|
||||
await treeA!.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -800,7 +800,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeB = childTrees.find(c => c.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
await treeB!.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -829,7 +829,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeC = childTrees.find(ch => ch.roomId === c);
|
||||
expect(treeC).toBeDefined();
|
||||
await treeC.setOrder(1);
|
||||
await treeC!.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -858,7 +858,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeB = childTrees.find(ch => ch.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
await treeB!.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -890,9 +890,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
return Promise.resolve({ content_uri: mxc });
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
@@ -904,7 +903,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
metadata: true, // additional content from test
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
@@ -950,9 +949,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
return Promise.resolve({ content_uri: mxc });
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
@@ -967,7 +965,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(contents).toMatchObject({
|
||||
...content,
|
||||
"m.new_content": content,
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
@@ -1012,7 +1010,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const file = tree.getFile(fileEventId);
|
||||
expect(file).toBeDefined();
|
||||
expect(file.indexEvent).toBe(fileEvent);
|
||||
expect(file!.indexEvent).toBe(fileEvent);
|
||||
});
|
||||
|
||||
it('should return falsy for unknown files', () => {
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { MatrixEvent } from "../../../src";
|
||||
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
||||
import {
|
||||
@@ -261,7 +263,7 @@ describe('Beacon', () => {
|
||||
roomId,
|
||||
);
|
||||
// less than the original event
|
||||
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
|
||||
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts! - 1000;
|
||||
|
||||
beacon.update(oldUpdateEvent);
|
||||
// didnt update
|
||||
@@ -431,6 +433,27 @@ describe('Beacon', () => {
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should ignore invalid beacon events", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
const ev = new MatrixEvent({
|
||||
type: M_BEACON_INFO.name,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: beacon.beaconInfoId,
|
||||
},
|
||||
},
|
||||
});
|
||||
beacon.addLocations([ev]);
|
||||
|
||||
expect(beacon.latestLocationEvent).toBeFalsy();
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when beacon is live with a start timestamp is in the future', () => {
|
||||
it('ignores locations before the beacon start timestamp', () => {
|
||||
const startTimestamp = now + 60000;
|
||||
|
||||
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { EventType } from "../../../src";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
|
||||
describe('MatrixEvent', () => {
|
||||
it('should create copies of itself', () => {
|
||||
@@ -84,4 +87,81 @@ describe('MatrixEvent', () => {
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: {
|
||||
body: "Test",
|
||||
},
|
||||
event_id: "$event1:server",
|
||||
});
|
||||
await ev.attemptDecryption({
|
||||
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
|
||||
} as unknown as Crypto);
|
||||
expect(ev.isEncrypted()).toBeTruthy();
|
||||
expect(ev.isBeingDecrypted()).toBeFalsy();
|
||||
expect(ev.isDecryptionFailure()).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("applyVisibilityEvent", () => {
|
||||
it("should emit VisibilityChange if a change was made", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "Test",
|
||||
},
|
||||
event_id: "$event1:server",
|
||||
});
|
||||
|
||||
const prom = emitPromise(ev, MatrixEventEvent.VisibilityChange);
|
||||
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId()!, reason: null });
|
||||
await prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
const eventId = 'test_encrypted_event';
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { Thread } from "../../../src/models/thread";
|
||||
import { mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
|
||||
describe('Thread', () => {
|
||||
describe("constructor", () => {
|
||||
@@ -25,4 +29,52 @@ describe('Thread', () => {
|
||||
}).toThrow("element-web#22141: A thread requires a room in order to function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasUserReadEvent", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(
|
||||
myUserId,
|
||||
"DEVICE",
|
||||
"ACCESS_TOKEN",
|
||||
undefined,
|
||||
{ timelineSupport: false },
|
||||
);
|
||||
client = testClient.client;
|
||||
room = new Room("123", client, myUserId);
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("considers own events with no RR as read", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: [myUserId],
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("considers other events with no RR as unread", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { Feature, ServerSupport } from "../../src/feature";
|
||||
import {
|
||||
EventType,
|
||||
fixNotificationCountOnDecryption,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
NotificationCountType,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from "../../src/matrix";
|
||||
import { IActionsObject } from "../../src/pushprocessor";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
|
||||
import { mkEvent, mock } from "../test-utils/test-utils";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
let event: MatrixEvent;
|
||||
let threadEvent: MatrixEvent;
|
||||
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let THREAD_ID;
|
||||
|
||||
function mkPushAction(notify, highlight): IActionsObject {
|
||||
return {
|
||||
notify,
|
||||
tweaks: {
|
||||
highlight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("fixNotificationCountOnDecryption", () => {
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
mockClient.reEmitter = mock(ReEmitter, 'ReEmitter');
|
||||
mockClient.canSupport = new Map();
|
||||
Object.keys(Feature).forEach(feature => {
|
||||
mockClient.canSupport.set(feature as Feature, ServerSupport.Stable);
|
||||
});
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "");
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
event = mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello world!",
|
||||
},
|
||||
event: true,
|
||||
}, mockClient);
|
||||
|
||||
THREAD_ID = event.getId();
|
||||
threadEvent = mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: THREAD_ID,
|
||||
},
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Thread reply",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
room.createThread(THREAD_ID, event, [threadEvent], false);
|
||||
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
});
|
||||
|
||||
it("changes the room count to highlight on decryption", () => {
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("changes the thread count to highlight on decryption", () => {
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, threadEvent);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("emits events", () => {
|
||||
const cb = jest.fn();
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
expect(cb).toHaveBeenLastCalledWith({ highlight: 0, total: 1 });
|
||||
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 5);
|
||||
expect(cb).toHaveBeenLastCalledWith({ highlight: 5, total: 1 });
|
||||
|
||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 5);
|
||||
expect(cb).toHaveBeenLastCalledWith({ highlight: 5 }, "$123");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
|
||||
import { mkPusher } from '../test-utils/test-utils';
|
||||
|
||||
const realSetTimeout = setTimeout;
|
||||
function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
let client: MatrixClient;
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
describe("Pushers", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
});
|
||||
});
|
||||
|
||||
describe("supports remotely toggling push notifications", () => {
|
||||
it("migration support when connecting to a legacy homeserver", async () => {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3881": false,
|
||||
},
|
||||
});
|
||||
httpBackend.when("GET", "/pushers").respond(200, {
|
||||
pushers: [
|
||||
mkPusher(),
|
||||
mkPusher({ [PUSHER_ENABLED.name]: true }),
|
||||
mkPusher({ [PUSHER_ENABLED.name]: false }),
|
||||
],
|
||||
});
|
||||
|
||||
const promise = client.getPushers();
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
|
||||
const response = await promise;
|
||||
|
||||
expect(response.pushers[0][PUSHER_ENABLED.name]).toBe(true);
|
||||
expect(response.pushers[1][PUSHER_ENABLED.name]).toBe(true);
|
||||
expect(response.pushers[2][PUSHER_ENABLED.name]).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+136
-18
@@ -1,6 +1,6 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, MatrixClient, MatrixEvent } from "../../src";
|
||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
@@ -163,6 +163,22 @@ describe('NotificationService', function() {
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.room_one_to_one",
|
||||
},
|
||||
{
|
||||
rule_id: ".org.matrix.msc3914.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
{
|
||||
kind: "call_started",
|
||||
},
|
||||
],
|
||||
actions: ["notify", { set_tweak: "sound", value: "default" }],
|
||||
},
|
||||
],
|
||||
"room": [],
|
||||
"sender": [],
|
||||
@@ -209,32 +225,32 @@ describe('NotificationService', function() {
|
||||
msgtype: "m.text",
|
||||
},
|
||||
});
|
||||
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules);
|
||||
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules!);
|
||||
pushProcessor = new PushProcessor(matrixClient);
|
||||
});
|
||||
|
||||
// User IDs
|
||||
|
||||
it('should bing on a user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
|
||||
testEvent.event.content!.body = "Hello @ali:matrix.org, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID with an @.', function() {
|
||||
testEvent.event.content.body = "Hello @ali, how are you?";
|
||||
testEvent.event.content!.body = "Hello @ali, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID without @.', function() {
|
||||
testEvent.event.content.body = "Hello ali, how are you?";
|
||||
testEvent.event.content!.body = "Hello ali, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
|
||||
testEvent.event.content!.body = "Hello @AlI:matrix.org, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
@@ -242,13 +258,13 @@ describe('NotificationService', function() {
|
||||
// Display names
|
||||
|
||||
it('should bing on a display name.', function() {
|
||||
testEvent.event.content.body = "Hello Alice M, how are you?";
|
||||
testEvent.event.content!.body = "Hello Alice M, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive display name.', function() {
|
||||
testEvent.event.content.body = "Hello ALICE M, how are you?";
|
||||
testEvent.event.content!.body = "Hello ALICE M, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
@@ -256,43 +272,43 @@ describe('NotificationService', function() {
|
||||
// Bing words
|
||||
|
||||
it('should bing on a bing word.', function() {
|
||||
testEvent.event.content.body = "I really like coffee";
|
||||
testEvent.event.content!.body = "I really like coffee";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on case-insensitive bing words.', function() {
|
||||
testEvent.event.content.body = "Coffee is great";
|
||||
testEvent.event.content!.body = "Coffee is great";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on wildcard (.*) bing words.', function() {
|
||||
testEvent.event.content.body = "It was foomahbar I think.";
|
||||
testEvent.event.content!.body = "It was foomahbar I think.";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character group ([abc]) bing words.', function() {
|
||||
testEvent.event.content.body = "Ping!";
|
||||
testEvent.event.content!.body = "Ping!";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "Pong!";
|
||||
testEvent.event.content!.body = "Pong!";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character range ([a-z]) bing words.', function() {
|
||||
testEvent.event.content.body = "I ate 6 pies";
|
||||
testEvent.event.content!.body = "I ate 6 pies";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character negation ([!a]) bing words.', function() {
|
||||
testEvent.event.content.body = "boke";
|
||||
testEvent.event.content!.body = "boke";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "bake";
|
||||
testEvent.event.content!.body = "bake";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
@@ -316,7 +332,7 @@ describe('NotificationService', function() {
|
||||
// invalid
|
||||
|
||||
it('should gracefully handle bad input.', function() {
|
||||
testEvent.event.content.body = { "foo": "bar" };
|
||||
testEvent.event.content!.body = { "foo": "bar" };
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
@@ -336,4 +352,106 @@ describe('NotificationService', function() {
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
});
|
||||
|
||||
describe("group call started push rule", () => {
|
||||
beforeEach(() => {
|
||||
matrixClient.pushRules!.global!.underride!.find(r => r.rule_id === ".m.rule.fallback")!.enabled = false;
|
||||
});
|
||||
|
||||
const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => {
|
||||
testEvent = utils.mkEvent({
|
||||
type: "org.matrix.msc3401.call",
|
||||
room: testRoomId,
|
||||
user: "@alice:foo",
|
||||
skey: "state_key",
|
||||
event: true,
|
||||
content: content,
|
||||
prev_content: prevContent,
|
||||
});
|
||||
|
||||
return pushProcessor.actionsForEvent(testEvent);
|
||||
};
|
||||
|
||||
const assertDoesNotify = (actions: IActionsObject): void => {
|
||||
expect(actions?.notify).toBeTruthy();
|
||||
expect(actions?.tweaks?.sound).toBeTruthy();
|
||||
expect(actions?.tweaks?.highlight).toBeFalsy();
|
||||
};
|
||||
|
||||
const assertDoesNotNotify = (actions: IActionsObject): void => {
|
||||
expect(actions?.notify).toBeFalsy();
|
||||
expect(actions?.tweaks?.sound).toBeFalsy();
|
||||
expect(actions?.tweaks?.highlight).toBeFalsy();
|
||||
};
|
||||
|
||||
it.each(
|
||||
["m.ring", "m.prompt"],
|
||||
)("should notify when new group call event appears with %s intent", (intent: string) => {
|
||||
assertDoesNotify(getActionsForEvent({}, {
|
||||
"m.intent": intent,
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should notify when a call is un-terminated", () => {
|
||||
assertDoesNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
"m.terminated": "All users left",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should not notify when call is terminated", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
"m.terminated": "All users left",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should ignore with m.room intent", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({}, {
|
||||
"m.intent": "m.room",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
describe("ignoring non-relevant state changes", () => {
|
||||
it("should ignore intent changes", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.video",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should ignore name changes", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "New call",
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,11 +17,13 @@ limitations under the License.
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import { indexedDB as fakeIndexedDB } from 'fake-indexeddb';
|
||||
|
||||
import { IHttpOpts, IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
|
||||
import { IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
|
||||
import { logger } from '../../src/logger';
|
||||
import { IStore } from '../../src/store';
|
||||
import { flushPromises } from '../test-utils/flushPromises';
|
||||
import { removeElement } from "../../src/utils";
|
||||
|
||||
const FAKE_USER = "@alice:example.org";
|
||||
const FAKE_DEVICE_ID = "AAAAAAAA";
|
||||
@@ -47,19 +49,6 @@ enum StoreType {
|
||||
IndexedDB = 'IndexedDB',
|
||||
}
|
||||
|
||||
// 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;
|
||||
function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
async function flushAndRunTimersUntil(cond: () => boolean) {
|
||||
while (!cond()) {
|
||||
await flushPromises();
|
||||
@@ -75,6 +64,8 @@ describe.each([
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
let store: IStore;
|
||||
@@ -89,7 +80,7 @@ describe.each([
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: httpBackend.requestFn as IHttpOpts["request"],
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
@@ -140,11 +131,11 @@ describe.each([
|
||||
],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// flush, as per comment in first test
|
||||
await flushPromises();
|
||||
@@ -164,7 +155,7 @@ describe.each([
|
||||
],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// Asserting that another request is never made is obviously
|
||||
// a bit tricky - we just flush the queue what should hopefully
|
||||
@@ -200,7 +191,7 @@ describe.each([
|
||||
],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
logger.info("Advancing clock to just before expected retry time...");
|
||||
@@ -215,7 +206,7 @@ describe.each([
|
||||
jest.advanceTimersByTime(2000);
|
||||
await flushPromises();
|
||||
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries on retryImmediately()", async function() {
|
||||
@@ -223,7 +214,7 @@ describe.each([
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(null, 1, 20)]);
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
@@ -239,13 +230,13 @@ describe.each([
|
||||
FAKE_MSG,
|
||||
],
|
||||
});
|
||||
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.retryImmediately();
|
||||
|
||||
// longer timeout here to try & avoid flakiness
|
||||
expect(await httpBackend.flush(null, 1, 3000)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 3000)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries on when client is started", async function() {
|
||||
@@ -269,13 +260,13 @@ describe.each([
|
||||
FAKE_MSG,
|
||||
],
|
||||
});
|
||||
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.stopClient();
|
||||
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
|
||||
|
||||
expect(await httpBackend.flush(null, 1, 20)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries when a message is retried", async function() {
|
||||
@@ -283,7 +274,7 @@ describe.each([
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(null, 1, 20)]);
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
@@ -300,7 +291,7 @@ describe.each([
|
||||
],
|
||||
});
|
||||
|
||||
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
const dummyEvent = new MatrixEvent({
|
||||
@@ -311,7 +302,7 @@ describe.each([
|
||||
} as unknown as Room;
|
||||
client.resendEvent(dummyEvent, mockRoom);
|
||||
|
||||
expect(await httpBackend.flush(null, 1, 20)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("splits many messages into multiple HTTP requests", async function() {
|
||||
@@ -328,12 +319,12 @@ describe.each([
|
||||
});
|
||||
}
|
||||
|
||||
const expectedCounts = [20, 1];
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
).check((request) => {
|
||||
expect(Object.keys(request.data.messages).length).toEqual(20);
|
||||
expect(removeElement(expectedCounts, c => c === Object.keys(request.data.messages).length)).toBeTruthy();
|
||||
}).respond(200, {});
|
||||
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
).check((request) => {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
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 MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { ReceiptType } from '../../src/@types/read_receipts';
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Feature, ServerSupport } from '../../src/feature';
|
||||
import { EventType } from '../../src/matrix';
|
||||
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
|
||||
import { encodeUri } from '../../src/utils';
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
|
||||
// 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;
|
||||
function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
let client: MatrixClient;
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
const THREAD_ID = "$thread_event_id";
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
const threadEvent = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@bob:matrix.org",
|
||||
room: ROOM_ID,
|
||||
content: {
|
||||
"body": "Hello from a thread",
|
||||
"m.relates_to": {
|
||||
"event_id": THREAD_ID,
|
||||
"m.in_reply_to": {
|
||||
"event_id": THREAD_ID,
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const roomEvent = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@bob:matrix.org",
|
||||
room: ROOM_ID,
|
||||
content: {
|
||||
"body": "Hello from a room",
|
||||
},
|
||||
});
|
||||
|
||||
function mockServerSideSupport(client, serverSideSupport: ServerSupport) {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
|
||||
}
|
||||
|
||||
describe("Read receipt", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
});
|
||||
client.isGuest = () => false;
|
||||
});
|
||||
|
||||
describe("sendReceipt", () => {
|
||||
it("sends a thread read receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toEqual(THREAD_ID);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends an unthreaded receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReadReceipt(threadEvent, ReceiptType.Read, true);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: roomEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(roomEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt when there's no server support", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a valid room read receipt even when body omitted", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data).toEqual({});
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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 callbacks from "../../src/realtime-callbacks";
|
||||
|
||||
let wallTime = 1234567890;
|
||||
@@ -37,7 +53,7 @@ describe("realtime-callbacks", function() {
|
||||
|
||||
it("should set 'this' to the global object", function() {
|
||||
let passed = false;
|
||||
const callback = function() {
|
||||
const callback = function(this: typeof global) {
|
||||
expect(this).toBe(global); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
passed = true;
|
||||
|
||||
@@ -18,10 +18,11 @@ import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("Relations", function() {
|
||||
it("should deduplicate annotations", function() {
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const relations = new Relations("m.annotation", "m.reaction", room);
|
||||
|
||||
// Create an instance of an annotation
|
||||
@@ -43,7 +44,7 @@ describe("Relations", function() {
|
||||
// Add the event once and check results
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
@@ -53,7 +54,7 @@ describe("Relations", function() {
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
@@ -66,7 +67,7 @@ describe("Relations", function() {
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventB);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
@@ -98,7 +99,7 @@ describe("Relations", function() {
|
||||
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
@@ -112,7 +113,7 @@ describe("Relations", function() {
|
||||
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
@@ -126,7 +127,7 @@ describe("Relations", function() {
|
||||
});
|
||||
|
||||
it("should re-use Relations between all timeline sets in a room", async () => {
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const timelineSet1 = new EventTimelineSet(room);
|
||||
const timelineSet2 = new EventTimelineSet(room);
|
||||
expect(room.relations).toBe(timelineSet1.relations);
|
||||
@@ -135,7 +136,7 @@ describe("Relations", function() {
|
||||
|
||||
it("should ignore m.replace for state events", async () => {
|
||||
const userId = "@bob:example.com";
|
||||
const room = new Room("room123", null, userId);
|
||||
const room = new Room("room123", null!, userId);
|
||||
const relations = new Relations("m.replace", "m.room.topic", room);
|
||||
|
||||
// Create an instance of a state event with rel_type m.replace
|
||||
@@ -179,4 +180,28 @@ describe("Relations", function() {
|
||||
expect(badlyEditedTopic.replacingEvent()).toBe(null);
|
||||
expect(badlyEditedTopic.getContent().topic).toBe("topic");
|
||||
});
|
||||
|
||||
it("getSortedAnnotationsByKey should return null for non-annotation relations", async () => {
|
||||
const userId = "@user:server";
|
||||
const room = new Room("room123", new TestClient(userId).client, userId);
|
||||
const relations = new Relations("m.replace", "m.room.message", room);
|
||||
|
||||
// Create an instance of an annotation
|
||||
const eventData = {
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.room.message",
|
||||
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"rel_type": "m.replace",
|
||||
},
|
||||
},
|
||||
};
|
||||
const eventA = new MatrixEvent(eventData);
|
||||
|
||||
relations.addEvent(eventA);
|
||||
expect(relations.getSortedAnnotationsByKey()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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 { logger } from "../../../src/logger";
|
||||
import {
|
||||
RendezvousFailureListener,
|
||||
RendezvousFailureReason,
|
||||
RendezvousTransport,
|
||||
RendezvousTransportDetails,
|
||||
} from "../../../src/rendezvous";
|
||||
import { sleep } from '../../../src/utils';
|
||||
|
||||
export class DummyTransport<D extends RendezvousTransportDetails, T> implements RendezvousTransport<T> {
|
||||
otherParty?: DummyTransport<D, T>;
|
||||
etag?: string;
|
||||
lastEtagReceived?: string;
|
||||
data: T | undefined;
|
||||
|
||||
ready = false;
|
||||
cancelled = false;
|
||||
|
||||
constructor(private name: string, private mockDetails: D) {}
|
||||
onCancelled?: RendezvousFailureListener;
|
||||
|
||||
details(): Promise<RendezvousTransportDetails> {
|
||||
return Promise.resolve(this.mockDetails);
|
||||
}
|
||||
|
||||
async send(data: T): Promise<void> {
|
||||
logger.info(
|
||||
`[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${
|
||||
JSON.stringify(data)} where etag matches ${this.etag}`,
|
||||
);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (!this.cancelled) {
|
||||
if (!this.etag || (this.otherParty?.etag && this.otherParty?.etag === this.etag)) {
|
||||
this.data = data;
|
||||
this.etag = Math.random().toString();
|
||||
this.lastEtagReceived = this.etag;
|
||||
this.otherParty!.etag = this.etag;
|
||||
this.otherParty!.data = data;
|
||||
logger.info(`[${this.name}] => [${this.otherParty?.name}] Sent with etag ${this.etag}`);
|
||||
return;
|
||||
}
|
||||
logger.info(`[${this.name}] Sleeping to retry send after etag ${this.etag}`);
|
||||
await sleep(250);
|
||||
}
|
||||
}
|
||||
|
||||
async receive(): Promise<T | undefined> {
|
||||
logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (!this.cancelled) {
|
||||
if (!this.lastEtagReceived || this.lastEtagReceived !== this.etag) {
|
||||
this.lastEtagReceived = this.etag;
|
||||
logger.info(
|
||||
`[${this.otherParty?.name}] => [${this.name}] Received data: ` +
|
||||
`${JSON.stringify(this.data)} with etag ${this.etag}`,
|
||||
);
|
||||
return this.data;
|
||||
}
|
||||
logger.info(`[${this.name}] Sleeping to retry receive after etag ${
|
||||
this.lastEtagReceived} as remote is ${this.etag}`);
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
cancel(reason: RendezvousFailureReason): Promise<void> {
|
||||
this.cancelled = true;
|
||||
this.onCancelled?.(reason);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.cancelled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
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 { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels';
|
||||
import { decodeBase64 } from '../../../src/crypto/olmlib';
|
||||
import { DummyTransport } from './DummyTransport';
|
||||
|
||||
function makeTransport(name: string) {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'dummy' });
|
||||
}
|
||||
|
||||
describe('ECDHv1', function() {
|
||||
beforeAll(async function() {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
describe('with crypto', () => {
|
||||
it("initiator wants to sign in", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await alice.send(message);
|
||||
const bobReceive = await bob.receive();
|
||||
expect(bobReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("initiator wants to reciprocate", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await bob.send(message);
|
||||
const aliceReceive = await alice.receive();
|
||||
expect(aliceReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("double connect", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
expect(alice.connect()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("closed", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
alice.close();
|
||||
|
||||
expect(alice.connect()).rejects.toThrow();
|
||||
expect(alice.send({})).rejects.toThrow();
|
||||
expect(alice.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("require ciphertext", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
// send a message without encryption
|
||||
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
expect(bob.receive()).rejects.toThrowError();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("ciphertext before set up", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
|
||||
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
|
||||
expect(alice.receive()).rejects.toThrowError();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,602 @@
|
||||
/*
|
||||
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 MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import '../../olm-loader';
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
RendezvousCode,
|
||||
RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from "../../../src/rendezvous";
|
||||
import {
|
||||
ECDHv1RendezvousCode,
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv1RendezvousChannel,
|
||||
} from "../../../src/rendezvous/channels";
|
||||
import { MatrixClient } from "../../../src";
|
||||
import {
|
||||
MSC3886SimpleHttpRendezvousTransport,
|
||||
MSC3886SimpleHttpRendezvousTransportDetails,
|
||||
} from "../../../src/rendezvous/transports";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
import { decodeBase64 } from "../../../src/crypto/olmlib";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
|
||||
function makeMockClient(opts: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
deviceKey?: string;
|
||||
msc3882Enabled: boolean;
|
||||
msc3886Enabled: boolean;
|
||||
devices?: Record<string, Partial<DeviceInfo>>;
|
||||
verificationFunction?: (
|
||||
userId: string, deviceId: string, verified: boolean, blocked: boolean, known: boolean,
|
||||
) => void;
|
||||
crossSigningIds?: Record<string, string>;
|
||||
}): MatrixClient {
|
||||
return {
|
||||
getVersions() {
|
||||
return {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": opts.msc3882Enabled,
|
||||
"org.matrix.msc3886": opts.msc3886Enabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
getUserId() { return opts.userId; },
|
||||
getDeviceId() { return opts.deviceId; },
|
||||
getDeviceEd25519Key() { return opts.deviceKey; },
|
||||
baseUrl: "https://example.com",
|
||||
crypto: {
|
||||
getStoredDevice(userId: string, deviceId: string) {
|
||||
return opts.devices?.[deviceId] ?? null;
|
||||
},
|
||||
setDeviceVerification: opts.verificationFunction,
|
||||
crossSigningInfo: {
|
||||
getId(key: string) {
|
||||
return opts.crossSigningIds?.[key];
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
function makeTransport(name: string, uri = 'https://test.rz/123456') {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'http.v1', uri });
|
||||
}
|
||||
|
||||
describe("Rendezvous", function() {
|
||||
beforeAll(async function() {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetchFn;
|
||||
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
transports = [];
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
transports.forEach(x => x.cleanup());
|
||||
});
|
||||
|
||||
it("generate and cancel", async function() {
|
||||
const alice = makeMockClient({
|
||||
userId: "@alice:example.com",
|
||||
deviceId: "DEVICEID",
|
||||
msc3886Enabled: false,
|
||||
msc3882Enabled: true,
|
||||
});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client: alice,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
|
||||
expect(aliceRz.code).toBeUndefined();
|
||||
|
||||
const codePromise = aliceRz.generateCode();
|
||||
await httpBackend.flush('');
|
||||
|
||||
await aliceRz.generateCode();
|
||||
|
||||
expect(typeof aliceRz.code).toBe('string');
|
||||
|
||||
await codePromise;
|
||||
|
||||
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
|
||||
|
||||
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
|
||||
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri)
|
||||
.toEqual("https://fallbackserver/rz/123");
|
||||
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
|
||||
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
|
||||
await httpBackend.flush('');
|
||||
expect(cancelPromise).resolves.toBeUndefined();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
|
||||
await aliceRz.close();
|
||||
});
|
||||
|
||||
it("no protocols", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.finish',
|
||||
outcome: 'unsupported',
|
||||
});
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.finish', outcome: 'unsupported' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'bad protocol' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("decline on existing device", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
await aliceRz.declineLoginOnExistingDevice();
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: 'm.login.finish', outcome: 'declined' });
|
||||
});
|
||||
|
||||
it("approve on existing device + no verification", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobCompleteProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: 'm.login.finish', outcome: 'success' });
|
||||
})();
|
||||
|
||||
await confirmProm;
|
||||
await bobCompleteProm;
|
||||
});
|
||||
|
||||
async function completeLogin(devices: Record<string, Partial<DeviceInfo>>) {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const aliceVerification = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
devices,
|
||||
deviceKey: 'aaaa',
|
||||
verificationFunction: aliceVerification,
|
||||
crossSigningIds: {
|
||||
master: 'mmmmm',
|
||||
},
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobLoginProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: 'm.login.finish', outcome: 'success', device_id: 'BOB', device_key: 'bbbb' });
|
||||
})();
|
||||
|
||||
expect(await confirmProm).toEqual('BOB');
|
||||
await bobLoginProm;
|
||||
|
||||
return {
|
||||
aliceTransport,
|
||||
aliceEcdh,
|
||||
aliceRz,
|
||||
bobTransport,
|
||||
bobEcdh,
|
||||
};
|
||||
}
|
||||
|
||||
it("approve on existing device + verification", async function() {
|
||||
const { bobEcdh, aliceRz } = await completeLogin({
|
||||
BOB: {
|
||||
getFingerprint: () => "bbbb",
|
||||
},
|
||||
});
|
||||
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
|
||||
|
||||
const bobVerifyProm = (async () => {
|
||||
const verified = await bobEcdh.receive();
|
||||
expect(verified).toEqual({
|
||||
type: 'm.login.finish',
|
||||
outcome: 'verified',
|
||||
verifying_device_id: 'ALICE',
|
||||
verifying_device_key: 'aaaa',
|
||||
master_key: 'mmmmm',
|
||||
});
|
||||
})();
|
||||
|
||||
await verifyProm;
|
||||
await bobVerifyProm;
|
||||
});
|
||||
|
||||
it("device not online within timeout", async function() {
|
||||
const { aliceRz } = await completeLogin({});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("device appears online within timeout", async function() {
|
||||
const devices: Record<string, Partial<DeviceInfo>> = {};
|
||||
const { aliceRz } = await completeLogin(devices);
|
||||
// device appears after 1 second
|
||||
setTimeout(() => {
|
||||
devices.BOB = {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
}, 1000);
|
||||
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
|
||||
});
|
||||
|
||||
it("device appears online after timeout", async function() {
|
||||
const devices: Record<string, Partial<DeviceInfo>> = {};
|
||||
const { aliceRz } = await completeLogin(devices);
|
||||
// device appears after 1 second
|
||||
setTimeout(() => {
|
||||
devices.BOB = {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
}, 1500);
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("mismatched device key", async function() {
|
||||
const { aliceRz } = await completeLogin({
|
||||
BOB: {
|
||||
getFingerprint: () => "XXXX",
|
||||
},
|
||||
});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(/different key/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
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 MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { MatrixClient } from "../../../src";
|
||||
import { RendezvousFailureReason } from "../../../src/rendezvous";
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
|
||||
|
||||
function makeMockClient(opts: { userId: string, deviceId: string, msc3886Enabled: boolean}): MatrixClient {
|
||||
return {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886");
|
||||
},
|
||||
getUserId() { return opts.userId; },
|
||||
getDeviceId() { return opts.deviceId; },
|
||||
requestLoginToken() {
|
||||
return Promise.resolve({ login_token: "token" });
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
describe("SimpleHttpRendezvousTransport", function() {
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetch;
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
});
|
||||
|
||||
async function postAndCheckLocation(
|
||||
msc3886Enabled: boolean,
|
||||
fallbackRzServer: string,
|
||||
locationResponse: string,
|
||||
expectedFinalLocation: string,
|
||||
) {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn });
|
||||
{ // initial POST
|
||||
const expectedPostLocation = msc3886Enabled ?
|
||||
`${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous` :
|
||||
fallbackRzServer;
|
||||
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", expectedPostLocation).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: locationResponse,
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
await prom;
|
||||
}
|
||||
const details = await simpleHttpTransport.details();
|
||||
expect(details.uri).toBe(expectedFinalLocation);
|
||||
|
||||
{ // first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", expectedFinalLocation).response = {
|
||||
body: {},
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toEqual({});
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
}
|
||||
}
|
||||
it("should throw an error when no server available", function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
|
||||
expect(simpleHttpTransport.send({})).rejects.toThrowError("Invalid rendezvous URI");
|
||||
});
|
||||
|
||||
it("POST to fallback server", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST with no location", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
expect(prom).rejects.toThrowError();
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
});
|
||||
|
||||
it("POST with absolute path response", async function() {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz",
|
||||
"/123",
|
||||
"https://fallbackserver/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to built-in MSC3886 implementation", async function() {
|
||||
await postAndCheckLocation(
|
||||
true,
|
||||
"https://fallbackserver/rz",
|
||||
"123",
|
||||
"https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function() {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function() {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to follow 307 to other server", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 307,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz",
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz/123",
|
||||
etag: "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST and GET", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{ // initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{ // first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
{ // subsequent GET which should have etag from previous request
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-none-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and PUTs", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{ // initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
await prom;
|
||||
}
|
||||
{ // first PUT without etag
|
||||
const prom = simpleHttpTransport.send({ a: "b" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => {
|
||||
expect(headers["if-match"]).toBeUndefined();
|
||||
expect(data).toEqual({ a: "b" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
"etag": "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
await prom;
|
||||
}
|
||||
{ // subsequent PUT which should have etag from previous request
|
||||
const prom = simpleHttpTransport.send({ c: "d" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
"etag": "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and DELETE", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{ // Create
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{ // Cancel
|
||||
const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("details before ready", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.details()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("send after cancelled", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("receive before ready", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.receive()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("404 failure callback", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined();
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("404 failure callback mapped to expired", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
{ // initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
expires: "Thu, 01 Jan 1970 00:00:00 GMT",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
await prom;
|
||||
}
|
||||
{ // GET with 404 to simulate expiry
|
||||
expect(simpleHttpTransport.receive()).resolves.toBeUndefined();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Expired);
|
||||
}
|
||||
});
|
||||
});
|
||||
+138
-26
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
||||
import { RoomState } from "../../src";
|
||||
import { EventType, RoomState } from "../../src";
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -142,33 +142,72 @@ describe("RoomMember", function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not honor string power levels.",
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
it("should not honor string power levels.", function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(20);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(20);
|
||||
expect(emitCount).toEqual(1);
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(20);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(20);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should no-op if given a non-state or unrelated event", () => {
|
||||
const fn = jest.spyOn(member, "emit");
|
||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||
member.setPowerLevelEvent(utils.mkEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
skey: "invalid",
|
||||
event: true,
|
||||
}));
|
||||
const nonStateEv = utils.mkEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
delete nonStateEv.event.state_key;
|
||||
member.setPowerLevelEvent(nonStateEv);
|
||||
member.setPowerLevelEvent(utils.mkEvent({
|
||||
type: EventType.Sticker,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {},
|
||||
event: true,
|
||||
}));
|
||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTypingEvent", function() {
|
||||
@@ -234,6 +273,79 @@ describe("RoomMember", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isKicked", () => {
|
||||
it("should return false if membership is not `leave`", () => {
|
||||
const member1 = new RoomMember(roomId, userA);
|
||||
member1.membership = "join";
|
||||
expect(member1.isKicked()).toBeFalsy();
|
||||
|
||||
const member2 = new RoomMember(roomId, userA);
|
||||
member2.membership = "invite";
|
||||
expect(member2.isKicked()).toBeFalsy();
|
||||
|
||||
const member3 = new RoomMember(roomId, userA);
|
||||
expect(member3.isKicked()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if the membership event is unknown", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "leave";
|
||||
expect(member.isKicked()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if the member left of their own accord", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "leave";
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userA,
|
||||
mship: "leave",
|
||||
skey: userA,
|
||||
});
|
||||
expect(member.isKicked()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true if the member's leave was sent by another user", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "leave";
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userB,
|
||||
mship: "leave",
|
||||
skey: userA,
|
||||
});
|
||||
expect(member.isKicked()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDMInviter", () => {
|
||||
it("should return userId of the sender of the invite if is_direct=true", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "invite";
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userB,
|
||||
mship: "invite",
|
||||
skey: userA,
|
||||
});
|
||||
member.events.member.event.content!.is_direct = true;
|
||||
expect(member.getDMInviter()).toBe(userB);
|
||||
});
|
||||
|
||||
it("should not return userId of the sender of the invite if is_direct=false", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "invite";
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userB,
|
||||
mship: "invite",
|
||||
skey: userA,
|
||||
});
|
||||
member.events.member.event.content!.is_direct = false;
|
||||
expect(member.getDMInviter()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setMembershipEvent", function() {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("RoomState", function() {
|
||||
state.on(RoomStateEvent.Members, function(ev, st, mem) {
|
||||
expect(ev).toEqual(memberEvents[emitCount]);
|
||||
expect(st).toEqual(state);
|
||||
expect(mem).toEqual(state.getMember(ev.getSender()));
|
||||
expect(mem).toEqual(state.getMember(ev.getSender()!));
|
||||
emitCount += 1;
|
||||
});
|
||||
state.setStateEvents(memberEvents);
|
||||
@@ -303,92 +303,92 @@ describe("RoomState", function() {
|
||||
state.setStateEvents(events, { timelineWasEmpty: true });
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beacon events', () => {
|
||||
it('adds new beacon info events to state and emits', () => {
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
describe('beacon events', () => {
|
||||
it('adds new beacon info events to state and emits', () => {
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
state.setStateEvents([beaconEvent]);
|
||||
|
||||
expect(state.beacons.size).toEqual(1);
|
||||
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
|
||||
expect(beaconInstance).toBeTruthy();
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
|
||||
});
|
||||
expect(state.beacons.size).toEqual(1);
|
||||
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
|
||||
expect(beaconInstance).toBeTruthy();
|
||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
|
||||
});
|
||||
|
||||
it('does not add redacted beacon info events to state', () => {
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
it('does not add redacted beacon info events to state', () => {
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
|
||||
// no beacon added
|
||||
expect(state.beacons.size).toEqual(0);
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
|
||||
// no new beacon emit
|
||||
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
|
||||
});
|
||||
// no beacon added
|
||||
expect(state.beacons.size).toEqual(0);
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
|
||||
// no new beacon emit
|
||||
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
|
||||
});
|
||||
|
||||
it('updates existing beacon info events in state', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
|
||||
it('updates existing beacon info events in state', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([updatedBeaconEvent]);
|
||||
state.setStateEvents([updatedBeaconEvent]);
|
||||
|
||||
// same Beacon
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
||||
// updated liveness
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
|
||||
});
|
||||
// same Beacon
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
||||
// updated liveness
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
|
||||
});
|
||||
|
||||
it('destroys and removes redacted beacon events', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
it('destroys and removes redacted beacon events', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
|
||||
expect(destroySpy).toHaveBeenCalled();
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
|
||||
});
|
||||
expect(destroySpy).toHaveBeenCalled();
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
|
||||
});
|
||||
|
||||
it('updates live beacon ids once after setting state events', () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
|
||||
it('updates live beacon ids once after setting state events', () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
|
||||
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
|
||||
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
|
||||
|
||||
// called once
|
||||
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
|
||||
// called once
|
||||
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
|
||||
|
||||
// live beacon is now not live
|
||||
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
|
||||
);
|
||||
// live beacon is now not live
|
||||
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
|
||||
);
|
||||
|
||||
state.setStateEvents([updatedLiveBeaconEvent]);
|
||||
state.setStateEvents([updatedLiveBeaconEvent]);
|
||||
|
||||
expect(state.hasLiveBeacons).toBe(false);
|
||||
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
|
||||
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
|
||||
});
|
||||
expect(state.hasLiveBeacons).toBe(false);
|
||||
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
|
||||
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1007,4 +1007,20 @@ describe("RoomState", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("mayClientSendStateEvent", () => {
|
||||
it("should return false if the user isn't authenticated", () => {
|
||||
expect(state.mayClientSendStateEvent("m.room.message", {
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
credentials: {},
|
||||
} as unknown as MatrixClient)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if the user is a guest", () => {
|
||||
expect(state.mayClientSendStateEvent("m.room.message", {
|
||||
isGuest: jest.fn().mockReturnValue(true),
|
||||
credentials: { userId: userA },
|
||||
} as unknown as MatrixClient)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+499
-110
File diff suppressed because it is too large
Load Diff
@@ -160,10 +160,10 @@ describe("MatrixScheduler", function() {
|
||||
const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true });
|
||||
|
||||
const buckets = {};
|
||||
buckets[eventA.getId()] = "queue_A";
|
||||
buckets[eventD.getId()] = "queue_A";
|
||||
buckets[eventB.getId()] = "queue_B";
|
||||
buckets[eventC.getId()] = "queue_B";
|
||||
buckets[eventA.getId()!] = "queue_A";
|
||||
buckets[eventD.getId()!] = "queue_A";
|
||||
buckets[eventB.getId()!] = "queue_B";
|
||||
buckets[eventC.getId()!] = "queue_B";
|
||||
|
||||
retryFn = function() {
|
||||
return 0;
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'jest-localstorage-mock';
|
||||
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||
import { defer } from "../../../src/utils";
|
||||
|
||||
describe("IndexedDBStore", () => {
|
||||
afterEach(() => {
|
||||
@@ -59,7 +60,7 @@ describe("IndexedDBStore", () => {
|
||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
|
||||
|
||||
// Simulate a broken IDB
|
||||
(store.backend as LocalIndexedDBStoreBackend)["db"].transaction = (): IDBTransaction => {
|
||||
(store.backend as LocalIndexedDBStoreBackend)["db"]!.transaction = (): IDBTransaction => {
|
||||
const err = new Error("Failed to execute 'transaction' on 'IDBDatabase': " +
|
||||
"The database connection is closing.");
|
||||
err.name = "InvalidStateError";
|
||||
@@ -111,4 +112,57 @@ describe("IndexedDBStore", () => {
|
||||
await store.setPendingEvents(roomId, []);
|
||||
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
|
||||
});
|
||||
|
||||
it("should resolve isNewlyCreated to true if no database existed initially", async () => {
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db1",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("should resolve isNewlyCreated to false if database existed already", async () => {
|
||||
let store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db2",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db2",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
|
||||
const deferred = defer<Event>();
|
||||
// seed db3 to Version 1 so it forces a migration
|
||||
const req = indexedDB.open("matrix-js-sdk:db3", 1);
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result;
|
||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||
db.createObjectStore("accountData", { keyPath: ["type"] });
|
||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||
};
|
||||
req.onsuccess = deferred.resolve;
|
||||
await deferred.promise;
|
||||
req.result.close();
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db3",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,12 @@ const RES_WITH_AGE = {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
unread_thread_notifications: {
|
||||
"$143273582443PhrSn:example.org": {
|
||||
highlight_count: 0,
|
||||
notification_count: 1,
|
||||
},
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
Object.freeze({
|
||||
@@ -439,6 +445,13 @@ describe("SyncAccumulator", function() {
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should retrieve unread thread notifications", () => {
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
const output = sa.getJSON();
|
||||
expect(output.roomsData.join["!foo:bar"]
|
||||
.unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const mockClient = {
|
||||
function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline {
|
||||
const room = new Room(ROOM_ID, mockClient, USER_ID);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("TimelineWindow", function() {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.getEventTimeline.mockResolvedValue(undefined);
|
||||
mockClient.paginateEventTimeline.mockReturnValue(undefined);
|
||||
mockClient.paginateEventTimeline.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe("load", function() {
|
||||
|
||||
+19
-4
@@ -26,9 +26,7 @@ describe("utils", function() {
|
||||
foo: "bar",
|
||||
baz: "beer@",
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual(
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
expect(utils.encodeParams(params).toString()).toEqual("foo=bar&baz=beer%40");
|
||||
});
|
||||
|
||||
it("should handle boolean and numeric values", function() {
|
||||
@@ -37,7 +35,24 @@ describe("utils", function() {
|
||||
number: 12345,
|
||||
boolean: false,
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
|
||||
expect(utils.encodeParams(params).toString()).toEqual("string=foobar&number=12345&boolean=false");
|
||||
});
|
||||
|
||||
it("should handle string arrays", () => {
|
||||
const params = {
|
||||
via: ["one", "two", "three"],
|
||||
};
|
||||
expect(utils.encodeParams(params).toString()).toEqual("via=one&via=two&via=three");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeParams", () => {
|
||||
it("should be able to decode multiple values into an array", () => {
|
||||
const params = "foo=bar&via=a&via=b&via=c";
|
||||
expect(utils.decodeParams(params)).toEqual({
|
||||
foo: "bar",
|
||||
via: ["a", "b", "c"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1040
-339
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user