tests
This commit is contained in:
+6
-1
@@ -2,13 +2,18 @@ use crate::parsing::parse_args;
|
||||
|
||||
mod parsing;
|
||||
mod error;
|
||||
mod utils;
|
||||
|
||||
fn main() {
|
||||
match parse_args(std::env::args_os()) {
|
||||
Err(e) => eprintln!("{e}"),
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(cmd) => {
|
||||
if let Err(e) = cmd.perform() {
|
||||
eprintln!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
-126
@@ -1,13 +1,12 @@
|
||||
use std::ffi::OsString;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use age::ssh::Identity;
|
||||
use flate2::Compression;
|
||||
use flate2::read::GzDecoder;
|
||||
use flate2::write::GzEncoder;
|
||||
use clap::{Parser, Subcommand};
|
||||
use ssh_key::{Algorithm, LineEnding, PrivateKey};
|
||||
use crate::error::DrawerError;
|
||||
use crate::utils::*;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "drawer", about = "Encrypt and decrypt project directories as drawer files")]
|
||||
@@ -28,6 +27,11 @@ pub enum Command {
|
||||
Info(InfoOperation),
|
||||
}
|
||||
|
||||
pub trait Operation {
|
||||
fn perform(&self) -> Result<(), DrawerError>;
|
||||
fn validate(&self) -> Result<(), DrawerError>;
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
||||
match self {
|
||||
@@ -79,84 +83,6 @@ where
|
||||
Ok(cli.command)
|
||||
}
|
||||
|
||||
fn key_path(key: &Option<PathBuf>) -> Result<PathBuf, DrawerError> {
|
||||
if let Some(k) = key {
|
||||
return Ok(k.clone());
|
||||
}
|
||||
let val = std::env::var("DRAWER_KEY").map_err(|_| DrawerError::NoKeyPath)?;
|
||||
Ok(PathBuf::from(val))
|
||||
}
|
||||
|
||||
fn identity(key: &Option<PathBuf>, drawer_file: &Path) -> Result<(PathBuf, Identity), DrawerError> {
|
||||
let kp = key_path(key)?;
|
||||
let file = std::fs::File::open(&kp)
|
||||
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let id = age::ssh::Identity::from_buffer(reader, None)
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
Ok((kp, id))
|
||||
}
|
||||
|
||||
fn decrypt(encrypted: Vec<u8>, key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
||||
let (kp, id) = identity(key, drawer_file)?;
|
||||
let decryptor = age::Decryptor::new(encrypted.as_slice())
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
let mut decrypted: Vec<u8> = Vec::new();
|
||||
let mut stream = decryptor.decrypt(std::iter::once(&id as &dyn age::Identity))
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
std::io::Read::read_to_end(&mut stream, &mut decrypted)
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
fn encrypt(buf: &[u8], key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
||||
let (_, id) = identity(key, drawer_file)?;
|
||||
let recipient = age::ssh::Recipient::try_from(id)
|
||||
.map_err(|_| DrawerError::EncryptFailed)?;
|
||||
let mut encrypted: Vec<u8> = Vec::new();
|
||||
let encryptor = age::Encryptor::with_recipients(
|
||||
std::iter::once(&recipient as &dyn age::Recipient)
|
||||
).map_err(|_| DrawerError::EncryptFailed)?;
|
||||
let mut writer = encryptor.wrap_output(&mut encrypted)
|
||||
.map_err(|_| DrawerError::EncryptFailed)?;
|
||||
writer.write_all(buf).map_err(|_| DrawerError::EncryptFailed)?;
|
||||
writer.finish().map_err(|_| DrawerError::EncryptFailed)?;
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
fn default_target(drawer_file: &Path, single_file: Option<&Path>) -> PathBuf {
|
||||
match single_file {
|
||||
Some(p) => PathBuf::from(p.file_name().unwrap_or_default()),
|
||||
None => PathBuf::from(drawer_file.file_stem().unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_drawer_extension(drawer_file: &Path) -> Result<(), DrawerError> {
|
||||
if drawer_file.extension().and_then(|e| e.to_str()) != Some("drawer") {
|
||||
return Err(DrawerError::DrawerFileInvalidExtension(drawer_file.to_path_buf()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_drawer_file_exists(drawer_file: &Path) -> Result<(), DrawerError> {
|
||||
if !drawer_file.exists() {
|
||||
return Err(DrawerError::DrawerFileNotFound(drawer_file.to_path_buf()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ssh_key(key: &Option<PathBuf>) -> Result<(), DrawerError> {
|
||||
let kp = key_path(key)?;
|
||||
let file = std::fs::File::open(&kp)
|
||||
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let id = age::ssh::Identity::from_buffer(reader, None)
|
||||
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
||||
age::ssh::Recipient::try_from(id)
|
||||
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum ArchiveInfo {
|
||||
SingleFile { size: u64 },
|
||||
Folder { file_count: usize },
|
||||
@@ -205,11 +131,16 @@ fn single_file_entry(data: &[u8]) -> Option<PathBuf> {
|
||||
Some(entry.path().ok()?.into_owned())
|
||||
}
|
||||
|
||||
impl OpenOperation {
|
||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
||||
impl Operation for OpenOperation {
|
||||
fn validate(&self) -> Result<(), DrawerError> {
|
||||
validate_drawer_extension(&self.drawer_file)?;
|
||||
validate_ssh_key(&self.key)?;
|
||||
validate_drawer_file_exists(&self.drawer_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perform(&self) -> Result<(), DrawerError> {
|
||||
self.validate()?;
|
||||
|
||||
let encrypted = std::fs::read(&self.drawer_file)
|
||||
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
|
||||
@@ -254,39 +185,10 @@ impl OpenOperation {
|
||||
}
|
||||
}
|
||||
|
||||
impl CloseOperation {
|
||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
||||
impl Operation for CloseOperation {
|
||||
fn validate(&self) -> Result<(), DrawerError> {
|
||||
validate_drawer_extension(&self.drawer_file)?;
|
||||
validate_ssh_key(&self.key)?;
|
||||
self.validate_close()?;
|
||||
|
||||
let target = self.target_path.as_ref().cloned()
|
||||
.unwrap_or_else(|| default_target(&self.drawer_file, None));
|
||||
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
{
|
||||
let gz = GzEncoder::new(&mut buf, Compression::default());
|
||||
let mut archive = tar::Builder::new(gz);
|
||||
if target.is_dir() {
|
||||
archive.append_dir_all(".", &target)
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
} else {
|
||||
let name = target.file_name().unwrap_or_default();
|
||||
archive.append_path_with_name(&target, name)
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
}
|
||||
let gz = archive.into_inner()
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
gz.finish()
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
}
|
||||
|
||||
let encrypted = encrypt(&buf, &self.key, &self.drawer_file)?;
|
||||
std::fs::write(&self.drawer_file, &encrypted)
|
||||
.map_err(|_| DrawerError::WriteFailed(self.drawer_file.clone()))
|
||||
}
|
||||
|
||||
fn validate_close(&self) -> Result<(), DrawerError> {
|
||||
let target = self.target_path.as_ref().cloned()
|
||||
.unwrap_or_else(|| default_target(&self.drawer_file, None));
|
||||
if self.target_path.is_none() {
|
||||
@@ -315,20 +217,42 @@ impl CloseOperation {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perform(&self) -> Result<(), DrawerError> {
|
||||
self.validate()?;
|
||||
|
||||
let target = self.target_path.as_ref().cloned()
|
||||
.unwrap_or_else(|| default_target(&self.drawer_file, None));
|
||||
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
{
|
||||
let gz = GzEncoder::new(&mut buf, Compression::default());
|
||||
let mut archive = tar::Builder::new(gz);
|
||||
if target.is_dir() {
|
||||
archive.append_dir_all(".", &target)
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
} else {
|
||||
let name = target.file_name().unwrap_or_default();
|
||||
archive.append_path_with_name(&target, name)
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
}
|
||||
let gz = archive.into_inner()
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
gz.finish()
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
}
|
||||
|
||||
let encrypted = encrypt(&buf, &self.key, &self.drawer_file)?;
|
||||
std::fs::write(&self.drawer_file, &encrypted)
|
||||
.map_err(|_| DrawerError::WriteFailed(self.drawer_file.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyOperation {
|
||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
||||
self.validate()?;
|
||||
let private_key = PrivateKey::random(&mut rand::rngs::OsRng, Algorithm::Ed25519)
|
||||
.map_err(|_| DrawerError::KeyGenerateFailed)?;
|
||||
let pem = private_key.to_openssh(LineEnding::LF)
|
||||
.map_err(|_| DrawerError::KeyGenerateFailed)?;
|
||||
std::fs::write(&self.filename, pem.as_bytes())
|
||||
.map_err(|_| DrawerError::KeyOutputFailed(self.filename.clone()))
|
||||
}
|
||||
|
||||
impl Operation for KeyOperation {
|
||||
fn validate(&self) -> Result<(), DrawerError> {
|
||||
if self.filename.exists() {
|
||||
return Err(DrawerError::KeyOutputFailed(self.filename.clone()));
|
||||
}
|
||||
let parent = match self.filename.parent() {
|
||||
None => Path::new("."),
|
||||
Some(p) if p == Path::new("") => Path::new("."),
|
||||
@@ -339,13 +263,28 @@ impl KeyOperation {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perform(&self) -> Result<(), DrawerError> {
|
||||
self.validate()?;
|
||||
let private_key = PrivateKey::random(&mut rand::rngs::OsRng, Algorithm::Ed25519)
|
||||
.map_err(|_| DrawerError::KeyGenerateFailed)?;
|
||||
let pem = private_key.to_openssh(LineEnding::LF)
|
||||
.map_err(|_| DrawerError::KeyGenerateFailed)?;
|
||||
std::fs::write(&self.filename, pem.as_bytes())
|
||||
.map_err(|_| DrawerError::KeyOutputFailed(self.filename.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl InfoOperation {
|
||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
||||
impl Operation for InfoOperation {
|
||||
fn validate(&self) -> Result<(), DrawerError> {
|
||||
validate_drawer_extension(&self.drawer_file)?;
|
||||
validate_ssh_key(&self.key)?;
|
||||
validate_drawer_file_exists(&self.drawer_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perform(&self) -> Result<(), DrawerError> {
|
||||
self.validate()?;
|
||||
|
||||
let encrypted = std::fs::read(&self.drawer_file)
|
||||
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use age::ssh::Identity;
|
||||
use crate::error::DrawerError;
|
||||
|
||||
pub fn key_path(key: &Option<PathBuf>) -> Result<PathBuf, DrawerError> {
|
||||
if let Some(k) = key {
|
||||
return Ok(k.clone());
|
||||
}
|
||||
let val = std::env::var("DRAWER_KEY").map_err(|_| DrawerError::NoKeyPath)?;
|
||||
Ok(PathBuf::from(val))
|
||||
}
|
||||
|
||||
pub fn identity(key: &Option<PathBuf>, drawer_file: &Path) -> Result<(PathBuf, Identity), DrawerError> {
|
||||
let kp = key_path(key)?;
|
||||
let file = std::fs::File::open(&kp)
|
||||
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let id = age::ssh::Identity::from_buffer(reader, None)
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
Ok((kp, id))
|
||||
}
|
||||
|
||||
pub fn decrypt(encrypted: Vec<u8>, key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
||||
let (kp, id) = identity(key, drawer_file)?;
|
||||
let decryptor = age::Decryptor::new(encrypted.as_slice())
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
let mut decrypted: Vec<u8> = Vec::new();
|
||||
let mut stream = decryptor.decrypt(std::iter::once(&id as &dyn age::Identity))
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
std::io::Read::read_to_end(&mut stream, &mut decrypted)
|
||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
pub fn encrypt(buf: &[u8], key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
||||
let (_, id) = identity(key, drawer_file)?;
|
||||
let recipient = age::ssh::Recipient::try_from(id)
|
||||
.map_err(|_| DrawerError::EncryptFailed)?;
|
||||
let mut encrypted: Vec<u8> = Vec::new();
|
||||
let encryptor = age::Encryptor::with_recipients(
|
||||
std::iter::once(&recipient as &dyn age::Recipient)
|
||||
).map_err(|_| DrawerError::EncryptFailed)?;
|
||||
let mut writer = encryptor.wrap_output(&mut encrypted)
|
||||
.map_err(|_| DrawerError::EncryptFailed)?;
|
||||
writer.write_all(buf).map_err(|_| DrawerError::EncryptFailed)?;
|
||||
writer.finish().map_err(|_| DrawerError::EncryptFailed)?;
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
pub fn default_target(drawer_file: &Path, single_file: Option<&Path>) -> PathBuf {
|
||||
match single_file {
|
||||
Some(p) => PathBuf::from(p.file_name().unwrap_or_default()),
|
||||
None => PathBuf::from(drawer_file.file_stem().unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_drawer_extension(drawer_file: &Path) -> Result<(), DrawerError> {
|
||||
if drawer_file.extension().and_then(|e| e.to_str()) != Some("drawer") {
|
||||
return Err(DrawerError::DrawerFileInvalidExtension(drawer_file.to_path_buf()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_drawer_file_exists(drawer_file: &Path) -> Result<(), DrawerError> {
|
||||
if !drawer_file.exists() {
|
||||
return Err(DrawerError::DrawerFileNotFound(drawer_file.to_path_buf()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_ssh_key(key: &Option<PathBuf>) -> Result<(), DrawerError> {
|
||||
let kp = key_path(key)?;
|
||||
let file = std::fs::File::open(&kp)
|
||||
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let id = age::ssh::Identity::from_buffer(reader, None)
|
||||
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
||||
age::ssh::Recipient::try_from(id)
|
||||
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DRAWER=./target/debug/drawer
|
||||
TEST_DIR="$(cd "$(dirname "$0")/test" && pwd)"
|
||||
WORK="$TMPDIR/drawer_test_$$"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
pass() { echo "PASS: $1"; }
|
||||
fail() { echo "FAIL: $1"; exit 1; }
|
||||
|
||||
# Expect a command to succeed
|
||||
ok() {
|
||||
local label="$1"; shift
|
||||
local rc=0
|
||||
"$@" 2>/dev/null || rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (exited $rc)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Expect a command to fail
|
||||
err() {
|
||||
local label="$1"; shift
|
||||
local rc=0
|
||||
"$@" 2>/dev/null || rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
fail "$label (expected failure but succeeded)"
|
||||
else
|
||||
pass "$label"
|
||||
fi
|
||||
}
|
||||
|
||||
cargo build -q
|
||||
|
||||
# ── key ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
KEY="$WORK/test.key"
|
||||
|
||||
ok "key: generate" $DRAWER key "$KEY"
|
||||
err "key: refuse existing file" $DRAWER key "$KEY"
|
||||
err "key: refuse missing parent" $DRAWER key "$WORK/nosuchdir/test.key"
|
||||
|
||||
# ── close ────────────────────────────────────────────────────────────────────
|
||||
|
||||
DRAWER_FILE="$WORK/test.drawer"
|
||||
|
||||
ok "close: pack test dir" $DRAWER close -i "$KEY" "$DRAWER_FILE" "$TEST_DIR"
|
||||
err "close: refuse without key" DRAWER_KEY="" $DRAWER close "$DRAWER_FILE" "$TEST_DIR"
|
||||
err "close: refuse bad key" $DRAWER close -i "$WORK/notakey" "$DRAWER_FILE" "$TEST_DIR"
|
||||
err "close: refuse bad extension" $DRAWER close -i "$KEY" "$WORK/test.notadrawer" "$TEST_DIR"
|
||||
|
||||
# ── open ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
OUT="$WORK/out"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
ok "open: unpack drawer" $DRAWER open -i "$KEY" "$DRAWER_FILE" "$OUT"
|
||||
ok "open: file1.md present" test -f "$OUT/file1.md"
|
||||
ok "open: file2.md present" test -f "$OUT/file2.md"
|
||||
ok "open: contents match file1.md" diff "$TEST_DIR/file1.md" "$OUT/file1.md"
|
||||
ok "open: contents match file2.md" diff "$TEST_DIR/file2.md" "$OUT/file2.md"
|
||||
|
||||
err "open: refuse non-empty target" $DRAWER open -i "$KEY" "$DRAWER_FILE" "$OUT"
|
||||
ok "open: force into non-empty" $DRAWER open -i "$KEY" -f "$DRAWER_FILE" "$OUT"
|
||||
|
||||
err "open: refuse bad extension" $DRAWER open -i "$KEY" "$WORK/test.notadrawer" "$OUT"
|
||||
err "open: refuse missing drawer" $DRAWER open -i "$KEY" "$WORK/nosuch.drawer" "$OUT"
|
||||
err "open: refuse without key" $DRAWER open "$DRAWER_FILE" "$OUT"
|
||||
err "open: refuse bad key" $DRAWER open -i "$WORK/notakey" "$DRAWER_FILE" "$OUT"
|
||||
|
||||
# ── open: inferred target path ───────────────────────────────────────────────
|
||||
|
||||
INFER_DRAWER="$WORK/inferred.drawer"
|
||||
INFER_OUT="$WORK/inferred"
|
||||
|
||||
ok "close: inferred source" $DRAWER close -i "$KEY" "$INFER_DRAWER" "$TEST_DIR"
|
||||
mkdir -p "$INFER_OUT"
|
||||
ok "open: inferred target" $DRAWER open -i "$KEY" "$INFER_DRAWER" "$INFER_OUT"
|
||||
ok "open: inferred file1.md" test -f "$INFER_OUT/file1.md"
|
||||
|
||||
# ── info ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
$DRAWER info -i "$KEY" "$DRAWER_FILE" 2>/dev/null | grep -q "folder, 2 files" \
|
||||
&& pass "info: reports folder" || fail "info: reports folder"
|
||||
err "info: refuse bad extension" $DRAWER info -i "$KEY" "$WORK/test.notadrawer"
|
||||
err "info: refuse missing drawer" $DRAWER info -i "$KEY" "$WORK/nosuch.drawer"
|
||||
err "info: refuse without key" $DRAWER info "$DRAWER_FILE"
|
||||
|
||||
# ── single-file drawer ───────────────────────────────────────────────────────
|
||||
|
||||
SINGLE_SRC="$TEST_DIR/file1.md"
|
||||
SINGLE_DRAWER="$WORK/file1.drawer"
|
||||
SINGLE_OUT_DIR="$WORK/single_out"
|
||||
mkdir -p "$SINGLE_OUT_DIR"
|
||||
|
||||
ok "close: single file" $DRAWER close -i "$KEY" "$SINGLE_DRAWER" "$SINGLE_SRC"
|
||||
$DRAWER info -i "$KEY" "$SINGLE_DRAWER" 2>/dev/null | grep -q "file, " \
|
||||
&& pass "info: reports single file" || fail "info: reports single file"
|
||||
ok "open: single file" $DRAWER open -i "$KEY" "$SINGLE_DRAWER" "$SINGLE_OUT_DIR/file1.md"
|
||||
ok "open: single file contents" diff "$SINGLE_SRC" "$SINGLE_OUT_DIR/file1.md"
|
||||
|
||||
# ── close: drawer-inside-target guard ────────────────────────────────────────
|
||||
|
||||
# Pending
|
||||
# err "close: refuse drawer inside target" $DRAWER close -i "$KEY" "$TEST_DIR/../test.drawer" "$TEST_DIR"
|
||||
|
||||
# ── env var key ──────────────────────────────────────────────────────────────
|
||||
|
||||
ENV_OUT="$WORK/env_out"
|
||||
mkdir -p "$ENV_OUT"
|
||||
|
||||
ok "open: key from DRAWER_KEY env" env DRAWER_KEY="$KEY" $DRAWER open "$DRAWER_FILE" "$ENV_OUT"
|
||||
ok "open: env key file1.md" test -f "$ENV_OUT/file1.md"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
rm -rf "$WORK"
|
||||
echo ""
|
||||
echo "All tests passed."
|
||||
Reference in New Issue
Block a user