Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: mock api calls and test functionality calling an API #164

Merged
merged 11 commits into from
May 14, 2024
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion crates/pop-cli/src/commands/new/parachain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ async fn guide_user_to_generate_parachain() -> Result<NewParachainCommand> {
let template = display_select_options(provider)?;

let url = url::Url::parse(&template.repository_url()?).expect("valid repository url");
let latest_3_releases = GitHub::get_latest_n_releases(3, &url).await?;
// Get only the latest 3 releases
let latest_3_releases: Vec<Release> = get_latest_3_releases(url).await?;

let mut release_name = None;
if latest_3_releases.len() > 0 {
Expand Down Expand Up @@ -261,6 +262,23 @@ fn check_destination_path(name_template: &String) -> Result<&Path> {
Ok(destination_path)
}

async fn get_latest_3_releases(url: url::Url) -> Result<Vec<Release>> {
let repo = GitHub::parse(url.as_str())?;
let mut latest_3_releases: Vec<Release> = repo
.get_latest_releases()
.await?
.into_iter()
.filter(|r| !r.prerelease)
.take(3)
.collect();
// Get the commit sha for the releases
for release in latest_3_releases.iter_mut() {
let commit = repo.get_commit_sha_from_release(&release.tag_name).await?;
release.commit = Some(commit);
}
Ok(latest_3_releases)
}

fn display_release_versions_to_user(releases: Vec<Release>) -> Result<String> {
let mut prompt = cliclack::select("Select a specific release:".to_string());
for (i, release) in releases.iter().enumerate() {
Expand Down
3 changes: 3 additions & 0 deletions crates/pop-parachains/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ walkdir.workspace = true
# Zombienet
zombienet-sdk.workspace = true
zombienet-support.workspace = true

[dev-dependencies]
mockito.workspace = true
4 changes: 2 additions & 2 deletions crates/pop-parachains/src/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,8 @@ impl Zombienet {
}

async fn latest_polkadot_release() -> Result<String, Error> {
let repo = Url::parse(POLKADOT_SDK).expect("repository url valid");
match GitHub::get_latest_releases(&repo).await {
let repo = GitHub::parse(POLKADOT_SDK)?;
match repo.get_latest_releases().await {
Ok(releases) => {
// Fetching latest releases
for release in releases {
Expand Down
176 changes: 133 additions & 43 deletions crates/pop-parachains/src/utils/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,58 +142,62 @@ impl Git {
}
}

pub struct GitHub;
pub struct GitHub {
pub org: String,
pub name: String,
api: String,
}

impl GitHub {
const GITHUB: &'static str = "github.com";
pub async fn get_latest_releases(repo: &Url) -> Result<Vec<Release>> {

pub fn parse(url: &str) -> Result<Self> {
let url = Url::parse(url)?;
Ok(Self {
org: Self::org(&url)?.into(),
name: Self::name(&url)?.into(),
api: "https://api.github.com".into(),
})
}

// Overrides the api base url for testing
#[cfg(test)]
fn with_api(mut self, api: impl Into<String>) -> Self {
self.api = api.into();
self
}

pub async fn get_latest_releases(&self) -> Result<Vec<Release>> {
static APP_USER_AGENT: &str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

let client = reqwest::ClientBuilder::new().user_agent(APP_USER_AGENT).build()?;
let response = client
.get(format!(
"https://github.com/gitapi/repos/{}/{}/releases",
Self::org(repo)?,
Self::name(repo)?
))
.send()
.await?;
let url = self.api_releases_url();
let response = client.get(url).send().await?;
Ok(response.json::<Vec<Release>>().await?)
}

pub async fn get_latest_n_releases(number: usize, repo: &Url) -> Result<Vec<Release>> {
pub async fn get_commit_sha_from_release(&self, tag_name: &str) -> Result<String> {
static APP_USER_AGENT: &str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

let client = reqwest::ClientBuilder::new().user_agent(APP_USER_AGENT).build()?;
let response = client.get(self.api_tag_information(tag_name)).send().await?;
let value = response.json::<serde_json::Value>().await?;
let commit = value
.get("object")
.and_then(|v| v.get("sha"))
.and_then(|v| v.as_str())
.map(|v| v.to_owned())
.ok_or(Error::Git("the github release tag sha was not found".to_string()))?;
Ok(commit)
}

let mut releases: Vec<Release> = Self::get_latest_releases(repo)
.await?
.into_iter()
.filter(|r| !r.prerelease)
.take(number)
.collect();
// Additional lookup for commit sha
for release in releases.iter_mut() {
let response = client
.get(format!(
"https://github.com/gitapi/repos/{}/{}/git/ref/tags/{}",
Self::org(repo)?,
Self::name(repo)?,
&release.tag_name
))
.send()
.await?;
let value = response.json::<serde_json::Value>().await?;
let commit = value
.get("object")
.and_then(|v| v.get("sha"))
.and_then(|v| v.as_str())
.map(|v| v.to_owned())
.ok_or(Error::Git("the github release tag sha was not found".to_string()))?;
release.commit = Some(commit);
}
Ok(releases)
fn api_releases_url(&self) -> String {
format!("{}/repos/{}/{}/releases", self.api, self.org, self.name)
}

fn api_tag_information(&self, tag_name: &str) -> String {
format!("{}/repos/{}/{}/git/ref/tags/{}", self.api, self.org, self.name, tag_name)
}

fn org(repo: &Url) -> Result<&str> {
Expand All @@ -219,12 +223,13 @@ impl GitHub {
pub(crate) fn release(repo: &Url, tag: &str, artifact: &str) -> String {
format!("{}/releases/download/{tag}/{artifact}", repo.as_str())
}

pub(crate) fn convert_to_ssh_url(url: &Url) -> String {
format!("git@{}:{}.git", url.host_str().unwrap_or(Self::GITHUB), &url.path()[1..])
}
}

#[derive(serde::Deserialize)]
#[derive(Debug, PartialEq, serde::Deserialize)]
pub struct Release {
pub tag_name: String,
pub name: String,
Expand All @@ -235,14 +240,99 @@ pub struct Release {
#[cfg(test)]
mod tests {
use super::*;
use mockito::{Mock, Server};

const BASE_PARACHAIN: &str = "https://github.com/r0gue-io/base-parachain";
const POLKADOT_SDK: &str = "https://github.com/paritytech/polkadot-sdk";

async fn releases_mock(mock_server: &mut Server, repo: &GitHub, payload: &str) -> Mock {
mock_server
.mock("GET", format!("/repos/{}/{}/releases", repo.org, repo.name).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(payload)
.create_async()
.await
}

async fn tag_mock(mock_server: &mut Server, repo: &GitHub, tag: &str, payload: &str) -> Mock {
mock_server
.mock("GET", format!("/repos/{}/{}/git/ref/tags/{tag}", repo.org, repo.name).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(payload)
.create_async()
.await
}

#[tokio::test]
async fn test_get_latest_releases() -> Result<(), Box<dyn std::error::Error>> {
let mut mock_server = Server::new_async().await;

let expected_payload = r#"[{
"tag_name": "polkadot-v1.10.0",
"name": "Polkadot v1.10.0",
"prerelease": false
}]"#;
let repo = GitHub::parse(BASE_PARACHAIN)?.with_api(&mock_server.url());
let mock = releases_mock(&mut mock_server, &repo, expected_payload).await;
let latest_release = repo.get_latest_releases().await?;
assert_eq!(
latest_release[0],
Release {
tag_name: "polkadot-v1.10.0".to_string(),
name: "Polkadot v1.10.0".into(),
prerelease: false,
commit: None
}
);
mock.assert_async().await;
Ok(())
}

#[tokio::test]
async fn get_releases_with_commit_sha() -> Result<(), Box<dyn std::error::Error>> {
let mut mock_server = Server::new_async().await;

let expected_payload = r#"{
"ref": "refs/tags/polkadot-v1.11.0",
"node_id": "REF_kwDOKDT1SrpyZWZzL3RhZ3MvcG9sa2Fkb3QtdjEuMTEuMA",
"url": "https://github.com/gitapi/repos/paritytech/polkadot-sdk/git/refs/tags/polkadot-v1.11.0",
"object": {
"sha": "0bb6249268c0b77d2834640b84cb52fdd3d7e860",
"type": "commit",
"url": "https://github.com/gitapi/repos/paritytech/polkadot-sdk/git/commits/0bb6249268c0b77d2834640b84cb52fdd3d7e860"
}
}"#;
let repo = GitHub::parse(BASE_PARACHAIN)?.with_api(&mock_server.url());
let mock = tag_mock(&mut mock_server, &repo, "polkadot-v1.11.0", expected_payload).await;
let hash = repo.get_commit_sha_from_release("polkadot-v1.11.0").await?;
assert_eq!(hash, "0bb6249268c0b77d2834640b84cb52fdd3d7e860");
mock.assert_async().await;
Ok(())
}

#[test]
fn test_get_releases_api_url() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(
GitHub::parse(POLKADOT_SDK)?.api_releases_url(),
"https://github.com/gitapi/repos/paritytech/polkadot-sdk/releases"
);
Ok(())
}

#[test]
fn test_url_api_tag_information() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(
GitHub::parse(POLKADOT_SDK)?.api_tag_information("polkadot-v1.11.0"),
"https://github.com/gitapi/repos/paritytech/polkadot-sdk/git/ref/tags/polkadot-v1.11.0"
);
Ok(())
}

#[test]
fn test_parse_org() -> Result<(), Box<dyn std::error::Error>> {
let url = Url::parse(BASE_PARACHAIN)?;
let org = GitHub::org(&url)?;
assert_eq!(org, "r0gue-io");
assert_eq!(GitHub::parse(BASE_PARACHAIN)?.org, "r0gue-io");
Ok(())
}

Expand Down
Loading