- 2001
.body(Body::empty()) - 2002
.unwrap(); - 2003
let resp = app.oneshot(req).await.unwrap(); - 2004
assert_eq!(resp.status(), StatusCode::OK); - 2005
let json = body_json(resp).await; - 2006
assert_eq!(json["total"], 0); - 2007
assert!(json["approvals"].is_array()); - 2008
} - 2009
- 2010
#[tokio::test] - 2011
async fn outcome_review_rejects_unknown_verdict() { - 2012
let state = test_state(); - 2013
let token = (*state.auth_token).clone(); - 2014
let app = authed_app(&state); - 2015
let req = Request::builder() - 2016
.method("POST") - 2017
.uri("/sessions/missing/outcome-review") - 2018
.header("authorization", format!("Bearer {token}")) - 2019
.header("content-type", "application/json") - 2020
.body(Body::from(r#"{"verdict":"maybe"}"#)) - 2021
.unwrap(); - 2022
let resp = app.oneshot(req).await.unwrap(); - 2023
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - 2024
} - 2025
- 2026
/// These three were read by the console and never sent. The console's - 2027
/// own `ConfigInfo` declared all of them, so nothing caught it: the - 2028
/// approval picker could not show what was in force, "Effective sandbox" - 2029
/// rendered its loading placeholder forever, and the workers toggle - 2030
/// rendered unchecked whatever the real value was. - 2031
#[tokio::test] - 2032
async fn config_endpoint_reports_approval_mode_sandbox_and_workers() { - 2033
let state = test_state(); - 2034
let axum::Json(json) = crate::admin::get_config_admin(axum::extract::State(state)).await; - 2035
assert!(json["approval_mode"].is_string(), "{json}"); - 2036
assert!(json["sandbox"].is_string(), "{json}"); - 2037
assert!(json["workers"].is_boolean(), "{json}"); - 2038
} - 2039
- 2040
#[tokio::test] - 2041
async fn config_endpoint_returns_ok() { - 2042
let state = test_state(); - 2043
let token = (*state.auth_token).clone(); - 2044
let app = authed_app(&state); - 2045
let req = Request::builder() - 2046
.uri("/admin/api/config") - 2047
.header("authorization", format!("Bearer {token}")) - 2048
.body(Body::empty()) - 2049
.unwrap(); - 2050
let resp = app.oneshot(req).await.unwrap(); - 2051
assert_eq!(resp.status(), StatusCode::OK); - 2052
let json = body_json(resp).await; - 2053
assert!(json["provider"].is_string()); - 2054
} - 2055
- 2056
// ---- Allowlist admin routes (docs/design/34-channel-onboarding.md) ---- - 2057
- 2058
#[tokio::test] - 2059
async fn allowlist_list_returns_entries_array() { - 2060
let state = test_state(); - 2061
let token = (*state.auth_token).clone(); - 2062
let app = authed_app(&state); - 2063
let req = Request::builder() - 2064
.uri("/admin/api/gateway/allowlist") - 2065
.header("authorization", format!("Bearer {token}")) - 2066
.body(Body::empty()) - 2067
.unwrap(); - 2068
let resp = app.oneshot(req).await.unwrap(); - 2069
assert_eq!(resp.status(), StatusCode::OK); - 2070
let json = body_json(resp).await; - 2071
// Not asserting emptiness: a developer's own global config.toml may - 2072
// legitimately seed `gateway.chat_allowlist` entries on load, and - 2073
// that is not this test's concern — only that the route responds - 2074
// with the documented shape. - 2075
assert!(json["entries"].is_array()); - 2076
} - 2077
- 2078
#[tokio::test] - 2079
async fn allowlist_list_requires_auth() { - 2080
let state = test_state(); - 2081
let app = authed_app(&state); - 2082
let req = Request::builder() - 2083
.uri("/admin/api/gateway/allowlist") - 2084
.body(Body::empty()) - 2085
.unwrap(); - 2086
let resp = app.oneshot(req).await.unwrap(); - 2087
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); - 2088
} - 2089
- 2090
#[tokio::test] - 2091
async fn allowlist_approve_defaults_workspace_to_core_cwd_and_shows_it() { - 2092
let state = test_state(); - 2093
let token = (*state.auth_token).clone(); - 2094
let app = authed_app(&state); - 2095
let req = Request::builder() - 2096
.method("POST") - 2097
.uri("/admin/api/gateway/allowlist/telegram%3A42/approve") - 2098
.header("authorization", format!("Bearer {token}")) - 2099
.header("content-type", "application/json") - 2100
.body(Body::from("{}")) - 2101
.unwrap(); - 2102
let resp = app.oneshot(req).await.unwrap(); - 2103
assert_eq!(resp.status(), StatusCode::OK); - 2104
let json = body_json(resp).await; - 2105
assert_eq!(json["status"], "allowed"); - 2106
// The point of this endpoint: the workspace is explicit in the - 2107
// response even when the caller didn't supply one. - 2108
let ws = json["workspace"].as_str().expect("workspace must be shown"); - 2109
assert_eq!(std::path::Path::new(ws), state.core.cwd().as_path()); - 2110
} - 2111
- 2112
#[tokio::test] - 2113
async fn allowlist_approve_with_explicit_workspace_and_route() { - 2114
let state = test_state(); - 2115
let token = (*state.auth_token).clone(); - 2116
let app = authed_app(&state); - 2117
let req = Request::builder() - 2118
.method("POST") - 2119
.uri("/admin/api/gateway/allowlist/telegram%3A43/approve") - 2120
.header("authorization", format!("Bearer {token}")) - 2121
.header("content-type", "application/json") - 2122
.body(Body::from( - 2123
r#"{"workspace":"/tmp/somewhere","route":{"provider":"anthropic","model":"sonnet"}}"#, - 2124
)) - 2125
.unwrap(); - 2126
let resp = app.oneshot(req).await.unwrap(); - 2127
assert_eq!(resp.status(), StatusCode::OK); - 2128
let json = body_json(resp).await; - 2129
assert_eq!(json["workspace"], "/tmp/somewhere"); - 2130
assert_eq!(json["route"]["provider"], "anthropic"); - 2131
assert_eq!(json["route"]["model"], "sonnet"); - 2132
} - 2133
- 2134
// ---- PATCH .../allowlist/{key} (docs/design/34 follow-up) ---------- - 2135
- 2136
async fn approve(app: &axum::Router, token: &str, key: &str, body: &str) { - 2137
let req = Request::builder() - 2138
.method("POST") - 2139
.uri(format!("/admin/api/gateway/allowlist/{key}/approve")) - 2140
.header("authorization", format!("Bearer {token}")) - 2141
.header("content-type", "application/json") - 2142
.body(Body::from(body.to_string())) - 2143
.unwrap(); - 2144
let resp = app.clone().oneshot(req).await.unwrap(); - 2145
assert_eq!(resp.status(), StatusCode::OK); - 2146
} - 2147
- 2148
/// Approve and return the entry JSON, for the permission-mode tests. - 2149
async fn approve_json( - 2150
app: &axum::Router, - 2151
token: &str, - 2152
key: &str, - 2153
body: &str, - 2154
) -> (StatusCode, serde_json::Value) { - 2155
let req = Request::builder() - 2156
.method("POST") - 2157
.uri(format!("/admin/api/gateway/allowlist/{key}/approve")) - 2158
.header("authorization", format!("Bearer {token}")) - 2159
.header("content-type", "application/json") - 2160
.body(Body::from(body.to_string())) - 2161
.unwrap(); - 2162
let resp = app.clone().oneshot(req).await.unwrap(); - 2163
let status = resp.status(); - 2164
if status != StatusCode::OK { - 2165
return (status, serde_json::Value::Null); - 2166
} - 2167
(status, body_json(resp).await) - 2168
} - 2169
- 2170
/// A workspace directory whose own config fixes `mode` — the ceiling - 2171
/// a channel override is capped against. - 2172
fn workspace_dir_with_mode(mode: &str) -> tempfile::TempDir { - 2173
let dir = tempfile::tempdir().unwrap(); - 2174
let vak = dir.path().join(".vak"); - 2175
std::fs::create_dir_all(&vak).unwrap(); - 2176
std::fs::write( - 2177
vak.join("config.toml"), - 2178
format!("permission_mode = \"{mode}\"\n"), - 2179
) - 2180
.unwrap(); - 2181
dir - 2182
} - 2183
- 2184
#[tokio::test] - 2185
async fn approve_without_permission_mode_inherits_the_workspace() { - 2186
let state = test_state(); - 2187
let token = (*state.auth_token).clone(); - 2188
let app = authed_app(&state); - 2189
let ws = workspace_dir_with_mode("read-only"); - 2190
let (status, json) = approve_json( - 2191
&app, - 2192
&token, - 2193
"telegram%3A60", - 2194
&serde_json::json!({ "workspace": ws.path() }).to_string(), - 2195
) - 2196
.await; - 2197
assert_eq!(status, StatusCode::OK); - 2198
// No pin stored, and the effective mode is simply the workspace's. - 2199
assert!(json["permission_mode"].is_null()); - 2200
assert_eq!(json["effective_permission_mode"], "read-only"); - 2201
assert_eq!(json["permission_capped"], false); - 2202
} - 2203
- 2204
#[tokio::test] - 2205
async fn approve_with_permission_mode_at_or_below_workspace_is_kept() { - 2206
let state = test_state(); - 2207
let token = (*state.auth_token).clone(); - 2208
let app = authed_app(&state); - 2209
let ws = workspace_dir_with_mode("full-access"); - 2210
let (status, json) = approve_json( - 2211
&app, - 2212
&token, - 2213
"telegram%3A61", - 2214
&serde_json::json!({ "workspace": ws.path(), "permission_mode": "read-only" }) - 2215
.to_string(), - 2216
) - 2217
.await; - 2218
assert_eq!(status, StatusCode::OK); - 2219
assert_eq!(json["permission_mode"], "read-only"); - 2220
assert_eq!(json["workspace_permission_mode"], "full-access"); - 2221
assert_eq!(json["effective_permission_mode"], "read-only"); - 2222
assert_eq!(json["permission_capped"], false); - 2223
} - 2224
- 2225
/// The security-relevant admin path: an operator asking for more than - 2226
/// the workspace itself allows gets the workspace's mode, not theirs, - 2227
/// and the reduction lands in the security log. - 2228
#[tokio::test] - 2229
async fn approve_with_over_broad_permission_mode_is_capped_and_audited() { - 2230
let state = test_state(); - 2231
let token = (*state.auth_token).clone(); - 2232
let app = authed_app(&state); - 2233
let ws = workspace_dir_with_mode("read-only"); - 2234
let (status, json) = approve_json( - 2235
&app, - 2236
&token, - 2237
"telegram%3A62", - 2238
&serde_json::json!({ "workspace": ws.path(), "permission_mode": "full-access" }) - 2239
.to_string(), - 2240
) - 2241
.await; - 2242
assert_eq!(status, StatusCode::OK); - 2243
// The request is recorded verbatim... - 2244
assert_eq!(json["permission_mode"], "full-access"); - 2245
// ...but what the channel actually gets is the workspace's ceiling. - 2246
assert_eq!(json["effective_permission_mode"], "read-only"); - 2247
assert_eq!(json["permission_capped"], true); - 2248
- 2249
let events = vak_core::security_events::list(&state.core.sessions_home(), 50); - 2250
assert!( - 2251
events.iter().any( - 2252
|e| e.kind == vak_core::security_events::EventKind::PermissionCapped - 2253
&& e.detail.contains("requested=full-access") - 2254
&& e.detail.contains("capped_to=read-only") - 2255
), - 2256
"a capped grant must be visible in the audit log, got {events:?}" - 2257
); - 2258
} - 2259
- 2260
#[tokio::test] - 2261
async fn approve_rejects_an_unparseable_permission_mode() { - 2262
let state = test_state(); - 2263
let token = (*state.auth_token).clone(); - 2264
let app = authed_app(&state); - 2265
let (status, _) = approve_json( - 2266
&app, - 2267
&token, - 2268
"telegram%3A63", - 2269
r#"{"permission_mode":"god-mode"}"#, - 2270
) - 2271
.await; - 2272
// A typo must be a hard 400, never a silent inherit. - 2273
assert_eq!(status, StatusCode::BAD_REQUEST); - 2274
} - 2275
- 2276
#[tokio::test] - 2277
async fn patch_sets_and_clears_a_permission_pin() { - 2278
let state = test_state(); - 2279
let token = (*state.auth_token).clone(); - 2280
let app = authed_app(&state); - 2281
let ws = workspace_dir_with_mode("full-access"); - 2282
approve( - 2283
&app, - 2284
&token, - 2285
"telegram%3A64", - 2286
&serde_json::json!({ "workspace": ws.path() }).to_string(), - 2287
) - 2288
.await; - 2289
- 2290
let resp = patch_allowlist( - 2291
&app, - 2292
&token, - 2293
"telegram%3A64", - 2294
&serde_json::json!({ "workspace": ws.path(), "permission_mode": "workspace-write" }) - 2295
.to_string(), - 2296
) - 2297
.await; - 2298
assert_eq!(resp.status(), StatusCode::OK); - 2299
let json = body_json(resp).await; - 2300
assert_eq!(json["permission_mode"], "workspace-write"); - 2301
assert_eq!(json["effective_permission_mode"], "workspace-write"); - 2302
- 2303
// An omitted field clears the pin back to inherit, the same way an - 2304
// empty route object clears a pinned route. - 2305
let resp = patch_allowlist( - 2306
&app, - 2307
&token, - 2308
"telegram%3A64", - 2309
&serde_json::json!({ "workspace": ws.path() }).to_string(), - 2310
) - 2311
.await; - 2312
let json = body_json(resp).await; - 2313
assert!(json["permission_mode"].is_null()); - 2314
assert_eq!(json["effective_permission_mode"], "full-access"); - 2315
} - 2316
- 2317
#[tokio::test] - 2318
async fn patch_with_over_broad_permission_mode_is_capped() { - 2319
let state = test_state(); - 2320
let token = (*state.auth_token).clone(); - 2321
let app = authed_app(&state); - 2322
let ws = workspace_dir_with_mode("workspace-write"); - 2323
approve( - 2324
&app, - 2325
&token, - 2326
"telegram%3A65", - 2327
&serde_json::json!({ "workspace": ws.path() }).to_string(), - 2328
) - 2329
.await; - 2330
let resp = patch_allowlist( - 2331
&app, - 2332
&token, - 2333
"telegram%3A65", - 2334
&serde_json::json!({ "workspace": ws.path(), "permission_mode": "full-access" }) - 2335
.to_string(), - 2336
) - 2337
.await; - 2338
assert_eq!(resp.status(), StatusCode::OK); - 2339
let json = body_json(resp).await; - 2340
assert_eq!(json["effective_permission_mode"], "workspace-write"); - 2341
assert_eq!(json["permission_capped"], true); - 2342
} - 2343
- 2344
/// Editing a route from the *binding* surface must not silently drop a - 2345
/// channel's permission pin — that surface never mentions permissions. - 2346
#[tokio::test] - 2347
async fn binding_route_write_through_preserves_the_permission_pin() { - 2348
let state = test_state(); - 2349
let ws = workspace_dir_with_mode("full-access"); - 2350
state.gateway.allowlist_approve( - 2351
&state.core, - 2352
"telegram:66", - 2353
ws.path().to_path_buf(), - 2354
None, - 2355
None, - 2356
Some(vak_config::PermissionMode::ReadOnly), - 2357
vak_config::ChannelPolicy::default(), - 2358
None, - 2359
true, - 2360
"admin", - 2361
); - 2362
state.gateway.allowlist_patch_route_if_allowed( - 2363
&state.core, - 2364
"telegram:66", - 2365
Some(crate::gateway::AllowlistRoute { - 2366
provider: "anthropic".into(), - 2367
model: "sonnet".into(), - 2368
}), - 2369
); - 2370
let entry = state.gateway.allowlist_get("telegram:66").unwrap(); - 2371
assert_eq!( - 2372
entry.permission_mode, - 2373
Some(vak_config::PermissionMode::ReadOnly) - 2374
); - 2375
assert_eq!(entry.route.unwrap().model, "sonnet"); - 2376
} - 2377
- 2378
async fn patch_allowlist( - 2379
app: &axum::Router, - 2380
token: &str, - 2381
key: &str, - 2382
body: &str, - 2383
) -> axum::response::Response { - 2384
let req = Request::builder() - 2385
.method("PATCH") - 2386
.uri(format!("/admin/api/gateway/allowlist/{key}")) - 2387
.header("authorization", format!("Bearer {token}")) - 2388
.header("content-type", "application/json") - 2389
.body(Body::from(body.to_string())) - 2390
.unwrap(); - 2391
app.clone().oneshot(req).await.unwrap() - 2392
} - 2393
- 2394
#[tokio::test] - 2395
async fn allowlist_patch_edits_workspace_in_place_keeping_provenance() { - 2396
let state = test_state(); - 2397
let token = (*state.auth_token).clone(); - 2398
let app = authed_app(&state); - 2399
approve(&app, &token, "telegram%3A50", r#"{"workspace":"/tmp/one"}"#).await; - 2400
let before = state.gateway.allowlist_get("telegram:50").unwrap(); - 2401
- 2402
let resp = - 2403
patch_allowlist(&app, &token, "telegram%3A50", r#"{"workspace":"/tmp/two"}"#).await; - 2404
assert_eq!(resp.status(), StatusCode::OK); - 2405
let json = body_json(resp).await; - 2406
assert_eq!(json["workspace"], "/tmp/two"); - 2407
// A re-point is not a re-approval: added_at/added_by must survive. - 2408
assert_eq!(json["added_at"], before.added_at); - 2409
assert_eq!(json["added_by"], before.added_by); - 2410
assert_eq!(json["status"], "allowed"); - 2411
} - 2412
- 2413
#[tokio::test] - 2414
async fn allowlist_patch_clears_a_pinned_route_with_an_empty_route_object() { - 2415
let state = test_state(); - 2416
let token = (*state.auth_token).clone(); - 2417
let app = authed_app(&state); - 2418
approve( - 2419
&app, - 2420
&token, - 2421
"telegram%3A51", - 2422
r#"{"workspace":"/tmp/one","route":{"provider":"anthropic","model":"sonnet"}}"#, - 2423
) - 2424
.await; - 2425
let resp = patch_allowlist( - 2426
&app, - 2427
&token, - 2428
"telegram%3A51", - 2429
r#"{"workspace":"/tmp/one","route":{}}"#, - 2430
) - 2431
.await; - 2432
assert_eq!(resp.status(), StatusCode::OK); - 2433
assert!( - 2434
state - 2435
.gateway - 2436
.allowlist_get("telegram:51") - 2437
.unwrap() - 2438
.route - 2439
.is_none() - 2440
); - 2441
} - 2442
- 2443
#[tokio::test] - 2444
async fn allowlist_patch_rotates_through_the_existing_stale_detection_path() { - 2445
// docs/design/34: an edited workspace/route must take effect the - 2446
// way a stale binding already does — by dropping the cached route - 2447
// revision so the next inbound message re-derives it, never by - 2448
// mutating state the binding/session layer doesn't know about. - 2449
let state = test_state(); - 2450
let token = (*state.auth_token).clone(); - 2451
let app = authed_app(&state); - 2452
approve(&app, &token, "telegram%3A52", r#"{"workspace":"/tmp/one"}"#).await; - 2453
state.gateway.set_route_override( - 2454
&state.core, - 2455
"telegram:52".into(), - 2456
Some(("anthropic".into(), "sonnet".into())), - 2457
); - 2458
// A bound session with a frozen revision, as dispatch would leave it. - 2459
state - 2460
.gateway - 2461
.bind_for_test("telegram:52", "sess-1", "channel:anthropic:sonnet"); - 2462
assert!( - 2463
state - 2464
.gateway - 2465
.route_revision_for_test("telegram:52") - 2466
.is_some() - 2467
); - 2468
- 2469
let resp = - 2470
patch_allowlist(&app, &token, "telegram%3A52", r#"{"workspace":"/tmp/two"}"#).await; - 2471
assert_eq!(resp.status(), StatusCode::OK); - 2472
assert!( - 2473
state - 2474
.gateway - 2475
.route_revision_for_test("telegram:52") - 2476
.is_none() - 2477
); - 2478
// The ledger binding itself is preserved — rotation replaces the - 2479
// session on the next message, it never deletes history. - 2480
assert_eq!( - 2481
state.gateway.session_id_for_test("telegram:52").as_deref(), - 2482
Some("sess-1") - 2483
); - 2484
} - 2485
- 2486
#[tokio::test] - 2487
async fn allowlist_patch_is_404_for_unknown_and_non_allowed_keys() { - 2488
let state = test_state(); - 2489
let token = (*state.auth_token).clone(); - 2490
let app = authed_app(&state); - 2491
let resp = patch_allowlist( - 2492
&app, - 2493
&token, - 2494
"telegram%3Anever-seen", - 2495
r#"{"workspace":"/tmp"}"#, - 2496
) - 2497
.await; - 2498
assert_eq!(resp.status(), StatusCode::NOT_FOUND); - 2499
- 2500
// Denied entries move through approve/deny, never through edit. - 2501
state - 2502
.gateway - 2503
.allowlist_deny(&state.core, "telegram:53", "admin"); - 2504
let resp = patch_allowlist(&app, &token, "telegram%3A53", r#"{"workspace":"/tmp"}"#).await; - 2505
assert_eq!(resp.status(), StatusCode::NOT_FOUND); - 2506
} - 2507
- 2508
#[tokio::test] - 2509
async fn allowlist_patch_rejects_a_key_that_is_not_surface_chat() { - 2510
let state = test_state(); - 2511
let token = (*state.auth_token).clone(); - 2512
let app = authed_app(&state); - 2513
let resp = patch_allowlist(&app, &token, "nocolon", r#"{"workspace":"/tmp"}"#).await; - 2514
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - 2515
} - 2516
- 2517
#[tokio::test] - 2518
async fn binding_route_patch_writes_through_to_the_allowed_entry() { - 2519
// One source of truth: changing the route from the pre-existing - 2520
// binding editor must not leave the allowlist entry holding a - 2521
// different, stale answer to "what does this channel route to". - 2522
let state = test_state(); - 2523
let token = (*state.auth_token).clone(); - 2524
let app = authed_app(&state); - 2525
approve( - 2526
&app, - 2527
&token, - 2528
"telegram%3A54", - 2529
r#"{"workspace":"/tmp/one","route":{"provider":"anthropic","model":"sonnet"}}"#, - 2530
) - 2531
.await; - 2532
let req = Request::builder() - 2533
.method("PATCH") - 2534
.uri("/admin/api/gateway/bindings/telegram%3A54") - 2535
.header("authorization", format!("Bearer {token}")) - 2536
.header("content-type", "application/json") - 2537
.body(Body::from(r#"{}"#)) - 2538
.unwrap(); - 2539
let resp = app.clone().oneshot(req).await.unwrap(); - 2540
assert_eq!(resp.status(), StatusCode::OK); - 2541
let entry = state.gateway.allowlist_get("telegram:54").unwrap(); - 2542
assert!( - 2543
entry.route.is_none(), - 2544
"entry kept a route the binding cleared" - 2545
); - 2546
// The workspace is untouched by a route-only edit. - 2547
assert_eq!(entry.workspace.unwrap().to_string_lossy(), "/tmp/one"); - 2548
} - 2549
- 2550
#[test] - 2551
fn scratch_workspace_detection() { - 2552
assert!(super::is_scratch_workspace("/tmp/foo")); - 2553
assert!(super::is_scratch_workspace("/private/tmp/foo")); - 2554
assert!(super::is_scratch_workspace( - 2555
"/var/folders/0g/xyz/T/.tmpAbC123" - 2556
)); - 2557
assert!(super::is_scratch_workspace( - 2558
"/private/var/folders/0g/xyz/T/.tmpAbC123" - 2559
)); - 2560
assert!(super::is_scratch_workspace("/Users/x/anywhere/.tmpZZZZZZ")); - 2561
assert!(!super::is_scratch_workspace("/Users/x/Projects/vakcoder")); - 2562
assert!(!super::is_scratch_workspace("/Users/x/Projects/tmp-tool")); - 2563
} - 2564
- 2565
#[tokio::test] - 2566
async fn gateway_status_lists_known_workspaces_including_the_gateway_cwd() { - 2567
let state = test_state(); - 2568
let token = (*state.auth_token).clone(); - 2569
let app = authed_app(&state); - 2570
let req = Request::builder() - 2571
.uri("/admin/api/gateway/status") - 2572
.header("authorization", format!("Bearer {token}")) - 2573
.body(Body::empty()) - 2574
.unwrap(); - 2575
let json = body_json(app.oneshot(req).await.unwrap()).await; - 2576
let known = json["known_workspaces"].as_array().unwrap(); - 2577
assert!( - 2578
known - 2579
.iter() - 2580
.any(|w| w.as_str() == Some(&state.core.cwd().display().to_string())), - 2581
"the gateway's own workspace must always be offered" - 2582
); - 2583
} - 2584
- 2585
#[tokio::test] - 2586
async fn gateway_status_lists_allowed_channel_before_first_message() { - 2587
let state = test_state(); - 2588
let token = (*state.auth_token).clone(); - 2589
let app = authed_app(&state); - 2590
approve(&app, &token, "telegram%3A67", r#"{}"#).await; - 2591
let req = Request::builder() - 2592
.uri("/admin/api/gateway/status") - 2593
.header("authorization", format!("Bearer {token}")) - 2594
.body(Body::empty()) - 2595
.unwrap(); - 2596
let json = body_json(app.oneshot(req).await.unwrap()).await; - 2597
let binding = json["bindings"] - 2598
.as_array() - 2599
.unwrap() - 2600
.iter() - 2601
.find(|binding| binding["target"] == "telegram:67") - 2602
.expect("approved cold channel must be visible"); - 2603
assert!(binding["session_id"].is_null()); - 2604
assert_eq!(binding["stale"], false); - 2605
} - 2606
- 2607
#[tokio::test] - 2608
async fn gateway_status_reports_an_existing_bindings_configured_workspace() { - 2609
let state = test_state(); - 2610
let token = (*state.auth_token).clone(); - 2611
let app = authed_app(&state); - 2612
approve( - 2613
&app, - 2614
&token, - 2615
"telegram%3A68", - 2616
r#"{"workspace":"/tmp/chat-workspace"}"#, - 2617
) - 2618
.await; - 2619
state - 2620
.gateway - 2621
.set_route_override(&state.core, "telegram:68".into(), None); - 2622
- 2623
let req = Request::builder() - 2624
.uri("/admin/api/gateway/status") - 2625
.header("authorization", format!("Bearer {token}")) - 2626
.body(Body::empty()) - 2627
.unwrap(); - 2628
let json = body_json(app.oneshot(req).await.unwrap()).await; - 2629
let binding = json["bindings"] - 2630
.as_array() - 2631
.unwrap() - 2632
.iter() - 2633
.find(|binding| binding["target"] == "telegram:68") - 2634
.expect("existing binding must be visible"); - 2635
assert_eq!(binding["workspace"], "/tmp/chat-workspace"); - 2636
assert_eq!(binding["configured_workspace"], "/tmp/chat-workspace"); - 2637
assert_eq!(binding["stale"], false); - 2638
} - 2639
- 2640
#[tokio::test] - 2641
async fn allowlist_deny_then_revoke_of_denied_is_404() { - 2642
let state = test_state(); - 2643
let token = (*state.auth_token).clone(); - 2644
let app = authed_app(&state); - 2645
let req = Request::builder() - 2646
.method("POST") - 2647
.uri("/admin/api/gateway/allowlist/telegram%3A44/deny") - 2648
.header("authorization", format!("Bearer {token}")) - 2649
.body(Body::empty()) - 2650
.unwrap(); - 2651
let resp = app.oneshot(req).await.unwrap(); - 2652
assert_eq!(resp.status(), StatusCode::OK); - 2653
let json = body_json(resp).await; - 2654
assert_eq!(json["status"], "denied"); - 2655
- 2656
// Revoke only applies to *allowed* entries; a denied one 404s. - 2657
let app2 = authed_app(&state); - 2658
let req = Request::builder() - 2659
.method("DELETE") - 2660
.uri("/admin/api/gateway/allowlist/telegram%3A44") - 2661
.header("authorization", format!("Bearer {token}")) - 2662
.body(Body::empty()) - 2663
.unwrap(); - 2664
let resp = app2.oneshot(req).await.unwrap(); - 2665
assert_eq!(resp.status(), StatusCode::NOT_FOUND); - 2666
} - 2667
- 2668
#[tokio::test] - 2669
async fn allowlist_revoke_unknown_key_is_404() { - 2670
let state = test_state(); - 2671
let token = (*state.auth_token).clone(); - 2672
let app = authed_app(&state); - 2673
let req = Request::builder() - 2674
.method("DELETE") - 2675
.uri("/admin/api/gateway/allowlist/telegram%3Anever-seen") - 2676
.header("authorization", format!("Bearer {token}")) - 2677
.body(Body::empty()) - 2678
.unwrap(); - 2679
let resp = app.oneshot(req).await.unwrap(); - 2680
assert_eq!(resp.status(), StatusCode::NOT_FOUND); - 2681
} - 2682
- 2683
#[tokio::test] - 2684
async fn allowlist_approve_then_revoke_roundtrip() { - 2685
let state = test_state(); - 2686
let token = (*state.auth_token).clone(); - 2687
let app = authed_app(&state); - 2688
let req = Request::builder() - 2689
.method("POST") - 2690
.uri("/admin/api/gateway/allowlist/telegram%3A45/approve") - 2691
.header("authorization", format!("Bearer {token}")) - 2692
.header("content-type", "application/json") - 2693
.body(Body::from("{}")) - 2694
.unwrap(); - 2695
let resp = app.oneshot(req).await.unwrap(); - 2696
assert_eq!(resp.status(), StatusCode::OK); - 2697
- 2698
let app2 = authed_app(&state); - 2699
let req = Request::builder() - 2700
.method("DELETE") - 2701
.uri("/admin/api/gateway/allowlist/telegram%3A45") - 2702
.header("authorization", format!("Bearer {token}")) - 2703
.body(Body::empty()) - 2704
.unwrap(); - 2705
let resp = app2.oneshot(req).await.unwrap(); - 2706
assert_eq!(resp.status(), StatusCode::OK); - 2707
- 2708
let app3 = authed_app(&state); - 2709
let req = Request::builder() - 2710
.uri("/admin/api/gateway/allowlist") - 2711
.header("authorization", format!("Bearer {token}")) - 2712
.body(Body::empty()) - 2713
.unwrap(); - 2714
let resp = app3.oneshot(req).await.unwrap(); - 2715
let json = body_json(resp).await; - 2716
let has_key = json["entries"] - 2717
.as_array() - 2718
.unwrap() - 2719
.iter() - 2720
.any(|e| e["key"] == "telegram:45"); - 2721
assert!(!has_key, "revoked entry must not remain in the list"); - 2722
} - 2723
} - 2724
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.