wasmer_wasix/runtime/resolver/
backend_source.rs

1use std::{
2    path::{MAIN_SEPARATOR_STR, 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            && let Err(e) = cache.update(&package_name, &response)
295        {
296            tracing::warn!(
297                package_name,
298                error = &*e,
299                "An error occurred while caching the GraphQL response",
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
464            .join(package_name.replace(MAIN_SEPARATOR_STR, "#"))
465    }
466
467    fn lookup_cached_query(&self, package_name: &str) -> Result<Option<WebQuery>, Error> {
468        let filename = self.path(package_name);
469
470        let _span =
471            tracing::debug_span!("lookup_cached_query", filename=%filename.display()).entered();
472
473        tracing::trace!("Reading cached entry from disk");
474        let json = match std::fs::read(&filename) {
475            Ok(json) => json,
476            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
477                tracing::debug!("Cache miss");
478                return Ok(None);
479            }
480            Err(e) => {
481                return Err(
482                    Error::new(e).context(format!("Unable to read \"{}\"", filename.display()))
483                );
484            }
485        };
486
487        let entry: CacheEntry = match serde_json::from_slice(&json) {
488            Ok(entry) => entry,
489            Err(e) => {
490                // If the entry is invalid, we should delete it to avoid work
491                // in the future
492                let _ = std::fs::remove_file(&filename);
493
494                return Err(Error::new(e).context("Unable to parse the cached query"));
495            }
496        };
497
498        if !entry.is_still_valid(self.timeout) {
499            tracing::debug!(timestamp = entry.unix_timestamp, "Cached entry is stale");
500            let _ = std::fs::remove_file(&filename);
501            return Ok(None);
502        }
503
504        if entry.package_name != package_name {
505            let _ = std::fs::remove_file(&filename);
506            anyhow::bail!(
507                "The cached response at \"{}\" corresponds to the \"{}\" package, but expected \"{}\"",
508                filename.display(),
509                entry.package_name,
510                package_name,
511            );
512        }
513
514        Ok(Some(entry.response))
515    }
516
517    fn update(&self, package_name: &str, response: &WebQuery) -> Result<(), Error> {
518        let entry = CacheEntry {
519            unix_timestamp: SystemTime::UNIX_EPOCH
520                .elapsed()
521                .unwrap_or_default()
522                .as_secs(),
523            package_name: package_name.to_string(),
524            response: response.clone(),
525        };
526
527        let _ = std::fs::create_dir_all(&self.cache_dir);
528
529        // First, save our cache entry to disk
530        let mut temp = tempfile::NamedTempFile::new_in(&self.cache_dir)
531            .context("Unable to create a temporary file")?;
532        serde_json::to_writer_pretty(&mut temp, &entry)
533            .context("Unable to serialize the cache entry")?;
534        temp.as_file()
535            .sync_all()
536            .context("Flushing the temp file failed")?;
537
538        // Now we've saved our cache entry we need to move it to the right
539        // location. We do this in two steps so concurrent queries don't see
540        // the cache entry until it has been completely written.
541        let filename = self.path(package_name);
542        tracing::debug!(
543            filename=%filename.display(),
544            package_name,
545            "Saving the query to disk",
546        );
547
548        if let Some(parent) = filename.parent() {
549            let _ = std::fs::create_dir_all(parent);
550        }
551        temp.persist(&filename).with_context(|| {
552            format!(
553                "Unable to persist the temp file to \"{}\"",
554                filename.display()
555            )
556        })?;
557
558        Ok(())
559    }
560}
561
562#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
563struct CacheEntry {
564    unix_timestamp: u64,
565    package_name: String,
566    response: WebQuery,
567}
568
569impl CacheEntry {
570    fn is_still_valid(&self, timeout: Duration) -> bool {
571        let timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(self.unix_timestamp);
572
573        match timestamp.elapsed() {
574            Ok(duration) if duration <= timeout => true,
575            Ok(_) => {
576                // The cached response is too old
577                false
578            }
579            Err(_) => {
580                // It looks like the current time is **after** the time this
581                // entry was recorded. That probably indicates a clock issue
582                // so we should mark the cached value as invalid.
583                false
584            }
585        }
586    }
587}
588
589#[allow(dead_code)]
590pub const WASMER_WEBC_QUERY_ALL: &str = r#"{
591    getPackage(name: "$NAME") {
592        packageName
593        namespace
594        versions {
595          version
596          isArchived
597          v2: distribution(version: V2) {
598            piritaDownloadUrl
599            piritaSha256Hash
600            webcManifest
601          }
602          v3: distribution(version: V3) {
603            piritaDownloadUrl
604            piritaSha256Hash
605            webcManifest
606          }
607        }
608    }
609    info {
610        defaultFrontend
611    }
612}"#;
613
614pub const WASMER_WEBC_QUERY_BY_HASH: &str = r#"{
615    getPackageRelease(hash: "$HASH") {
616        piritaManifest
617        isArchived
618        webcUrl
619    }
620}"#;
621
622#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
623pub struct Reply<T> {
624    pub data: T,
625}
626
627#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
628struct GetPackageRelease {
629    #[serde(rename = "getPackageRelease")]
630    get_package_release: Option<PackageWebc>,
631}
632
633#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
634struct PackageWebc {
635    #[serde(rename = "piritaManifest")]
636    pub pirita_manifest: String,
637    #[serde(rename = "isArchived")]
638    pub is_archived: bool,
639    #[serde(rename = "webcUrl")]
640    pub webc_url: url::Url,
641}
642
643impl PackageWebc {
644    fn try_into_summary(self, hash: PackageHash) -> Result<PackageSummary, anyhow::Error> {
645        let manifest: Manifest = serde_json::from_str(&self.pirita_manifest)
646            .context("Unable to deserialize the manifest")?;
647
648        let id = PackageId::Hash(hash.clone());
649
650        let info = PackageInfo::from_manifest(id, &manifest, webc::Version::V3)
651            .context("could not convert the manifest ")?;
652
653        Ok(PackageSummary {
654            pkg: info,
655            dist: DistributionInfo {
656                webc: self.webc_url,
657                // TODO: replace with different hash type?
658                webc_sha256: WebcHash(hash.as_sha256().context("invalid hash")?.0),
659            },
660        })
661    }
662}
663
664#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
665pub struct WebQuery {
666    #[serde(rename = "data")]
667    pub data: WebQueryData,
668}
669
670#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
671pub struct WebQueryData {
672    #[serde(rename = "getPackage")]
673    pub get_package: Option<WebQueryGetPackage>,
674    pub info: WebQueryInfo,
675}
676
677#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
678pub struct WebQueryInfo {
679    #[serde(rename = "defaultFrontend")]
680    pub default_frontend: Url,
681}
682
683#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
684pub struct WebQueryGetPackage {
685    #[serde(rename = "packageName")]
686    pub package_name: String,
687    pub namespace: String,
688    pub versions: Vec<WebQueryGetPackageVersion>,
689}
690
691#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
692pub struct WebQueryGetPackageVersion {
693    pub version: String,
694    /// Has the package been archived?
695    #[serde(rename = "isArchived", default)]
696    pub is_archived: bool,
697    pub v2: WebQueryGetPackageVersionDistribution,
698    pub v3: WebQueryGetPackageVersionDistribution,
699}
700
701#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
702pub enum WebCVersion {
703    #[default]
704    V2,
705    V3,
706}
707
708impl From<WebCVersion> for webc::Version {
709    fn from(val: WebCVersion) -> Self {
710        match val {
711            WebCVersion::V2 => webc::Version::V2,
712            WebCVersion::V3 => webc::Version::V3,
713        }
714    }
715}
716
717#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
718pub struct WebQueryGetPackageVersionDistribution {
719    #[serde(rename = "piritaDownloadUrl")]
720    pub pirita_download_url: Option<Url>,
721    #[serde(rename = "piritaSha256Hash")]
722    pub pirita_sha256_hash: Option<String>,
723    #[serde(rename = "webcManifest")]
724    pub webc_manifest: Option<String>,
725}
726
727#[cfg(test)]
728mod tests {
729    use std::{str::FromStr, sync::Mutex};
730
731    use http::{HeaderMap, StatusCode};
732
733    use crate::{
734        http::HttpResponse,
735        runtime::resolver::inputs::{DistributionInfo, PackageInfo},
736    };
737
738    use super::*;
739
740    // You can check the response with:
741    // curl https://registry.wasmer.io/graphql \
742    //      -H "Content-Type: application/json" \
743    //      -X POST \
744    //      -d '@wasmer_pack_cli_request.json' > wasmer_pack_cli_response.json
745    const WASMER_PACK_CLI_REQUEST: &[u8] = br#"
746    {
747        "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}"
748    }
749    "#;
750    const WASMER_PACK_CLI_RESPONSE: &[u8] = br#"
751    {
752        "data": {
753          "getPackage": {
754            "packageName": "wasmer-pack-cli",
755            "namespace": "wasmer",
756            "versions": [
757              {
758                "version": "0.7.1",
759                "isArchived": false,
760                "v2": {
761                    "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\"}",
762                  "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.1/wasmer-pack-cli-0.7.1.webc",
763                  "piritaSha256Hash": "e821047f446dd20fb6b43a1648fe98b882276dfc480f020df6f00a49f69771fa"
764                },
765                "v3": {
766                    "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\"}",
767                  "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.1/wasmer-pack-cli-0.7.1.webc",
768                  "piritaSha256Hash": "e821047f446dd20fb6b43a1648fe98b882276dfc480f020df6f00a49f69771fa"
769                }
770              },
771              {
772                "version": "0.7.0",
773                "isArchived": false,
774                "v2": {
775                    "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\"}",
776                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.0/wasmer-pack-cli-0.7.0.webc",
777                    "piritaSha256Hash": "d085869201aa602673f70abbd5e14e5a6936216fa93314c5b103cda3da56e29e"
778                },
779                "v3": {
780                    "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\"}",
781                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.0/wasmer-pack-cli-0.7.0.webc",
782                    "piritaSha256Hash": "d085869201aa602673f70abbd5e14e5a6936216fa93314c5b103cda3da56e29e"
783                }
784              },
785              {
786                "version": "0.6.0",
787                "isArchived": false,
788                "v2": {
789                    "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\"}",
790                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc",
791                    "piritaSha256Hash": "7e1add1640d0037ff6a726cd7e14ea36159ec2db8cb6debd0e42fa2739bea52b"
792                },
793                "v3": {
794                    "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\"}",
795                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc",
796                    "piritaSha256Hash": "7e1add1640d0037ff6a726cd7e14ea36159ec2db8cb6debd0e42fa2739bea52b"
797                }
798              },
799              {
800                "version": "0.5.3",
801                "isArchived": false,
802                "v2": {
803                    "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\"}",
804                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.3/wasmer-pack-cli-0.5.3.webc",
805                    "piritaSha256Hash": "44fdcdde23d34175887243d7c375e4e4a7e6e2cd1ae063ebffbede4d1f68f14a"
806                },
807                "v3": {
808                    "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\"}",
809                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.3/wasmer-pack-cli-0.5.3.webc",
810                    "piritaSha256Hash": "44fdcdde23d34175887243d7c375e4e4a7e6e2cd1ae063ebffbede4d1f68f14a"
811                }
812              },
813              {
814                "version": "0.5.2",
815                "isArchived": false,
816                "v2": {
817                    "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\"}",
818                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.2/wasmer-pack-cli-0.5.2.webc",
819                    "piritaSha256Hash": "d1dbc8168c3a2491a7158017a9c88df9e0c15bed88ebcd6d9d756e4b03adde95"
820                },
821                "v3": {
822                    "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\"}",
823                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.2/wasmer-pack-cli-0.5.2.webc",
824                    "piritaSha256Hash": "d1dbc8168c3a2491a7158017a9c88df9e0c15bed88ebcd6d9d756e4b03adde95"
825                }
826              },
827              {
828                "version": "0.5.1",
829                "isArchived": false,
830                "v2": {
831                    "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\"}",
832                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.1/wasmer-pack-cli-0.5.1.webc",
833                    "piritaSha256Hash": "c42924619660e2befd69b5c72729388985dcdcbf912d51a00015237fec3e1ade"
834                },
835                "v3": {
836                    "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\"}",
837                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.1/wasmer-pack-cli-0.5.1.webc",
838                    "piritaSha256Hash": "c42924619660e2befd69b5c72729388985dcdcbf912d51a00015237fec3e1ade"
839                }
840              },
841              {
842                "version": "0.5.0",
843                "isArchived": false,
844                "v2": {
845                    "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\"}",
846                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0/wasmer-pack-cli-0.5.0.webc",
847                    "piritaSha256Hash": "d30ca468372faa96469163d2d1546dd34be9505c680677e6ab86a528a268e5f5"
848                },
849                "v3": {
850                    "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\"}",
851                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0/wasmer-pack-cli-0.5.0.webc",
852                    "piritaSha256Hash": "d30ca468372faa96469163d2d1546dd34be9505c680677e6ab86a528a268e5f5"
853                }
854              },
855              {
856                "version": "0.5.0-rc.1",
857                "isArchived": false,
858                "v2": {
859                    "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\"}",
860                    "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",
861                    "piritaSha256Hash": "0cd5d6e4c33c92c52784afed3a60c056953104d719717948d4663ff2521fe2bb"
862                },
863                "v3": {
864                    "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\"}",
865                    "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",
866                    "piritaSha256Hash": "0cd5d6e4c33c92c52784afed3a60c056953104d719717948d4663ff2521fe2bb"
867                }
868              }
869            ]
870          },
871          "info": {
872            "defaultFrontend": "https://wasmer.io"
873          }
874        }
875      }
876    "#;
877
878    #[derive(Debug)]
879    struct DummyClient {
880        requests: Mutex<Vec<HttpRequest>>,
881        responses: Mutex<Vec<HttpResponse>>,
882    }
883
884    impl DummyClient {
885        fn new(responses: Vec<HttpResponse>) -> Self {
886            DummyClient {
887                requests: Mutex::new(Vec::new()),
888                responses: Mutex::new(responses),
889            }
890        }
891
892        fn take_requests(&self) -> Vec<HttpRequest> {
893            std::mem::take(&mut *self.requests.lock().unwrap())
894        }
895    }
896
897    impl HttpClient for DummyClient {
898        fn request(
899            &self,
900            request: HttpRequest,
901        ) -> futures::future::BoxFuture<'_, Result<HttpResponse, anyhow::Error>> {
902            self.requests.lock().unwrap().push(request);
903            let response = self.responses.lock().unwrap().remove(0);
904            Box::pin(async { Ok(response) })
905        }
906    }
907
908    #[tokio::test]
909    async fn run_known_query() {
910        let response = HttpResponse {
911            body: Some(WASMER_PACK_CLI_RESPONSE.to_vec()),
912            redirected: false,
913            status: StatusCode::OK,
914            headers: HeaderMap::new(),
915        };
916        let client = Arc::new(DummyClient::new(vec![response]));
917        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
918        let request = PackageSource::from_str("wasmer/wasmer-pack-cli@^0.6").unwrap();
919        let source = BackendSource::new(registry_endpoint, client.clone());
920
921        let summaries = source.query(&request).await.unwrap();
922
923        assert_eq!(
924            summaries,
925            [PackageSummary {
926                pkg: PackageInfo {
927                    id: PackageId::new_named("wasmer/wasmer-pack-cli", Version::new(0, 6, 0)),
928                    dependencies: Vec::new(),
929                    commands: vec![crate::runtime::resolver::Command {
930                        name: "wasmer-pack".to_string(),
931                    },],
932                    entrypoint: Some("wasmer-pack".to_string()),
933                    filesystem: vec![],
934                },
935                dist: DistributionInfo {
936                    webc: "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc"
937                        .parse()
938                        .unwrap(),
939                    webc_sha256: WebcHash::from_bytes([
940                        126, 26, 221, 22, 64, 208, 3, 127, 246, 167, 38, 205, 126, 20, 234, 54, 21,
941                        158, 194, 219, 140, 182, 222, 189, 14, 66, 250, 39, 57, 190, 165, 43,
942                    ]),
943                }
944            }]
945        );
946        let requests = client.take_requests();
947        assert_eq!(requests.len(), 1);
948        let request = &requests[0];
949        assert_eq!(request.method, http::Method::POST);
950        assert_eq!(request.url.as_str(), BackendSource::WASMER_PROD_ENDPOINT);
951        assert_eq!(request.headers.len(), 2);
952        assert_eq!(request.headers["User-Agent"], USER_AGENT);
953        assert_eq!(request.headers["Content-Type"], "application/json");
954        let body: serde_json::Value =
955            serde_json::from_slice(request.body.as_deref().unwrap()).unwrap();
956        let expected_body: serde_json::Value =
957            serde_json::from_slice(WASMER_PACK_CLI_REQUEST).unwrap();
958        assert_eq!(body, expected_body);
959    }
960
961    /// For the full context, see #3946 on GitHub or the original conversation
962    /// [on
963    /// Slack](https://wasmerio.slack.com/archives/C03MX4KL6KH/p1685706988500919).
964    #[tokio::test]
965    async fn skip_package_versions_with_missing_fields() {
966        let body = serde_json::json! {
967            {
968                "data": {
969                    "getPackage": {
970                        "packageName": "cowsay",
971                        "namespace": "_",
972                        "versions": [
973                            {
974                                "version": "0.2.0",
975                                "v2": {
976                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/_/cowsay/cowsay-0.2.0.webc",
977                                    "piritaSha256Hash": "9586938a0a89219dafe4ae97a901c56d4b3e2a9941520d1309ae880c9a1868c9",
978                                    "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}}}}}",
979                                },
980                                "v3": {
981                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/_/cowsay/cowsay-0.2.0.webc",
982                                    "piritaSha256Hash": "9586938a0a89219dafe4ae97a901c56d4b3e2a9941520d1309ae880c9a1868c9",
983                                    "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}}}}}",
984                                }
985                            },
986                            {
987                                "version": "0.1.3",
988                                "v2": {
989                                    "piritaDownloadUrl": "https://example.com/",
990                                    "piritaSha256Hash": "1234asdf"
991                                },
992                                "v3": {
993                                    "piritaDownloadUrl": "https://example.com/",
994                                    "piritaSha256Hash": "1234asdf"
995                                }
996                            },
997                            {
998                                "version": "0.1.2",
999                                "v2": {
1000                                    "piritaDownloadUrl": "https://example.com/",
1001                                    "piritaSha256Hash": "1234asdf",
1002                                    "webcManifest": "{}",
1003                                },
1004                                "v3": {
1005                                    "piritaDownloadUrl": "https://example.com/",
1006                                    "piritaSha256Hash": "1234asdf",
1007                                    "webcManifest": "{}",
1008                                }
1009                            },
1010                            {
1011                                "version": "0.1.3",
1012                                "v2": {
1013                                    "webcManifest": "{}",
1014                                    "piritaDownloadUrl": "https://example.com/",
1015                                    "piritaSha256Hash": "1234asdf"
1016                                },
1017                                "v3": {
1018                                    "webcManifest": "{}",
1019                                    "piritaDownloadUrl": "https://example.com/",
1020                                    "piritaSha256Hash": "1234asdf"
1021                                }
1022                            }
1023                        ]
1024                    },
1025                    "info": {
1026                        "defaultFrontend": "https://wasmer.io/",
1027                    },
1028                }
1029            }
1030
1031        };
1032        let response = HttpResponse {
1033            body: Some(serde_json::to_vec(&body).unwrap()),
1034            redirected: false,
1035            status: StatusCode::OK,
1036            headers: HeaderMap::new(),
1037        };
1038        let client = Arc::new(DummyClient::new(vec![response]));
1039        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1040        let request = PackageSource::from_str("_/cowsay").unwrap();
1041        let source = BackendSource::new(registry_endpoint, client.clone());
1042
1043        let summaries = source.query(&request).await.unwrap();
1044
1045        assert_eq!(summaries.len(), 1);
1046        assert_eq!(
1047            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1048            "0.2.0"
1049        );
1050    }
1051
1052    #[tokio::test]
1053    async fn skip_archived_package_versions() {
1054        let body = serde_json::json! {
1055            {
1056                "data": {
1057                    "getPackage": {
1058                        "packageName": "python",
1059                        "namespace": "wasmer",
1060                        "versions": [
1061                            {
1062                                "version": "3.12.2",
1063                                "isArchived": true,
1064                                "v2": {
1065                                    "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\"}",
1066                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.5-a11e0414-c68d-473c-958f-fc96ef7adb20.webc",
1067                                    "piritaSha256Hash": "7771ed54376c16da86581736fad84fb761a049915902a7070e854965be0d5874"
1068                                },
1069                                "v3": {
1070                                    "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\"}",
1071                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.5-a11e0414-c68d-473c-958f-fc96ef7adb20.webc",
1072                                    "piritaSha256Hash": "7771ed54376c16da86581736fad84fb761a049915902a7070e854965be0d5874"
1073                                }
1074                            },
1075                            {
1076                                "version": "3.12.1",
1077                                "isArchived": false,
1078                                "v2": {
1079                                    "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\"}",
1080                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.2-ed98c999-fcda-4f80-96dc-7c0f8be8baa6.webc",
1081                                    "piritaSha256Hash": "7835401e3ca1977ba05b5e51541363783b8a7700da270dd851f10fe2e4f27f07"
1082                                },
1083                                "v3": {
1084                                    "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\"}",
1085                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.2-ed98c999-fcda-4f80-96dc-7c0f8be8baa6.webc",
1086                                    "piritaSha256Hash": "7835401e3ca1977ba05b5e51541363783b8a7700da270dd851f10fe2e4f27f07"
1087                                }
1088                            },
1089                            {
1090                                "version": "3.12.0",
1091                                "isArchived": true,
1092                                "v2": {
1093                                    "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\"}",
1094                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-32065e5e-84fe-4483-a380-0aa750772a3a.webc",
1095                                    "piritaSha256Hash": "e5d6e9d16db988eb323e34e2c152ebfb32dc7043d6b7ddc00ad57d3beae24adb"
1096                                },
1097                                "v3": {
1098                                    "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\"}",
1099                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-32065e5e-84fe-4483-a380-0aa750772a3a.webc",
1100                                    "piritaSha256Hash": "e5d6e9d16db988eb323e34e2c152ebfb32dc7043d6b7ddc00ad57d3beae24adb"
1101                                }
1102                            },
1103                        ]
1104                    },
1105                    "info": {
1106                        "defaultFrontend": "https://wasmer.io/",
1107                    },
1108                }
1109            }
1110        };
1111        let response = HttpResponse {
1112            body: Some(serde_json::to_vec(&body).unwrap()),
1113            redirected: false,
1114            status: StatusCode::OK,
1115            headers: HeaderMap::new(),
1116        };
1117        let client = Arc::new(DummyClient::new(vec![response]));
1118        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1119        let request = PackageSource::from_str("wasmer/python").unwrap();
1120        let source = BackendSource::new(registry_endpoint, client.clone());
1121
1122        let summaries = source.query(&request).await.unwrap();
1123
1124        assert_eq!(summaries.len(), 1);
1125        assert_eq!(
1126            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1127            "3.12.1"
1128        );
1129    }
1130
1131    #[tokio::test]
1132    async fn query_the_backend_again_if_cached_queries_dont_match() {
1133        let cached_value = serde_json::from_value(serde_json::json! {
1134            {
1135                "data": {
1136                    "getPackage": {
1137                        "packageName": "python",
1138                        "namespace": "wasmer",
1139                        "versions": [
1140                            {
1141                                "version": "3.12.0",
1142                                "v2": {
1143                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1144                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1145                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1146                                },
1147                                "v3": {
1148                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1149                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1150                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1151                                }
1152                            },
1153                        ]
1154                    },
1155                    "info": {
1156                        "defaultFrontend": "https://wasmer.io/",
1157                    },
1158                }
1159            }
1160        }).unwrap();
1161        let body = serde_json::json! {
1162            {
1163                "data": {
1164                    "getPackage": {
1165                        "packageName": "python",
1166                        "namespace": "wasmer",
1167                        "versions": [
1168                            {
1169                                "version": "4.0.0",
1170                                "v2": {
1171                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"4.0.0\", \"description\": \"Python\"}}}",
1172                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@4.0.0",
1173                                    "piritaSha256Hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1174                                },
1175                                "v3": {
1176                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"4.0.0\", \"description\": \"Python\"}}}",
1177                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@4.0.0",
1178                                    "piritaSha256Hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1179                                }
1180                            },
1181                            {
1182                                "version": "3.12.0",
1183                                "v2": {
1184                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1185                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1186                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1187                                },
1188                                "v3": {
1189                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1190                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1191                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1192                                }
1193                            },
1194                        ]
1195                    },
1196                    "info": {
1197                        "defaultFrontend": "https://wasmer.io/",
1198                    },
1199                }
1200            }
1201        };
1202        let response = HttpResponse {
1203            body: Some(serde_json::to_vec(&body).unwrap()),
1204            redirected: false,
1205            status: StatusCode::OK,
1206            headers: HeaderMap::new(),
1207        };
1208        let client = Arc::new(DummyClient::new(vec![response]));
1209        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1210        let request = PackageSource::from_str("wasmer/python@4.0.0").unwrap();
1211        let temp = tempfile::tempdir().unwrap();
1212        let source = BackendSource::new(registry_endpoint, client.clone())
1213            .with_local_cache(temp.path(), Duration::from_secs(0));
1214        source
1215            .cache
1216            .as_ref()
1217            .unwrap()
1218            .update("wasmer/python", &cached_value)
1219            .unwrap();
1220
1221        let summaries = source.query(&request).await.unwrap();
1222
1223        assert_eq!(summaries.len(), 1);
1224        assert_eq!(
1225            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1226            "4.0.0"
1227        );
1228    }
1229}