Design-MD panel — closes the last TS-vs-Rust parity gap: - New floating op-editor-ui design_md_panel + design_md_markdown renderer: collapsible theme/colour/typography/component/layout/ notes sections, colour swatches, inline markdown highlighting. - op-editor-core: parse_design_md parser + panel UI state. - Host: View-menu toggle, top-most paint + hit-test, drag, .md import/export; bumps the vendor/jian DesignMdSpec pointer. i18n coverage: - Git panel, merge/conflict/error dialogs and GitError messages routed through op-i18n; git locale tables split into <base>_git.rs under the 800-line cap. - Closed 10 app-wide hardcoded-string gaps (AI chat, Figma import, file menu, layer context menu, property-panel fill, update / file-picker / load-error dialogs, accesskit labels).
174 lines
5.5 KiB
Rust
174 lines
5.5 KiB
Rust
//! Credential + SSH-key store tests: key generate / list / load /
|
|
//! delete / import, name-validation hardening, and the JSON-backed
|
|
//! `AuthStore` (set / get / replace / remove / persistence / perms).
|
|
|
|
use crate::tests::{ssh_keygen_available, unique_temp_dir};
|
|
use crate::{AuthStore, Credential, SshKeyStore};
|
|
|
|
#[test]
|
|
fn ssh_key_generate_list_load_and_delete() {
|
|
if !ssh_keygen_available() {
|
|
return;
|
|
}
|
|
let dir = unique_temp_dir("ssh");
|
|
let store = SshKeyStore::at(&dir);
|
|
assert!(store.list().expect("list empty").is_empty());
|
|
|
|
let key = store.generate("id_test", "op@test").expect("generate");
|
|
assert_eq!(key.name, "id_test");
|
|
assert!(key.public_key.starts_with("ssh-ed25519 "));
|
|
assert!(key.public_key.contains("op@test"));
|
|
assert!(key.private_path.is_file());
|
|
|
|
let listed = store.list().expect("list");
|
|
assert_eq!(listed.len(), 1);
|
|
assert_eq!(listed[0], key);
|
|
|
|
// Re-generating an existing key name is refused (no overwrite).
|
|
assert!(store.generate("id_test", "x").is_err());
|
|
|
|
store.delete("id_test").expect("delete");
|
|
assert!(store.list().expect("list after delete").is_empty());
|
|
// Deleting a missing key is a tolerated no-op.
|
|
store.delete("id_test").expect("delete is idempotent");
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_store_set_get_replace_remove_and_persist() {
|
|
let dir = unique_temp_dir("auth");
|
|
let path = dir.join("git-auth.json");
|
|
let store = AuthStore::at(&path);
|
|
|
|
// Empty store.
|
|
assert!(store.hosts().expect("hosts").is_empty());
|
|
assert!(store.get("github.com").expect("get").is_none());
|
|
|
|
// Store an HTTPS credential.
|
|
let https = Credential::Https {
|
|
username: "octocat".to_string(),
|
|
token: "ghp_secret".to_string(),
|
|
};
|
|
store.set("github.com", https.clone()).expect("set https");
|
|
assert_eq!(store.get("github.com").expect("get"), Some(https));
|
|
|
|
// A second host with an SSH credential; both must survive a
|
|
// reload from a fresh handle on the same file.
|
|
store
|
|
.set(
|
|
"git.example.com",
|
|
Credential::Ssh {
|
|
key_name: "id_work".to_string(),
|
|
},
|
|
)
|
|
.expect("set ssh");
|
|
let reloaded = AuthStore::at(&path);
|
|
assert_eq!(
|
|
reloaded.hosts().expect("hosts"),
|
|
vec!["git.example.com".to_string(), "github.com".to_string()]
|
|
);
|
|
|
|
// Re-setting a host replaces its credential.
|
|
store
|
|
.set(
|
|
"github.com",
|
|
Credential::Ssh {
|
|
key_name: "id_personal".to_string(),
|
|
},
|
|
)
|
|
.expect("replace");
|
|
assert_eq!(
|
|
store.get("github.com").expect("get"),
|
|
Some(Credential::Ssh {
|
|
key_name: "id_personal".to_string()
|
|
})
|
|
);
|
|
|
|
// Remove — and removing an absent host is a tolerated no-op.
|
|
store.remove("github.com").expect("remove");
|
|
assert!(store.get("github.com").expect("get").is_none());
|
|
store.remove("github.com").expect("remove is idempotent");
|
|
assert_eq!(
|
|
store.hosts().expect("hosts"),
|
|
vec!["git.example.com".to_string()]
|
|
);
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn auth_store_file_is_created_owner_only() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let dir = unique_temp_dir("auth-perms");
|
|
let path = dir.join("git-auth.json");
|
|
let store = AuthStore::at(&path);
|
|
store
|
|
.set(
|
|
"host",
|
|
Credential::Ssh {
|
|
key_name: "k".to_string(),
|
|
},
|
|
)
|
|
.expect("set");
|
|
// The store holds secrets — it must never be world-readable,
|
|
// not even momentarily during the write.
|
|
let mode = std::fs::metadata(&path)
|
|
.expect("metadata")
|
|
.permissions()
|
|
.mode()
|
|
& 0o777;
|
|
assert_eq!(mode, 0o600, "credential store must be owner-only (0600)");
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn ssh_key_store_rejects_directory_escaping_names() {
|
|
// No `ssh-keygen` needed: name validation runs first, before any
|
|
// filesystem work or subprocess.
|
|
let store = SshKeyStore::at(unique_temp_dir("ssh-reject"));
|
|
for bad in ["..", ".", "", "../escape", "sub/key", "/abs/key", "a\\b"] {
|
|
assert!(
|
|
store.generate(bad, "c").is_err(),
|
|
"generate must reject `{bad}`"
|
|
);
|
|
assert!(store.load(bad).is_err(), "load must reject `{bad}`");
|
|
assert!(store.delete(bad).is_err(), "delete must reject `{bad}`");
|
|
assert!(
|
|
store
|
|
.import(std::path::Path::new("/nonexistent"), bad)
|
|
.is_err(),
|
|
"import must reject `{bad}`"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ssh_key_import_copies_the_pair() {
|
|
if !ssh_keygen_available() {
|
|
return;
|
|
}
|
|
// Generate a key in a "source" store, then import it into a
|
|
// separate store under a new name.
|
|
let src_dir = unique_temp_dir("ssh-src");
|
|
let src = SshKeyStore::at(&src_dir);
|
|
let original = src.generate("orig", "src@test").expect("generate");
|
|
|
|
let dst_dir = unique_temp_dir("ssh-dst");
|
|
let dst = SshKeyStore::at(&dst_dir);
|
|
let imported = dst
|
|
.import(&original.private_path, "imported")
|
|
.expect("import");
|
|
|
|
assert_eq!(imported.name, "imported");
|
|
// The public key is carried over verbatim.
|
|
assert_eq!(imported.public_key, original.public_key);
|
|
assert!(dst_dir.join("imported").is_file());
|
|
assert!(dst_dir.join("imported.pub").is_file());
|
|
|
|
for dir in [src_dir, dst_dir] {
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
}
|