Skip to content

Commit

Permalink
test: mock api calls and test functionality calling an API (#164)
Browse files Browse the repository at this point in the history
* test: remove unused code

* test: main

* test: helpers functions

* test: space between test methods

* test: fix space between test

* test: get latest releases

* test: refactor and test get commit hash

* refactor: renaming and restructure

* refactor: improve github api (#166)

---------

Co-authored-by: Frank Bell <[email protected]>
  • Loading branch information
AlexD10S and evilrobot-01 authored May 14, 2024
1 parent 23dbc4e commit b0660cc
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 46 deletions.
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://api.github.com/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://api.github.com/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://api.github.com/repos/paritytech/polkadot-sdk/git/refs/tags/polkadot-v1.11.0",
"object": {
"sha": "0bb6249268c0b77d2834640b84cb52fdd3d7e860",
"type": "commit",
"url": "https://api.github.com/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://api.github.com/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://api.github.com/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

0 comments on commit b0660cc

Please sign in to comment.