teams-cli 0.2.5__tar.gz → 0.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {teams_cli-0.2.5 → teams_cli-0.2.6}/CHANGELOG.md +8 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/Cargo.lock +163 -2
- {teams_cli-0.2.5 → teams_cli-0.2.6}/Cargo.toml +5 -3
- {teams_cli-0.2.5 → teams_cli-0.2.6}/PKG-INFO +11 -1
- {teams_cli-0.2.5 → teams_cli-0.2.6}/README.md +10 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/install.rs +216 -27
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/macos.rs +33 -5
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop.rs +1 -1
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop_people.js +34 -27
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/js/people.test.mjs +2 -1
- {teams_cli-0.2.5 → teams_cli-0.2.6}/.github/workflows/ci.yml +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/.github/workflows/dependencies.yml +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/.github/workflows/release.yml +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/.gitignore +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/.mise.toml +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/LICENSE +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/Makefile +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/build.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/docs/releases.md +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/examples/validate_schema.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/pyproject.toml +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/scripts/smoke-release.py +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/auth/pass.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/auth/process.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/auth/wsl.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/auth/wsl_credentials.ps1 +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/auth.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/backend.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/cli.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/config.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/delivery.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/avatars.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/browser.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/browser_children.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/browser_core.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/browser_install.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/browser_lifetime.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/browser_setup.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/cdp.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/connection.ps1 +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/connection.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/helper.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/owner.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/session.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop/windows.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop.js +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop_cache.js +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop_core.js +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/desktop_send.js +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/error.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/graph.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/lib.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/main.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/output.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/render.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/schema.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/setup/capabilities.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/setup.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/teams_cli_alias.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/src/tui.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/browser_lifetime.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/cli.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/connection.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/connection.test.ps1 +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/credentials.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/desktop.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/desktop_scripts.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/desktop_scripts.test.cjs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/fixtures/clispec-v0.3.json +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/package_alias.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/pass_credentials.sh +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/release_smoke.rs +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/tests/wsl_credentials.test.ps1 +0 -0
- {teams_cli-0.2.5 → teams_cli-0.2.6}/vership.toml +0 -0
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.6](https://github.com/rvben/teams-cli/compare/v0.2.5...v0.2.6) - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `teams people search` finds work and school colleagues again with the desktop backend. It now uses the same directory search as the Teams people picker; the lookup it used before returned nothing, or failed with "Teams could not resolve this person" in a headless browser.
|
|
15
|
+
- **desktop**: accept signed macOS system UIDs ([b7a1521](https://github.com/rvben/teams-cli/commit/b7a152189d2bc636cebe5ca67f24172dd702ae75))
|
|
16
|
+
- **desktop**: validate helper archives with tar-codec ([b955dd8](https://github.com/rvben/teams-cli/commit/b955dd8ea93cb302d72107cd2701e2adc8380ab3))
|
|
17
|
+
|
|
10
18
|
## [0.2.5](https://github.com/rvben/teams-cli/compare/v0.2.4...v0.2.5) - 2026-10-21
|
|
11
19
|
|
|
12
20
|
### Fixed
|
|
@@ -37,6 +37,12 @@ version = "0.2.21"
|
|
|
37
37
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
38
38
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
|
39
39
|
|
|
40
|
+
[[package]]
|
|
41
|
+
name = "ambient-authority"
|
|
42
|
+
version = "0.0.2"
|
|
43
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
44
|
+
checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b"
|
|
45
|
+
|
|
40
46
|
[[package]]
|
|
41
47
|
name = "anstream"
|
|
42
48
|
version = "1.0.0"
|
|
@@ -102,6 +108,19 @@ dependencies = [
|
|
|
102
108
|
"num-traits",
|
|
103
109
|
]
|
|
104
110
|
|
|
111
|
+
[[package]]
|
|
112
|
+
name = "archive-trait"
|
|
113
|
+
version = "0.0.15"
|
|
114
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
115
|
+
checksum = "999bf88cc483664cab77700ca2fdfb0415a73412abe284ef5bc2319d00938e00"
|
|
116
|
+
dependencies = [
|
|
117
|
+
"cap-std",
|
|
118
|
+
"thiserror 2.0.20",
|
|
119
|
+
"tokio",
|
|
120
|
+
"walkdir",
|
|
121
|
+
"windows-sys 0.60.2",
|
|
122
|
+
]
|
|
123
|
+
|
|
105
124
|
[[package]]
|
|
106
125
|
name = "assert-json-diff"
|
|
107
126
|
version = "2.0.2"
|
|
@@ -127,6 +146,18 @@ dependencies = [
|
|
|
127
146
|
"wait-timeout",
|
|
128
147
|
]
|
|
129
148
|
|
|
149
|
+
[[package]]
|
|
150
|
+
name = "async-compression"
|
|
151
|
+
version = "0.4.51"
|
|
152
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
153
|
+
checksum = "9d5755640120b24961f8231e05892e4a4f7e0d04e8aeca161508c9c397946970"
|
|
154
|
+
dependencies = [
|
|
155
|
+
"compression-codecs",
|
|
156
|
+
"compression-core",
|
|
157
|
+
"pin-project-lite",
|
|
158
|
+
"tokio",
|
|
159
|
+
]
|
|
160
|
+
|
|
130
161
|
[[package]]
|
|
131
162
|
name = "atomic"
|
|
132
163
|
version = "0.6.1"
|
|
@@ -296,6 +327,36 @@ version = "1.12.1"
|
|
|
296
327
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
297
328
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
298
329
|
|
|
330
|
+
[[package]]
|
|
331
|
+
name = "cap-primitives"
|
|
332
|
+
version = "4.0.3"
|
|
333
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
334
|
+
checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18"
|
|
335
|
+
dependencies = [
|
|
336
|
+
"ambient-authority",
|
|
337
|
+
"fs-set-times",
|
|
338
|
+
"io-extras",
|
|
339
|
+
"io-lifetimes 3.0.1",
|
|
340
|
+
"ipnet",
|
|
341
|
+
"maybe-owned",
|
|
342
|
+
"rustix",
|
|
343
|
+
"rustix-linux-procfs",
|
|
344
|
+
"windows-sys 0.61.2",
|
|
345
|
+
"winx",
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
[[package]]
|
|
349
|
+
name = "cap-std"
|
|
350
|
+
version = "4.0.3"
|
|
351
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
352
|
+
checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8"
|
|
353
|
+
dependencies = [
|
|
354
|
+
"cap-primitives",
|
|
355
|
+
"io-extras",
|
|
356
|
+
"io-lifetimes 3.0.1",
|
|
357
|
+
"rustix",
|
|
358
|
+
]
|
|
359
|
+
|
|
299
360
|
[[package]]
|
|
300
361
|
name = "castaway"
|
|
301
362
|
version = "0.2.4"
|
|
@@ -428,6 +489,23 @@ dependencies = [
|
|
|
428
489
|
"static_assertions",
|
|
429
490
|
]
|
|
430
491
|
|
|
492
|
+
[[package]]
|
|
493
|
+
name = "compression-codecs"
|
|
494
|
+
version = "0.4.46"
|
|
495
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
496
|
+
checksum = "cae36683101b2899f48bd31f8db9839437356188e3b1ff23096e73dab09ba8fe"
|
|
497
|
+
dependencies = [
|
|
498
|
+
"compression-core",
|
|
499
|
+
"flate2",
|
|
500
|
+
"memchr",
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
[[package]]
|
|
504
|
+
name = "compression-core"
|
|
505
|
+
version = "0.4.34"
|
|
506
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
507
|
+
checksum = "2ebdda89edc534bf8d5dd2857562633b6495557e5846552ade994b99ed13c36a"
|
|
508
|
+
|
|
431
509
|
[[package]]
|
|
432
510
|
name = "const-oid"
|
|
433
511
|
version = "0.10.2"
|
|
@@ -959,6 +1037,17 @@ dependencies = [
|
|
|
959
1037
|
"num",
|
|
960
1038
|
]
|
|
961
1039
|
|
|
1040
|
+
[[package]]
|
|
1041
|
+
name = "fs-set-times"
|
|
1042
|
+
version = "0.20.3"
|
|
1043
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1044
|
+
checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
|
|
1045
|
+
dependencies = [
|
|
1046
|
+
"io-lifetimes 2.0.4",
|
|
1047
|
+
"rustix",
|
|
1048
|
+
"windows-sys 0.52.0",
|
|
1049
|
+
]
|
|
1050
|
+
|
|
962
1051
|
[[package]]
|
|
963
1052
|
name = "fs_extra"
|
|
964
1053
|
version = "1.3.0"
|
|
@@ -1419,6 +1508,28 @@ dependencies = [
|
|
|
1419
1508
|
"syn 3.0.4",
|
|
1420
1509
|
]
|
|
1421
1510
|
|
|
1511
|
+
[[package]]
|
|
1512
|
+
name = "io-extras"
|
|
1513
|
+
version = "0.19.0"
|
|
1514
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1515
|
+
checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f"
|
|
1516
|
+
dependencies = [
|
|
1517
|
+
"io-lifetimes 3.0.1",
|
|
1518
|
+
"windows-sys 0.60.2",
|
|
1519
|
+
]
|
|
1520
|
+
|
|
1521
|
+
[[package]]
|
|
1522
|
+
name = "io-lifetimes"
|
|
1523
|
+
version = "2.0.4"
|
|
1524
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1525
|
+
checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
|
|
1526
|
+
|
|
1527
|
+
[[package]]
|
|
1528
|
+
name = "io-lifetimes"
|
|
1529
|
+
version = "3.0.1"
|
|
1530
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1531
|
+
checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96"
|
|
1532
|
+
|
|
1422
1533
|
[[package]]
|
|
1423
1534
|
name = "ipnet"
|
|
1424
1535
|
version = "2.12.1"
|
|
@@ -1780,6 +1891,12 @@ dependencies = [
|
|
|
1780
1891
|
"winapi",
|
|
1781
1892
|
]
|
|
1782
1893
|
|
|
1894
|
+
[[package]]
|
|
1895
|
+
name = "maybe-owned"
|
|
1896
|
+
version = "0.3.4"
|
|
1897
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1898
|
+
checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
|
|
1899
|
+
|
|
1783
1900
|
[[package]]
|
|
1784
1901
|
name = "memchr"
|
|
1785
1902
|
version = "2.8.3"
|
|
@@ -2750,6 +2867,16 @@ dependencies = [
|
|
|
2750
2867
|
"windows-sys 0.61.2",
|
|
2751
2868
|
]
|
|
2752
2869
|
|
|
2870
|
+
[[package]]
|
|
2871
|
+
name = "rustix-linux-procfs"
|
|
2872
|
+
version = "0.1.1"
|
|
2873
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2874
|
+
checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
|
|
2875
|
+
dependencies = [
|
|
2876
|
+
"once_cell",
|
|
2877
|
+
"rustix",
|
|
2878
|
+
]
|
|
2879
|
+
|
|
2753
2880
|
[[package]]
|
|
2754
2881
|
name = "rustls"
|
|
2755
2882
|
version = "0.23.46"
|
|
@@ -3185,11 +3312,34 @@ dependencies = [
|
|
|
3185
3312
|
"xattr",
|
|
3186
3313
|
]
|
|
3187
3314
|
|
|
3315
|
+
[[package]]
|
|
3316
|
+
name = "tar-codec"
|
|
3317
|
+
version = "0.0.15"
|
|
3318
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3319
|
+
checksum = "b9dad615e50d181afea4bafea23dacb8e33ed52dde06503813570634b932d467"
|
|
3320
|
+
dependencies = [
|
|
3321
|
+
"archive-trait",
|
|
3322
|
+
"tar-framing",
|
|
3323
|
+
"thiserror 2.0.20",
|
|
3324
|
+
"tokio",
|
|
3325
|
+
]
|
|
3326
|
+
|
|
3327
|
+
[[package]]
|
|
3328
|
+
name = "tar-framing"
|
|
3329
|
+
version = "0.0.15"
|
|
3330
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3331
|
+
checksum = "563e8181b47c9cdcce569eae3ad431eec71543d6269dd60c7bd2350f70bcd233"
|
|
3332
|
+
dependencies = [
|
|
3333
|
+
"thiserror 2.0.20",
|
|
3334
|
+
"tokio",
|
|
3335
|
+
]
|
|
3336
|
+
|
|
3188
3337
|
[[package]]
|
|
3189
3338
|
name = "teams-cli"
|
|
3190
|
-
version = "0.2.
|
|
3339
|
+
version = "0.2.6"
|
|
3191
3340
|
dependencies = [
|
|
3192
3341
|
"assert_cmd",
|
|
3342
|
+
"async-compression",
|
|
3193
3343
|
"base64 0.23.1",
|
|
3194
3344
|
"clap",
|
|
3195
3345
|
"clap_complete",
|
|
@@ -3210,6 +3360,7 @@ dependencies = [
|
|
|
3210
3360
|
"serde_json",
|
|
3211
3361
|
"sha2",
|
|
3212
3362
|
"tar",
|
|
3363
|
+
"tar-codec",
|
|
3213
3364
|
"tempfile",
|
|
3214
3365
|
"thiserror 2.0.20",
|
|
3215
3366
|
"tokio",
|
|
@@ -3229,7 +3380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
3229
3380
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
|
3230
3381
|
dependencies = [
|
|
3231
3382
|
"fastrand",
|
|
3232
|
-
"getrandom 0.3
|
|
3383
|
+
"getrandom 0.4.3",
|
|
3233
3384
|
"once_cell",
|
|
3234
3385
|
"rustix",
|
|
3235
3386
|
"windows-sys 0.61.2",
|
|
@@ -4121,6 +4272,16 @@ version = "1.0.4"
|
|
|
4121
4272
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4122
4273
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
|
4123
4274
|
|
|
4275
|
+
[[package]]
|
|
4276
|
+
name = "winx"
|
|
4277
|
+
version = "0.36.4"
|
|
4278
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4279
|
+
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
|
4280
|
+
dependencies = [
|
|
4281
|
+
"bitflags 2.13.1",
|
|
4282
|
+
"windows-sys 0.52.0",
|
|
4283
|
+
]
|
|
4284
|
+
|
|
4124
4285
|
[[package]]
|
|
4125
4286
|
name = "wiremock"
|
|
4126
4287
|
version = "0.6.5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "teams-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.6"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.90"
|
|
6
6
|
description = "A Microsoft Teams CLI and TUI designed equally for humans and agents"
|
|
@@ -22,12 +22,12 @@ name = "teams-cli"
|
|
|
22
22
|
path = "src/teams_cli_alias.rs"
|
|
23
23
|
|
|
24
24
|
[dependencies]
|
|
25
|
+
async-compression = { version = "0.4", features = ["tokio", "gzip"] }
|
|
25
26
|
base64 = "0.23"
|
|
26
27
|
clap = { version = "4", features = ["derive", "env"] }
|
|
27
28
|
clap_complete = "4"
|
|
28
29
|
crossterm = "0.29"
|
|
29
30
|
dirs = "6"
|
|
30
|
-
flate2 = "1"
|
|
31
31
|
futures-util = { version = "0.3", default-features = false, features = ["sink"] }
|
|
32
32
|
keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service", "vendored"] }
|
|
33
33
|
open = "5"
|
|
@@ -37,7 +37,7 @@ reqwest = { version = "0.13", default-features = false, features = ["rustls", "j
|
|
|
37
37
|
serde = { version = "1", features = ["derive"] }
|
|
38
38
|
serde_json = "1"
|
|
39
39
|
sha2 = "0.10"
|
|
40
|
-
tar = "0.
|
|
40
|
+
tar-codec = "=0.0.15"
|
|
41
41
|
thiserror = "2"
|
|
42
42
|
tempfile = "3"
|
|
43
43
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "io-util", "time", "process", "io-std"] }
|
|
@@ -64,6 +64,8 @@ windows-sys = { version = "0.61", features = [
|
|
|
64
64
|
libc = "0.2"
|
|
65
65
|
|
|
66
66
|
[dev-dependencies]
|
|
67
|
+
flate2 = "1"
|
|
68
|
+
tar = "0.4"
|
|
67
69
|
assert_cmd = "2"
|
|
68
70
|
jsonschema = "0.49.8"
|
|
69
71
|
predicates = "3"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: teams-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
License-File: LICENSE
|
|
@@ -24,6 +24,16 @@ For humans, no arguments open guided onboarding on first use and the TUI afterwa
|
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
|
+
### Homebrew
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
brew install rvben/tap/teams-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Homebrew installs native binaries on macOS and builds from source on Linux.
|
|
34
|
+
|
|
35
|
+
### Other installation methods
|
|
36
|
+
|
|
27
37
|
```console
|
|
28
38
|
pipx install teams-cli
|
|
29
39
|
# or
|
|
@@ -8,6 +8,16 @@ For humans, no arguments open guided onboarding on first use and the TUI afterwa
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
### Homebrew
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
brew install rvben/tap/teams-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Homebrew installs native binaries on macOS and builds from source on Linux.
|
|
18
|
+
|
|
19
|
+
### Other installation methods
|
|
20
|
+
|
|
11
21
|
```console
|
|
12
22
|
pipx install teams-cli
|
|
13
23
|
# or
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
use crate::error::AppError;
|
|
4
4
|
use sha2::{Digest, Sha256};
|
|
5
5
|
use std::{
|
|
6
|
-
io::Read,
|
|
7
6
|
path::{Path, PathBuf},
|
|
8
7
|
time::Duration,
|
|
9
8
|
};
|
|
9
|
+
use tar_codec::{Archive as _, Member, MemberPayload as _, TarArchive};
|
|
10
|
+
use tokio::io::AsyncReadExt as _;
|
|
10
11
|
|
|
11
12
|
use super::helper::PROTOCOL;
|
|
12
13
|
|
|
@@ -133,7 +134,15 @@ async fn ensure_state(state: HelperState, version: &str) -> Result<PathBuf, AppE
|
|
|
133
134
|
"{asset} does not match its published checksum; nothing was installed"
|
|
134
135
|
)));
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
+
// Member payloads borrow the decoder across awaits. Parse on a blocking
|
|
138
|
+
// worker so the downloader remains Send on Rust 1.90, and decompression
|
|
139
|
+
// does not occupy an async runtime worker.
|
|
140
|
+
let helper = tokio::task::spawn_blocking(move || {
|
|
141
|
+
tokio::runtime::Handle::current().block_on(extract(&archive))
|
|
142
|
+
})
|
|
143
|
+
.await
|
|
144
|
+
.map_err(|_| AppError::Desktop("could not read the helper archive".into()))??;
|
|
145
|
+
install(&path, &helper)?;
|
|
137
146
|
Ok(path)
|
|
138
147
|
}
|
|
139
148
|
|
|
@@ -192,30 +201,73 @@ fn checksum(sums: &str, name: &str) -> Option<String> {
|
|
|
192
201
|
})
|
|
193
202
|
}
|
|
194
203
|
|
|
195
|
-
/// Only `teams.exe` is
|
|
196
|
-
|
|
204
|
+
/// Only one top-level `teams.exe` is accepted. Validate the entire archive
|
|
205
|
+
/// before installing it, including entries after the executable and gzip CRC.
|
|
206
|
+
async fn extract(archive: &[u8]) -> Result<Vec<u8>, AppError> {
|
|
197
207
|
let invalid = || AppError::Desktop("the helper archive is not valid".into());
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
if archive.len() > MAX_ARCHIVE {
|
|
209
|
+
return Err(invalid());
|
|
210
|
+
}
|
|
211
|
+
let mut reader =
|
|
212
|
+
async_compression::tokio::bufread::GzipDecoder::new(archive).take(MAX_ARCHIVE as u64 + 1);
|
|
213
|
+
let mut helper = None;
|
|
214
|
+
{
|
|
215
|
+
let mut entries = TarArchive::new(&mut reader).members();
|
|
216
|
+
let mut chunk = Vec::new();
|
|
217
|
+
while let Some(entry) = entries.next().await.map_err(|_| invalid())? {
|
|
218
|
+
if let Member::File {
|
|
219
|
+
metadata,
|
|
220
|
+
size,
|
|
221
|
+
mut payload,
|
|
222
|
+
..
|
|
223
|
+
} = entry
|
|
224
|
+
{
|
|
225
|
+
// Match the release layout without normalizing away traversal.
|
|
226
|
+
let is_helper = matches!(metadata.path.as_str(), "teams.exe" | "./teams.exe");
|
|
227
|
+
if is_helper {
|
|
228
|
+
if helper.is_some() || size > MAX_ARCHIVE as u64 {
|
|
229
|
+
return Err(invalid());
|
|
230
|
+
}
|
|
231
|
+
let mut bytes = Vec::new();
|
|
232
|
+
while payload
|
|
233
|
+
.next_chunk(&mut chunk, 64 * 1024)
|
|
234
|
+
.await
|
|
235
|
+
.map_err(|_| invalid())?
|
|
236
|
+
{
|
|
237
|
+
if bytes.len() + chunk.len() > MAX_ARCHIVE {
|
|
238
|
+
return Err(invalid());
|
|
239
|
+
}
|
|
240
|
+
bytes.extend_from_slice(&chunk);
|
|
241
|
+
}
|
|
242
|
+
if !bytes.starts_with(b"MZ") {
|
|
243
|
+
return Err(invalid());
|
|
244
|
+
}
|
|
245
|
+
helper = Some(bytes);
|
|
246
|
+
} else {
|
|
247
|
+
payload.skip().await.map_err(|_| invalid())?;
|
|
248
|
+
}
|
|
249
|
+
} else if !matches!(entry, Member::Directory { .. }) {
|
|
250
|
+
// Release packages contain only directories and regular files.
|
|
213
251
|
return Err(invalid());
|
|
214
252
|
}
|
|
215
|
-
return Ok(bytes);
|
|
216
253
|
}
|
|
217
254
|
}
|
|
218
|
-
|
|
255
|
+
// The tar end marker can precede padding and the gzip trailer. Reading to
|
|
256
|
+
// EOF also makes the decompressed-size budget cover ignored archive data.
|
|
257
|
+
let mut padding = [0; 8192];
|
|
258
|
+
loop {
|
|
259
|
+
let read = reader.read(&mut padding).await.map_err(|_| invalid())?;
|
|
260
|
+
if read == 0 {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
if padding[..read].iter().any(|&byte| byte != 0) {
|
|
264
|
+
return Err(invalid());
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if reader.limit() == 0 || !reader.get_ref().get_ref().is_empty() {
|
|
268
|
+
return Err(invalid());
|
|
269
|
+
}
|
|
270
|
+
helper.ok_or_else(invalid)
|
|
219
271
|
}
|
|
220
272
|
|
|
221
273
|
/// Writes atomically, marks it executable for WSL interop, and removes other versions.
|
|
@@ -249,6 +301,12 @@ fn install(path: &Path, bytes: &[u8]) -> Result<(), AppError> {
|
|
|
249
301
|
mod tests {
|
|
250
302
|
use super::*;
|
|
251
303
|
|
|
304
|
+
#[test]
|
|
305
|
+
fn helper_downloader_can_be_sent_between_runtime_workers() {
|
|
306
|
+
fn assert_send(_: impl Send) {}
|
|
307
|
+
assert_send(ensure());
|
|
308
|
+
}
|
|
309
|
+
|
|
252
310
|
#[tokio::test]
|
|
253
311
|
async fn legacy_release_needs_a_built_helper_but_accepts_explicit_or_cached_builds() {
|
|
254
312
|
let temp = tempfile::tempdir().unwrap();
|
|
@@ -299,14 +357,145 @@ mod tests {
|
|
|
299
357
|
assert_eq!(hex(&[0, 15, 255]), "000fff");
|
|
300
358
|
}
|
|
301
359
|
|
|
302
|
-
#[test]
|
|
303
|
-
fn extracts_only_the_top_level_windows_executable() {
|
|
360
|
+
#[tokio::test]
|
|
361
|
+
async fn extracts_only_the_top_level_windows_executable() {
|
|
304
362
|
let exe = b"MZ fixture";
|
|
305
363
|
let bytes = archive(&[("./README.md", b"readme"), ("./teams.exe", exe)]);
|
|
306
|
-
assert_eq!(extract(&bytes).unwrap(), exe);
|
|
307
|
-
assert!(
|
|
308
|
-
|
|
309
|
-
|
|
364
|
+
assert_eq!(extract(&bytes).await.unwrap(), exe);
|
|
365
|
+
assert!(
|
|
366
|
+
extract(&archive(&[("./nested/teams.exe", exe)]))
|
|
367
|
+
.await
|
|
368
|
+
.is_err()
|
|
369
|
+
);
|
|
370
|
+
assert!(
|
|
371
|
+
extract(&archive(&[("./teams.exe", b"#!/bin/sh")]))
|
|
372
|
+
.await
|
|
373
|
+
.is_err()
|
|
374
|
+
);
|
|
375
|
+
assert!(extract(b"not an archive").await.is_err());
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#[tokio::test]
|
|
379
|
+
async fn extracts_a_helper_across_multiple_payload_chunks() {
|
|
380
|
+
let mut exe = vec![0x5a; 3 * 64 * 1024 + 17];
|
|
381
|
+
exe[..2].copy_from_slice(b"MZ");
|
|
382
|
+
assert_eq!(
|
|
383
|
+
extract(&archive(&[("teams.exe", &exe)])).await.unwrap(),
|
|
384
|
+
exe
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#[cfg(unix)]
|
|
389
|
+
#[tokio::test]
|
|
390
|
+
async fn accepts_the_release_workflows_system_tar_packaging() {
|
|
391
|
+
let temp = tempfile::tempdir().unwrap();
|
|
392
|
+
let package = temp.path().join("package");
|
|
393
|
+
std::fs::create_dir(&package).unwrap();
|
|
394
|
+
std::fs::write(package.join("teams.exe"), b"MZ release fixture").unwrap();
|
|
395
|
+
std::fs::write(package.join("README.md"), b"readme").unwrap();
|
|
396
|
+
let output = temp.path().join("helper.tar.gz");
|
|
397
|
+
// Match .github/workflows/release.yml, including the '.' root entry.
|
|
398
|
+
assert!(
|
|
399
|
+
std::process::Command::new("tar")
|
|
400
|
+
.arg("-C")
|
|
401
|
+
.arg(&package)
|
|
402
|
+
.arg("-czf")
|
|
403
|
+
.arg(&output)
|
|
404
|
+
.arg(".")
|
|
405
|
+
.status()
|
|
406
|
+
.unwrap()
|
|
407
|
+
.success()
|
|
408
|
+
);
|
|
409
|
+
assert_eq!(
|
|
410
|
+
extract(&std::fs::read(output).unwrap()).await.unwrap(),
|
|
411
|
+
b"MZ release fixture"
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[tokio::test]
|
|
416
|
+
async fn rejects_duplicate_helpers_and_damage_after_the_helper() {
|
|
417
|
+
let duplicate = archive(&[("teams.exe", b"MZ first"), ("./teams.exe", b"MZ second")]);
|
|
418
|
+
assert!(extract(&duplicate).await.is_err());
|
|
419
|
+
|
|
420
|
+
let mut damaged = archive(&[("teams.exe", b"MZ fixture")]);
|
|
421
|
+
let crc = damaged.len() - 8;
|
|
422
|
+
damaged[crc] ^= 1;
|
|
423
|
+
assert!(extract(&damaged).await.is_err(), "gzip CRC must be checked");
|
|
424
|
+
damaged.truncate(crc);
|
|
425
|
+
assert!(
|
|
426
|
+
extract(&damaged).await.is_err(),
|
|
427
|
+
"missing trailer must fail"
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// A malformed second tar header must not be hidden by finding the
|
|
431
|
+
// executable first. Recompress so the gzip checksum remains valid.
|
|
432
|
+
use std::io::{Read as _, Write as _};
|
|
433
|
+
let valid = archive(&[("teams.exe", b"MZ fixture"), ("README.md", b"readme")]);
|
|
434
|
+
let mut raw = Vec::new();
|
|
435
|
+
flate2::read::GzDecoder::new(valid.as_slice())
|
|
436
|
+
.read_to_end(&mut raw)
|
|
437
|
+
.unwrap();
|
|
438
|
+
raw[1024] ^= 1;
|
|
439
|
+
let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
|
|
440
|
+
gz.write_all(&raw).unwrap();
|
|
441
|
+
assert!(extract(&gz.finish().unwrap()).await.is_err());
|
|
442
|
+
|
|
443
|
+
let mut trailing = archive(&[("teams.exe", b"MZ fixture")]);
|
|
444
|
+
trailing.extend_from_slice(b"junk");
|
|
445
|
+
assert!(
|
|
446
|
+
extract(&trailing).await.is_err(),
|
|
447
|
+
"trailing compressed data must fail"
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
let mut concatenated = archive(&[("teams.exe", b"MZ first")]);
|
|
451
|
+
concatenated.extend_from_slice(&archive(&[("teams.exe", b"MZ second")]));
|
|
452
|
+
assert!(
|
|
453
|
+
extract(&concatenated).await.is_err(),
|
|
454
|
+
"multiple gzip streams must fail"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[tokio::test]
|
|
459
|
+
async fn rejects_links_even_after_a_valid_helper() {
|
|
460
|
+
let mut builder = tar::Builder::new(flate2::write::GzEncoder::new(
|
|
461
|
+
Vec::new(),
|
|
462
|
+
flate2::Compression::fast(),
|
|
463
|
+
));
|
|
464
|
+
let mut file = tar::Header::new_gnu();
|
|
465
|
+
file.set_size(2);
|
|
466
|
+
file.set_mode(0o644);
|
|
467
|
+
builder
|
|
468
|
+
.append_data(&mut file, "teams.exe", &b"MZ"[..])
|
|
469
|
+
.unwrap();
|
|
470
|
+
let mut link = tar::Header::new_gnu();
|
|
471
|
+
link.set_entry_type(tar::EntryType::Symlink);
|
|
472
|
+
link.set_size(0);
|
|
473
|
+
link.set_mode(0o777);
|
|
474
|
+
builder
|
|
475
|
+
.append_link(&mut link, "other", "teams.exe")
|
|
476
|
+
.unwrap();
|
|
477
|
+
let bytes = builder.into_inner().unwrap().finish().unwrap();
|
|
478
|
+
assert!(extract(&bytes).await.is_err());
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
#[tokio::test]
|
|
482
|
+
async fn bounds_decompressed_data_including_ignored_files_and_padding() {
|
|
483
|
+
// Compressed input is small, but ignored payloads still consume the
|
|
484
|
+
// same total budget as the helper itself.
|
|
485
|
+
let bytes = archive(&[("README.md", &vec![0; MAX_ARCHIVE]), ("teams.exe", b"MZ")]);
|
|
486
|
+
assert!(bytes.len() < MAX_ARCHIVE);
|
|
487
|
+
assert!(extract(&bytes).await.is_err());
|
|
488
|
+
|
|
489
|
+
use std::io::{Read as _, Write as _};
|
|
490
|
+
let valid = archive(&[("teams.exe", b"MZ")]);
|
|
491
|
+
let mut raw = Vec::new();
|
|
492
|
+
flate2::read::GzDecoder::new(valid.as_slice())
|
|
493
|
+
.read_to_end(&mut raw)
|
|
494
|
+
.unwrap();
|
|
495
|
+
raw.resize(MAX_ARCHIVE + 1, 0);
|
|
496
|
+
let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
|
|
497
|
+
gz.write_all(&raw).unwrap();
|
|
498
|
+
assert!(extract(&gz.finish().unwrap()).await.is_err());
|
|
310
499
|
}
|
|
311
500
|
|
|
312
501
|
#[test]
|
|
@@ -132,14 +132,20 @@ fn parse_processes(text: &str) -> Result<HashMap<u32, Process>, AppError> {
|
|
|
132
132
|
.map(|line| {
|
|
133
133
|
let invalid = || AppError::Desktop("invalid process ancestry metadata".into());
|
|
134
134
|
let mut rest = line.trim();
|
|
135
|
-
let mut
|
|
135
|
+
let mut field = || {
|
|
136
136
|
let (value, tail) = rest.split_once(char::is_whitespace).ok_or_else(invalid)?;
|
|
137
137
|
rest = tail.trim_start();
|
|
138
|
-
|
|
138
|
+
Ok::<_, AppError>(value)
|
|
139
139
|
};
|
|
140
|
-
let pid =
|
|
141
|
-
let uid =
|
|
142
|
-
|
|
140
|
+
let pid = field()?.parse::<u32>().map_err(|_| invalid())?;
|
|
141
|
+
let uid = field()?;
|
|
142
|
+
// macOS ps prints some uid_t values as signed integers (for
|
|
143
|
+
// example nobody as -2). Preserve their underlying 32-bit value.
|
|
144
|
+
let uid = uid
|
|
145
|
+
.parse::<u32>()
|
|
146
|
+
.or_else(|_| uid.parse::<i32>().map(|uid| uid as u32))
|
|
147
|
+
.map_err(|_| invalid())?;
|
|
148
|
+
let parent = field()?.parse::<u32>().map_err(|_| invalid())?;
|
|
143
149
|
if rest.is_empty() {
|
|
144
150
|
return Err(invalid());
|
|
145
151
|
}
|
|
@@ -184,6 +190,28 @@ fn teams_owner(
|
|
|
184
190
|
mod tests {
|
|
185
191
|
use super::*;
|
|
186
192
|
|
|
193
|
+
#[test]
|
|
194
|
+
fn signed_system_uids_do_not_break_listener_ownership_checks() {
|
|
195
|
+
let installed = [PathBuf::from("/Applications/Teams")];
|
|
196
|
+
let processes = parse_processes(
|
|
197
|
+
"1 0 0 /sbin/launchd\n2 -2 1 /system/service\n3 501 1 /Applications/Teams\n4 -2 3 /helper\n5 4294967294 1 /system/service\n",
|
|
198
|
+
)
|
|
199
|
+
.unwrap();
|
|
200
|
+
assert_eq!(processes[&2].uid, u32::MAX - 1);
|
|
201
|
+
assert_eq!(processes[&5].uid, processes[&2].uid);
|
|
202
|
+
assert!(teams_owner(&processes, 3, 501, &installed));
|
|
203
|
+
assert!(!teams_owner(&processes, 2, 501, &installed));
|
|
204
|
+
assert!(!teams_owner(&processes, 4, 501, &installed));
|
|
205
|
+
for malformed in [
|
|
206
|
+
"-2 501 1 /process",
|
|
207
|
+
"2 -2147483649 1 /process",
|
|
208
|
+
"2 4294967296 1 /process",
|
|
209
|
+
"2 501 -1 /process",
|
|
210
|
+
] {
|
|
211
|
+
assert!(parse_processes(malformed).is_err());
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
187
215
|
#[test]
|
|
188
216
|
fn ownership_requires_the_installed_path_and_current_user_throughout_the_chain() {
|
|
189
217
|
let installed = [PathBuf::from(
|
|
@@ -76,29 +76,6 @@ async (p, select) => {
|
|
|
76
76
|
],
|
|
77
77
|
});
|
|
78
78
|
if (p.op === "search") {
|
|
79
|
-
const query = operationDoc(
|
|
80
|
-
"query",
|
|
81
|
-
"TeamsCliSearchPeople",
|
|
82
|
-
[
|
|
83
|
-
["searchQuery", "String"],
|
|
84
|
-
["peoplePickerFilters", "PeoplePickerFiltersInput"],
|
|
85
|
-
],
|
|
86
|
-
[
|
|
87
|
-
F(
|
|
88
|
-
"searchUsers",
|
|
89
|
-
[
|
|
90
|
-
"id",
|
|
91
|
-
"displayName",
|
|
92
|
-
"email",
|
|
93
|
-
"mri",
|
|
94
|
-
"userPrincipalName",
|
|
95
|
-
"type",
|
|
96
|
-
"userType",
|
|
97
|
-
],
|
|
98
|
-
["searchQuery", "peoplePickerFilters"],
|
|
99
|
-
),
|
|
100
|
-
],
|
|
101
|
-
);
|
|
102
79
|
const filters = {
|
|
103
80
|
name: "newChat",
|
|
104
81
|
includeAADTenant: true,
|
|
@@ -211,13 +188,43 @@ async (p, select) => {
|
|
|
211
188
|
users.unshift(...found.data.instantScdSearch);
|
|
212
189
|
}
|
|
213
190
|
} else {
|
|
191
|
+
// Work accounts search the directory the way Teams' people picker does.
|
|
192
|
+
// searchUsers answers empty there, and instantScdSearch is refused.
|
|
193
|
+
const query = operationDoc(
|
|
194
|
+
"query",
|
|
195
|
+
"TeamsCliSearchPeople",
|
|
196
|
+
[
|
|
197
|
+
["searchQuery", "String"],
|
|
198
|
+
["peoplePickerFilters", "PeoplePickerFiltersInput"],
|
|
199
|
+
],
|
|
200
|
+
[
|
|
201
|
+
F(
|
|
202
|
+
"searchSuggestionsForPeoplePicker",
|
|
203
|
+
[F("peopleResults", fields)],
|
|
204
|
+
["searchQuery", "peoplePickerFilters"],
|
|
205
|
+
),
|
|
206
|
+
],
|
|
207
|
+
);
|
|
214
208
|
const r = await client.query({
|
|
215
209
|
query,
|
|
216
|
-
variables: {
|
|
217
|
-
|
|
210
|
+
variables: {
|
|
211
|
+
searchQuery: p.query,
|
|
212
|
+
peoplePickerFilters: {
|
|
213
|
+
...filters,
|
|
214
|
+
includeFeatures: [
|
|
215
|
+
"AADTenant",
|
|
216
|
+
"Guest",
|
|
217
|
+
"B2BMembers",
|
|
218
|
+
"Federated",
|
|
219
|
+
"FederatedConsumer",
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
fetchPolicy: "no-cache",
|
|
218
224
|
});
|
|
219
|
-
|
|
220
|
-
if (!Array.isArray(
|
|
225
|
+
const sets = r.data?.searchSuggestionsForPeoplePicker;
|
|
226
|
+
if (!Array.isArray(sets)) return { error: "layout" };
|
|
227
|
+
users = sets.flatMap((set) => set?.peopleResults || []);
|
|
221
228
|
}
|
|
222
229
|
const seen = new Set(),
|
|
223
230
|
items = [];
|
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import vm from 'node:vm';
|
|
5
5
|
const source=fs.readFileSync(new URL('../../src/desktop_people.js',import.meta.url),'utf8');
|
|
6
6
|
function fixture({existing=false,failQuery=false,failMutation=false,policy=false,wrongMembers=false,cached,live}={}){
|
|
7
|
-
const calls=[];const client={query:async r=>{calls.push(['query',r]);if(failQuery)throw Error();const name=r.query.definitions[0].selectionSet.selections[0].name.value;return{data:{[name]: name==='chatsWithUsers'?(existing?[{id:'19:existing@thread.v2',memberIds:wrongMembers?['8:live:other','8:live:me']:['8:live:person','8:live:me']}]:[]):name==='getCacheResults'?(cached??{peopleResults:[{id:'8:live:person',displayName:'Person'}]}):(live??[{id:'8:live:person',displayName:'Person'}])}}},mutate:async r=>{calls.push(['mutate',r]);if(failMutation)throw Error();return{data:{createChat:{conversation:{id:'19:new@thread.v2',memberIds:['8:live:me','8:live:person']},hasIBPolicyRejectedUsers:policy}}}}};
|
|
7
|
+
const calls=[];const client={query:async r=>{calls.push(['query',r]);if(failQuery)throw Error();const name=r.query.definitions[0].selectionSet.selections[0].name.value;return{data:{[name]: name==='chatsWithUsers'?(existing?[{id:'19:existing@thread.v2',memberIds:wrongMembers?['8:live:other','8:live:me']:['8:live:person','8:live:me']}]:[]):name==='getCacheResults'?(cached??{peopleResults:[{id:'8:live:person',displayName:'Person'}]}):name==='searchSuggestionsForPeoplePicker'?[{peopleResults:[]},{peopleResults:live??[{id:'8:live:person',displayName:'Person'}]}]:(live??[{id:'8:live:person',displayName:'Person'}])}}},mutate:async r=>{calls.push(['mutate',r]);if(failMutation)throw Error();return{data:{createChat:{conversation:{id:'19:new@thread.v2',memberIds:['8:live:me','8:live:person']},hasIBPolicyRejectedUsers:policy}}}}};
|
|
8
8
|
const req=result=>{const q={result};queueMicrotask(()=>q.onsuccess?.());return q};
|
|
9
9
|
const db={transaction:()=>({objectStore:()=>({getAll:()=>req([{objectId:'account',mri:'8:live:me'}])})}),close(){}};
|
|
10
10
|
const run=vm.runInNewContext(`(${source})`,{window:{__teamsCliCore:{ready:true,client}},indexedDB:{databases:async()=>[],open:()=>req(db)}});
|
|
@@ -35,3 +35,4 @@ test('cached suggestions must match every search term and live metadata takes pr
|
|
|
35
35
|
const none=fixture({cached:{mfuContactsResults:[{id:'8:live:wrong',displayName:'Unrelated'}]},live:[]});
|
|
36
36
|
assert.equal((await none.run({op:'search',personal:true,query:'absent@example.test',limit:20})).items.length,0);
|
|
37
37
|
});
|
|
38
|
+
test('work search reads people from every people picker result set without mutation',async()=>{const f=fixture({live:[{mri:'8:orgid:person',id:'person',displayName:'Person'},{id:'not-an-mri'}]});const r=await f.run({op:'search',personal:false,query:'Person',limit:20});assert.deepEqual(Array.from(r.items,i=>i.id),['8:orgid:person']);assert.equal(f.calls.length,1);assert.equal(f.calls[0][1].query.definitions[0].selectionSet.selections[0].name.value,'searchSuggestionsForPeoplePicker');assert.equal(f.calls[0][1].variables.searchQuery,'Person')});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|