Google binds its session cookies to the device (Device Bound Session Credentials) and signs out sessions whose cookies look stolen, which can sign the owner out of their real Chrome. The default is now storebound's own profile per store; storebound starts Chrome itself (--user-data-dir, --remote-debugging-port=0, no --enable-automation, navigator.webdriver false) and attaches over CDP. --use-chrome-session google|apple is an explicit opt-in that warns. /signin/rejected is said plainly.
82 lines
5.2 KiB
Rust
82 lines
5.2 KiB
Rust
//! Desktop D3 writes: owner actions are dry runs from the window like every write; only `write_with_confirm` runs them,
|
|
//! after a dialog that names the action on its button and says users see it at once. Tokens are bound to exactly the
|
|
//! method and the { project, args } sent (the same vector as the TypeScript side checks).
|
|
use serde_json::json;
|
|
use storebound_desktop::commands::{console_fill_dialog, dialog_labels, dialog_summary, owner_summary};
|
|
use storebound_desktop::confirm::{canonical, mac, mint};
|
|
use storebound_desktop::policy::{sanitize_js_call, Kind, CONSOLE_FILL, CONSOLE_FILL_NOTE};
|
|
|
|
#[test]
|
|
fn owner_actions_are_dry_runs_from_the_window() {
|
|
let p = sanitize_js_call("owner.play_rollout", Some(Kind::Owner), &json!({ "args": { "action": "complete" } })).unwrap();
|
|
assert_eq!(p["dryRun"], json!(true));
|
|
for bad in [json!({ "dryRun": false }), json!({ "confirmToken": "sbt1.x" }), json!({ "credentials": {} }), json!({ "project": "/x" })] {
|
|
assert!(sanitize_js_call("owner.reply_send", Some(Kind::Owner), &bad).is_err(), "{bad}");
|
|
}
|
|
assert_eq!(Kind::parse("owner"), Some(Kind::Owner));
|
|
let w = sanitize_js_call("reviews.reply", Some(Kind::Write), &json!({ "args": { "file": "play-1.md", "text": "Thanks" } })).unwrap();
|
|
assert_eq!(w["dryRun"], json!(true));
|
|
}
|
|
|
|
#[test]
|
|
fn owner_dialogs_name_the_action() {
|
|
let (title, ok) = dialog_labels(Kind::Owner, "Change the Play staged rollout");
|
|
assert_eq!(title, "Owner action: Change the Play staged rollout");
|
|
assert_eq!(ok, "Yes: Change the Play staged rollout");
|
|
assert_eq!(dialog_labels(Kind::Write, "Push Play listing text"), ("Apply: Push Play listing text".to_string(), "Apply".to_string()));
|
|
let s = owner_summary("Send one approved reply", "Sends one approved, unsent reply draft.", "Prism Puddle");
|
|
assert!(s.contains("Prism Puddle") && s.contains("right away") && s.contains("your decision"));
|
|
assert!(dialog_summary("Push", "d", "A", true, false).contains("not public until you submit"));
|
|
}
|
|
|
|
#[test]
|
|
fn tokens_bind_method_project_and_args_of_each_write() {
|
|
let secret = [9u8; 32];
|
|
let writes = [
|
|
("play.listing", json!({})),
|
|
("apple.listing", json!({ "version": "2.4.0" })),
|
|
("apple.screenshots", json!({})),
|
|
("apple.testflight_distribute", json!({ "build": "52", "groups": "Studio team" })),
|
|
("reviews.reply", json!({ "file": "play-r-p1.md", "text": "Thanks!" })),
|
|
("owner.play_rollout", json!({ "action": "percent", "percent": 50 })),
|
|
("owner.ios_phased", json!({ "action": "resume", "version": "2.3.0" })),
|
|
("owner.reply_send", json!({ "file": "play-r-p1.md" })),
|
|
];
|
|
for (method, args) in writes {
|
|
let bound = json!({ "project": "/apps/prism", "args": args });
|
|
let t = mint(&secret, method, &bound, 1_000, "AAAAAAAAAAAAAAAAAAAAAA").unwrap();
|
|
let sig = t.rsplit('.').next().unwrap().to_string();
|
|
assert_eq!(sig, mac(&secret, method, &bound, 61_000, "AAAAAAAAAAAAAAAAAAAAAA").unwrap());
|
|
// any change of method, project or args changes the signature
|
|
assert_ne!(sig, mac(&secret, "play.images", &bound, 61_000, "AAAAAAAAAAAAAAAAAAAAAA").unwrap(), "{method}");
|
|
assert_ne!(sig, mac(&secret, method, &json!({ "project": "/apps/other", "args": bound["args"] }), 61_000, "AAAAAAAAAAAAAAAAAAAAAA").unwrap());
|
|
let mut more = bound.clone();
|
|
more["args"]["x"] = json!(1);
|
|
assert_ne!(sig, mac(&secret, method, &more, 61_000, "AAAAAAAAAAAAAAAAAAAAAA").unwrap());
|
|
}
|
|
// a rollout share is a safe integer (percent), so it can be bound; fractions as numbers cannot
|
|
assert!(canonical(&json!({ "percent": 50 })).is_ok());
|
|
assert!(canonical(&json!({ "fraction": 0.5 })).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn console_fill_is_a_dry_run_from_the_window_and_its_dialog_lists_the_answers() {
|
|
let p = sanitize_js_call(CONSOLE_FILL, Some(Kind::Write), &json!({ "args": { "form": "apple-age-rating" } })).unwrap();
|
|
assert_eq!(p["dryRun"], json!(true));
|
|
assert!(sanitize_js_call(CONSOLE_FILL, Some(Kind::Write), &json!({ "dryRun": false })).is_err());
|
|
let data = json!({ "title": "Age rating", "uncertain": 1, "answers": [
|
|
{ "id": "violence", "label": "Cartoon violence", "value": "None", "certain": true, "source": "apple.ageRating" },
|
|
{ "id": "gambling", "label": "Simulated gambling", "value": "", "certain": false, "source": "" },
|
|
] });
|
|
let (title, text, ok) = console_fill_dialog("Prism Puddle", &data);
|
|
assert_eq!(title, "Fill in console: Age rating");
|
|
assert_eq!(ok, "Open the browser");
|
|
assert!(text.contains("Prism Puddle") && text.contains(CONSOLE_FILL_NOTE));
|
|
assert!(text.contains("uses storebound's own browser profile") && text.contains("cookies are never copied"));
|
|
assert!(text.contains("- Cartoon violence: None") && text.contains("- Simulated gambling: you answer this"));
|
|
let (_, empty, _) = console_fill_dialog("A", &json!(null));
|
|
assert!(empty.contains("every field is yours"));
|
|
let many: Vec<_> = (0..400).map(|i| json!({ "label": format!("Question {i}"), "value": "Yes", "certain": true })).collect();
|
|
let (_, long, _) = console_fill_dialog("A", &json!({ "title": "T", "answers": many }));
|
|
assert!(long.contains("more in the app") && long.chars().count() < 3_200);
|
|
}
|