Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
httpd: Add api tests
xphoniex committed 3 years ago
commit d98073a55d222c141421ad0c9247eee4f8943728
parent 134bb7a847e3afcadf339bad521346c657d57c85
6 files changed +648 -0
modified radicle-httpd/Cargo.toml
@@ -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"] }
modified radicle-httpd/src/api.rs
@@ -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");
added radicle-httpd/src/api/test.rs
@@ -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()
+
    }
+
}
modified radicle-httpd/src/api/v1/delegates.rs
@@ -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"
+
              }
+
            ])
+
        );
+
    }
+
}
modified radicle-httpd/src/api/v1/projects.rs
@@ -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": []
+
              }
+
            ])
+
        );
+
    }
+
}
modified radicle-httpd/src/api/v1/stats.rs
@@ -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
+
                }
+
            })
+
        );
+
    }
+
}