ge_man_lib/download/
mod.rs

1//! Functionality relating to downloading GE release assets.
2//!
3//! This module interfaces with the GitHub API of the `proton-ge-custom` and `wine-ge-custom` repository. It provides:
4//! * A struct for downloading release assets from the above repositories
5//! * Structs containing the downloaded data
6use std::fmt::Display;
7use std::io::Read;
8
9use lazy_static::lazy_static;
10use reqwest::blocking::Response;
11
12use crate::download::github::{GithubDownload, GithubDownloader};
13use crate::download::response::{
14    CompatibilityToolTag, DownloadedArchive, DownloadedAssets, DownloadedChecksum, GeAsset, GeRelease,
15};
16use crate::error::GithubError;
17use crate::tag::{Tag, TagKind, WineTagKind};
18
19mod github;
20
21pub mod response;
22
23#[cfg(test)]
24mod mime {
25    pub const APPLICATION_GZIP: &str = "application/gzip";
26    pub const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
27    pub const BINARY_OCTET_STREAM: &str = "binary/octet-stream";
28}
29
30const GITHUB_API_URL: &str = "https://api.github.com";
31const PROTON_GE_RELEASE_LATEST_URL: &str = "repos/GloriousEggroll/proton-ge-custom/releases/latest";
32const PROTON_GE_RELEASE_TAGS_URL: &str = "repos/GloriousEggroll/proton-ge-custom/releases/tags";
33const WINE_GE_RELEASE_TAGS_URL: &str = "repos/GloriousEggroll/wine-ge-custom/releases/tags";
34const WINE_GE_TAGS_URL: &str = "repos/GloriousEggroll/wine-ge-custom/tags";
35
36lazy_static! {
37    static ref GITHUB_PROTON_GE_LATEST_URL: String = format!("{}/{}", GITHUB_API_URL, PROTON_GE_RELEASE_LATEST_URL);
38    static ref GITHUB_PROTON_GE_TAG_URL: String = format!("{}/{}", GITHUB_API_URL, PROTON_GE_RELEASE_TAGS_URL);
39    static ref GITHUB_WINE_GE_RELEASE_TAG_URL: String = format!("{}/{}", GITHUB_API_URL, WINE_GE_RELEASE_TAGS_URL);
40    static ref GITHUB_WINE_GE_TAGS_URL: String = format!("{}/{}", GITHUB_API_URL, WINE_GE_TAGS_URL);
41}
42
43/// Trait defining how to determine the progress for a `Read` type.
44///
45/// This trait helps with providing progress information when performing a download. Currently, this trait is
46/// mostly focused on providing a working implementation with the `indicatif` crate due to that crate being used in
47/// `ge_man` itself. Therefore, this trait might not work too well with other progress tracking crates.
48pub trait ReadProgressWrapper {
49    /// Tells the implementing struct how to construct itself. For example, `indicatif` requires a progress bar
50    /// struct to be created that performs the tracking of a `Read` type.
51    ///
52    /// # Examples
53    ///
54    /// ```ignore
55    /// use ge_man_lib::download::ReadProgressWrapper;
56    ///
57    /// fn init(self: Box<Self>, len: u64) -> Box<dyn ReadProgressWrapper> {
58    ///     let pb = ProgressBar::with_draw_target(len, ProgressDrawTarget::stdout())
59    ///     .with_style(style())
60    ///     .with_message("Downloading archive:");
61    ///
62    ///     Box::new(DownloadProgressTracker::new(pb))
63    /// }
64    /// ```
65    fn init(self: Box<Self>, len: u64, asset: &GeAsset) -> Box<dyn ReadProgressWrapper>;
66    /// Wrap the actual `Read` type, which is the resource to be downloaded, with a progress tracking reader.
67    fn wrap(&self, reader: Box<dyn Read>) -> Box<dyn Read>;
68    /// Define how the implementing struct should finish tracking progress.
69    fn finish(&self, release: &GeAsset);
70}
71
72/// Data required to perform a download requests against the GitHub API for a GE version.
73///
74/// The information in this struct is used to perform various requests against the GitHub API to download the assets
75/// of a GE version release.
76pub struct DownloadRequest {
77    /// The GitHub release to download. If the `tag` is `None` the latest version is assumed.
78    pub tag: Option<String>,
79    /// The GE version kind (GE Proton / Wine GE).
80    pub kind: TagKind,
81    /// Wrapper to track the download progress.
82    pub progress_wrapper: Box<dyn ReadProgressWrapper>,
83    /// Should the checksum file be downloaded.
84    pub download_checksum: bool,
85}
86
87impl DownloadRequest {
88    pub fn new(
89        tag: Option<String>,
90        kind: TagKind,
91        progress_wrapper: Box<dyn ReadProgressWrapper>,
92        download_checksum: bool,
93    ) -> Self {
94        DownloadRequest {
95            tag,
96            kind,
97            progress_wrapper,
98            download_checksum,
99        }
100    }
101}
102
103/// Trait defining methods for fetching release data.
104///
105/// This trait mostly exists for testing purposes so consuming crates can crate a mock from this trait.
106pub trait GeDownload {
107    fn fetch_release(&self, tag: Option<String>, kind: TagKind) -> Result<GeRelease, GithubError>;
108    fn download_release_assets(&self, request: DownloadRequest) -> Result<DownloadedAssets, GithubError>;
109}
110
111/// Default implementation for the `GeDownload` trait.
112pub struct GeDownloader {
113    github_downloader: Box<dyn GithubDownload>,
114}
115
116impl GeDownloader {
117    pub fn new(github_downloader: Box<dyn GithubDownload>) -> Self {
118        GeDownloader { github_downloader }
119    }
120
121    fn create_url<S: AsRef<str>>(&self, tag: Option<S>, kind: &TagKind) -> Result<String, GithubError>
122    where
123        S: AsRef<str> + Display,
124    {
125        let tag = tag.as_ref();
126        match kind {
127            TagKind::Proton => {
128                if let Some(t) = tag {
129                    Ok(format!("{}/{}", *GITHUB_PROTON_GE_TAG_URL, t))
130                } else {
131                    Ok(String::from(&*GITHUB_PROTON_GE_LATEST_URL))
132                }
133            }
134            TagKind::Wine { kind: wine_kind } => {
135                if let Some(t) = tag {
136                    Ok(format!("{}/{}", *GITHUB_WINE_GE_RELEASE_TAG_URL, t))
137                } else {
138                    self.find_latest_wine_ge_release_tag(wine_kind)
139                        .map(|t| format!("{}/{}", *GITHUB_WINE_GE_RELEASE_TAG_URL, t))
140                }
141            }
142        }
143    }
144
145    fn create_wine_ge_tags_url(&self, page: u8) -> String {
146        format!("{}?page={}", *GITHUB_WINE_GE_TAGS_URL, page)
147    }
148
149    fn find_latest_wine_ge_release_tag(&self, kind: &WineTagKind) -> Result<Tag, GithubError> {
150        let mut page = 1;
151        loop {
152            let mut tag_names: Vec<String> = self
153                .fetch_wine_ge_tags(page)?
154                .json::<Vec<CompatibilityToolTag>>()?
155                .into_iter()
156                .map(Into::into)
157                .collect();
158
159            if tag_names.is_empty() {
160                return Err(GithubError::NoTags);
161            }
162
163            if let WineTagKind::LolWineGe = kind {
164                tag_names.retain(|t| t.contains("LoL"));
165            } else {
166                tag_names.retain(|t| !t.contains("LoL"));
167            }
168
169            let latest_tag = tag_names.into_iter().map(|t| Tag::from(t)).max_by(Tag::cmp);
170            if let Some(t) = latest_tag {
171                return Ok(t);
172            }
173            page += 1
174        }
175    }
176
177    fn fetch_wine_ge_tags(&self, page: u8) -> Result<Response, GithubError> {
178        let url = self.create_wine_ge_tags_url(page);
179        self.github_downloader.download_from_url(&url)
180    }
181
182    fn download_archive(
183        &self,
184        progress_wrapper: Box<dyn ReadProgressWrapper>,
185        asset: &GeAsset,
186    ) -> Result<DownloadedArchive, GithubError> {
187        let response = self.github_downloader.download_from_url(&asset.browser_download_url)?;
188
189        let tar_size: u64 = response.content_length().unwrap();
190        let mut compressed_archive: Vec<u8> = Vec::with_capacity(tar_size as usize);
191
192        let progress_wrapper = progress_wrapper.init(tar_size, asset);
193        progress_wrapper
194            .wrap(Box::new(response))
195            .read_to_end(&mut compressed_archive)
196            .unwrap();
197        progress_wrapper.finish(asset);
198
199        Ok(DownloadedArchive::new(compressed_archive, String::from(&asset.name)))
200    }
201
202    fn download_checksum(&self, asset: &GeAsset) -> Result<DownloadedChecksum, GithubError> {
203        let mut response = self.github_downloader.download_from_url(&asset.browser_download_url)?;
204
205        let file_size = response.content_length().unwrap();
206        let mut checksum_str = String::with_capacity(file_size as usize);
207        response.read_to_string(&mut checksum_str).unwrap();
208
209        Ok(DownloadedChecksum::new(checksum_str, String::from(&asset.name)))
210    }
211}
212
213impl GeDownload for GeDownloader {
214    /// Get release information for a GitHub release.
215    ///
216    /// This method fetches information from a GE version GitHub release via the GitHub API.
217    ///
218    /// If the `tag` is a `None` this method will fetch the latest release.
219    ///
220    /// The relevant APIs for this method are:
221    /// * <https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases/tags>
222    /// * <https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases/latest>
223    /// * <https://api.github.com/repos/GloriousEggroll/wine-ge-custom/releases/tags>
224    /// * <https://api.github.com/repos/GloriousEggroll/wine-ge-custom/tags>
225    ///
226    /// # Errors
227    ///
228    /// This method returns an error in the following situations:
229    /// * The GitHub API returned no tags
230    /// * The GitHub API returned a not OK HTTP response
231    /// * A release was found but that release has no assets
232    /// * Reqwest could not fetch the resource from GitHub
233    /// * The API response could not be converted into a struct with serde
234    fn fetch_release(&self, tag: Option<String>, kind: TagKind) -> Result<GeRelease, GithubError> {
235        let tag = tag.as_ref();
236        let url = self.create_url(tag, &kind)?;
237        self.github_downloader.download_from_url(&url).and_then(|response| {
238            response
239                .json::<GeRelease>()
240                .map_err(|err| GithubError::ReqwestError { source: err })
241        })
242    }
243
244    /// Download the assets of a GE version release.
245    ///
246    /// # Errors
247    ///
248    /// This method will return an error in the following situations:
249    /// * The GitHub API returned no tags
250    /// * The GitHub API returned a not OK HTTP response
251    /// * A release was found but that release has no assets
252    /// * Reqwest could not fetch the resource from GitHub
253    /// * The API response could not be converted into a struct with serde
254    fn download_release_assets(&self, request: DownloadRequest) -> Result<DownloadedAssets, GithubError> {
255        let DownloadRequest {
256            tag,
257            kind,
258            progress_wrapper,
259            download_checksum: skip_checksum,
260        } = request;
261
262        let release = self.fetch_release(tag, kind)?;
263        if release.assets.is_empty() {
264            return Err(GithubError::ReleaseHasNoAssets {
265                tag: release.tag_name,
266                kind,
267            });
268        }
269
270        let downloaded_checksum = match skip_checksum {
271            false => Some(self.download_checksum(release.checksum_asset())?),
272            true => None,
273        };
274
275        let downloaded_archive = self.download_archive(progress_wrapper, release.tar_asset())?;
276
277        Ok(DownloadedAssets::new(
278            release.tag_name,
279            downloaded_archive,
280            downloaded_checksum,
281        ))
282    }
283}
284
285impl Default for GeDownloader {
286    fn default() -> Self {
287        let github_downloader = Box::new(GithubDownloader::new());
288        GeDownloader::new(github_downloader)
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use crate::download::mime::{APPLICATION_GZIP, APPLICATION_OCTET_STREAM};
295    use httpmock::Method::GET;
296    use httpmock::MockServer;
297    use mockall::mock;
298    use reqwest::blocking::Response;
299
300    use super::*;
301
302    lazy_static! {
303        static ref RELEASES: &'static str = "test_resources/responses/releases";
304        static ref ASSETS: &'static str = "test_resources/assets";
305        static ref TAGS: &'static str = "test_resources/responses/tags";
306        pub static ref PROTON_GE: String = format!("{}/proton-ge-release.json", *RELEASES);
307        pub static ref WINE_GE: String = format!("{}/wine-ge-release.json", *RELEASES);
308        pub static ref WINE_GE_LOL: String = format!("{}/wine-ge-lol-release.json", *RELEASES);
309        pub static ref NO_TAGS: String = format!("{}/empty.json", *TAGS);
310        pub static ref WINE_GE_TAGS: String = format!("{}/wine_ge.json", *TAGS);
311        pub static ref WINE_GE_LOL_TAGS: String = format!("{}/wine_ge_lol.json", *TAGS);
312        pub static ref TEST_TAR_GZ: String = format!("{}/{}", *ASSETS, "test.tar.gz");
313        pub static ref TEST_SHA512SUM: String = format!("{}/{}", *ASSETS, "test-gz.sha512sum");
314    }
315
316    mock! {
317        ProgressWrapper {}
318        impl ReadProgressWrapper for ProgressWrapper {
319            fn init(self: Box<Self>, len: u64, asset: &GeAsset) -> Box<dyn ReadProgressWrapper>;
320            fn wrap(&self, reader: Box<dyn Read>) -> Box<dyn Read>;
321            fn finish(&self, release: &GeAsset);
322        }
323    }
324
325    pub fn download_url(server: Option<&str>, tag: &str, kind: &TagKind, file_name: &str) -> String {
326        let url = download_url_without_server(tag, kind, file_name);
327
328        if let Some(host) = server {
329            format!("{}{}", host, url)
330        } else {
331            format!("SERVER{}", url)
332        }
333    }
334
335    pub fn download_url_without_server(tag: &str, kind: &TagKind, file_name: &str) -> String {
336        let repo = match kind {
337            TagKind::Proton => "proton",
338            TagKind::Wine { .. } => "wine",
339        };
340        format!(
341            "/GloriousEggroll/{}-ge-custom/releases/download/{}/{}",
342            repo, tag, file_name
343        )
344    }
345
346    pub fn mock_url(kind: &TagKind, server: &str) -> String {
347        let file_path = match kind {
348            TagKind::Proton => &*PROTON_GE,
349            TagKind::Wine { kind: wine_kind } => match wine_kind {
350                WineTagKind::WineGe => &*WINE_GE,
351                WineTagKind::LolWineGe => &*WINE_GE_LOL,
352            },
353        };
354        std::fs::read_to_string(file_path).unwrap().replace("SERVER", server)
355    }
356
357    struct FetchSpecifiedReleaseTestData {
358        given_tag: Option<String>,
359        expected_tag: String,
360        kind: TagKind,
361        github_resource: String,
362        body_content_file: String,
363        gzip_name: String,
364        gzip_download_url: String,
365        checksum_name: String,
366        checksum_download_url: String,
367    }
368
369    impl FetchSpecifiedReleaseTestData {
370        pub fn new(
371            tag: Option<&str>,
372            expected_tag: &str,
373            kind: TagKind,
374            gzip_name: &str,
375            checksum_name: &str,
376            github_resource: &str,
377            body_content_file: &str,
378        ) -> Self {
379            let github_resource = if tag.is_some() {
380                format!("{}/{}", github_resource, tag.unwrap())
381            } else {
382                String::from(github_resource)
383            };
384            let tag = tag.map(String::from);
385
386            let gzip_download_url = download_url(None, expected_tag, &kind, gzip_name);
387            let checksum_download_url = download_url(None, expected_tag, &kind, checksum_name);
388
389            let expected_tag = String::from(expected_tag);
390            let body_content_file = String::from(body_content_file);
391            let gzip_name = String::from(gzip_name);
392            let checksum_name = String::from(checksum_name);
393            FetchSpecifiedReleaseTestData {
394                given_tag: tag,
395                expected_tag,
396                kind,
397                github_resource,
398                body_content_file,
399                gzip_name,
400                gzip_download_url,
401                checksum_name,
402                checksum_download_url,
403            }
404        }
405    }
406
407    struct FetchLatestReleaseTestData {
408        tags_body_from_file: String,
409        github_tags_resource: String,
410        expected_specific_release: FetchSpecifiedReleaseTestData,
411    }
412
413    impl FetchLatestReleaseTestData {
414        pub fn new<S: Into<String>>(
415            tags_body_from_file: S,
416            github_tags_resource: S,
417            mut expected_specific_release: FetchSpecifiedReleaseTestData,
418        ) -> Self {
419            let tags_body_from_file = tags_body_from_file.into();
420            let github_tags_resource = github_tags_resource.into();
421            expected_specific_release.github_resource = format!(
422                "{}/{}",
423                expected_specific_release.github_resource, expected_specific_release.expected_tag
424            );
425
426            FetchLatestReleaseTestData {
427                tags_body_from_file,
428                github_tags_resource,
429                expected_specific_release,
430            }
431        }
432    }
433
434    struct FetchReleaseContentTestData {
435        expected_tag: String,
436        kind: TagKind,
437        compressed_tar_file_name: String,
438        checksum_file_name: String,
439        release_url: String,
440    }
441
442    impl FetchReleaseContentTestData {
443        pub fn new<S: Into<String>>(
444            tag: S,
445            kind: &TagKind,
446            compressed_tar_file_name: S,
447            checksum_file_name: S,
448            release_url: S,
449        ) -> Self {
450            let expected_tag = tag.into();
451            let kind = kind.clone();
452            let compressed_tar_file_name = compressed_tar_file_name.into();
453            let checksum_file_name = checksum_file_name.into();
454            let release_url = release_url.into();
455
456            FetchReleaseContentTestData {
457                expected_tag,
458                kind,
459                compressed_tar_file_name,
460                checksum_file_name,
461                release_url,
462            }
463        }
464    }
465
466    struct MockGithubDownloader {
467        host: String,
468    }
469
470    impl MockGithubDownloader {
471        pub fn new<S: Into<String>>(host: S) -> Self {
472            MockGithubDownloader { host: host.into() }
473        }
474    }
475
476    impl GithubDownload for MockGithubDownloader {
477        fn download_from_url(&self, url: &str) -> Result<Response, GithubError> {
478            let find_index = match url.find("repos") {
479                Some(i) => i,
480                None => url.find("G").unwrap(),
481            };
482
483            let target = url.split_at(find_index).1;
484            let mocked_url = format!("{}/{}", self.host, target);
485
486            match reqwest::blocking::get(&mocked_url) {
487                Ok(resp) => Ok(resp),
488                Err(err) => panic!("Get request failed during integration test: {:?}", err),
489            }
490        }
491    }
492
493    fn fetch_release_test(test_data: FetchSpecifiedReleaseTestData) {
494        let server = MockServer::start();
495
496        let mock = server.mock(|when, then| {
497            when.method(GET).path(format!("/{}", &test_data.github_resource));
498            then.status(200)
499                .header("Content-Type", "application/json")
500                .body_from_file(&test_data.body_content_file);
501        });
502
503        let github_downloader = Box::new(MockGithubDownloader::new(String::from(server.base_url())));
504        let tool_downloader = GeDownloader::new(github_downloader);
505
506        let release = tool_downloader
507            .fetch_release(test_data.given_tag, test_data.kind)
508            .unwrap();
509        let gzip = release.tar_asset();
510        let checksum = release.checksum_asset();
511        assert_eq!(release.tag_name, test_data.expected_tag);
512        assert_eq!(gzip.name, test_data.gzip_name);
513        assert_eq!(gzip.browser_download_url, test_data.gzip_download_url);
514        assert_eq!(gzip.content_type, APPLICATION_GZIP);
515        assert_eq!(checksum.name, test_data.checksum_name);
516        assert_eq!(checksum.browser_download_url, test_data.checksum_download_url);
517        assert_eq!(checksum.content_type, APPLICATION_OCTET_STREAM);
518
519        mock.assert();
520    }
521
522    fn fetch_latest_wine_or_lol_release_test(test_data: FetchLatestReleaseTestData) {
523        let server = MockServer::start();
524        let FetchLatestReleaseTestData {
525            expected_specific_release: test_data,
526            tags_body_from_file,
527            github_tags_resource,
528        } = test_data;
529
530        let tags_mock = server.mock(|when, then| {
531            when.method(GET)
532                .path(format!("/{}", github_tags_resource))
533                .query_param("page", "1");
534            then.status(200)
535                .header("Content-Type", "application/json")
536                .body_from_file(tags_body_from_file);
537        });
538
539        let release_mock = server.mock(|when, then| {
540            when.method(GET).path(format!("/{}", &test_data.github_resource));
541            then.status(200)
542                .header("Content-Type", "application/json")
543                .body_from_file(&test_data.body_content_file);
544        });
545
546        let github_downloader = Box::new(MockGithubDownloader::new(String::from(server.base_url())));
547        let tool_downloader = GeDownloader::new(github_downloader);
548
549        let release = tool_downloader
550            .fetch_release(test_data.given_tag, test_data.kind)
551            .unwrap();
552        let gzip = release.tar_asset();
553        let checksum = release.checksum_asset();
554
555        release_mock.assert();
556        tags_mock.assert();
557
558        assert_eq!(release.tag_name, test_data.expected_tag);
559        assert_eq!(gzip.name, test_data.gzip_name);
560        assert_eq!(gzip.browser_download_url, test_data.gzip_download_url);
561        assert_eq!(gzip.content_type, APPLICATION_GZIP);
562        assert_eq!(checksum.name, test_data.checksum_name);
563        assert_eq!(checksum.browser_download_url, test_data.checksum_download_url);
564        assert_eq!(checksum.content_type, APPLICATION_OCTET_STREAM);
565    }
566
567    #[test]
568    fn fetch_proton_ge_release() {
569        let expected_tag = "6.20-GE-1";
570        let given_tag = Some(expected_tag);
571        let gzip = "Proton-6.20-GE-1.tar.gz";
572        let checksum = "Proton-6.20-GE-1.sha512sum";
573        fetch_release_test(FetchSpecifiedReleaseTestData::new(
574            given_tag,
575            expected_tag,
576            TagKind::Proton,
577            gzip,
578            checksum,
579            PROTON_GE_RELEASE_TAGS_URL,
580            &*PROTON_GE,
581        ));
582    }
583
584    #[test]
585    fn fetch_wine_ge_release() {
586        let expected_tag = "6.20-GE-1";
587        let given_tag = Some(expected_tag);
588        let gzip = "wine-lutris-ge-6.20-1-x86_64.tar.gz";
589        let checksum = "wine-lutris-ge-6.20-1-x86_64.sha512sum";
590        fetch_release_test(FetchSpecifiedReleaseTestData::new(
591            given_tag,
592            expected_tag,
593            TagKind::wine(),
594            gzip,
595            checksum,
596            WINE_GE_RELEASE_TAGS_URL,
597            &*WINE_GE,
598        ));
599    }
600
601    #[test]
602    fn fetch_wine_lol_ge_release() {
603        let expected_tag = "6.16-GE-3-LoL";
604        let given_tag = Some(expected_tag);
605        let gzip = "wine-lutris-ge-6.16-3-lol-x86_64.tar.gz";
606        let checksum = "wine-lutris-ge-6.16-3-lol-x86_64.sha512sum";
607        fetch_release_test(FetchSpecifiedReleaseTestData::new(
608            given_tag,
609            expected_tag,
610            TagKind::lol(),
611            gzip,
612            checksum,
613            WINE_GE_RELEASE_TAGS_URL,
614            &*WINE_GE_LOL,
615        ));
616    }
617
618    #[test]
619    fn fetch_latest_proton_release() {
620        let expected_tag = "6.20-GE-1";
621        let given_tag = None;
622        let gzip = "Proton-6.20-GE-1.tar.gz";
623        let checksum = "Proton-6.20-GE-1.sha512sum";
624        fetch_release_test(FetchSpecifiedReleaseTestData::new(
625            given_tag,
626            expected_tag,
627            TagKind::Proton,
628            gzip,
629            checksum,
630            PROTON_GE_RELEASE_LATEST_URL,
631            &*PROTON_GE,
632        ));
633    }
634
635    #[test]
636    fn fetch_latest_wine_ge_release() {
637        let expected_tag = "6.20-GE-1";
638        let given_tag = None;
639        let gzip = "wine-lutris-ge-6.20-1-x86_64.tar.gz";
640        let checksum = "wine-lutris-ge-6.20-1-x86_64.sha512sum";
641        let expected_data = FetchSpecifiedReleaseTestData::new(
642            given_tag,
643            expected_tag,
644            TagKind::wine(),
645            gzip,
646            checksum,
647            WINE_GE_RELEASE_TAGS_URL,
648            &*WINE_GE,
649        );
650
651        fetch_latest_wine_or_lol_release_test(FetchLatestReleaseTestData::new(
652            &*WINE_GE_TAGS,
653            &WINE_GE_TAGS_URL.to_owned(),
654            expected_data,
655        ));
656    }
657
658    #[test]
659    fn fetch_latest_wine_ge_lol_release() {
660        let expected_tag = "6.16-GE-3-LoL";
661        let given_tag = None;
662        let gzip = "wine-lutris-ge-6.16-3-lol-x86_64.tar.gz";
663        let checksum = "wine-lutris-ge-6.16-3-lol-x86_64.sha512sum";
664        let expected_data = FetchSpecifiedReleaseTestData::new(
665            given_tag,
666            expected_tag,
667            TagKind::lol(),
668            gzip,
669            checksum,
670            WINE_GE_RELEASE_TAGS_URL,
671            &*WINE_GE_LOL,
672        );
673        fetch_latest_wine_or_lol_release_test(FetchLatestReleaseTestData::new(
674            &*WINE_GE_LOL_TAGS,
675            &WINE_GE_TAGS_URL.to_owned(),
676            expected_data,
677        ));
678    }
679
680    #[test]
681    fn fetch_latest_wine_ge_lol_version_from_second_tags_page() {
682        let tag = "6.16-GE-3-LoL";
683        let kind = TagKind::lol();
684        let gzip_file_name = "wine-lutris-ge-6.16-3-lol-x86_64.tar.gz";
685        let checksum_file_name = "wine-lutris-ge-6.16-3-lol-x86_64.sha512sum";
686
687        let server = MockServer::start();
688
689        let first_page_tags = server.mock(|when, then| {
690            when.method(GET)
691                .path(format!("/{}", WINE_GE_TAGS_URL))
692                .query_param("page", "1");
693            then.status(200)
694                .header("Content-Type", "application/json")
695                .body_from_file(&*WINE_GE_TAGS);
696        });
697
698        let second_page_tags = server.mock(|when, then| {
699            when.method(GET)
700                .path(format!("/{}", WINE_GE_TAGS_URL))
701                .query_param("page", "2");
702            then.status(200)
703                .header("Content-Type", "application/json")
704                .body_from_file(&*WINE_GE_LOL_TAGS);
705        });
706
707        let release_mock = server.mock(|when, then| {
708            when.method(GET).path(format!("/{}/{}", WINE_GE_RELEASE_TAGS_URL, tag));
709            then.status(200)
710                .header("Content-Type", "application/json")
711                .body(mock_url(&kind, &server.base_url()));
712        });
713
714        let github_downloader = Box::new(MockGithubDownloader::new(String::from(server.base_url())));
715        let tool_downloader = GeDownloader::new(github_downloader);
716
717        let release = tool_downloader.fetch_release(None, TagKind::lol()).unwrap();
718        let gzip = release.tar_asset();
719        let checksum = release.checksum_asset();
720
721        first_page_tags.assert();
722        second_page_tags.assert();
723        release_mock.assert();
724
725        assert_eq!(release.tag_name, tag);
726        assert_eq!(gzip.name, gzip_file_name);
727        assert_eq!(
728            gzip.browser_download_url,
729            download_url(Some(&server.base_url()), tag, &kind, gzip_file_name)
730        );
731        assert_eq!(gzip.content_type, APPLICATION_GZIP);
732        assert_eq!(checksum.name, checksum_file_name);
733        assert_eq!(
734            checksum.browser_download_url,
735            download_url(Some(&server.base_url()), tag, &kind, checksum_file_name)
736        );
737        assert_eq!(checksum.content_type, APPLICATION_OCTET_STREAM);
738    }
739
740    #[test]
741    fn fetch_latest_version_but_could_not_find_latest_tag() {
742        let server = MockServer::start();
743
744        let first_page_tags = server.mock(|when, then| {
745            when.method(GET)
746                .path(format!("/{}", WINE_GE_TAGS_URL))
747                .query_param("page", "1");
748            then.status(200)
749                .header("Content-Type", "application/json")
750                .body_from_file(&*WINE_GE_TAGS);
751        });
752
753        let second_page_tags = server.mock(|when, then| {
754            when.method(GET)
755                .path(format!("/{}", WINE_GE_TAGS_URL))
756                .query_param("page", "2");
757            then.status(200)
758                .header("Content-Type", "application/json")
759                .body_from_file(&*NO_TAGS);
760        });
761
762        let github_downloader = Box::new(MockGithubDownloader::new(String::from(server.base_url())));
763        let tool_downloader = GeDownloader::new(github_downloader);
764
765        let release = tool_downloader.fetch_release(None, TagKind::lol());
766        assert!(release.is_err());
767        let err = release.err().unwrap();
768        assert!(
769            matches!(err, GithubError::NoTags),
770            "Result contains unexpected error: {:?}",
771            err
772        );
773
774        first_page_tags.assert();
775        second_page_tags.assert();
776    }
777
778    fn fetch_release_content_test(test_data: FetchReleaseContentTestData) {
779        let FetchReleaseContentTestData {
780            expected_tag,
781            kind,
782            compressed_tar_file_name: gzip_file_name,
783            checksum_file_name,
784            release_url,
785        } = test_data;
786
787        let server = MockServer::start();
788
789        let release_mock = server.mock(|when, then| {
790            when.method(GET).path(format!("/{}/{}", release_url, expected_tag));
791            then.status(200)
792                .header("Content-Type", "application/json")
793                .body(mock_url(&kind, &server.base_url()));
794        });
795
796        let gzip_asset = server.mock(|when, then| {
797            when.method(GET)
798                .path(download_url_without_server(&expected_tag, &kind, &gzip_file_name));
799            then.status(200)
800                .header("Content-Type", "application/gzip")
801                .body_from_file(&*TEST_TAR_GZ);
802        });
803
804        let checksum_asset = server.mock(|when, then| {
805            when.method(GET)
806                .path(download_url_without_server(&expected_tag, &kind, &checksum_file_name));
807            then.status(200)
808                .header("Content-Type", "application/octet-stream")
809                .body_from_file(&*TEST_SHA512SUM);
810        });
811
812        // TODO: Assertions could be improved here, however, that will require changes to the test structure. In
813        //  general some things (APPLICATION_XZ) have changed since these tests were written.
814        let mut progress_wrapper = MockProgressWrapper::new();
815        progress_wrapper.expect_finish().never();
816        progress_wrapper.expect_wrap().never();
817        progress_wrapper.expect_init().once().returning(|_, _| {
818            let mut initialized_prog_wrapper = MockProgressWrapper::new();
819            initialized_prog_wrapper.expect_init().never();
820            initialized_prog_wrapper.expect_wrap().once().returning(|reader| reader);
821            initialized_prog_wrapper.expect_finish().once().returning(|_| ());
822
823            Box::new(initialized_prog_wrapper)
824        });
825
826        let github_downloader = Box::new(MockGithubDownloader::new(String::from(server.base_url())));
827        let tool_downloader = GeDownloader::new(github_downloader);
828
829        let request = DownloadRequest::new(Some(expected_tag), kind, Box::new(progress_wrapper), false);
830        let fetched_assets = tool_downloader.download_release_assets(request).unwrap();
831
832        release_mock.assert();
833        gzip_asset.assert();
834        checksum_asset.assert();
835
836        let expected_gzip_content = std::fs::read(&*TEST_TAR_GZ).unwrap();
837        let expected_checksum_content = std::fs::read_to_string(&*TEST_SHA512SUM).unwrap();
838
839        let downloaded_tar = fetched_assets.compressed_archive;
840        let downloaded_checksum = fetched_assets.checksum.unwrap();
841
842        assert_eq!(downloaded_tar.compressed_content, expected_gzip_content);
843        assert_eq!(downloaded_tar.file_name, gzip_file_name);
844        assert_eq!(downloaded_checksum.checksum, expected_checksum_content);
845        assert_eq!(downloaded_checksum.file_name, checksum_file_name);
846    }
847
848    #[test]
849    fn fetch_release_content_should_download_data_for_wine_ge() {
850        let expected_tag = "6.16-GE-3-LoL";
851        let kind = TagKind::lol();
852        let gzip_file_name = "wine-lutris-ge-6.16-3-lol-x86_64.tar.gz";
853        let checksum_file_name = "wine-lutris-ge-6.16-3-lol-x86_64.sha512sum";
854
855        let data = FetchReleaseContentTestData::new(
856            expected_tag,
857            &kind,
858            gzip_file_name,
859            checksum_file_name,
860            WINE_GE_RELEASE_TAGS_URL,
861        );
862        fetch_release_content_test(data);
863    }
864
865    #[test]
866    fn fetch_release_content_should_download_data_for_proton_ge() {
867        let expected_tag = "6.20-GE-1";
868        let kind = TagKind::Proton;
869        let gzip_file_name = "Proton-6.20-GE-1.tar.gz";
870        let checksum_file_name = "Proton-6.20-GE-1.sha512sum";
871
872        let data = FetchReleaseContentTestData::new(
873            expected_tag,
874            &kind,
875            gzip_file_name,
876            checksum_file_name,
877            PROTON_GE_RELEASE_TAGS_URL,
878        );
879        fetch_release_content_test(data);
880    }
881}