coliru

A minimal, flexible, dotfile installer
git clone https://git.ashermorgan.net/coliru/
Log | Files | Refs | README

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:
M.github/workflows/build-and-test.yml | 5+++++
Msrc/main.rs | 1+
Asrc/ssh.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/utils.rs | 17++++++++++-------
Atests/.temp/ssh/.gitignore | 6++++++
Mtests/common/mod.rs | 12++++++------
Atests/server/Dockerfile | 17+++++++++++++++++
Atests/server/compose.yml | 13+++++++++++++
Atests/server/entry.sh | 9+++++++++
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