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            && 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.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, Default)]
701pub enum WebCVersion {
702    #[default]
703    V2,
704    V3,
705}
706
707impl From<WebCVersion> for webc::Version {
708    fn from(val: WebCVersion) -> Self {
709        match val {
710            WebCVersion::V2 => webc::Version::V2,
711            WebCVersion::V3 => webc::Version::V3,
712        }
713    }
714}
715
716#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
717pub struct WebQueryGetPackageVersionDistribution {
718    #[serde(rename = "piritaDownloadUrl")]
719    pub pirita_download_url: Option<Url>,
720    #[serde(rename = "piritaSha256Hash")]
721    pub pirita_sha256_hash: Option<String>,
722    #[serde(rename = "webcManifest")]
723    pub webc_manifest: Option<String>,
724}
725
726#[cfg(test)]
727mod tests {
728    use std::{str::FromStr, sync::Mutex};
729
730    use http::{HeaderMap, StatusCode};
731
732    use crate::{
733        http::HttpResponse,
734        runtime::resolver::inputs::{DistributionInfo, PackageInfo},
735    };
736
737    use super::*;
738
739    // You can check the response with:
740    // curl https://registry.wasmer.io/graphql \
741    //      -H "Content-Type: application/json" \
742    //      -X POST \
743    //      -d '@wasmer_pack_cli_request.json' > wasmer_pack_cli_response.json
744    const WASMER_PACK_CLI_REQUEST: &[u8] = br#"
745    {
746        "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}"
747    }
748    "#;
749    const WASMER_PACK_CLI_RESPONSE: &[u8] = br#"
750    {
751        "data": {
752          "getPackage": {
753            "packageName": "wasmer-pack-cli",
754            "namespace": "wasmer",
755            "versions": [
756              {
757                "version": "0.7.1",
758                "isArchived": false,
759                "v2": {
760                    "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\"}",
761                  "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.1/wasmer-pack-cli-0.7.1.webc",
762                  "piritaSha256Hash": "e821047f446dd20fb6b43a1648fe98b882276dfc480f020df6f00a49f69771fa"
763                },
764                "v3": {
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              },
770              {
771                "version": "0.7.0",
772                "isArchived": false,
773                "v2": {
774                    "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\"}",
775                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.0/wasmer-pack-cli-0.7.0.webc",
776                    "piritaSha256Hash": "d085869201aa602673f70abbd5e14e5a6936216fa93314c5b103cda3da56e29e"
777                },
778                "v3": {
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              },
784              {
785                "version": "0.6.0",
786                "isArchived": false,
787                "v2": {
788                    "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\"}",
789                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc",
790                    "piritaSha256Hash": "7e1add1640d0037ff6a726cd7e14ea36159ec2db8cb6debd0e42fa2739bea52b"
791                },
792                "v3": {
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              },
798              {
799                "version": "0.5.3",
800                "isArchived": false,
801                "v2": {
802                    "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\"}",
803                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.3/wasmer-pack-cli-0.5.3.webc",
804                    "piritaSha256Hash": "44fdcdde23d34175887243d7c375e4e4a7e6e2cd1ae063ebffbede4d1f68f14a"
805                },
806                "v3": {
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              },
812              {
813                "version": "0.5.2",
814                "isArchived": false,
815                "v2": {
816                    "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\"}",
817                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.2/wasmer-pack-cli-0.5.2.webc",
818                    "piritaSha256Hash": "d1dbc8168c3a2491a7158017a9c88df9e0c15bed88ebcd6d9d756e4b03adde95"
819                },
820                "v3": {
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              },
826              {
827                "version": "0.5.1",
828                "isArchived": false,
829                "v2": {
830                    "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\"}",
831                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.1/wasmer-pack-cli-0.5.1.webc",
832                    "piritaSha256Hash": "c42924619660e2befd69b5c72729388985dcdcbf912d51a00015237fec3e1ade"
833                },
834                "v3": {
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              },
840              {
841                "version": "0.5.0",
842                "isArchived": false,
843                "v2": {
844                    "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\"}",
845                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0/wasmer-pack-cli-0.5.0.webc",
846                    "piritaSha256Hash": "d30ca468372faa96469163d2d1546dd34be9505c680677e6ab86a528a268e5f5"
847                },
848                "v3": {
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              },
854              {
855                "version": "0.5.0-rc.1",
856                "isArchived": false,
857                "v2": {
858                    "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\"}",
859                    "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",
860                    "piritaSha256Hash": "0cd5d6e4c33c92c52784afed3a60c056953104d719717948d4663ff2521fe2bb"
861                },
862                "v3": {
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              }
868            ]
869          },
870          "info": {
871            "defaultFrontend": "https://wasmer.io"
872          }
873        }
874      }
875    "#;
876
877    #[derive(Debug)]
878    struct DummyClient {
879        requests: Mutex<Vec<HttpRequest>>,
880        responses: Mutex<Vec<HttpResponse>>,
881    }
882
883    impl DummyClient {
884        fn new(responses: Vec<HttpResponse>) -> Self {
885            DummyClient {
886                requests: Mutex::new(Vec::new()),
887                responses: Mutex::new(responses),
888            }
889        }
890
891        fn take_requests(&self) -> Vec<HttpRequest> {
892            std::mem::take(&mut *self.requests.lock().unwrap())
893        }
894    }
895
896    impl HttpClient for DummyClient {
897        fn request(
898            &self,
899            request: HttpRequest,
900        ) -> futures::future::BoxFuture<'_, Result<HttpResponse, anyhow::Error>> {
901            self.requests.lock().unwrap().push(request);
902            let response = self.responses.lock().unwrap().remove(0);
903            Box::pin(async { Ok(response) })
904        }
905    }
906
907    #[tokio::test]
908    async fn run_known_query() {
909        let response = HttpResponse {
910            body: Some(WASMER_PACK_CLI_RESPONSE.to_vec()),
911            redirected: false,
912            status: StatusCode::OK,
913            headers: HeaderMap::new(),
914        };
915        let client = Arc::new(DummyClient::new(vec![response]));
916        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
917        let request = PackageSource::from_str("wasmer/wasmer-pack-cli@^0.6").unwrap();
918        let source = BackendSource::new(registry_endpoint, client.clone());
919
920        let summaries = source.query(&request).await.unwrap();
921
922        assert_eq!(
923            summaries,
924            [PackageSummary {
925                pkg: PackageInfo {
926                    id: PackageId::new_named("wasmer/wasmer-pack-cli", Version::new(0, 6, 0)),
927                    dependencies: Vec::new(),
928                    commands: vec![crate::runtime::resolver::Command {
929                        name: "wasmer-pack".to_string(),
930                    },],
931                    entrypoint: Some("wasmer-pack".to_string()),
932                    filesystem: vec![],
933                },
934                dist: DistributionInfo {
935                    webc: "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc"
936                        .parse()
937                        .unwrap(),
938                    webc_sha256: WebcHash::from_bytes([
939                        126, 26, 221, 22, 64, 208, 3, 127, 246, 167, 38, 205, 126, 20, 234, 54, 21,
940                        158, 194, 219, 140, 182, 222, 189, 14, 66, 250, 39, 57, 190, 165, 43,
941                    ]),
942                }
943            }]
944        );
945        let requests = client.take_requests();
946        assert_eq!(requests.len(), 1);
947        let request = &requests[0];
948        assert_eq!(request.method, http::Method::POST);
949        assert_eq!(request.url.as_str(), BackendSource::WASMER_PROD_ENDPOINT);
950        assert_eq!(request.headers.len(), 2);
951        assert_eq!(request.headers["User-Agent"], USER_AGENT);
952        assert_eq!(request.headers["Content-Type"], "application/json");
953        let body: serde_json::Value =
954            serde_json::from_slice(request.body.as_deref().unwrap()).unwrap();
955        let expected_body: serde_json::Value =
956            serde_json::from_slice(WASMER_PACK_CLI_REQUEST).unwrap();
957        assert_eq!(body, expected_body);
958    }
959
960    /// For the full context, see #3946 on GitHub or the original conversation
961    /// [on
962    /// Slack](https://wasmerio.slack.com/archives/C03MX4KL6KH/p1685706988500919).
963    #[tokio::test]
964    async fn skip_package_versions_with_missing_fields() {
965        let body = serde_json::json! {
966            {
967                "data": {
968                    "getPackage": {
969                        "packageName": "cowsay",
970                        "namespace": "_",
971                        "versions": [
972                            {
973                                "version": "0.2.0",
974                                "v2": {
975                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/_/cowsay/cowsay-0.2.0.webc",
976                                    "piritaSha256Hash": "9586938a0a89219dafe4ae97a901c56d4b3e2a9941520d1309ae880c9a1868c9",
977                                    "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}}}}}",
978                                },
979                                "v3": {
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                            },
985                            {
986                                "version": "0.1.3",
987                                "v2": {
988                                    "piritaDownloadUrl": "https://example.com/",
989                                    "piritaSha256Hash": "1234asdf"
990                                },
991                                "v3": {
992                                    "piritaDownloadUrl": "https://example.com/",
993                                    "piritaSha256Hash": "1234asdf"
994                                }
995                            },
996                            {
997                                "version": "0.1.2",
998                                "v2": {
999                                    "piritaDownloadUrl": "https://example.com/",
1000                                    "piritaSha256Hash": "1234asdf",
1001                                    "webcManifest": "{}",
1002                                },
1003                                "v3": {
1004                                    "piritaDownloadUrl": "https://example.com/",
1005                                    "piritaSha256Hash": "1234asdf",
1006                                    "webcManifest": "{}",
1007                                }
1008                            },
1009                            {
1010                                "version": "0.1.3",
1011                                "v2": {
1012                                    "webcManifest": "{}",
1013                                    "piritaDownloadUrl": "https://example.com/",
1014                                    "piritaSha256Hash": "1234asdf"
1015                                },
1016                                "v3": {
1017                                    "webcManifest": "{}",
1018                                    "piritaDownloadUrl": "https://example.com/",
1019                                    "piritaSha256Hash": "1234asdf"
1020                                }
1021                            }
1022                        ]
1023                    },
1024                    "info": {
1025                        "defaultFrontend": "https://wasmer.io/",
1026                    },
1027                }
1028            }
1029
1030        };
1031        let response = HttpResponse {
1032            body: Some(serde_json::to_vec(&body).unwrap()),
1033            redirected: false,
1034            status: StatusCode::OK,
1035            headers: HeaderMap::new(),
1036        };
1037        let client = Arc::new(DummyClient::new(vec![response]));
1038        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1039        let request = PackageSource::from_str("_/cowsay").unwrap();
1040        let source = BackendSource::new(registry_endpoint, client.clone());
1041
1042        let summaries = source.query(&request).await.unwrap();
1043
1044        assert_eq!(summaries.len(), 1);
1045        assert_eq!(
1046            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1047            "0.2.0"
1048        );
1049    }
1050
1051    #[tokio::test]
1052    async fn skip_archived_package_versions() {
1053        let body = serde_json::json! {
1054            {
1055                "data": {
1056                    "getPackage": {
1057                        "packageName": "python",
1058                        "namespace": "wasmer",
1059                        "versions": [
1060                            {
1061                                "version": "3.12.2",
1062                                "isArchived": true,
1063                                "v2": {
1064                                    "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\"}",
1065                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.5-a11e0414-c68d-473c-958f-fc96ef7adb20.webc",
1066                                    "piritaSha256Hash": "7771ed54376c16da86581736fad84fb761a049915902a7070e854965be0d5874"
1067                                },
1068                                "v3": {
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                            },
1074                            {
1075                                "version": "3.12.1",
1076                                "isArchived": false,
1077                                "v2": {
1078                                    "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\"}",
1079                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.2-ed98c999-fcda-4f80-96dc-7c0f8be8baa6.webc",
1080                                    "piritaSha256Hash": "7835401e3ca1977ba05b5e51541363783b8a7700da270dd851f10fe2e4f27f07"
1081                                },
1082                                "v3": {
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                            },
1088                            {
1089                                "version": "3.12.0",
1090                                "isArchived": true,
1091                                "v2": {
1092                                    "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\"}",
1093                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-32065e5e-84fe-4483-a380-0aa750772a3a.webc",
1094                                    "piritaSha256Hash": "e5d6e9d16db988eb323e34e2c152ebfb32dc7043d6b7ddc00ad57d3beae24adb"
1095                                },
1096                                "v3": {
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                            },
1102                        ]
1103                    },
1104                    "info": {
1105                        "defaultFrontend": "https://wasmer.io/",
1106                    },
1107                }
1108            }
1109        };
1110        let response = HttpResponse {
1111            body: Some(serde_json::to_vec(&body).unwrap()),
1112            redirected: false,
1113            status: StatusCode::OK,
1114            headers: HeaderMap::new(),
1115        };
1116        let client = Arc::new(DummyClient::new(vec![response]));
1117        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1118        let request = PackageSource::from_str("wasmer/python").unwrap();
1119        let source = BackendSource::new(registry_endpoint, client.clone());
1120
1121        let summaries = source.query(&request).await.unwrap();
1122
1123        assert_eq!(summaries.len(), 1);
1124        assert_eq!(
1125            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1126            "3.12.1"
1127        );
1128    }
1129
1130    #[tokio::test]
1131    async fn query_the_backend_again_if_cached_queries_dont_match() {
1132        let cached_value = serde_json::from_value(serde_json::json! {
1133            {
1134                "data": {
1135                    "getPackage": {
1136                        "packageName": "python",
1137                        "namespace": "wasmer",
1138                        "versions": [
1139                            {
1140                                "version": "3.12.0",
1141                                "v2": {
1142                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1143                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1144                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1145                                },
1146                                "v3": {
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                            },
1152                        ]
1153                    },
1154                    "info": {
1155                        "defaultFrontend": "https://wasmer.io/",
1156                    },
1157                }
1158            }
1159        }).unwrap();
1160        let body = serde_json::json! {
1161            {
1162                "data": {
1163                    "getPackage": {
1164                        "packageName": "python",
1165                        "namespace": "wasmer",
1166                        "versions": [
1167                            {
1168                                "version": "4.0.0",
1169                                "v2": {
1170                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"4.0.0\", \"description\": \"Python\"}}}",
1171                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@4.0.0",
1172                                    "piritaSha256Hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1173                                },
1174                                "v3": {
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                            },
1180                            {
1181                                "version": "3.12.0",
1182                                "v2": {
1183                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1184                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1185                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1186                                },
1187                                "v3": {
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                            },
1193                        ]
1194                    },
1195                    "info": {
1196                        "defaultFrontend": "https://wasmer.io/",
1197                    },
1198                }
1199            }
1200        };
1201        let response = HttpResponse {
1202            body: Some(serde_json::to_vec(&body).unwrap()),
1203            redirected: false,
1204            status: StatusCode::OK,
1205            headers: HeaderMap::new(),
1206        };
1207        let client = Arc::new(DummyClient::new(vec![response]));
1208        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1209        let request = PackageSource::from_str("wasmer/python@4.0.0").unwrap();
1210        let temp = tempfile::tempdir().unwrap();
1211        let source = BackendSource::new(registry_endpoint, client.clone())
1212            .with_local_cache(temp.path(), Duration::from_secs(0));
1213        source
1214            .cache
1215            .as_ref()
1216            .unwrap()
1217            .update("wasmer/python", &cached_value)
1218            .unwrap();
1219
1220        let summaries = source.query(&request).await.unwrap();
1221
1222        assert_eq!(summaries.len(), 1);
1223        assert_eq!(
1224            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1225            "4.0.0"
1226        );
1227    }
1228}