From 6e4f06705431f88a1c1594decf3c8343e7c045c1 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Thu, 26 Dec 2024 12:08:47 +0100 Subject: [PATCH] test(tvix/nar-bridge): add NARInfo handler tests Change-Id: I245ab38c30bb27c941274b2621aecccb695dacd0 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12918 Autosubmit: flokli Tested-by: BuildkiteCI Reviewed-by: raitobezarius --- tvix/nar-bridge/src/narinfo.rs | 160 +++++++++++++++++++++++++++++++++ tvix/store/src/fixtures.rs | 15 ++++ 2 files changed, 175 insertions(+) diff --git a/tvix/nar-bridge/src/narinfo.rs b/tvix/nar-bridge/src/narinfo.rs index 8f4727ca1..448aebcd6 100644 --- a/tvix/nar-bridge/src/narinfo.rs +++ b/tvix/nar-bridge/src/narinfo.rs @@ -155,3 +155,163 @@ fn gen_narinfo_str(path_info: &PathInfo) -> String { narinfo.url = &url; narinfo.to_string() } + +#[cfg(test)] +mod tests { + use std::{num::NonZero, sync::Arc}; + + use axum::http::Method; + use nix_compat::nixbase32; + use tracing_test::traced_test; + use tvix_castore::{ + blobservice::{BlobService, MemoryBlobService}, + directoryservice::{DirectoryService, MemoryDirectoryService}, + }; + use tvix_store::{ + fixtures::{DUMMY_PATH_DIGEST, NAR_CONTENTS_SYMLINK, PATH_INFO, PATH_INFO_SYMLINK}, + path_info::PathInfo, + pathinfoservice::{MemoryPathInfoService, PathInfoService}, + }; + + use crate::AppState; + + /// Accepts a router without state, and returns a [axum_test::TestServer]. + fn gen_server( + router: axum::Router, + ) -> ( + axum_test::TestServer, + impl BlobService, + impl DirectoryService, + impl PathInfoService, + ) { + let blob_service = Arc::new(MemoryBlobService::default()); + let directory_service = Arc::new(MemoryDirectoryService::default()); + let path_info_service = Arc::new(MemoryPathInfoService::default()); + + let app = router.with_state(AppState::new( + blob_service.clone(), + directory_service.clone(), + path_info_service.clone(), + NonZero::new(100).unwrap(), + )); + + ( + axum_test::TestServer::new(app).unwrap(), + blob_service, + directory_service, + path_info_service, + ) + } + + fn gen_nix_like_narinfo(path_info: &PathInfo) -> String { + let mut narinfo = path_info.to_narinfo(); + + let url = format!("nar/{}.nar", nixbase32::encode(&path_info.nar_sha256)); + narinfo.url = &url; + narinfo.to_string() + } + + /// HEAD and GET for a NARInfo for which there's no PathInfo should fail. + #[traced_test] + #[tokio::test] + async fn test_get_head_not_found() { + let (server, _blob_service, _directory_service, _path_info_service) = + gen_server(crate::gen_router(100)); + + let url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST)); + + // HEAD + server + .method(Method::HEAD, url) + .expect_failure() + .await + .assert_status_not_found(); + + // GET + server + .get(url) + .expect_failure() + .await + .assert_status_not_found(); + } + + /// HEAD and GET for a NARInfo for which there's a PathInfo stored succeeds. + #[traced_test] + #[tokio::test] + async fn test_get_head_found() { + let (server, _blob_service, _directory_service, path_info_service) = + gen_server(crate::gen_router(100)); + + let url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST)); + + path_info_service + .put(PATH_INFO.clone()) + .await + .expect("put pathinfo"); + + server + .method(Method::HEAD, url) + .expect_success() + .await + .assert_status_ok(); + + // GET + let narinfo_bytes = server.get(url).expect_success().await.into_bytes(); + + assert_eq!(crate::narinfo::gen_narinfo_str(&PATH_INFO), narinfo_bytes); + } + + /// Uploading a NARInfo without the NAR previously uploaded should fail. + #[traced_test] + #[tokio::test] + async fn test_put_without_prev_nar_fail() { + let (server, _blob_service, _directory_service, _path_info_service) = + gen_server(crate::gen_router(100)); + + // Produce a NARInfo the same way nix does. + // FUTUREWORK: add tests for NARInfo with unsupported formats + // (again referring with compression for example) + let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK); + + server + .put(&format!( + "{}.narinfo", + nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256) + )) + .text(narinfo_str) + .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO) + .expect_failure() + .await; + } + + // Upload a NAR, then a PathInfo referring to that upload. + #[traced_test] + #[tokio::test] + async fn test_upload_nar_then_narinfo() { + let (server, _blob_service, _directory_service, _path_info_service) = + gen_server(crate::gen_router(100)); + + // upload NAR + server + .put(&format!( + "/nar/{}.nar", + nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256) + )) + .bytes(NAR_CONTENTS_SYMLINK[..].into()) + .expect_success() + .await; + + let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK); + + // upload NARInfo + server + .put(&format!( + "/{}.narinfo", + nixbase32::encode(PATH_INFO_SYMLINK.store_path.digest()) + )) + .text(narinfo_str) + .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO) + .expect_success() + .await; + } +} diff --git a/tvix/store/src/fixtures.rs b/tvix/store/src/fixtures.rs index d8c4e97fe..c861f2c65 100644 --- a/tvix/store/src/fixtures.rs +++ b/tvix/store/src/fixtures.rs @@ -1,4 +1,5 @@ use crate::pathinfoservice::PathInfo; +use md5::Digest; use nix_compat::nixhash::{CAHash, NixHash}; use nix_compat::store_path::StorePath; use std::sync::LazyLock; @@ -140,3 +141,17 @@ pub static PATH_INFO: LazyLock = LazyLock::new(|| PathInfo { deriver: None, ca: Some(CAHash::Nar(NixHash::Sha256([0; 32]))), }); + +/// A PathInfo message for the store path with CASTORE_NODE_SYMLINK as root node. +pub static PATH_INFO_SYMLINK: LazyLock = LazyLock::new(|| PathInfo { + store_path: DUMMY_PATH.clone(), + node: CASTORE_NODE_SYMLINK.clone(), + references: vec![], + nar_size: NAR_CONTENTS_SYMLINK.len() as u64, + nar_sha256: sha2::Sha256::new_with_prefix(NAR_CONTENTS_SYMLINK.as_slice()) + .finalize() + .into(), + signatures: vec![], + deriver: None, + ca: None, +});