summaryrefslogtreecommitdiff
path: root/server/tests
diff options
context:
space:
mode:
Diffstat (limited to 'server/tests')
-rw-r--r--server/tests/common/mod.rs340
-rw-r--r--server/tests/integration_test.rs128
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(&params)
+ .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));
+ }
+}