releasekit 0.1.0

Client-agnostic library for fetching releases from git forges
Documentation
use releasekit::Forge;
use releasekit::client::{HeaderMap, HttpClient, Response};
use releasekit::platform::Gitea;

#[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#"[
        {
            "id": 1,
            "tag_name": "v1.0.0",
            "name": "Version 1.0",
            "prerelease": false,
            "published_at": "2025-01-15T10:30:00Z",
            "body": "First stable release",
            "assets": [
                {
                    "id": 1,
                    "name": "app-x86_64.AppImage",
                    "size": 5242880,
                    "browser_download_url": "https://codeberg.org/owner/repo/releases/download/v1.0.0/app-x86_64.AppImage"
                },
                {
                    "id": 2,
                    "name": "app-x86_64.tar.gz",
                    "size": 3145728,
                    "browser_download_url": "https://codeberg.org/owner/repo/releases/download/v1.0.0/app-x86_64.tar.gz"
                }
            ]
        },
        {
            "id": 2,
            "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#"{
        "id": 3,
        "tag_name": "v2.0.0",
        "name": "Version 2.0",
        "prerelease": false,
        "published_at": "2025-06-01T12:00:00Z",
        "body": "Major update",
        "assets": [
            {
                "id": 3,
                "name": "app.tar.gz",
                "size": 1024,
                "browser_download_url": "https://codeberg.org/owner/repo/releases/download/v2.0.0/app.tar.gz"
            }
        ]
    }"#
}

#[test]
fn parses_multiple_releases() {
    let client = MockClient {
        response: sample_releases_json().to_string(),
    };
    let gt = Gitea::new(client, "https://codeberg.org");
    let releases = gt.fetch_releases("owner/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://codeberg.org/owner/repo/releases/download/v1.0.0/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 gt = Gitea::new(client, "https://codeberg.org");
    let releases = gt.fetch_releases("owner/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 gt = Gitea::new(client, "https://codeberg.org").with_token("test-token-123");
    let _ = gt.fetch_releases("owner/repo", None).unwrap();

    assert_eq!(got_auth.borrow().as_deref(), Some("token test-token-123"));
}

#[test]
fn uses_correct_api_url() {
    use std::rc::Rc;

    #[derive(Clone)]
    struct InspectClient {
        response: String,
        got_url: Rc<std::cell::RefCell<Option<String>>>,
    }

    impl HttpClient for InspectClient {
        fn get(&self, url: &str, _headers: &HeaderMap) -> releasekit::error::Result<Response> {
            *self.got_url.borrow_mut() = Some(url.to_string());
            Ok(Response {
                status: 200,
                body: self.response.clone(),
            })
        }
    }

    let got_url = Rc::new(std::cell::RefCell::new(None));
    let client = InspectClient {
        response: sample_releases_json().to_string(),
        got_url: got_url.clone(),
    };
    let gt = Gitea::new(client, "https://codeberg.org");
    let _ = gt.fetch_releases("owner/repo", None).unwrap();

    assert_eq!(
        got_url.borrow().as_deref(),
        Some("https://codeberg.org/api/v1/repos/owner/repo/releases?limit=50")
    );
}

#[test]
fn custom_gitea_instance() {
    use std::rc::Rc;

    #[derive(Clone)]
    struct InspectClient {
        response: String,
        got_url: Rc<std::cell::RefCell<Option<String>>>,
    }

    impl HttpClient for InspectClient {
        fn get(&self, url: &str, _headers: &HeaderMap) -> releasekit::error::Result<Response> {
            *self.got_url.borrow_mut() = Some(url.to_string());
            Ok(Response {
                status: 200,
                body: self.response.clone(),
            })
        }
    }

    let got_url = Rc::new(std::cell::RefCell::new(None));
    let client = InspectClient {
        response: sample_single_release_json().to_string(),
        got_url: got_url.clone(),
    };
    let gt = Gitea::new(client, "https://gitea.example.com");
    let _ = gt.fetch_releases("owner/repo", Some("v2.0.0")).unwrap();

    assert_eq!(
        got_url.borrow().as_deref(),
        Some("https://gitea.example.com/api/v1/repos/owner/repo/releases/tags/v2.0.0")
    );
}