commit eeefb789d1597e532c7b5bafad6ede78ac5859ef
parent 5e1672a6ae69184e24aacd3e147039dd63fa7324
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sun, 30 Jun 2024 14:07:21 -0700
Implement ssh::send_dir and update tests
Diffstat:
9 files changed, 147 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
@@ -17,6 +17,11 @@ jobs:
- name: Build
run: cargo build --verbose
+ - name: Start SSH server
+ run: |
+ echo 'PUID=1001' > tests/server/.env
+ docker compose -f tests/server/compose.yml up -d
+
- name: Run tests
run: cargo test --verbose
diff --git a/src/main.rs b/src/main.rs
@@ -1,6 +1,7 @@
mod cli;
mod core;
mod manifest;
+mod ssh;
mod tags;
mod utils;
diff --git a/src/ssh.rs b/src/ssh.rs
@@ -0,0 +1,80 @@
+use std::process::Command;
+
+/// Recursively copy a directory to another machine via SCP
+#[allow(dead_code)]
+fn send_dir(src: &str, dst: &str, host: &str) -> Result<(), String> {
+ let mut cmd = Command::new("scp");
+ if cfg!(test) {
+ // SSH options and port for test server hard coded for now
+ cmd.args(["-o", "StrictHostKeyChecking=no", "-P", "2222"]);
+ }
+ cmd.args(["-r", src, &format!("{host}:{dst}")]);
+
+ let status = cmd.status().map_err(|why| why.to_string())?;
+ if status.success() {
+ Ok(())
+ } else {
+ Err(format!("SCP exited with {status}"))
+ }
+}
+
+#[cfg(test)]
+#[path = "../tests/common/mod.rs"]
+mod common;
+
+#[cfg(test)]
+#[cfg(target_family = "unix")]
+mod tests {
+ use super::*;
+ use common::{SSH_HOST, read_file, setup_integration, write_file};
+
+ use std::fs;
+ use std::path::PathBuf;
+
+ #[test]
+ fn test_send_dir_basic() {
+ let tmp = setup_integration("test_send_dir_basic");
+
+ write_file(&tmp.dir.join("foo"), "contents of foo");
+ write_file(&tmp.dir.join("bar"), "contents of bar");
+ let dst = "~/test_send_dir_basic";
+ let dst_real = PathBuf::from("tests/.temp/ssh/test_send_dir_basic");
+ if dst_real.exists() {
+ fs::remove_dir_all(&dst_real).unwrap();
+ }
+
+ let result = send_dir(tmp.dir.to_str().unwrap(), dst, SSH_HOST);
+
+ assert_eq!(result.is_ok(), true);
+ let contents1 = read_file(&dst_real.join("foo"));
+ assert_eq!(contents1, "contents of foo");
+ let contents2 = read_file(&dst_real.join("bar"));
+ assert_eq!(contents2, "contents of bar");
+
+ fs::remove_dir_all(&dst_real).unwrap();
+ }
+
+ #[test]
+ fn test_send_dir_nested() {
+ let tmp = setup_integration("test_send_dir_nested");
+
+ write_file(&tmp.dir.join("foo"), "contents of foo");
+ fs::create_dir_all(&tmp.dir.join("dir")).unwrap();
+ write_file(&tmp.dir.join("dir").join("bar"), "contents of bar");
+ let dst = "~/test_send_dir_nested";
+ let dst_real = PathBuf::from("tests/.temp/ssh/test_send_dir_nested");
+ if dst_real.exists() {
+ fs::remove_dir_all(&dst_real).unwrap();
+ }
+
+ let result = send_dir(tmp.dir.to_str().unwrap(), dst, SSH_HOST);
+
+ assert_eq!(result.is_ok(), true);
+ let contents1 = read_file(&dst_real.join("foo"));
+ assert_eq!(contents1, "contents of foo");
+ let contents2 = read_file(&dst_real.join("dir").join("bar"));
+ assert_eq!(contents2, "contents of bar");
+
+ fs::remove_dir_all(&dst_real).unwrap();
+ }
+}
diff --git a/src/utils.rs b/src/utils.rs
@@ -259,21 +259,24 @@ mod tests {
#[test]
#[cfg(target_family = "unix")]
fn test_link_file_relative_source() {
- let tmp = setup_integration("test_link_file_relative_source");
+ let dir = PathBuf::from("tests/.temp/ssh/test_link_file_relative_source");
+ fs::create_dir_all(&dir).unwrap();
- let src = &tmp.dir.join("foo");
- let src_rel = "test_link_file_relative_source/foo";
- let dst = &tmp.dir.join("dir1").join("dir2").join("bar");
- write_file(src, "old contents of foo");
+ let src = absolute(&dir.join("foo")).unwrap();
+ let src_rel = "tests/.temp/ssh/test_link_file_relative_source/foo";
+ let dst = &dir.join("dir1").join("dir2").join("bar");
+ write_file(&src, "old contents of foo");
let result = link_file(src_rel, dst.to_str().unwrap());
- write_file(src, "new contents of foo");
+ write_file(&src, "new contents of foo");
let contents = fs::read_to_string(dst).unwrap();
let link = fs::read_link(dst).unwrap();
assert_eq!(result.is_ok(), true);
assert_eq!(contents, "new contents of foo");
- assert_eq!(&link, src); // src changed to absolute path
+ assert_eq!(link, src); // src changed to absolute path
+
+ fs::remove_dir_all(&dir).unwrap();
}
#[test]
diff --git a/tests/.temp/ssh/.gitignore b/tests/.temp/ssh/.gitignore
@@ -0,0 +1,6 @@
+# This directory is mounted to the SSH docker container used for E2E tests. All
+# children are ignored, but the .gitignore file guarantees that the directory
+# will exist.
+
+./*
+!.gitignore
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
@@ -6,7 +6,8 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
-pub const SSH_HOST: &str = "test@localhost:2222";
+/// The SSH test server
+pub const SSH_HOST: &str = "test@localhost"; // TODO: add explicit port
/// Stores the path to a temporary directory that is automatically deleted
/// when the value is dropped.
@@ -29,15 +30,14 @@ impl TempDir {
}
}
-/// Creates a temporary directory with a certain name and sets $HOME and the
-/// CWD to the parent directory.
+/// Creates a temporary directory with a certain name and sets $HOME to the
+/// parent directory.
///
-/// All tests in this module use the same values for $HOME and the CWD,
-/// which prevents issues when tests are run in multiple threads.
+/// All tests in this module use the same values for $HOME, which prevents
+/// issues when tests are run in multiple threads.
pub fn setup_integration(name: &str) -> TempDir {
let dir = TempDir::new(name);
let root = dir.dir.parent().unwrap();
- env::set_current_dir(root).unwrap();
if cfg!(target_family = "unix") {
env::set_var("HOME", root);
}
diff --git a/tests/server/Dockerfile b/tests/server/Dockerfile
@@ -0,0 +1,17 @@
+# An SSH server with authentication disabled for the user "test"
+
+FROM alpine
+
+RUN apk update && apk add openssh
+
+RUN ssh-keygen -A
+
+RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
+RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config
+RUN echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
+
+COPY entry.sh /entry.sh
+
+EXPOSE 22
+
+CMD ["/entry.sh"]
diff --git a/tests/server/compose.yml b/tests/server/compose.yml
@@ -0,0 +1,13 @@
+services:
+ coliru-ssh:
+ environment:
+ - PUID # loaded from .env, =1000 by default
+ build:
+ context: .
+ ports:
+ - 2222:22
+ volumes:
+ - type: bind
+ source: ../.temp/ssh
+ target: /home/test
+ restart: unless-stopped
diff --git a/tests/server/entry.sh b/tests/server/entry.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+
+PUID=${PUID:-1000}
+echo "Creating user test with PUID=$PUID ..."
+adduser -h /home/test -s /bin/sh -D -u "$PUID" test
+passwd -d test
+
+echo "Starting sshd ..."
+/usr/sbin/sshd -D