diff options
Diffstat (limited to 'server/tests')
| -rw-r--r-- | server/tests/common/mod.rs | 340 | ||||
| -rw-r--r-- | server/tests/integration_test.rs | 128 |
2 files changed, 468 insertions, 0 deletions
diff --git a/server/tests/common/mod.rs b/server/tests/common/mod.rs new file mode 100644 index 0000000..0eef90b --- /dev/null +++ b/server/tests/common/mod.rs @@ -0,0 +1,340 @@ +use reqwest::Client; +use std::collections::HashMap; +use std::env; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use test_context::AsyncTestContext; +use testdir::testdir; +use tokio::fs; +use tokio::process::Command; + +use eyeballs_api::api_model; + +pub struct DockerComposeContext { + docker_dir: PathBuf, + test_dir: PathBuf, + url: String, + remote_git: String, +} + +async fn run(cmd: &mut Command, name: &str) -> Result<(), anyhow::Error> { + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()); + let child = cmd.spawn()?; + let output = child.wait_with_output().await?; + + if output.status.success() { + Ok(()) + } else { + Err(anyhow::Error::msg(format!( + "{name} failed with exitcode: {}\n{:?}\n{}", + output.status, + cmd.as_std().get_args(), + std::str::from_utf8(output.stderr.as_slice()).unwrap_or(""), + ))) + } +} + +async fn setup_ssh_file( + base: impl AsRef<Path>, + host: &str, + port: &str, + identity_file: impl AsRef<Path>, +) -> Result<(), anyhow::Error> { + let full_identity_file = identity_file.as_ref().canonicalize()?; + + fs::write( + base.as_ref().join("ssh_config"), + format!( + "Host {host} + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + UpdateHostKeys no + Port {port} + User git + IdentityFile {} +", + full_identity_file.to_str().unwrap(), + ), + ) + .await?; + + Ok(()) +} + +async fn git_clone(base: impl AsRef<Path>) -> Result<(), anyhow::Error> { + let mut cmd = Command::new("git"); + cmd.arg("clone"); + cmd.arg("ssh://localhost/srv/git/fake.git"); + + cmd.env("GIT_SSH_COMMAND", "ssh -F ssh_config"); + cmd.current_dir(base); + + run(&mut cmd, "git clone").await +} + +async fn git_cmd(base: impl AsRef<Path>, args: &[&str]) -> Result<(), anyhow::Error> { + let mut cmd = Command::new("git"); + cmd.arg("-C"); + cmd.arg("fake"); + for arg in args { + cmd.arg(arg); + } + + cmd.env("GIT_SSH_COMMAND", "ssh -F ../ssh_config"); + cmd.current_dir(base); + + run(&mut cmd, "git command").await +} + +impl DockerComposeContext { + pub fn url(&self) -> &str { + self.url.as_str() + } + + pub fn remote_git(&self) -> &str { + self.remote_git.as_str() + } + + pub async fn setup_ssh_key(&self, base: &str, key: &str) -> Result<(), anyhow::Error> { + let base_dir = self.test_dir.join(base); + fs::create_dir(&base_dir).await?; + let identity_file = base_dir.join("id_key"); + fs::write(&identity_file, key).await?; + let permissions = Permissions::from_mode(0o600); + fs::set_permissions(&identity_file, permissions).await?; + setup_ssh_file(&base_dir, "localhost", "10022", &identity_file).await?; + Ok(()) + } + + pub async fn git_clone(&self, base: &str) -> Result<(), anyhow::Error> { + git_clone(self.test_dir.join(base)).await + } + + pub fn git_dir(&self, base: &str) -> PathBuf { + self.test_dir.join(base).join("fake") + } + + pub async fn git_cmd(&self, base: &str, args: &[&str]) -> Result<(), anyhow::Error> { + git_cmd(self.test_dir.join(base), args).await + } +} + +impl AsyncTestContext for DockerComposeContext { + async fn setup() -> DockerComposeContext { + let cargo_dir = match env::var("CARGO_MANIFEST_DIR") { + Ok(pathstr) => PathBuf::from(pathstr), + Err(e) => panic!("CARGO_MANIFEST_DIR not set: {e:?}"), + }; + let ctx = DockerComposeContext { + docker_dir: cargo_dir.join("../docker/integration_test"), + test_dir: testdir!(), + url: "http://localhost:18000".to_string(), + remote_git: "ssh://git@remote_git/srv/git/fake.git".to_string(), + }; + + // Build githook, needs to use musl to work with the rockstorm/git-server image + { + let mut cmd = Command::new("cargo"); + cmd.arg("build"); + cmd.arg("--target=x86_64-unknown-linux-musl"); + cmd.arg("--package"); + cmd.arg("eyeballs-githook"); + + cmd.current_dir(cargo_dir); + + run(&mut cmd, "cargo build eyeballs-githook") + .await + .expect("cargo build"); + } + + // Start docker compose up + { + let mut cmd = Command::new("docker"); + cmd.arg("compose"); + cmd.arg("up"); + // Build images before starting containers + cmd.arg("--build"); + // Recreate anonymous volumes instead of retrieving data from the previous containers + cmd.arg("--renew-anon-volumes"); + // Detached mode: Run containers in the background + cmd.arg("--detach"); + // Wait for services to be running|healthy. Implies detached mode + cmd.arg("--wait"); + // Assume "yes" as answer to all prompts and run non-interactively + cmd.arg("-y"); + + cmd.current_dir(&ctx.docker_dir); + + run(&mut cmd, "docker compose up") + .await + .expect("docker compose up"); + } + + let mod_path = ctx.test_dir.join("mod"); + fs::create_dir(&mod_path).await.expect("create mod"); + setup_ssh_file( + &mod_path, + "localhost", + "12222", + ctx.docker_dir.join("web/gitkey"), + ) + .await + .expect("ssh_config for remote_git"); + + // Setup fake remote repo + { + let mut cmd = Command::new("ssh"); + cmd.arg("-F"); + cmd.arg("ssh_config"); + cmd.arg("localhost"); + cmd.arg("mkdir /srv/git/fake.git"); + + cmd.current_dir(&mod_path); + + run(&mut cmd, "mkdir").await.expect("ssh mkdir"); + } + { + let mut cmd = Command::new("ssh"); + cmd.arg("-F"); + cmd.arg("ssh_config"); + cmd.arg("localhost"); + cmd.arg("git-init --bare --initial-branch=main /srv/git/fake.git"); + + cmd.current_dir(&mod_path); + + run(&mut cmd, "git-init").await.expect("ssh git-init"); + } + + git_clone(&mod_path).await.expect("git clone"); + + fs::write(mod_path.join("fake/README"), "Hello fellow fake person!") + .await + .expect("Write README"); + + git_cmd(&mod_path, &["add", "README"]) + .await + .expect("git add README"); + git_cmd(&mod_path, &["commit", "-m", "Initial commit"]) + .await + .expect("git commit README"); + git_cmd(&mod_path, &["push", "origin", "HEAD:main"]) + .await + .expect("git push"); + + ctx + } + + async fn teardown(self) { + let mut cmd = Command::new("docker"); + cmd.arg("compose"); + cmd.arg("down"); + // Remove named volumes declared in the "volumes" section of the Compose file and anonymous + // volumes attached to containers + cmd.arg("--volumes"); + + cmd.current_dir(&self.docker_dir); + + run(&mut cmd, "docker compose down") + .await + .expect("docker compose down"); + } +} + +pub fn create_client() -> Result<Client, anyhow::Error> { + Ok(Client::builder().cookie_store(true).build()?) +} + +pub async fn login( + ctx: &mut DockerComposeContext, + client: &mut Client, + username: &str, + password: &str, +) -> Result<(), anyhow::Error> { + let mut params = HashMap::new(); + params.insert("username", username); + params.insert("password", password); + let result = client + .post(format!("{}/api/v1/login", ctx.url())) + .form(¶ms) + .send() + .await?; + if result.status().is_success() { + Ok(()) + } else { + let content = result.text().await?; + Err(anyhow::Error::msg(content)) + } +} + +pub async fn user_key_add( + ctx: &mut DockerComposeContext, + client: &mut Client, + kind: &str, + data: &str, +) -> Result<api_model::UserKey, anyhow::Error> { + let data = api_model::UserKeyData { + kind, + data, + comment: None, + }; + let result = client + .post(format!("{}/api/v1/user/keys/add", ctx.url())) + .json(&data) + .send() + .await?; + if result.status().is_success() { + let project = result.json::<api_model::UserKey>().await?; + Ok(project) + } else { + let content = result.text().await?; + Err(anyhow::Error::msg(content)) + } +} + +pub async fn create_project( + ctx: &mut DockerComposeContext, + client: &mut Client, + projectid: &str, + remote: &str, +) -> Result<api_model::Project, anyhow::Error> { + let data = api_model::ProjectData { + title: None, + description: None, + remote: Some(remote), + main_branch: None, + }; + let result = client + .post(format!("{}/api/v1/project/{projectid}/new", ctx.url())) + .json(&data) + .send() + .await?; + if result.status().is_success() { + let project = result.json::<api_model::Project>().await?; + Ok(project) + } else { + let content = result.text().await?; + Err(anyhow::Error::msg(content)) + } +} + +pub async fn list_reviews( + ctx: &mut DockerComposeContext, + client: &mut Client, + projectid: &str, +) -> Result<api_model::Reviews, anyhow::Error> { + let result = client + .get(format!("{}/api/v1/project/{projectid}/reviews", ctx.url())) + .send() + .await?; + if result.status().is_success() { + let project = result.json::<api_model::Reviews>().await?; + Ok(project) + } else { + let content = result.text().await?; + Err(anyhow::Error::msg(content)) + } +} diff --git a/server/tests/integration_test.rs b/server/tests/integration_test.rs new file mode 100644 index 0000000..242655b --- /dev/null +++ b/server/tests/integration_test.rs @@ -0,0 +1,128 @@ +use std::thread::sleep; +use std::time::Duration; +use test_context::test_context; +use tokio::fs; + +mod common; + +const TESTKEY1: &str = "-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAIEAspbADQDeHyGkqeo6WLoPcEJ6+2B5X94cJUMopqNdh9Kee2YJGW5+ +PiUTPj2g/7fGk0zZkoXE3VxheKBdsRY8QuX/LsZdFBkC5OOWCWfB14mJKthPgGWlL9gybV +HyTHTVmhkBD3puhVMllUWHLq21sY3jdj4aon8rZNpHLD8mVmsAAAIIcJ0+zHCdPswAAAAH +c3NoLXJzYQAAAIEAspbADQDeHyGkqeo6WLoPcEJ6+2B5X94cJUMopqNdh9Kee2YJGW5+Pi +UTPj2g/7fGk0zZkoXE3VxheKBdsRY8QuX/LsZdFBkC5OOWCWfB14mJKthPgGWlL9gybVHy +THTVmhkBD3puhVMllUWHLq21sY3jdj4aon8rZNpHLD8mVmsAAAADAQABAAAAgHpEtaXxcy +GzQe5G+71lXU6JZXOXQGH/ShvE2B8Gd/GWpIRtfktYF7xqW7tgLEsHQj/0/HzRcs/vAJi6 +iorEY2pwDdSrBdklOZEyRUhvLnuDBrBhFMktZhumZOsKsGXE0ysnyEK8KCPYow7H8azchi +TzHSBGQyRut/y87zU/BT4pAAAAQF3f2MrjYstJot8SVqizkmVzX5SX8XhReCGEpAUeETNF +/inHlEmPl17rr6knzu/fiWC9hmjHfQ/QMgemhik/MmoAAABBAOhHNz7KgIc+4HlQJkAHxA +z/Juixg3nLmAKxar+WvABn1/brN4HmsI3VRvZnChpcsntuS3wm2mywCg1pGaKJPA0AAABB +AMTT22KcAbU6HOpb059GTr8geQaKd84lQOEchEEUkXI/5cxqNq4BjtQNMghaGbYPUwP/4H +syLbjecIEiDAa9JlcAAAANdGhlX2prQHdpbGxvdwECAwQFBg== +-----END OPENSSH PRIVATE KEY----- +"; +const TESTKEY1_PUB: &str = "AAAAB3NzaC1yc2EAAAADAQABAAAAgQCylsANAN4fIa\ +Sp6jpYug9wQnr7YHlf3hwlQyimo12H0p57ZgkZbn4+JRM+PaD/t8aTTNmShcTdXGF4oF2x\ +FjxC5f8uxl0UGQLk45YJZ8HXiYkq2E+AZaUv2DJtUfJMdNWaGQEPem6FUyWVRYcurbWxje\ +N2Phqifytk2kcsPyZWaw=="; +const TESTKEY2: &str = "-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAIEAsPcgaQGgRevDiPX7lve4AyycMIT8ZcnQ93z1IeIEWlTNzcRofI/8 +7tcvZL0rR/kHLGdbDYE2cfmvVa13cF0wPTPibaJP8vZbpF5s4yvJXLcDpC7gB/kTMQ0b72 +KFL6J/nsQreY8qaq/JNT2XMpHZ7lUHE8cLZO5KsJsImtowQksAAAIIl0WVZZdFlWUAAAAH +c3NoLXJzYQAAAIEAsPcgaQGgRevDiPX7lve4AyycMIT8ZcnQ93z1IeIEWlTNzcRofI/87t +cvZL0rR/kHLGdbDYE2cfmvVa13cF0wPTPibaJP8vZbpF5s4yvJXLcDpC7gB/kTMQ0b72KF +L6J/nsQreY8qaq/JNT2XMpHZ7lUHE8cLZO5KsJsImtowQksAAAADAQABAAAAgEZ1vxPQL+ +5nFu27czcC3uN0qaOv74bfujIwMLIS+cS1q1PYdfnSotS+HQKxR0Ba6P5HELvpzLHIxoUI +klvM3t11M+x6cLmZi4zLQufiwojsBCFFsDwAIW95CW2iNmRyPB4TJwOKKEmnRJnqFCDalk +bb+wOOpCLMCISVqhSVamEhAAAAQQCwcXfOGOJa0MgFiVoU2GQuLAXu4MBA3NWXKsD6gY8q +bZrXdZjEtASFi8BTp7x0FZZNg5VidqLuQrLa+u38KYAUAAAAQQDqqHxXCItVlmU1+iB7mX +Tih/NTiaJykswnAauKIO2X2okPY0pU/S1JSsGbb02pqBrTqGpdiUqESMdhAcoMCp7jAAAA +QQDBD2MOIH7HULFElpj09LYGi+y5Lnhbu4Rn97SIyZiLyYTFMcKhkDtEGF6myTtF9D16U7 +KtQ4lA6EyRX9rgP4N5AAAADXRoZV9qa0B3aWxsb3cBAgMEBQ== +-----END OPENSSH PRIVATE KEY----- +"; +const TESTKEY2_PUB: &str = "AAAAB3NzaC1yc2EAAAADAQABAAAAgQCw9yBpAaBF68\ +OI9fuW97gDLJwwhPxlydD3fPUh4gRaVM3NxGh8j/zu1y9kvStH+QcsZ1sNgTZx+a9VrXdw\ +XTA9M+Jtok/y9lukXmzjK8lctwOkLuAH+RMxDRvvYoUvon+exCt5jypqr8k1PZcykdnuVQ\ +cTxwtk7kqwmwia2jBCSw=="; + +#[test_context(common::DockerComposeContext)] +#[tokio::test] +async fn test_sanity(ctx: &mut common::DockerComposeContext) { + let mut client1 = common::create_client().expect("client1"); + common::login(ctx, &mut client1, "user01", "password1") + .await + .expect("user01 login"); + + common::user_key_add(ctx, &mut client1, "sha-rsa", TESTKEY1_PUB) + .await + .expect("user01 key add"); + ctx.setup_ssh_key("client1", TESTKEY1) + .await + .expect("user01 ssh_config setup"); + + let mut client2 = common::create_client().expect("client2"); + common::login(ctx, &mut client2, "user02", "password2") + .await + .expect("user02 login"); + + common::user_key_add(ctx, &mut client2, "sha-rsa", TESTKEY2_PUB) + .await + .expect("user02 key add"); + ctx.setup_ssh_key("client2", TESTKEY2) + .await + .expect("user02 ssh_config setup"); + + let remote_git = String::from(ctx.remote_git()); + common::create_project(ctx, &mut client1, "fake", &remote_git) + .await + .expect("create fake project"); + + ctx.git_clone("client1").await.expect("git clone user01"); + { + let dir = ctx.git_dir("client1"); + ctx.git_cmd("client1", &["config", "set", "user.name", "John Smith"]) + .await + .expect("config set"); + ctx.git_cmd( + "client1", + &["config", "set", "user.email", "user01@example.org"], + ) + .await + .expect("config set"); + ctx.git_cmd("client1", &["checkout", "-b", "user01/review1"]) + .await + .expect("checkout"); + fs::write(dir.join("README"), "Hello World!") + .await + .expect("rewrite README"); + fs::write(dir.join("empty"), "") + .await + .expect("create empty"); + ctx.git_cmd("client1", &["add", "README", "empty"]) + .await + .expect("git add"); + ctx.git_cmd("client1", &["commit", "-m", "Improve spelling"]) + .await + .expect("git commit"); + ctx.git_cmd( + "client1", + &["push", "--set-upstream", "origin", "user01/review1"], + ) + .await + .expect("git push"); + } + + for _ in 0..5 { + let reviews = common::list_reviews(ctx, &mut client2, "fake") + .await + .expect("list reviews"); + if reviews.reviews.len() > 0 { + assert_eq!(reviews.reviews[0].branch, "user01/review1"); + break; + } + sleep(Duration::from_millis(500)); + } +} |
