httpd: Add api tests

Signed-off-by: xphoniex <dj.2dixx@gmail.com>
This commit is contained in:
xphoniex 2023-01-12 19:18:02 +00:00 committed by Alexis Sellier
parent 897d439f95
commit 7962ac0a8e
No known key found for this signature in database
8 changed files with 653 additions and 2 deletions

4
Cargo.lock generated
View File

@ -2195,13 +2195,17 @@ dependencies = [
"hyper",
"lexopt",
"radicle",
"radicle-cli",
"radicle-crypto",
"radicle-surf",
"serde",
"serde_json",
"siwe",
"tempfile",
"thiserror",
"time 0.3.17",
"tokio",
"tower",
"tower-http",
"tracing",
"tracing-logfmt",

View File

@ -4,8 +4,7 @@ use std::ffi::OsString;
use anyhow::anyhow;
use radicle::crypto::ssh;
use radicle::profile;
use radicle::Profile;
use radicle::{profile, Profile};
use crate::git;
use crate::terminal as term;

View File

@ -43,3 +43,10 @@ version = "0.2.0"
git = "https://github.com/radicle-dev/radicle-git"
features = ["serde"]
rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09"
[dev-dependencies]
hyper = { version = "0.14.17", default-features = false, features = ["client"] }
radicle-cli = { path = "../radicle-cli" }
radicle-crypto = { path = "../radicle-crypto" }
tempfile = { version = "3.3.0" }
tower = { version = "0.4", features = ["util"] }

View File

@ -20,6 +20,8 @@ use radicle::Profile;
mod auth;
mod axum_extra;
mod error;
#[cfg(test)]
mod test;
mod v1;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@ -0,0 +1,141 @@
use std::path::Path;
use std::sync::Arc;
use std::{env, fs};
use axum::body::Body;
use axum::http::Request;
use axum::Router;
use serde_json::Value;
use tower::ServiceExt;
use radicle::cob::issue::Issues;
use radicle::git::raw as git2;
use radicle::storage::WriteStorage;
use radicle_cli::commands::rad_init;
use radicle_crypto::ssh::keystore::MemorySigner;
use radicle_crypto::Signer;
use crate::api::Context;
pub const HEAD: &str = "1e978d19f251cd9821d9d9a76d1bd436bf0690d5";
pub const HEAD_1: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
const PASSWORD: &str = "radicle";
pub fn seed(dir: &Path) -> Context {
let workdir = dir.join("hello-world");
let rad_home = dir.join("radicle");
env::set_var("RAD_PASSPHRASE", PASSWORD);
env::set_var("RAD_DEBUG", "1");
fs::create_dir_all(&workdir).unwrap();
fs::create_dir_all(&rad_home).unwrap();
// add commits to workdir (repo)
let repo = git2::Repository::init(&workdir).unwrap();
let tree =
radicle::git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap();
let sig_time = git2::Time::new(1673001014, 0);
let sig = git2::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap();
let oid = repo
.commit(Some("HEAD"), &sig, &sig, "Initial commit\n", &tree, &[])
.unwrap();
let commit = repo.find_commit(oid).unwrap();
repo.checkout_tree(tree.as_object(), None).unwrap();
fs::create_dir(workdir.join("dir1")).unwrap();
fs::write(
workdir.join("dir1").join("README"),
"Hello World from dir1!\n",
)
.unwrap();
let mut index = repo.index().unwrap();
index
.add_all(["."], git2::IndexAddOption::DEFAULT, None)
.unwrap();
index.write().unwrap();
let oid = index.write_tree().unwrap();
let tree = repo.find_tree(oid).unwrap();
repo.commit(
Some("HEAD"),
&sig,
&sig,
"Add another folder\n",
&tree,
&[&commit],
)
.unwrap();
// eq. rad auth
let profile = radicle::Profile::init(rad_home, PASSWORD.to_owned()).unwrap();
// rad init
rad_init::init(
rad_init::Options {
path: Some(workdir.clone()),
name: Some("hello-world".to_string()),
description: Some("Rad repository for tests".to_string()),
branch: None,
interactive: false.into(),
setup_signing: false,
set_upstream: false,
},
&profile,
)
.unwrap();
// eq. rad issue new
env::set_var("RAD_COMMIT_TIME", "1673001014");
let signer = MemorySigner::load(&profile.keystore, PASSWORD.to_owned().into()).unwrap();
let storage = &profile.storage;
let (_, id) = radicle::rad::repo(&workdir).unwrap();
let repo = storage.repository(id).unwrap();
let mut issues = Issues::open(*signer.public_key(), &repo).unwrap();
issues
.create(
"Issue #1".to_string(),
"Change 'hello world' to 'hello everyone'".to_string(),
&[],
&signer,
)
.unwrap();
Context {
profile: Arc::new(profile),
sessions: Default::default(),
}
}
pub async fn request(app: &Router, path: impl ToString) -> Response {
Response(
app.clone()
.oneshot(
Request::builder()
.uri(path.to_string())
.body(Body::empty())
.unwrap(),
)
.await
.unwrap(),
)
}
pub struct Response(axum::response::Response);
impl Response {
pub async fn json(self) -> Value {
let body = hyper::body::to_bytes(self.0.into_body()).await.unwrap();
serde_json::from_slice(&body).unwrap()
}
pub fn status(&self) -> axum::http::StatusCode {
self.0.status()
}
}

View File

@ -62,3 +62,38 @@ async fn delegates_projects_handler(
Ok::<_, Error>(Json(projects))
}
#[cfg(test)]
mod routes {
use axum::http::StatusCode;
use serde_json::json;
use crate::api::test::{self, request, HEAD};
#[tokio::test]
async fn test_delegates_projects() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(
&app,
"/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects",
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!([
{
"name": "hello-world",
"description": "Rad repository for tests",
"defaultBranch": "master",
"head": HEAD,
"patches": 0,
"issues": 1,
"id": "rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp"
}
])
);
}
}

View File

@ -467,3 +467,438 @@ fn stats(repo: &Repository, head: Oid) -> Result<Stats, Error> {
contributors: contributors.len(),
})
}
#[cfg(test)]
mod routes {
use axum::http::StatusCode;
use serde_json::json;
use crate::api::test::{self, request, HEAD, HEAD_1};
#[tokio::test]
async fn test_projects_root() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(&app, "/projects").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!([
{
"name": "hello-world",
"description": "Rad repository for tests",
"defaultBranch": "master",
"head": HEAD,
"patches": 0,
"issues": 1,
"id": "rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp"
}
])
);
}
#[tokio::test]
async fn test_projects() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!({
"name": "hello-world",
"description": "Rad repository for tests",
"defaultBranch": "master",
"head": HEAD,
"patches": 0,
"issues": 1,
"id": "rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp"
})
);
}
#[tokio::test]
async fn test_projects_commits_root() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits").await;
assert_eq!(response.status(), StatusCode::FOUND);
assert_eq!(
response.json().await,
json!({
"headers": [
{
"header": {
"sha1": HEAD,
"author": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"summary": "Add another folder",
"description": "",
"committer": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"committerTime": 1673001014
},
"diff": {
"added": [
{
"path": "dir1/README",
"diff": {
"type": "plain",
"hunks": [
{
"header": "@@ -0,0 +1 @@\n",
"lines": [
{
"line": "Hello World from dir1!\n",
"lineNo": 1,
"type": "addition"
}
]
}
]
}
}
],
"deleted": [],
"moved": [],
"copied": [],
"modified": [],
"stats": {
"filesChanged": 1,
"insertions": 1,
"deletions": 0
}
},
"branches": [
"refs/heads/master"
]
},
{
"header": {
"sha1": HEAD_1,
"author": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"summary": "Initial commit",
"description": "",
"committer": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"committerTime": 1673001014
},
"diff": {
"added": [
{
"path": "README",
"diff": {
"type": "plain",
"hunks": [
{
"header": "@@ -0,0 +1 @@\n",
"lines": [
{
"line": "Hello World!\n",
"lineNo": 1,
"type": "addition"
}
]
}
]
}
}
],
"deleted": [],
"moved": [],
"copied": [],
"modified": [],
"stats": {
"filesChanged": 1,
"insertions": 1,
"deletions": 0
}
},
"branches": [
"refs/heads/master"
]
}
],
"stats": {
"commits": 2,
"branches": 1,
"contributors": 1
}
})
);
}
#[tokio::test]
async fn test_projects_commits() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(
&app,
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits/{HEAD}"),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!({
"header": {
"sha1": HEAD,
"author": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"summary": "Add another folder",
"description": "",
"committer": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"committerTime": 1673001014
},
"diff": {
"added": [
{
"path": "dir1/README",
"diff": {
"type": "plain",
"hunks": [
{
"header": "@@ -0,0 +1 @@\n",
"lines": [
{
"line": "Hello World from dir1!\n",
"lineNo": 1,
"type": "addition"
}
]
}
]
}
}
],
"deleted": [],
"moved": [],
"copied": [],
"modified": [],
"stats": {
"filesChanged": 1,
"insertions": 1,
"deletions": 0
}
},
"branches": [
"refs/heads/master"
]
})
);
}
#[tokio::test]
async fn test_projects_tree() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(
&app,
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/tree/{HEAD}/"),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!({
"entries": [
{
"path": "README",
"name": "README",
"lastCommit": null,
"kind": "blob"
},
{
"path": "dir1",
"name": "dir1",
"lastCommit": null,
"kind": "tree"
}
],
"lastCommit": {
"sha1": HEAD,
"author": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"summary": "Add another folder",
"description": "",
"committer": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"committerTime": 1673001014
},
"name": "",
"path": "",
"stats": {
"branches": 1,
"commits": 2,
"contributors": 1
}
}
)
);
let response = request(
&app,
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/tree/{HEAD}/dir1"),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!({
"entries": [
{
"path": "dir1/README",
"name": "README",
"lastCommit": null,
"kind": "blob"
}
],
"lastCommit": null,
"name": "dir1",
"path": "dir1",
"stats": {
"branches": 1,
"commits": 2,
"contributors": 1
}
})
);
}
#[tokio::test]
async fn test_projects_remotes_root() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!([
{
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
"heads": {
"master": HEAD
},
"delegate": false
}
])
);
}
#[tokio::test]
async fn test_projects_remotes() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!({
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
"heads": {
"master": HEAD
},
"delegate": false
})
);
}
#[tokio::test]
async fn test_projects_blob() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(
&app,
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/blob/{HEAD}/README"),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!({
"binary": false,
"content": "Hello World!\n",
"lastCommit": {
"sha1": HEAD_1,
"author": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"summary": "Initial commit",
"description": "",
"committer": {
"name": "Alice Liddell",
"email": "alice@radicle.xyz"
},
"committerTime": 1673001014
},
"name": "README",
"path": "README"
})
);
}
#[tokio::test]
async fn test_projects_issues_root() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!([
{
"id": "458bbd9f6d47eed3d60cd905141687ad1f99251e",
"author": {
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
},
"title": "Issue #1",
"state": {
"status": "open"
},
"discussion": [
{
"author": {
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
},
"body": "Change 'hello world' to 'hello everyone'",
"reactions": [],
"timestamp": 1673001014,
"replyTo": null
}
],
"tags": []
}
])
);
}
}

View File

@ -22,3 +22,31 @@ async fn stats_handler(Extension(ctx): Extension<Context>) -> impl IntoResponse
json!({ "projects": { "count": projects }, "users": { "count": 0 } }),
))
}
#[cfg(test)]
mod routes {
use axum::http::StatusCode;
use serde_json::json;
use crate::api::test::{self, request};
#[tokio::test]
async fn test_stats() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = request(&app, "/stats").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!({
"projects": {
"count": 1
},
"users": {
"count": 0
}
})
);
}
}