use releasekit::Forge;
use releasekit::client::{HeaderMap, HttpClient, Response};
use releasekit::platform::GitHub;
#[derive(Clone)]
struct MockClient {
response: String,
}
impl HttpClient for MockClient {
fn get(&self, _url: &str, _headers: &HeaderMap) -> releasekit::error::Result<Response> {
Ok(Response {
status: 200,
body: self.response.clone(),
})
}
}
fn sample_releases_json() -> &'static str {
r#"[
{
"tag_name": "v1.0.0",
"name": "Version 1.0",
"prerelease": false,
"published_at": "2025-01-15T10:30:00Z",
"body": "First stable release",
"assets": [
{
"name": "app-x86_64.AppImage",
"size": 5242880,
"browser_download_url": "https://example.com/app-x86_64.AppImage"
},
{
"name": "app-x86_64.tar.gz",
"size": 3145728,
"browser_download_url": "https://example.com/app-x86_64.tar.gz"
}
]
},
{
"tag_name": "v0.9.0",
"name": null,
"prerelease": true,
"published_at": "2024-12-01T08:00:00Z",
"body": null,
"assets": []
}
]"#
}
fn sample_single_release_json() -> &'static str {
r#"{
"tag_name": "v2.0.0",
"name": "Version 2.0",
"prerelease": false,
"published_at": "2025-06-01T12:00:00Z",
"body": "Major update",
"assets": [
{
"name": "app.tar.gz",
"size": 1024,
"browser_download_url": "https://example.com/app.tar.gz"
}
]
}"#
}
#[test]
fn parses_multiple_releases() {
let client = MockClient {
response: sample_releases_json().to_string(),
};
let gh = GitHub::new(client);
let releases = gh.fetch_releases("test/repo", None).unwrap();
assert_eq!(releases.len(), 2);
let first = &releases[0];
assert_eq!(first.tag(), "v1.0.0");
assert_eq!(first.name(), Some("Version 1.0"));
assert!(!first.is_prerelease());
assert_eq!(first.published_at(), "2025-01-15T10:30:00Z");
assert_eq!(first.body(), Some("First stable release"));
assert_eq!(first.assets().len(), 2);
let asset = &first.assets()[0];
assert_eq!(asset.name(), "app-x86_64.AppImage");
assert_eq!(asset.size(), Some(5_242_880));
assert_eq!(asset.url(), "https://example.com/app-x86_64.AppImage");
let second = &releases[1];
assert_eq!(second.tag(), "v0.9.0");
assert_eq!(second.name(), None);
assert!(second.is_prerelease());
assert_eq!(second.body(), None);
assert!(second.assets().is_empty());
}
#[test]
fn parses_single_release() {
let client = MockClient {
response: sample_single_release_json().to_string(),
};
let gh = GitHub::new(client);
let releases = gh.fetch_releases("test/repo", Some("v2.0.0")).unwrap();
assert_eq!(releases.len(), 1);
assert_eq!(releases[0].tag(), "v2.0.0");
assert_eq!(releases[0].assets().len(), 1);
}
#[test]
fn with_token_sends_auth_header() {
use std::rc::Rc;
#[derive(Clone)]
struct InspectClient {
response: String,
got_auth: Rc<std::cell::RefCell<Option<String>>>,
}
impl HttpClient for InspectClient {
fn get(&self, _url: &str, headers: &HeaderMap) -> releasekit::error::Result<Response> {
for (k, v) in headers.iter() {
if k == "Authorization" {
*self.got_auth.borrow_mut() = Some(v.to_string());
}
}
Ok(Response {
status: 200,
body: self.response.clone(),
})
}
}
let got_auth = Rc::new(std::cell::RefCell::new(None));
let client = InspectClient {
response: sample_releases_json().to_string(),
got_auth: got_auth.clone(),
};
let gh = GitHub::new(client).with_token("ghp_test123");
let _ = gh.fetch_releases("test/repo", None).unwrap();
assert_eq!(got_auth.borrow().as_deref(), Some("Bearer ghp_test123"));
}
#[test]
fn no_auth_header_without_token() {
use std::rc::Rc;
#[derive(Clone)]
struct InspectClient {
response: String,
has_auth: Rc<std::cell::RefCell<bool>>,
}
impl HttpClient for InspectClient {
fn get(&self, _url: &str, headers: &HeaderMap) -> releasekit::error::Result<Response> {
*self.has_auth.borrow_mut() = headers.iter().any(|(k, _)| k == "Authorization");
Ok(Response {
status: 200,
body: self.response.clone(),
})
}
}
let has_auth = Rc::new(std::cell::RefCell::new(false));
let client = InspectClient {
response: sample_releases_json().to_string(),
has_auth: has_auth.clone(),
};
let gh = GitHub::new(client);
let _ = gh.fetch_releases("test/repo", None).unwrap();
assert!(!*has_auth.borrow());
}
#[test]
fn token_from_env_fallback() {
use std::rc::Rc;
#[derive(Clone)]
struct InspectClient {
response: String,
got_auth: Rc<std::cell::RefCell<Option<String>>>,
}
impl HttpClient for InspectClient {
fn get(&self, _url: &str, headers: &HeaderMap) -> releasekit::error::Result<Response> {
for (k, v) in headers.iter() {
if k == "Authorization" {
*self.got_auth.borrow_mut() = Some(v.to_string());
}
}
Ok(Response {
status: 200,
body: self.response.clone(),
})
}
}
let got_auth = Rc::new(std::cell::RefCell::new(None));
let client = InspectClient {
response: sample_releases_json().to_string(),
got_auth: got_auth.clone(),
};
unsafe {
std::env::set_var("RELEASEKIT_TEST_TOKEN_FALLBACK", "env-token-123");
}
let gh = GitHub::new(client).with_token_from_env(&[
"RELEASEKIT_TEST_NONEXISTENT",
"RELEASEKIT_TEST_TOKEN_FALLBACK",
]);
let _ = gh.fetch_releases("test/repo", None).unwrap();
unsafe {
std::env::remove_var("RELEASEKIT_TEST_TOKEN_FALLBACK");
}
assert_eq!(got_auth.borrow().as_deref(), Some("Bearer env-token-123"));
}
#[test]
fn token_from_env_does_not_overwrite_explicit() {
use std::rc::Rc;
#[derive(Clone)]
struct InspectClient {
response: String,
got_auth: Rc<std::cell::RefCell<Option<String>>>,
}
impl HttpClient for InspectClient {
fn get(&self, _url: &str, headers: &HeaderMap) -> releasekit::error::Result<Response> {
for (k, v) in headers.iter() {
if k == "Authorization" {
*self.got_auth.borrow_mut() = Some(v.to_string());
}
}
Ok(Response {
status: 200,
body: self.response.clone(),
})
}
}
let got_auth = Rc::new(std::cell::RefCell::new(None));
let client = InspectClient {
response: sample_releases_json().to_string(),
got_auth: got_auth.clone(),
};
unsafe {
std::env::set_var("RELEASEKIT_TEST_TOKEN_OVERRIDE", "should-not-appear");
}
let gh = GitHub::new(client)
.with_token("explicit-token")
.with_token_from_env(&["RELEASEKIT_TEST_TOKEN_OVERRIDE"]);
let _ = gh.fetch_releases("test/repo", None).unwrap();
unsafe {
std::env::remove_var("RELEASEKIT_TEST_TOKEN_OVERRIDE");
}
assert_eq!(got_auth.borrow().as_deref(), Some("Bearer explicit-token"));
}