返回 CodeWhale
file_transactions_tests.rs
根目录 / crates / secrets / src / file_transactions_tests.rs
1 use super::*;
2 use std::process::{Command, Stdio};
3 use std::time::{Duration, Instant};
4
5 #[test]
6 fn transaction_child() {
7 let Some(path) = std::env::var_os("CW_CORE_STORE_CHILD") else {
8 return;
9 };
10 let path = PathBuf::from(path);
11 fs::write(path.with_extension("ready"), b"ready").unwrap();
12 let store = FileKeyringStore::new(&path);
13 match std::env::var("CW_CORE_STORE_OPERATION").unwrap().as_str() {
14 "set" => store.set("child", "fixture-child").unwrap(),
15 "delete" => store.delete("delete-me").unwrap(),
16 "migrate" => {
17 FileKeyringStore::migrate_legacy_file_if_needed(&path, &path.with_extension("legacy"))
18 .unwrap()
19 }
20 _ => unreachable!(),
21 }
22 }
23
24 #[test]
25 fn file_transactions_serialize_process_set_delete_and_legacy_migration() {
26 for operation in ["set", "delete", "migrate"] {
27 let dir = tempfile::tempdir().unwrap();
28 let path = dir.path().join("secrets.json");
29 let store = FileKeyringStore::new(&path);
30 store.set("delete-me", "fixture").unwrap();
31 FileKeyringStore::new(path.with_extension("legacy"))
32 .set("migrated", "fixture")
33 .unwrap();
34 let mut child = file_lock::with_write_lock(&path, |path| {
35 let mut child = Command::new(std::env::current_exe()?)
36 .args([
37 "--exact",
38 "file_transactions_tests::transaction_child",
39 "--nocapture",
40 ])
41 .env("CW_CORE_STORE_CHILD", path)
42 .env("CW_CORE_STORE_OPERATION", operation)
43 .stdout(Stdio::null())
44 .spawn()?;
45 let deadline = Instant::now() + Duration::from_secs(5);
46 while !path.with_extension("ready").exists() {
47 assert!(Instant::now() < deadline);
48 std::thread::sleep(Duration::from_millis(5));
49 }
50 std::thread::sleep(Duration::from_millis(100));
51 assert!(child.try_wait()?.is_none(), "writer bypassed shared lock");
52 let store = FileKeyringStore::new(path);
53 let mut blob = store.load_unlocked()?;
54 blob.entries
55 .insert("parent".into(), "fixture-parent".into());
56 blob.extra
57 .insert("metadata".into(), serde_json::json!({"future":true}));
58 store.store_unlocked(&blob)?;
59 Ok(child)
60 })
61 .unwrap();
62 assert!(child.wait().unwrap().success());
63 let blob = store.load_unlocked().unwrap();
64 assert_eq!(blob.entries["parent"], "fixture-parent");
65 assert_eq!(blob.extra["metadata"]["future"], true);
66 match operation {
67 "set" => assert_eq!(blob.entries["child"], "fixture-child"),
68 "delete" => assert!(!blob.entries.contains_key("delete-me")),
69 "migrate" => assert_eq!(blob.entries["migrated"], "fixture"),
70 _ => unreachable!(),
71 }
72 }
73 }
74
75 #[test]
76 fn file_transaction_failure_does_not_commit_and_releases_lock() {
77 let dir = tempfile::tempdir().unwrap();
78 let path = dir.path().join("secrets.json");
79 let store = FileKeyringStore::new(&path);
80 store.set("keep", "fixture").unwrap();
81 let before = fs::read(&path).unwrap();
82 assert!(
83 store
84 .mutate::<()>(|blob| {
85 blob.entries.clear();
86 Err(std::io::Error::other("fixture failure").into())
87 })
88 .is_err()
89 );
90 assert_eq!(fs::read(&path).unwrap(), before);
91 store.set("next", "fixture").unwrap();
92 }
93
94 #[cfg(unix)]
95 #[test]
96 fn file_transaction_rejects_symlink_store_and_lock_without_touching_targets() {
97 use std::os::unix::fs::symlink;
98 let dir = tempfile::tempdir().unwrap();
99 let path = dir.path().join("secrets.json");
100 let target = dir.path().join("target");
101 fs::write(&target, b"keep").unwrap();
102 symlink(&target, &path).unwrap();
103 let store = FileKeyringStore::new(&path);
104 assert!(store.set("key", "fixture").is_err());
105 assert!(store.get("key").is_err());
106 fs::remove_file(&path).unwrap();
107 fs::remove_file(path.with_extension("json.lock")).unwrap();
108 symlink(&target, path.with_extension("json.lock")).unwrap();
109 assert!(store.set("key", "fixture").is_err());
110 assert_eq!(fs::read(target).unwrap(), b"keep");
111 }
112
113 #[test]
114 fn account_companion_survives_refresh_but_not_account_switch_or_logout() {
115 let dir = tempfile::tempdir().unwrap();
116 let path = dir.path().join("secrets.json");
117 let store = FileKeyringStore::new(&path);
118 let slot = account::account_auth_slot("default", account::DEFAULT_ACCOUNT_API_BASE);
119 let companion = slot.replace("-auth-", "-device-");
120 let mut bundle = serde_json::json!({"schemaVersion":1,"apiBase":account::DEFAULT_ACCOUNT_API_BASE,"bundle":{"tokenType":"Bearer","accessToken":"fixture-a","refreshToken":"fixture-r","user":{"id":"account"},"session":{"id":"session"}}});
121 store.set(&slot, &bundle.to_string()).unwrap();
122 store.set(&companion, "opaque-fixture").unwrap();
123 bundle["bundle"]["accessToken"] = "fixture-b".into();
124 store.set(&slot, &bundle.to_string()).unwrap();
125 assert!(store.get(&companion).unwrap().is_some());
126 bundle["bundle"]["session"]["id"] = "different".into();
127 store.set(&slot, &bundle.to_string()).unwrap();
128 assert!(store.get(&companion).unwrap().is_none());
129 store.set(&companion, "opaque-fixture").unwrap();
130 store.delete(&slot).unwrap();
131 assert!(store.get(&companion).unwrap().is_none());
132 }
133
133 lines RUST