wasmer_wasix/runtime/resolver/
backend_source.rs

1use std::{
2    path::PathBuf,
3    sync::Arc,
4    time::{Duration, SystemTime},
5};
6
7use anyhow::{Context, Error};
8use http::{HeaderMap, Method};
9use semver::{Version, VersionReq};
10use url::Url;
11use wasmer_config::package::{NamedPackageId, PackageHash, PackageId, PackageIdent, PackageSource};
12use webc::metadata::Manifest;
13
14use crate::{
15    http::{HttpClient, HttpRequest, USER_AGENT},
16    runtime::resolver::{
17        DistributionInfo, PackageInfo, PackageSummary, QueryError, Source, WebcHash,
18    },
19};
20
21/// A [`Source`] which will resolve dependencies by pinging a Wasmer-like GraphQL
22/// endpoint.
23#[derive(Debug, Clone)]
24pub struct BackendSource {
25    registry_endpoint: Url,
26    client: Arc<dyn HttpClient + Send + Sync>,
27    cache: Option<FileSystemCache>,
28    token: Option<String>,
29    preferred_webc_version: webc::Version,
30}
31
32impl BackendSource {
33    pub const WASMER_DEV_ENDPOINT: &'static str = "https://registry.wasmer.wtf/graphql";
34    pub const WASMER_PROD_ENDPOINT: &'static str = "https://registry.wasmer.io/graphql";
35
36    pub fn new(registry_endpoint: Url, client: Arc<dyn HttpClient + Send + Sync>) -> Self {
37        BackendSource {
38            registry_endpoint,
39            client,
40            cache: None,
41            token: None,
42            preferred_webc_version: webc::Version::V3,
43        }
44    }
45
46    /// Cache query results locally.
47    pub fn with_local_cache(self, cache_dir: impl Into<PathBuf>, timeout: Duration) -> Self {
48        BackendSource {
49            cache: Some(FileSystemCache::new(cache_dir, timeout)),
50            ..self
51        }
52    }
53
54    pub fn with_auth_token(self, token: impl Into<String>) -> Self {
55        BackendSource {
56            token: Some(token.into()),
57            ..self
58        }
59    }
60
61    pub fn with_preferred_webc_version(self, version: webc::Version) -> Self {
62        BackendSource {
63            preferred_webc_version: version,
64            ..self
65        }
66    }
67
68    pub fn registry_endpoint(&self) -> &Url {
69        &self.registry_endpoint
70    }
71
72    #[tracing::instrument(level = "debug", skip_all)]
73    async fn query_graphql_named(&self, package_name: &str) -> Result<WebQuery, Error> {
74        #[derive(serde::Serialize)]
75        struct Body {
76            query: String,
77        }
78
79        let body = Body {
80            query: WASMER_WEBC_QUERY_ALL.replace("$NAME", package_name),
81        };
82
83        let request = HttpRequest {
84            url: self.registry_endpoint.clone(),
85            method: Method::POST,
86            body: Some(serde_json::to_string(&body)?.into_bytes()),
87            headers: self.headers(),
88            options: Default::default(),
89        };
90
91        tracing::debug!(%request.url, %request.method, "Querying the GraphQL API");
92        tracing::trace!(?request.headers, request.body=body.query.as_str());
93
94        let response = self.client.request(request).await?;
95
96        if !response.is_ok() {
97            let url = &self.registry_endpoint;
98            let status = response.status;
99
100            let body = if let Some(body) = &response.body {
101                String::from_utf8_lossy(body).into_owned()
102            } else {
103                "<no body>".to_string()
104            };
105
106            tracing::warn!(
107                %url,
108                %status,
109                package=%package_name,
110                %body,
111                "failed to query package info from registry"
112            );
113
114            anyhow::bail!("\"{url}\" replied with {status}");
115        }
116
117        let body = response.body.unwrap_or_default();
118        tracing::trace!(
119            %response.status,
120            %response.redirected,
121            ?response.headers,
122            "Received a response from GraphQL",
123        );
124
125        let response: WebQuery =
126            serde_json::from_slice(&body).context("Unable to deserialize the response")?;
127
128        Ok(response)
129    }
130
131    #[tracing::instrument(level = "debug", skip_all)]
132    async fn query_graphql_by_hash(
133        &self,
134        hash: &PackageHash,
135    ) -> Result<Option<PackageWebc>, Error> {
136        #[derive(serde::Serialize)]
137        struct Body {
138            query: String,
139        }
140
141        let body = Body {
142            query: WASMER_WEBC_QUERY_BY_HASH.replace("$HASH", &hash.to_string()),
143        };
144
145        let request = HttpRequest {
146            url: self.registry_endpoint.clone(),
147            method: Method::POST,
148            body: Some(serde_json::to_string(&body)?.into_bytes()),
149            headers: self.headers(),
150            options: Default::default(),
151        };
152
153        tracing::debug!(%request.url, %request.method, "Querying the GraphQL API");
154        tracing::trace!(?request.headers, request.body=body.query.as_str());
155
156        let response = self.client.request(request).await?;
157
158        if !response.is_ok() {
159            let url = &self.registry_endpoint;
160            let status = response.status;
161
162            let body = if let Some(body) = &response.body {
163                String::from_utf8_lossy(body).into_owned()
164            } else {
165                "<no body>".to_string()
166            };
167
168            tracing::warn!(
169                %url,
170                %status,
171                %hash,
172                %body,
173                "failed to query package info from registry"
174            );
175
176            anyhow::bail!("\"{url}\" replied with {status}");
177        }
178
179        let body = response.body.unwrap_or_default();
180        tracing::trace!(
181            %response.status,
182            %response.redirected,
183            ?response.headers,
184            "Received a response from GraphQL",
185        );
186
187        let response: Reply<GetPackageRelease> =
188            serde_json::from_slice(&body).context("Unable to deserialize the response")?;
189
190        Ok(response.data.get_package_release)
191    }
192
193    fn headers(&self) -> HeaderMap {
194        let mut headers = HeaderMap::new();
195        headers.insert("Content-Type", "application/json".parse().unwrap());
196        headers.insert("User-Agent", USER_AGENT.parse().unwrap());
197
198        if let Some(token) = self.token.as_deref() {
199            let raw_header = format!("Bearer {token}");
200
201            match http::HeaderValue::from_str(&raw_header) {
202                Ok(header) => {
203                    headers.insert(http::header::AUTHORIZATION, header);
204                }
205                Err(e) => {
206                    tracing::warn!(
207                        error = &e as &dyn std::error::Error,
208                        "Unable to parse the token into a header",
209                    );
210                }
211            }
212        }
213
214        headers
215    }
216
217    async fn query_by_hash(
218        &self,
219        hash: &PackageHash,
220    ) -> Result<Option<PackageSummary>, anyhow::Error> {
221        // FIXME: implementing caching!
222
223        let Some(data) = self.query_graphql_by_hash(hash).await? else {
224            return Ok(None);
225        };
226
227        let summary = data.try_into_summary(hash.clone())?;
228
229        Ok(Some(summary))
230    }
231}
232
233#[async_trait::async_trait]
234impl Source for BackendSource {
235    #[tracing::instrument(level = "debug", skip_all, fields(%package))]
236    async fn query(&self, package: &PackageSource) -> Result<Vec<PackageSummary>, QueryError> {
237        let (package_name, version_constraint) = match package {
238            PackageSource::Ident(PackageIdent::Named(n)) => (
239                n.full_name(),
240                n.version_opt().cloned().unwrap_or(semver::VersionReq::STAR),
241            ),
242            PackageSource::Ident(PackageIdent::Hash(hash)) => {
243                // TODO: implement caching!
244                match self.query_by_hash(hash).await {
245                    Ok(Some(summary)) => return Ok(vec![summary]),
246                    Ok(None) => {
247                        return Err(QueryError::NoMatches {
248                            query: package.clone(),
249                            archived_versions: Vec::new(),
250                        });
251                    }
252                    Err(error) => {
253                        return Err(QueryError::new_other(error, package));
254                    }
255                }
256            }
257            _ => {
258                return Err(QueryError::Unsupported {
259                    query: package.clone(),
260                });
261            }
262        };
263
264        if let Some(cache) = &self.cache {
265            match cache.lookup_cached_query(&package_name) {
266                Ok(Some(cached)) => {
267                    if let Ok(cached) = matching_package_summaries(
268                        package,
269                        cached,
270                        &version_constraint,
271                        self.preferred_webc_version,
272                    ) {
273                        tracing::debug!("Cache hit!");
274                        return Ok(cached);
275                    }
276                }
277                Ok(None) => {}
278                Err(e) => {
279                    tracing::warn!(
280                        package_name,
281                        error = &*e,
282                        "An unexpected error occurred while checking the local query cache",
283                    );
284                }
285            }
286        }
287
288        let response = self
289            .query_graphql_named(&package_name)
290            .await
291            .map_err(|error| QueryError::new_other(error, package))?;
292
293        if let Some(cache) = &self.cache {
294            if let Err(e) = cache.update(&package_name, &response) {
295                tracing::warn!(
296                    package_name,
297                    error = &*e,
298                    "An error occurred while caching the GraphQL response",
299                );
300            }
301        }
302
303        matching_package_summaries(
304            package,
305            response,
306            &version_constraint,
307            self.preferred_webc_version,
308        )
309    }
310}
311
312#[allow(clippy::result_large_err)]
313fn matching_package_summaries(
314    query: &PackageSource,
315    response: WebQuery,
316    version_constraint: &VersionReq,
317    preferred_webc_version: webc::Version,
318) -> Result<Vec<PackageSummary>, QueryError> {
319    let mut summaries = Vec::new();
320
321    let WebQueryGetPackage {
322        namespace,
323        package_name,
324        versions,
325        ..
326    } = response
327        .data
328        .get_package
329        .ok_or_else(|| QueryError::NotFound {
330            query: query.clone(),
331        })?;
332    let mut archived_versions = Vec::new();
333
334    for pkg_version in versions {
335        let version = match Version::parse(&pkg_version.version) {
336            Ok(v) => v,
337            Err(e) => {
338                tracing::debug!(
339                    pkg.version = pkg_version.version.as_str(),
340                    error = &e as &dyn std::error::Error,
341                    "Skipping a version because it doesn't have a valid version number",
342                );
343                continue;
344            }
345        };
346
347        if pkg_version.is_archived {
348            tracing::debug!(
349                pkg.version=%version,
350                "Skipping an archived version",
351            );
352            archived_versions.push(version);
353            continue;
354        }
355
356        if version_constraint.matches(&version) {
357            match decode_summary(
358                &namespace,
359                &package_name,
360                pkg_version,
361                preferred_webc_version,
362            ) {
363                Ok(summary) => summaries.push(summary),
364                Err(e) => {
365                    tracing::debug!(
366                        version=%version,
367                        error=&*e,
368                        "Skipping version because its metadata couldn't be parsed"
369                    );
370                }
371            }
372        }
373    }
374
375    if summaries.is_empty() {
376        Err(QueryError::NoMatches {
377            query: query.clone(),
378            archived_versions,
379        })
380    } else {
381        Ok(summaries)
382    }
383}
384
385fn decode_summary(
386    namespace: &str,
387    package_name: &str,
388    pkg_version: WebQueryGetPackageVersion,
389    preferred_webc_version: webc::Version,
390) -> Result<PackageSummary, Error> {
391    let WebQueryGetPackageVersion {
392        v2:
393            WebQueryGetPackageVersionDistribution {
394                pirita_sha256_hash: v2_pirita_sha256_hash,
395                pirita_download_url: v2_pirita_download_url,
396                webc_manifest: v2_manifest,
397            },
398        v3:
399            WebQueryGetPackageVersionDistribution {
400                pirita_sha256_hash: v3_pirita_sha256_hash,
401                pirita_download_url: v3_pirita_download_url,
402                webc_manifest: v3_manifest,
403            },
404        ..
405    } = pkg_version;
406
407    let (version, pirita_sha256_hash, pirita_download_url, manifest) =
408        if preferred_webc_version == webc::Version::V3 {
409            (
410                webc::Version::V3,
411                v3_pirita_sha256_hash,
412                v3_pirita_download_url,
413                v3_manifest,
414            )
415        } else {
416            (
417                webc::Version::V2,
418                v2_pirita_sha256_hash,
419                v2_pirita_download_url,
420                v2_manifest,
421            )
422        };
423
424    let id = PackageId::Named(NamedPackageId {
425        full_name: format!("{namespace}/{package_name}"),
426        version: pkg_version
427            .version
428            .parse()
429            .context("could not parse package version")?,
430    });
431
432    let manifest = manifest.context("missing Manifest")?;
433    let hash = pirita_sha256_hash.context("missing sha256")?;
434    let webc = pirita_download_url.context("missing download URL")?;
435
436    let manifest: Manifest = serde_json::from_slice(manifest.as_bytes())
437        .context("Unable to deserialize the manifest")?;
438
439    let webc_sha256 = WebcHash::parse_hex(&hash).context("invalid webc sha256 hash in manifest")?;
440
441    Ok(PackageSummary {
442        pkg: PackageInfo::from_manifest(id, &manifest, version)?,
443        dist: DistributionInfo { webc, webc_sha256 },
444    })
445}
446
447/// A local cache for package queries.
448#[derive(Debug, Clone)]
449struct FileSystemCache {
450    cache_dir: PathBuf,
451    timeout: Duration,
452}
453
454impl FileSystemCache {
455    fn new(cache_dir: impl Into<PathBuf>, timeout: Duration) -> Self {
456        FileSystemCache {
457            cache_dir: cache_dir.into(),
458            timeout,
459        }
460    }
461
462    fn path(&self, package_name: &str) -> PathBuf {
463        self.cache_dir.join(package_name)
464    }
465
466    fn lookup_cached_query(&self, package_name: &str) -> Result<Option<WebQuery>, Error> {
467        let filename = self.path(package_name);
468
469        let _span =
470            tracing::debug_span!("lookup_cached_query", filename=%filename.display()).entered();
471
472        tracing::trace!("Reading cached entry from disk");
473        let json = match std::fs::read(&filename) {
474            Ok(json) => json,
475            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
476                tracing::debug!("Cache miss");
477                return Ok(None);
478            }
479            Err(e) => {
480                return Err(
481                    Error::new(e).context(format!("Unable to read \"{}\"", filename.display()))
482                );
483            }
484        };
485
486        let entry: CacheEntry = match serde_json::from_slice(&json) {
487            Ok(entry) => entry,
488            Err(e) => {
489                // If the entry is invalid, we should delete it to avoid work
490                // in the future
491                let _ = std::fs::remove_file(&filename);
492
493                return Err(Error::new(e).context("Unable to parse the cached query"));
494            }
495        };
496
497        if !entry.is_still_valid(self.timeout) {
498            tracing::debug!(timestamp = entry.unix_timestamp, "Cached entry is stale");
499            let _ = std::fs::remove_file(&filename);
500            return Ok(None);
501        }
502
503        if entry.package_name != package_name {
504            let _ = std::fs::remove_file(&filename);
505            anyhow::bail!(
506                "The cached response at \"{}\" corresponds to the \"{}\" package, but expected \"{}\"",
507                filename.display(),
508                entry.package_name,
509                package_name,
510            );
511        }
512
513        Ok(Some(entry.response))
514    }
515
516    fn update(&self, package_name: &str, response: &WebQuery) -> Result<(), Error> {
517        let entry = CacheEntry {
518            unix_timestamp: SystemTime::UNIX_EPOCH
519                .elapsed()
520                .unwrap_or_default()
521                .as_secs(),
522            package_name: package_name.to_string(),
523            response: response.clone(),
524        };
525
526        let _ = std::fs::create_dir_all(&self.cache_dir);
527
528        // First, save our cache entry to disk
529        let mut temp = tempfile::NamedTempFile::new_in(&self.cache_dir)
530            .context("Unable to create a temporary file")?;
531        serde_json::to_writer_pretty(&mut temp, &entry)
532            .context("Unable to serialize the cache entry")?;
533        temp.as_file()
534            .sync_all()
535            .context("Flushing the temp file failed")?;
536
537        // Now we've saved our cache entry we need to move it to the right
538        // location. We do this in two steps so concurrent queries don't see
539        // the cache entry until it has been completely written.
540        let filename = self.path(package_name);
541        tracing::debug!(
542            filename=%filename.display(),
543            package_name,
544            "Saving the query to disk",
545        );
546
547        if let Some(parent) = filename.parent() {
548            let _ = std::fs::create_dir_all(parent);
549        }
550        temp.persist(&filename).with_context(|| {
551            format!(
552                "Unable to persist the temp file to \"{}\"",
553                filename.display()
554            )
555        })?;
556
557        Ok(())
558    }
559}
560
561#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
562struct CacheEntry {
563    unix_timestamp: u64,
564    package_name: String,
565    response: WebQuery,
566}
567
568impl CacheEntry {
569    fn is_still_valid(&self, timeout: Duration) -> bool {
570        let timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(self.unix_timestamp);
571
572        match timestamp.elapsed() {
573            Ok(duration) if duration <= timeout => true,
574            Ok(_) => {
575                // The cached response is too old
576                false
577            }
578            Err(_) => {
579                // It looks like the current time is **after** the time this
580                // entry was recorded. That probably indicates a clock issue
581                // so we should mark the cached value as invalid.
582                false
583            }
584        }
585    }
586}
587
588#[allow(dead_code)]
589pub const WASMER_WEBC_QUERY_ALL: &str = r#"{
590    getPackage(name: "$NAME") {
591        packageName
592        namespace
593        versions {
594          version
595          isArchived
596          v2: distribution(version: V2) {
597            piritaDownloadUrl
598            piritaSha256Hash
599            webcManifest
600          }
601          v3: distribution(version: V3) {
602            piritaDownloadUrl
603            piritaSha256Hash
604            webcManifest
605          }
606        }
607    }
608    info {
609        defaultFrontend
610    }
611}"#;
612
613pub const WASMER_WEBC_QUERY_BY_HASH: &str = r#"{
614    getPackageRelease(hash: "$HASH") {
615        piritaManifest
616        isArchived
617        webcUrl
618    }
619}"#;
620
621#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
622pub struct Reply<T> {
623    pub data: T,
624}
625
626#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
627struct GetPackageRelease {
628    #[serde(rename = "getPackageRelease")]
629    get_package_release: Option<PackageWebc>,
630}
631
632#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
633struct PackageWebc {
634    #[serde(rename = "piritaManifest")]
635    pub pirita_manifest: String,
636    #[serde(rename = "isArchived")]
637    pub is_archived: bool,
638    #[serde(rename = "webcUrl")]
639    pub webc_url: url::Url,
640}
641
642impl PackageWebc {
643    fn try_into_summary(self, hash: PackageHash) -> Result<PackageSummary, anyhow::Error> {
644        let manifest: Manifest = serde_json::from_str(&self.pirita_manifest)
645            .context("Unable to deserialize the manifest")?;
646
647        let id = PackageId::Hash(hash.clone());
648
649        let info = PackageInfo::from_manifest(id, &manifest, webc::Version::V3)
650            .context("could not convert the manifest ")?;
651
652        Ok(PackageSummary {
653            pkg: info,
654            dist: DistributionInfo {
655                webc: self.webc_url,
656                // TODO: replace with different hash type?
657                webc_sha256: WebcHash(hash.as_sha256().context("invalid hash")?.0),
658            },
659        })
660    }
661}
662
663#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
664pub struct WebQuery {
665    #[serde(rename = "data")]
666    pub data: WebQueryData,
667}
668
669#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
670pub struct WebQueryData {
671    #[serde(rename = "getPackage")]
672    pub get_package: Option<WebQueryGetPackage>,
673    pub info: WebQueryInfo,
674}
675
676#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
677pub struct WebQueryInfo {
678    #[serde(rename = "defaultFrontend")]
679    pub default_frontend: Url,
680}
681
682#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
683pub struct WebQueryGetPackage {
684    #[serde(rename = "packageName")]
685    pub package_name: String,
686    pub namespace: String,
687    pub versions: Vec<WebQueryGetPackageVersion>,
688}
689
690#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
691pub struct WebQueryGetPackageVersion {
692    pub version: String,
693    /// Has the package been archived?
694    #[serde(rename = "isArchived", default)]
695    pub is_archived: bool,
696    pub v2: WebQueryGetPackageVersionDistribution,
697    pub v3: WebQueryGetPackageVersionDistribution,
698}
699
700#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
701pub enum WebCVersion {
702    V2,
703    V3,
704}
705
706impl Default for WebCVersion {
707    fn default() -> Self {
708        Self::V2
709    }
710}
711
712impl From<WebCVersion> for webc::Version {
713    fn from(val: WebCVersion) -> Self {
714        match val {
715            WebCVersion::V2 => webc::Version::V2,
716            WebCVersion::V3 => webc::Version::V3,
717        }
718    }
719}
720
721#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
722pub struct WebQueryGetPackageVersionDistribution {
723    #[serde(rename = "piritaDownloadUrl")]
724    pub pirita_download_url: Option<Url>,
725    #[serde(rename = "piritaSha256Hash")]
726    pub pirita_sha256_hash: Option<String>,
727    #[serde(rename = "webcManifest")]
728    pub webc_manifest: Option<String>,
729}
730
731#[cfg(test)]
732mod tests {
733    use std::{str::FromStr, sync::Mutex};
734
735    use http::{HeaderMap, StatusCode};
736
737    use crate::{
738        http::HttpResponse,
739        runtime::resolver::inputs::{DistributionInfo, PackageInfo},
740    };
741
742    use super::*;
743
744    // You can check the response with:
745    // curl https://registry.wasmer.io/graphql \
746    //      -H "Content-Type: application/json" \
747    //      -X POST \
748    //      -d '@wasmer_pack_cli_request.json' > wasmer_pack_cli_response.json
749    const WASMER_PACK_CLI_REQUEST: &[u8] = br#"
750    {
751        "query":"{\n    getPackage(name: \"wasmer/wasmer-pack-cli\") {\n        packageName\n        namespace\n        versions {\n          version\n          isArchived\n          v2: distribution(version: V2) {\n            piritaDownloadUrl\n            piritaSha256Hash\n            webcManifest\n          }\n          v3: distribution(version: V3) {\n            piritaDownloadUrl\n            piritaSha256Hash\n            webcManifest\n          }\n        }\n    }\n    info {\n        defaultFrontend\n    }\n}"
752    }
753    "#;
754    const WASMER_PACK_CLI_RESPONSE: &[u8] = br#"
755    {
756        "data": {
757          "getPackage": {
758            "packageName": "wasmer-pack-cli",
759            "namespace": "wasmer",
760            "versions": [
761              {
762                "version": "0.7.1",
763                "isArchived": false,
764                "v2": {
765                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:gGeLZqPitpg893Jj/nvGa+1235RezSWA9FjssopzOZY=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
766                  "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.1/wasmer-pack-cli-0.7.1.webc",
767                  "piritaSha256Hash": "e821047f446dd20fb6b43a1648fe98b882276dfc480f020df6f00a49f69771fa"
768                },
769                "v3": {
770                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:gGeLZqPitpg893Jj/nvGa+1235RezSWA9FjssopzOZY=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
771                  "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.1/wasmer-pack-cli-0.7.1.webc",
772                  "piritaSha256Hash": "e821047f446dd20fb6b43a1648fe98b882276dfc480f020df6f00a49f69771fa"
773                }
774              },
775              {
776                "version": "0.7.0",
777                "isArchived": false,
778                "v2": {
779                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:FesCIAS6URjrIAAyy4G5u5HjJjGQBLGmnafjHPHRvqo=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
780                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.0/wasmer-pack-cli-0.7.0.webc",
781                    "piritaSha256Hash": "d085869201aa602673f70abbd5e14e5a6936216fa93314c5b103cda3da56e29e"
782                },
783                "v3": {
784                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:FesCIAS6URjrIAAyy4G5u5HjJjGQBLGmnafjHPHRvqo=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
785                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.0/wasmer-pack-cli-0.7.0.webc",
786                    "piritaSha256Hash": "d085869201aa602673f70abbd5e14e5a6936216fa93314c5b103cda3da56e29e"
787                }
788              },
789              {
790                "version": "0.6.0",
791                "isArchived": false,
792                "v2": {
793                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:CzzhNaav3gjBkCJECGbk7e+qAKurWbcIAzQvEqsr2Co=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.6.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
794                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc",
795                    "piritaSha256Hash": "7e1add1640d0037ff6a726cd7e14ea36159ec2db8cb6debd0e42fa2739bea52b"
796                },
797                "v3": {
798                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:CzzhNaav3gjBkCJECGbk7e+qAKurWbcIAzQvEqsr2Co=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.6.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
799                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc",
800                    "piritaSha256Hash": "7e1add1640d0037ff6a726cd7e14ea36159ec2db8cb6debd0e42fa2739bea52b"
801                }
802              },
803              {
804                "version": "0.5.3",
805                "isArchived": false,
806                "v2": {
807                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:qdiJVfpi4icJXdR7Y5US/pJ4PjqbAq9PkU+obMZIMlE=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.3\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
808                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.3/wasmer-pack-cli-0.5.3.webc",
809                    "piritaSha256Hash": "44fdcdde23d34175887243d7c375e4e4a7e6e2cd1ae063ebffbede4d1f68f14a"
810                },
811                "v3": {
812                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:qdiJVfpi4icJXdR7Y5US/pJ4PjqbAq9PkU+obMZIMlE=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.3\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
813                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.3/wasmer-pack-cli-0.5.3.webc",
814                    "piritaSha256Hash": "44fdcdde23d34175887243d7c375e4e4a7e6e2cd1ae063ebffbede4d1f68f14a"
815                }
816              },
817              {
818                "version": "0.5.2",
819                "isArchived": false,
820                "v2": {
821                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:xiwrUFAo+cU1xW/IE6MVseiyjNGHtXooRlkYKiOKzQc=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.2\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
822                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.2/wasmer-pack-cli-0.5.2.webc",
823                    "piritaSha256Hash": "d1dbc8168c3a2491a7158017a9c88df9e0c15bed88ebcd6d9d756e4b03adde95"
824                },
825                "v3": {
826                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:xiwrUFAo+cU1xW/IE6MVseiyjNGHtXooRlkYKiOKzQc=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.2\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
827                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.2/wasmer-pack-cli-0.5.2.webc",
828                    "piritaSha256Hash": "d1dbc8168c3a2491a7158017a9c88df9e0c15bed88ebcd6d9d756e4b03adde95"
829                }
830              },
831              {
832                "version": "0.5.1",
833                "isArchived": false,
834                "v2": {
835                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:TliPwutfkFvRite/3/k3OpLqvV0EBKGwyp3L5UjCuEI=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
836                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.1/wasmer-pack-cli-0.5.1.webc",
837                    "piritaSha256Hash": "c42924619660e2befd69b5c72729388985dcdcbf912d51a00015237fec3e1ade"
838                },
839                "v3": {
840                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:TliPwutfkFvRite/3/k3OpLqvV0EBKGwyp3L5UjCuEI=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
841                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.1/wasmer-pack-cli-0.5.1.webc",
842                    "piritaSha256Hash": "c42924619660e2befd69b5c72729388985dcdcbf912d51a00015237fec3e1ade"
843                }
844              },
845              {
846                "version": "0.5.0",
847                "isArchived": false,
848                "v2": {
849                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:6UD7NS4KtyNYa3TcnKOvd+kd3LxBCw+JQ8UWRpMXeC0=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
850                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0/wasmer-pack-cli-0.5.0.webc",
851                    "piritaSha256Hash": "d30ca468372faa96469163d2d1546dd34be9505c680677e6ab86a528a268e5f5"
852                },
853                "v3": {
854                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:6UD7NS4KtyNYa3TcnKOvd+kd3LxBCw+JQ8UWRpMXeC0=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
855                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0/wasmer-pack-cli-0.5.0.webc",
856                    "piritaSha256Hash": "d30ca468372faa96469163d2d1546dd34be9505c680677e6ab86a528a268e5f5"
857                }
858              },
859              {
860                "version": "0.5.0-rc.1",
861                "isArchived": false,
862                "v2": {
863                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ThybHIc2elJEcDdQiq5ffT1TVaNs70+WAqoKw4Tkh3E=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0-rc.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
864                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0-rc.1/wasmer-pack-cli-0.5.0-rc.1.webc",
865                    "piritaSha256Hash": "0cd5d6e4c33c92c52784afed3a60c056953104d719717948d4663ff2521fe2bb"
866                },
867                "v3": {
868                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ThybHIc2elJEcDdQiq5ffT1TVaNs70+WAqoKw4Tkh3E=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0-rc.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
869                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0-rc.1/wasmer-pack-cli-0.5.0-rc.1.webc",
870                    "piritaSha256Hash": "0cd5d6e4c33c92c52784afed3a60c056953104d719717948d4663ff2521fe2bb"
871                }
872              }
873            ]
874          },
875          "info": {
876            "defaultFrontend": "https://wasmer.io"
877          }
878        }
879      }
880    "#;
881
882    #[derive(Debug)]
883    struct DummyClient {
884        requests: Mutex<Vec<HttpRequest>>,
885        responses: Mutex<Vec<HttpResponse>>,
886    }
887
888    impl DummyClient {
889        fn new(responses: Vec<HttpResponse>) -> Self {
890            DummyClient {
891                requests: Mutex::new(Vec::new()),
892                responses: Mutex::new(responses),
893            }
894        }
895
896        fn take_requests(&self) -> Vec<HttpRequest> {
897            std::mem::take(&mut *self.requests.lock().unwrap())
898        }
899    }
900
901    impl HttpClient for DummyClient {
902        fn request(
903            &self,
904            request: HttpRequest,
905        ) -> futures::future::BoxFuture<'_, Result<HttpResponse, anyhow::Error>> {
906            self.requests.lock().unwrap().push(request);
907            let response = self.responses.lock().unwrap().remove(0);
908            Box::pin(async { Ok(response) })
909        }
910    }
911
912    #[tokio::test]
913    async fn run_known_query() {
914        let response = HttpResponse {
915            body: Some(WASMER_PACK_CLI_RESPONSE.to_vec()),
916            redirected: false,
917            status: StatusCode::OK,
918            headers: HeaderMap::new(),
919        };
920        let client = Arc::new(DummyClient::new(vec![response]));
921        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
922        let request = PackageSource::from_str("wasmer/wasmer-pack-cli@^0.6").unwrap();
923        let source = BackendSource::new(registry_endpoint, client.clone());
924
925        let summaries = source.query(&request).await.unwrap();
926
927        assert_eq!(
928            summaries,
929            [PackageSummary {
930                pkg: PackageInfo {
931                    id: PackageId::new_named("wasmer/wasmer-pack-cli", Version::new(0, 6, 0)),
932                    dependencies: Vec::new(),
933                    commands: vec![crate::runtime::resolver::Command {
934                        name: "wasmer-pack".to_string(),
935                    },],
936                    entrypoint: Some("wasmer-pack".to_string()),
937                    filesystem: vec![],
938                },
939                dist: DistributionInfo {
940                    webc: "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc"
941                        .parse()
942                        .unwrap(),
943                    webc_sha256: WebcHash::from_bytes([
944                        126, 26, 221, 22, 64, 208, 3, 127, 246, 167, 38, 205, 126, 20, 234, 54, 21,
945                        158, 194, 219, 140, 182, 222, 189, 14, 66, 250, 39, 57, 190, 165, 43,
946                    ]),
947                }
948            }]
949        );
950        let requests = client.take_requests();
951        assert_eq!(requests.len(), 1);
952        let request = &requests[0];
953        assert_eq!(request.method, http::Method::POST);
954        assert_eq!(request.url.as_str(), BackendSource::WASMER_PROD_ENDPOINT);
955        assert_eq!(request.headers.len(), 2);
956        assert_eq!(request.headers["User-Agent"], USER_AGENT);
957        assert_eq!(request.headers["Content-Type"], "application/json");
958        let body: serde_json::Value =
959            serde_json::from_slice(request.body.as_deref().unwrap()).unwrap();
960        let expected_body: serde_json::Value =
961            serde_json::from_slice(WASMER_PACK_CLI_REQUEST).unwrap();
962        assert_eq!(body, expected_body);
963    }
964
965    /// For the full context, see #3946 on GitHub or the original conversation
966    /// [on
967    /// Slack](https://wasmerio.slack.com/archives/C03MX4KL6KH/p1685706988500919).
968    #[tokio::test]
969    async fn skip_package_versions_with_missing_fields() {
970        let body = serde_json::json! {
971            {
972                "data": {
973                    "getPackage": {
974                        "packageName": "cowsay",
975                        "namespace": "_",
976                        "versions": [
977                            {
978                                "version": "0.2.0",
979                                "v2": {
980                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/_/cowsay/cowsay-0.2.0.webc",
981                                    "piritaSha256Hash": "9586938a0a89219dafe4ae97a901c56d4b3e2a9941520d1309ae880c9a1868c9",
982                                    "webcManifest": "{\"atoms\": {\"cowsay\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=\"}}, \"package\": {\"wapm\": {\"name\": \"cowsay\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"version\": \"0.2.0\", \"repository\": \"https://github.com/wapm-packages/cowsay\", \"description\": \"cowsay is a program that generates ASCII pictures of a cow with a message\"}}, \"commands\": {\"cowsay\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}, \"cowthink\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}}}",
983                                },
984                                "v3": {
985                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/_/cowsay/cowsay-0.2.0.webc",
986                                    "piritaSha256Hash": "9586938a0a89219dafe4ae97a901c56d4b3e2a9941520d1309ae880c9a1868c9",
987                                    "webcManifest": "{\"atoms\": {\"cowsay\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=\"}}, \"package\": {\"wapm\": {\"name\": \"cowsay\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"version\": \"0.2.0\", \"repository\": \"https://github.com/wapm-packages/cowsay\", \"description\": \"cowsay is a program that generates ASCII pictures of a cow with a message\"}}, \"commands\": {\"cowsay\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}, \"cowthink\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}}}",
988                                }
989                            },
990                            {
991                                "version": "0.1.3",
992                                "v2": {
993                                    "piritaDownloadUrl": "https://example.com/",
994                                    "piritaSha256Hash": "1234asdf"
995                                },
996                                "v3": {
997                                    "piritaDownloadUrl": "https://example.com/",
998                                    "piritaSha256Hash": "1234asdf"
999                                }
1000                            },
1001                            {
1002                                "version": "0.1.2",
1003                                "v2": {
1004                                    "piritaDownloadUrl": "https://example.com/",
1005                                    "piritaSha256Hash": "1234asdf",
1006                                    "webcManifest": "{}",
1007                                },
1008                                "v3": {
1009                                    "piritaDownloadUrl": "https://example.com/",
1010                                    "piritaSha256Hash": "1234asdf",
1011                                    "webcManifest": "{}",
1012                                }
1013                            },
1014                            {
1015                                "version": "0.1.3",
1016                                "v2": {
1017                                    "webcManifest": "{}",
1018                                    "piritaDownloadUrl": "https://example.com/",
1019                                    "piritaSha256Hash": "1234asdf"
1020                                },
1021                                "v3": {
1022                                    "webcManifest": "{}",
1023                                    "piritaDownloadUrl": "https://example.com/",
1024                                    "piritaSha256Hash": "1234asdf"
1025                                }
1026                            }
1027                        ]
1028                    },
1029                    "info": {
1030                        "defaultFrontend": "https://wasmer.io/",
1031                    },
1032                }
1033            }
1034
1035        };
1036        let response = HttpResponse {
1037            body: Some(serde_json::to_vec(&body).unwrap()),
1038            redirected: false,
1039            status: StatusCode::OK,
1040            headers: HeaderMap::new(),
1041        };
1042        let client = Arc::new(DummyClient::new(vec![response]));
1043        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1044        let request = PackageSource::from_str("_/cowsay").unwrap();
1045        let source = BackendSource::new(registry_endpoint, client.clone());
1046
1047        let summaries = source.query(&request).await.unwrap();
1048
1049        assert_eq!(summaries.len(), 1);
1050        assert_eq!(
1051            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1052            "0.2.0"
1053        );
1054    }
1055
1056    #[tokio::test]
1057    async fn skip_archived_package_versions() {
1058        let body = serde_json::json! {
1059            {
1060                "data": {
1061                    "getPackage": {
1062                        "packageName": "python",
1063                        "namespace": "wasmer",
1064                        "versions": [
1065                            {
1066                                "version": "3.12.2",
1067                                "isArchived": true,
1068                                "v2": {
1069                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ibsq6QL4qB4GtCE8IA2yfHVwI4fLoIGXsALsAx16y5M=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.2\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1070                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.5-a11e0414-c68d-473c-958f-fc96ef7adb20.webc",
1071                                    "piritaSha256Hash": "7771ed54376c16da86581736fad84fb761a049915902a7070e854965be0d5874"
1072                                },
1073                                "v3": {
1074                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ibsq6QL4qB4GtCE8IA2yfHVwI4fLoIGXsALsAx16y5M=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.2\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1075                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.5-a11e0414-c68d-473c-958f-fc96ef7adb20.webc",
1076                                    "piritaSha256Hash": "7771ed54376c16da86581736fad84fb761a049915902a7070e854965be0d5874"
1077                                }
1078                            },
1079                            {
1080                                "version": "3.12.1",
1081                                "isArchived": false,
1082                                "v2": {
1083                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.1\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1084                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.2-ed98c999-fcda-4f80-96dc-7c0f8be8baa6.webc",
1085                                    "piritaSha256Hash": "7835401e3ca1977ba05b5e51541363783b8a7700da270dd851f10fe2e4f27f07"
1086                                },
1087                                "v3": {
1088                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.1\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1089                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.2-ed98c999-fcda-4f80-96dc-7c0f8be8baa6.webc",
1090                                    "piritaSha256Hash": "7835401e3ca1977ba05b5e51541363783b8a7700da270dd851f10fe2e4f27f07"
1091                                }
1092                            },
1093                            {
1094                                "version": "3.12.0",
1095                                "isArchived": true,
1096                                "v2": {
1097                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.0\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1098                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-32065e5e-84fe-4483-a380-0aa750772a3a.webc",
1099                                    "piritaSha256Hash": "e5d6e9d16db988eb323e34e2c152ebfb32dc7043d6b7ddc00ad57d3beae24adb"
1100                                },
1101                                "v3": {
1102                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.0\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1103                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-32065e5e-84fe-4483-a380-0aa750772a3a.webc",
1104                                    "piritaSha256Hash": "e5d6e9d16db988eb323e34e2c152ebfb32dc7043d6b7ddc00ad57d3beae24adb"
1105                                }
1106                            },
1107                        ]
1108                    },
1109                    "info": {
1110                        "defaultFrontend": "https://wasmer.io/",
1111                    },
1112                }
1113            }
1114        };
1115        let response = HttpResponse {
1116            body: Some(serde_json::to_vec(&body).unwrap()),
1117            redirected: false,
1118            status: StatusCode::OK,
1119            headers: HeaderMap::new(),
1120        };
1121        let client = Arc::new(DummyClient::new(vec![response]));
1122        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1123        let request = PackageSource::from_str("wasmer/python").unwrap();
1124        let source = BackendSource::new(registry_endpoint, client.clone());
1125
1126        let summaries = source.query(&request).await.unwrap();
1127
1128        assert_eq!(summaries.len(), 1);
1129        assert_eq!(
1130            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1131            "3.12.1"
1132        );
1133    }
1134
1135    #[tokio::test]
1136    async fn query_the_backend_again_if_cached_queries_dont_match() {
1137        let cached_value = serde_json::from_value(serde_json::json! {
1138            {
1139                "data": {
1140                    "getPackage": {
1141                        "packageName": "python",
1142                        "namespace": "wasmer",
1143                        "versions": [
1144                            {
1145                                "version": "3.12.0",
1146                                "v2": {
1147                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1148                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1149                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1150                                },
1151                                "v3": {
1152                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1153                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1154                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1155                                }
1156                            },
1157                        ]
1158                    },
1159                    "info": {
1160                        "defaultFrontend": "https://wasmer.io/",
1161                    },
1162                }
1163            }
1164        }).unwrap();
1165        let body = serde_json::json! {
1166            {
1167                "data": {
1168                    "getPackage": {
1169                        "packageName": "python",
1170                        "namespace": "wasmer",
1171                        "versions": [
1172                            {
1173                                "version": "4.0.0",
1174                                "v2": {
1175                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"4.0.0\", \"description\": \"Python\"}}}",
1176                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@4.0.0",
1177                                    "piritaSha256Hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1178                                },
1179                                "v3": {
1180                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"4.0.0\", \"description\": \"Python\"}}}",
1181                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@4.0.0",
1182                                    "piritaSha256Hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1183                                }
1184                            },
1185                            {
1186                                "version": "3.12.0",
1187                                "v2": {
1188                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1189                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1190                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1191                                },
1192                                "v3": {
1193                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1194                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1195                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1196                                }
1197                            },
1198                        ]
1199                    },
1200                    "info": {
1201                        "defaultFrontend": "https://wasmer.io/",
1202                    },
1203                }
1204            }
1205        };
1206        let response = HttpResponse {
1207            body: Some(serde_json::to_vec(&body).unwrap()),
1208            redirected: false,
1209            status: StatusCode::OK,
1210            headers: HeaderMap::new(),
1211        };
1212        let client = Arc::new(DummyClient::new(vec![response]));
1213        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1214        let request = PackageSource::from_str("wasmer/python@4.0.0").unwrap();
1215        let temp = tempfile::tempdir().unwrap();
1216        let source = BackendSource::new(registry_endpoint, client.clone())
1217            .with_local_cache(temp.path(), Duration::from_secs(0));
1218        source
1219            .cache
1220            .as_ref()
1221            .unwrap()
1222            .update("wasmer/python", &cached_value)
1223            .unwrap();
1224
1225        let summaries = source.query(&request).await.unwrap();
1226
1227        assert_eq!(summaries.len(), 1);
1228        assert_eq!(
1229            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1230            "4.0.0"
1231        );
1232    }
1233}