summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-02-09 23:56:38 +0100
committerJoel Klinghed <the_jk@spawned.biz>2025-02-09 23:56:38 +0100
commitbf025b4977543a371df9dbdddfe9cc2f02f2a8d0 (patch)
treefc8937a3b5f3311ff7b8209aec3961668ac83d8c
parentbd74717e10fb36e19893c15941876b2383b94714 (diff)
First integration test
Sets up a whole slew of docker instances, all started from clean slate for test.
-rw-r--r--docker/integration_test/docker-compose.yaml77
-rw-r--r--docker/integration_test/web/Dockerfile15
-rw-r--r--docker/integration_test/web/gitkey16
-rw-r--r--docker/integration_test/web/gitkey.pub1
-rwxr-xr-xdocker/integration_test/web/setup.sh25
-rw-r--r--server/Cargo.lock276
-rw-r--r--server/Cargo.toml10
-rw-r--r--server/api/Cargo.toml9
-rw-r--r--server/api/src/api_model.rs (renamed from server/src/api_model.rs)25
-rw-r--r--server/api/src/lib.rs1
-rw-r--r--server/src/main.rs25
-rw-r--r--server/tests/common/mod.rs340
-rw-r--r--server/tests/integration_test.rs128
13 files changed, 918 insertions, 30 deletions
diff --git a/docker/integration_test/docker-compose.yaml b/docker/integration_test/docker-compose.yaml
new file mode 100644
index 0000000..628973f
--- /dev/null
+++ b/docker/integration_test/docker-compose.yaml
@@ -0,0 +1,77 @@
+services:
+ openldap:
+ image: bitnami/openldap:latest
+ ports:
+ - '1389'
+ - '1636'
+ environment:
+ - LDAP_ADMIN_USERNAME=admin
+ - LDAP_ADMIN_PASSWORD=adminpassword
+ - LDAP_USERS=user01,user02,user03
+ - LDAP_PASSWORDS=password1,password2,password3
+ volumes:
+ - 'it_openldap_data:/bitnami/openldap'
+ mariadb:
+ image: mariadb:latest
+ ports:
+ - '3306'
+ environment:
+ - MARIADB_USER=eyeballs
+ - MARIADB_PASSWORD=secret
+ - MARIADB_DATABASE=eyeballs
+ - MARIADB_RANDOM_ROOT_PASSWORD=1
+ healthcheck:
+ test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
+ start_period: 10s
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ volumes:
+ - 'it_mariadb_data:/var/lib/mysql'
+ remote_git:
+ image: rockstorm/git-server:latest
+ ports:
+ - '12222:22'
+ volumes:
+ - './web/gitkey.pub:/home/git/.ssh/authorized_keys'
+ - 'it_remote_git:/srv/git'
+ local_git:
+ image: rockstorm/git-server:latest
+ environment:
+ - SSH_AUTH_METHODS=publickey
+ depends_on:
+ - web
+ ports:
+ - '10022:22'
+ volumes:
+ - 'it_git_auth:/home/git/.ssh'
+ - 'it_git_repos:/srv/git'
+ web:
+ build:
+ context: ../../
+ dockerfile: ./docker/integration_test/web/Dockerfile
+ depends_on:
+ openldap:
+ condition: service_started
+ mariadb:
+ condition: service_healthy
+ environment:
+ - LDAP_URL=ldap://openldap:1389
+ - DB_URL=mysql://eyeballs:secret@mariadb:3306/eyeballs
+ ports:
+ - '18000:8000'
+ volumes:
+ - 'it_git_auth:/git/auth'
+ - 'it_git_repos:/git/repos'
+
+volumes:
+ it_openldap_data:
+ driver: local
+ it_mariadb_data:
+ driver: local
+ it_remote_git:
+ driver: local
+ it_git_auth:
+ driver: local
+ it_git_repos:
+ driver: local
diff --git a/docker/integration_test/web/Dockerfile b/docker/integration_test/web/Dockerfile
new file mode 100644
index 0000000..2ba7a5d
--- /dev/null
+++ b/docker/integration_test/web/Dockerfile
@@ -0,0 +1,15 @@
+FROM archlinux:base
+
+RUN pacman -Suy --noconfirm && pacman -S openssl git --noconfirm
+
+# Docker still have this really stupied idea that all files must be relative "context",
+# so context is set to ../.. relative the docker-compose.yaml
+COPY server/target/x86_64-unknown-linux-musl/debug/eyeballs-githook /app/eyeballs-githook
+COPY server/target/debug/eyeballs /app/eyeballs
+COPY docker/integration_test/web/setup.sh /app/setup.sh
+
+RUN mkdir -p -m 0700 /app/.ssh
+COPY docker/integration_test/web/gitkey /app/.ssh/id_rsa
+
+WORKDIR /app
+ENTRYPOINT /app/setup.sh
diff --git a/docker/integration_test/web/gitkey b/docker/integration_test/web/gitkey
new file mode 100644
index 0000000..a884da2
--- /dev/null
+++ b/docker/integration_test/web/gitkey
@@ -0,0 +1,16 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAIEA29qzykqWxz65PFU5LuUIOplhgCnuDEA0LD7lff6qhwIaa2WyXOC0
+q9yOpY9tB1T+rArpmRgo1iH2jNRw4Y4E7DERQNhEEhfIvse93HspeTIHuQmqWID7IxUDZJ
+JK55XA1DGJIZWTreNc/XeEOBdXgBPRxMQg92jRmxfwi+xzQ20AAAIIvPzf27z839sAAAAH
+c3NoLXJzYQAAAIEA29qzykqWxz65PFU5LuUIOplhgCnuDEA0LD7lff6qhwIaa2WyXOC0q9
+yOpY9tB1T+rArpmRgo1iH2jNRw4Y4E7DERQNhEEhfIvse93HspeTIHuQmqWID7IxUDZJJK
+55XA1DGJIZWTreNc/XeEOBdXgBPRxMQg92jRmxfwi+xzQ20AAAADAQABAAAAgQDT6DjjAo
+HSCeMBBCPZz2ffE3em0MNhi4C+JOGOT6iN+Lj+S0dfvjZmcHANo/Cy4HmX2ezOYzr5KkM5
++onS3dBfDj1ndLNZt4NCd9jeRAONetweXdA9AlJiSMJ5A33hMsACaMxrwzwKicexKz9pIw
+9hcxiIo28Xbak6MrTOBQWFAQAAAEEA1TCRvnzZTDIyzrQyyx85HiQRVU8ySm1fSSXh9Xvv
+m2RtSSRXlZuoVB6tUyMczGjHV76LzMmmJafa1CzyrJqLFwAAAEEA9q162hU6gPxvVIdKt3
+/W26oWaaSlDKZ21XxSRGQNYnfMpQqCzAIvAdRiaF+hE/H4Sl8GW7U4yDtsD3pUsS/OKwAA
+AEEA5Cm3Q9hps3iOwbEpo59yno/Z5VhU98UXhKGFLo3/nLcMDVehk7eRRdYtqDwcMNEmxZ
+T/hs0Hp1Ph4uv3td4AxwAAAA10aGVfamtAd2lsbG93AQIDBA==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/docker/integration_test/web/gitkey.pub b/docker/integration_test/web/gitkey.pub
new file mode 100644
index 0000000..1474e72
--- /dev/null
+++ b/docker/integration_test/web/gitkey.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDb2rPKSpbHPrk8VTku5Qg6mWGAKe4MQDQsPuV9/qqHAhprZbJc4LSr3I6lj20HVP6sCumZGCjWIfaM1HDhjgTsMRFA2EQSF8i+x73ceyl5Mge5CapYgPsjFQNkkkrnlcDUMYkhlZOt41z9d4Q4F1eAE9HExCD3aNGbF/CL7HNDbQ== the_jk@willow
diff --git a/docker/integration_test/web/setup.sh b/docker/integration_test/web/setup.sh
new file mode 100755
index 0000000..bd6c805
--- /dev/null
+++ b/docker/integration_test/web/setup.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+echo "[default]" > Rocket.toml
+echo "address = \"0.0.0.0\"" >> Rocket.toml
+echo "secret_key = \"itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=\"" >> Rocket.toml
+echo "session_max_age_days = 7" >> Rocket.toml
+echo "ldap_url = \"$LDAP_URL\"" >> Rocket.toml
+echo "ldap_users = \"ou=users,dc=example,dc=org\"" >> Rocket.toml
+echo "ldap_filter = \"(objectClass=posixAccount)\"" >> Rocket.toml
+echo "git_server_root = \"/git/repos\"" >> Rocket.toml
+echo "authorized_keys = \"/git/auth/authorized_keys\"" >> Rocket.toml
+echo "git_hook = \"/git/repos/eyeballs-githook\"" >> Rocket.toml
+echo "[default.databases.eyeballs]" >> Rocket.toml
+echo "url = \"$DB_URL\"" >> Rocket.toml
+
+export RUST_BACKTRACE=1
+export HOME=/app
+
+echo "Host remote_git" > /app/.ssh/config
+echo " StrictHostKeyChecking no" >> /app/.ssh/config
+
+# Hardlinks cannot cross devices, so copy to the /git/repos mount.
+cp /app/eyeballs-githook /git/repos/eyeballs-githook
+
+exec ./eyeballs
diff --git a/server/Cargo.lock b/server/Cargo.lock
index 5664b03..e44bc77 100644
--- a/server/Cargo.lock
+++ b/server/Cargo.lock
@@ -319,6 +319,24 @@ dependencies = [
]
[[package]]
+name = "cookie_store"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
+dependencies = [
+ "cookie",
+ "document-features",
+ "idna",
+ "log",
+ "publicsuffix",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -490,6 +508,15 @@ dependencies = [
]
[[package]]
+name = "document-features"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -551,15 +578,18 @@ name = "eyeballs"
version = "0.1.0"
dependencies = [
"anyhow",
+ "eyeballs-api",
"eyeballs-common",
"futures",
"ldap3",
+ "reqwest",
"rmp-serde",
"rocket",
"rocket_db_pools",
"serde",
"sqlx",
"stdext",
+ "test-context",
"testdir",
"time",
"tokio",
@@ -568,6 +598,14 @@ dependencies = [
]
[[package]]
+name = "eyeballs-api"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "utoipa",
+]
+
+[[package]]
name = "eyeballs-common"
version = "0.1.0"
dependencies = [
@@ -959,6 +997,29 @@ dependencies = [
]
[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http 1.2.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http 1.2.0",
+ "http-body 1.0.1",
+ "pin-project-lite",
+]
+
+[[package]]
name = "httparse"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -982,7 +1043,7 @@ dependencies = [
"futures-util",
"h2",
"http 0.2.12",
- "http-body",
+ "http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
@@ -995,6 +1056,44 @@ dependencies = [
]
[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.2.0",
+ "http-body 1.0.1",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.2.0",
+ "http-body 1.0.1",
+ "hyper 1.6.0",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1160,6 +1259,12 @@ dependencies = [
]
[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
name = "is-terminal"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1265,6 +1370,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
+name = "litrs"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+
+[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1729,6 +1840,22 @@ dependencies = [
]
[[package]]
+name = "psl-types"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
+
+[[package]]
+name = "publicsuffix"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
+dependencies = [
+ "idna",
+ "psl-types",
+]
+
+[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1841,6 +1968,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
+name = "reqwest"
+version = "0.12.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "cookie",
+ "cookie_store",
+ "futures-core",
+ "futures-util",
+ "http 1.2.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.6.0",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-registry",
+]
+
+[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1964,7 +2129,7 @@ dependencies = [
"either",
"futures",
"http 0.2.12",
- "hyper",
+ "hyper 0.14.32",
"indexmap",
"log",
"memchr",
@@ -2210,6 +2375,18 @@ dependencies = [
]
[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2589,6 +2766,15 @@ dependencies = [
]
[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2628,6 +2814,27 @@ dependencies = [
]
[[package]]
+name = "test-context"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb69cce03e432993e2dc1f93f7899b952300fcb6dc44191a1b830b60b8c3c8aa"
+dependencies = [
+ "futures",
+ "test-context-macros",
+]
+
+[[package]]
+name = "test-context-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97e0639209021e54dbe19cafabfc0b5574b078c37358945e6d473eabe39bb974"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.98",
+]
+
+[[package]]
name = "testdir"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2846,6 +3053,27 @@ dependencies = [
]
[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3154,6 +3382,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
+ "rustversion",
"wasm-bindgen-macro",
]
@@ -3172,6 +3401,19 @@ dependencies = [
]
[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3271,6 +3513,36 @@ dependencies = [
]
[[package]]
+name = "windows-registry"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
+dependencies = [
+ "windows-result",
+ "windows-strings",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+dependencies = [
+ "windows-result",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 18a6458..b9d0488 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -5,8 +5,8 @@ edition = "2021"
default-run = "eyeballs"
[workspace]
-members = ["hook"]
-default-members = [".", "common", "hook"]
+members = ["api", "hook"]
+default-members = [".", "api", "common", "hook"]
resolver = "2"
[workspace.dependencies]
@@ -15,9 +15,11 @@ rmp-serde = "1.3"
serde = { version = "1.0", features = ["derive"] }
testdir = "0.9.3"
tokio = { version = "1" }
+utoipa = { version = "5" }
[dependencies]
anyhow = "1.0"
+eyeballs-api = { path = "api" }
eyeballs-common = { path = "common" }
futures.workspace = true
ldap3 = { version = "0.11.5", default-features = false, features = [ "native-tls", "tls", "tls-native", "tokio-native-tls" ] }
@@ -28,9 +30,11 @@ serde.workspace = true
sqlx = { version = "0.7.0", default-features = false, features = ["macros", "migrate"] }
time = "0.3.34"
tokio = { workspace = true, features = ["process"] }
-utoipa = { version = "5", features = ["rocket_extras"] }
+utoipa = { workspace = true, features = ["rocket_extras"] }
utoipa-swagger-ui = { version = "9", features = ["rocket", "vendored"], default-features = false }
[dev-dependencies]
+reqwest = { version = "0.12.12", features = ["cookies", "json"], default-features = false }
stdext = "0.3.3"
+test-context = "0.4.1"
testdir.workspace = true
diff --git a/server/api/Cargo.toml b/server/api/Cargo.toml
new file mode 100644
index 0000000..0da892e
--- /dev/null
+++ b/server/api/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "eyeballs-api"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde.workspace = true
+utoipa.workspace = true
+
diff --git a/server/src/api_model.rs b/server/api/src/api_model.rs
index dbb42d8..3760f9e 100644
--- a/server/src/api_model.rs
+++ b/server/api/src/api_model.rs
@@ -1,4 +1,4 @@
-use rocket::serde::{Deserialize, Serialize};
+use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Copy, Clone, Deserialize, Serialize, ToSchema)]
@@ -50,6 +50,29 @@ pub enum UserReviewRole {
None,
}
+impl TryFrom<u8> for UserReviewRole {
+ type Error = &'static str;
+
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ match value {
+ 0 => Ok(UserReviewRole::None),
+ 1 => Ok(UserReviewRole::Reviewer),
+ 2 => Ok(UserReviewRole::Watcher),
+ _ => Err("Invalid role"),
+ }
+ }
+}
+
+impl From<UserReviewRole> for u8 {
+ fn from(value: UserReviewRole) -> u8 {
+ match value {
+ UserReviewRole::None => 0,
+ UserReviewRole::Reviewer => 1,
+ UserReviewRole::Watcher => 2,
+ }
+ }
+}
+
#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
pub struct User {
#[schema(example = "jsmith")]
diff --git a/server/api/src/lib.rs b/server/api/src/lib.rs
new file mode 100644
index 0000000..75860d1
--- /dev/null
+++ b/server/api/src/lib.rs
@@ -0,0 +1 @@
+pub mod api_model;
diff --git a/server/src/main.rs b/server/src/main.rs
index f07c372..9bdfeaf 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -14,6 +14,7 @@ use std::path::PathBuf;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
+use eyeballs_api::api_model;
use eyeballs_common::fs_utils;
use eyeballs_common::git;
use eyeballs_common::git_socket;
@@ -21,7 +22,6 @@ use eyeballs_common::git_socket;
#[cfg(test)]
mod tests;
-mod api_model;
mod auth;
mod authorized_keys;
mod db_utils;
@@ -58,29 +58,6 @@ struct Db(sqlx::MySqlPool);
)]
pub struct MainApi;
-impl TryFrom<u8> for api_model::UserReviewRole {
- type Error = &'static str;
-
- fn try_from(value: u8) -> Result<Self, Self::Error> {
- match value {
- 0 => Ok(api_model::UserReviewRole::None),
- 1 => Ok(api_model::UserReviewRole::Reviewer),
- 2 => Ok(api_model::UserReviewRole::Watcher),
- _ => Err("Invalid role"),
- }
- }
-}
-
-impl From<api_model::UserReviewRole> for u8 {
- fn from(value: api_model::UserReviewRole) -> u8 {
- match value {
- api_model::UserReviewRole::None => 0,
- api_model::UserReviewRole::Reviewer => 1,
- api_model::UserReviewRole::Watcher => 2,
- }
- }
-}
-
#[utoipa::path(
responses(
(status = 200, description = "Get all projects", body = api_model::Projects),
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));
+ }
+}