From b263c27da9422bb94e8c39e40ae238475c230e74 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Mon, 20 Apr 2026 18:32:45 -0400 Subject: [PATCH] test(cli): integration harness + basic flow tests Uses assert_cmd + tempfile to spin up a fresh vault per test. Covers init layout, add/list/get mask semantics, rm/restore/purge cycle, and generate smoke. Adds RELICARIO_TEST_PASSPHRASE env-var hatch in unlock_interactive and cmd_init so tests don't need a TTY. Also fixes read_params in session.rs to correctly parse the nested params.json format (kdf sub-object) rather than trying to deserialize the whole file as KdfParams. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 492 ++++++++++++++++++++-- crates/relicario-cli/Cargo.toml | 2 + crates/relicario-cli/src/main.rs | 14 +- crates/relicario-cli/src/session.rs | 22 +- crates/relicario-cli/tests/basic_flows.rs | 136 ++++++ crates/relicario-cli/tests/common/mod.rs | 117 +++++ 6 files changed, 741 insertions(+), 42 deletions(-) create mode 100644 crates/relicario-cli/tests/basic_flows.rs create mode 100644 crates/relicario-cli/tests/common/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 68b9fa6..a1baf86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,21 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_cmd" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -212,6 +227,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -455,6 +481,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -539,6 +571,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -566,6 +604,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "fax" version = "0.2.6" @@ -617,6 +661,21 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -683,6 +742,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "half" version = "2.7.1" @@ -694,6 +766,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.5.0" @@ -830,6 +917,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -867,6 +960,18 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + [[package]] name = "inout" version = "0.1.4" @@ -915,6 +1020,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.184" @@ -999,6 +1110,12 @@ dependencies = [ "pxfm", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1231,6 +1348,46 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1261,6 +1418,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -1288,7 +1451,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] @@ -1306,7 +1469,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -1346,15 +1509,22 @@ version = "0.1.0" dependencies = [ "anyhow", "arboard", + "assert_cmd", + "chrono", "clap", + "data-encoding", "dirs", "ed25519-dalek", "hex", + "image", + "predicates", "rand", "relicario-core", "rpassword", "serde", "serde_json", + "tempfile", + "url", "zeroize", ] @@ -1367,12 +1537,14 @@ dependencies = [ "chacha20poly1305", "chrono", "ed25519-dalek", - "getrandom", + "getrandom 0.2.17", "hex", + "hmac", "image", "rand", "serde", "serde_json", + "sha1", "sha2", "thiserror 2.0.18", "unicode-normalization", @@ -1385,26 +1557,36 @@ dependencies = [ name = "relicario-wasm" version = "0.1.0" dependencies = [ - "data-encoding", - "getrandom", - "hmac", + "getrandom 0.2.17", "image", - "js-sys", "relicario-core", + "serde", + "serde-wasm-bindgen", "serde_json", - "sha1", "wasm-bindgen", "wasm-bindgen-test", + "zeroize", ] [[package]] name = "rpassword" -version = "5.0.1" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" dependencies = [ "libc", - "winapi", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", ] [[package]] @@ -1466,6 +1648,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -1604,6 +1797,25 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -1723,6 +1935,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -1764,6 +1982,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1780,6 +2007,24 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.118" @@ -1874,6 +2119,40 @@ version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23cda5ecc67248c48d3e705d3e03e00af905769b78b9d2a1678b663b8b9d4472" +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.95" @@ -1890,22 +2169,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -1915,12 +2178,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.62.2" @@ -1989,6 +2246,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -2022,6 +2288,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -2032,7 +2314,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", @@ -2045,6 +2327,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" @@ -2057,6 +2345,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" @@ -2069,12 +2363,24 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" @@ -2087,6 +2393,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" @@ -2099,6 +2411,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" @@ -2111,6 +2429,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" @@ -2123,12 +2447,112 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/crates/relicario-cli/Cargo.toml b/crates/relicario-cli/Cargo.toml index 76574c5..1cb4766 100644 --- a/crates/relicario-cli/Cargo.toml +++ b/crates/relicario-cli/Cargo.toml @@ -29,3 +29,5 @@ data-encoding = "2" assert_cmd = "2" predicates = "3" tempfile = "3" +image = { version = "0.25", default-features = false, features = ["jpeg"] } +serde_json = "1" diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 2669307..fd30c0d 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -272,8 +272,18 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> { } // Passphrase with strength gate (audit H3). - let passphrase = Zeroizing::new(rpassword::prompt_password("Choose a passphrase: ")?); - let confirm = Zeroizing::new(rpassword::prompt_password("Confirm passphrase: ")?); + // RELICARIO_TEST_PASSPHRASE is a test-only escape hatch that bypasses the + // TTY prompt so integration tests can run without a real TTY. + let passphrase = if let Ok(p) = std::env::var("RELICARIO_TEST_PASSPHRASE") { + Zeroizing::new(p) + } else { + Zeroizing::new(rpassword::prompt_password("Choose a passphrase: ")?) + }; + let confirm = if std::env::var_os("RELICARIO_TEST_PASSPHRASE").is_some() { + passphrase.clone() + } else { + Zeroizing::new(rpassword::prompt_password("Confirm passphrase: ")?) + }; if passphrase.as_str() != confirm.as_str() { anyhow::bail!("passphrases do not match"); } diff --git a/crates/relicario-cli/src/session.rs b/crates/relicario-cli/src/session.rs index e4bb823..62c2c0b 100644 --- a/crates/relicario-cli/src/session.rs +++ b/crates/relicario-cli/src/session.rs @@ -39,10 +39,14 @@ impl UnlockedVault { .with_context(|| format!("failed to read reference image {}", image_path.display()))?; let image_secret = Zeroizing::new(imgsecret::extract(&image_bytes)?); - let passphrase = Zeroizing::new( - rpassword::prompt_password("Passphrase: ") - .context("failed to read passphrase")? - ); + let passphrase = if let Ok(p) = std::env::var("RELICARIO_TEST_PASSPHRASE") { + Zeroizing::new(p) + } else { + Zeroizing::new( + rpassword::prompt_password("Passphrase: ") + .context("failed to read passphrase")? + ) + }; let master_key = derive_master_key( passphrase.as_bytes(), @@ -104,10 +108,16 @@ fn read_salt(root: &Path) -> Result<[u8; 32]> { } fn read_params(root: &Path) -> Result { + // params.json layout: { "format_version": 2, "kdf": { "argon2_m": ..., ... }, ... } + // We extract only the "kdf" sub-object and deserialize it as KdfParams. + #[derive(serde::Deserialize)] + struct ParamsFile { + kdf: KdfParams, + } let s = fs::read_to_string(root.join(".relicario").join("params.json")) .context("failed to read .relicario/params.json")?; - let params: KdfParams = serde_json::from_str(&s).context("failed to parse params.json")?; - Ok(params) + let pf: ParamsFile = serde_json::from_str(&s).context("failed to parse params.json")?; + Ok(pf.kdf) } /// Locate the reference image path via `RELICARIO_IMAGE` env var or interactive prompt. diff --git a/crates/relicario-cli/tests/basic_flows.rs b/crates/relicario-cli/tests/basic_flows.rs new file mode 100644 index 0000000..dc493ec --- /dev/null +++ b/crates/relicario-cli/tests/basic_flows.rs @@ -0,0 +1,136 @@ +mod common; + +use assert_cmd::cargo::CommandCargoExt as _; +use common::TestVault; + +#[test] +fn init_creates_expected_layout() { + let v = TestVault::init(); + assert!(v.path().join(".relicario/salt").exists()); + assert!(v.path().join(".relicario/params.json").exists()); + assert!(v.path().join(".relicario/devices.json").exists()); + assert!(v.path().join("manifest.enc").exists()); + assert!(v.path().join("settings.enc").exists()); + assert!(v.path().join("reference.jpg").exists()); + assert!(v.path().join(".gitignore").exists()); + assert!(v.path().join(".git").is_dir()); +} + +#[test] +fn init_params_json_is_format_v2() { + let v = TestVault::init(); + let s = std::fs::read_to_string(v.path().join(".relicario/params.json")).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&s).unwrap(); + assert_eq!(parsed["format_version"], 2); + assert_eq!(parsed["kdf"]["algorithm"], "argon2id-v0x13"); + assert_eq!(parsed["aead"], "xchacha20poly1305"); +} + +#[test] +fn add_login_then_list_shows_it() { + let v = TestVault::init(); + let out = v.run(&[ + "add", + "login", + "--title", + "GitHub", + "--username", + "alice", + "--url", + "https://github.com", + "--password", + "hunter2", + ]); + assert!(out.status.success(), "add failed: {:?}", out); + let out = v.run(&["list"]); + let stdout = String::from_utf8(out.stdout).unwrap(); + assert!(stdout.contains("GitHub"), "list missing GitHub: {stdout}"); +} + +#[test] +fn get_masks_by_default_shows_with_flag() { + let v = TestVault::init(); + v.run(&[ + "add", + "login", + "--title", + "gmail", + "--username", + "u", + "--password", + "super-secret", + ]); + + let masked = v.run(&["get", "gmail"]); + let stdout = String::from_utf8(masked.stdout).unwrap(); + assert!(stdout.contains("********"), "expected masked: {stdout}"); + assert!( + !stdout.contains("super-secret"), + "leaked plaintext: {stdout}" + ); + + let shown = v.run(&["get", "gmail", "--show"]); + let stdout = String::from_utf8(shown.stdout).unwrap(); + assert!(stdout.contains("super-secret"), "expected plaintext: {stdout}"); +} + +#[test] +fn rm_restore_purge_cycle() { + let v = TestVault::init(); + v.run(&[ + "add", + "login", + "--title", + "target", + "--username", + "u", + "--password", + "p", + ]); + + let rm = v.run(&["rm", "target"]); + assert!(rm.status.success()); + + let out = v.run(&["list"]); + assert!(!String::from_utf8(out.stdout).unwrap().contains("target")); + + let out = v.run(&["list", "--trashed"]); + assert!(String::from_utf8(out.stdout).unwrap().contains("target")); + + let restore = v.run(&["restore", "target"]); + assert!(restore.status.success()); + let out = v.run(&["list"]); + assert!(String::from_utf8(out.stdout).unwrap().contains("target")); + + let purge = v.run(&["purge", "target"]); + assert!(purge.status.success()); + let out = v.run(&["list"]); + assert!(!String::from_utf8(out.stdout).unwrap().contains("target")); +} + +#[test] +fn generate_random_and_bip39() { + let dir = tempfile::TempDir::new().unwrap(); + + let out = std::process::Command::cargo_bin("relicario") + .unwrap() + .current_dir(dir.path()) + .args(["generate", "--length", "32"]) + .output() + .unwrap(); + assert!(out.status.success()); + assert_eq!( + String::from_utf8(out.stdout).unwrap().trim().len(), + 32 + ); + + let out = std::process::Command::cargo_bin("relicario") + .unwrap() + .current_dir(dir.path()) + .args(["generate", "--bip39", "--words", "5"]) + .output() + .unwrap(); + assert!(out.status.success()); + let phrase = String::from_utf8(out.stdout).unwrap(); + assert_eq!(phrase.trim().split(' ').count(), 5); +} diff --git a/crates/relicario-cli/tests/common/mod.rs b/crates/relicario-cli/tests/common/mod.rs new file mode 100644 index 0000000..b77ce7e --- /dev/null +++ b/crates/relicario-cli/tests/common/mod.rs @@ -0,0 +1,117 @@ +//! Shared helpers for CLI integration tests. +//! +//! `TestVault::init()` spins up a fresh vault in a `TempDir` using +//! `RELICARIO_TEST_PASSPHRASE` as the escape hatch (bypasses TTY prompts). +//! Every `run()` / `run_with_input()` call sets both `RELICARIO_IMAGE` and +//! `RELICARIO_TEST_PASSPHRASE`, so vault-mutating commands unlock without +//! interactive input. +//! +//! Note for Task 23 implementers: commands that prompt for a *new item +//! password* (i.e. `edit` when changing a Login password) also use +//! `rpassword`. Plumb `RELICARIO_TEST_ITEM_PASSWORD` through `cmd_edit` in +//! main.rs, or use an item type / edit path that avoids the rpassword call. + +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use assert_cmd::cargo::CommandCargoExt; +use tempfile::TempDir; + +pub struct TestVault { + pub dir: TempDir, + pub reference_image: PathBuf, + pub passphrase: String, +} + +impl TestVault { + pub fn init() -> Self { + let dir = TempDir::new().expect("tempdir"); + let carrier = make_test_jpeg(400, 300); + let carrier_path = dir.path().join("carrier.jpg"); + std::fs::write(&carrier_path, &carrier).unwrap(); + + let passphrase = "correct horse battery staple 2026".to_string(); + let ref_path = dir.path().join("reference.jpg"); + + let mut cmd = Command::cargo_bin("relicario").unwrap(); + cmd.current_dir(dir.path()) + .env("RELICARIO_TEST_PASSPHRASE", &passphrase) + .args([ + "init", + "--image", + carrier_path.to_str().unwrap(), + "--output", + ref_path.to_str().unwrap(), + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let out = cmd.output().unwrap(); + assert!( + out.status.success(), + "init failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + + Self { + dir, + reference_image: ref_path, + passphrase, + } + } + + pub fn path(&self) -> &Path { + self.dir.path() + } + + pub fn run(&self, args: &[&str]) -> std::process::Output { + let mut cmd = Command::cargo_bin("relicario").unwrap(); + cmd.current_dir(self.dir.path()) + .env("RELICARIO_IMAGE", &self.reference_image) + .env("RELICARIO_TEST_PASSPHRASE", &self.passphrase) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + cmd.output().unwrap() + } + + pub fn run_with_input(&self, args: &[&str], extra: &[&str]) -> std::process::Output { + let mut cmd = Command::cargo_bin("relicario").unwrap(); + cmd.current_dir(self.dir.path()) + .env("RELICARIO_IMAGE", &self.reference_image) + .env("RELICARIO_TEST_PASSPHRASE", &self.passphrase) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let mut child = cmd.spawn().unwrap(); + { + let stdin = child.stdin.as_mut().unwrap(); + for line in extra { + writeln!(stdin, "{line}").unwrap(); + } + } + child.wait_with_output().unwrap() + } +} + +pub fn make_test_jpeg(w: u32, h: u32) -> Vec { + use image::codecs::jpeg::JpegEncoder; + use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgb}; + + let img = ImageBuffer::from_fn(w, h, |x, y| { + Rgb([ + ((x * 7 + y * 13) % 256) as u8, + ((x * 11 + y * 3) % 256) as u8, + ((x * 5 + y * 17) % 256) as u8, + ]) + }); + let mut out = Vec::new(); + JpegEncoder::new_with_quality(&mut out, 92) + .write_image(img.as_raw(), w, h, ExtendedColorType::Rgb8) + .unwrap(); + out +}