#![cfg(feature = "docker")]
#![allow(
clippy::indexing_slicing,
clippy::unwrap_used,
clippy::let_underscore_must_use,
clippy::shadow_unrelated,
clippy::expect_used,
clippy::panic,
clippy::needless_borrows_for_generic_args
)]
use sha2::{Digest, Sha256};
use shardline_protocol::{
RepositoryProvider, RepositoryScope, SecretString, TokenClaims, TokenScope,
};
use shardline_server::{ObjectStorageAdapter, ServerConfig, ServerFrontend, ServerRole, app};
use shardline_server_core::{AuthProvider, auth::LocalHmacProvider};
use shardline_storage::S3ObjectStoreConfig;
use shardline_test_support::DockerLocalStack;
use std::{
num::{NonZeroU64, NonZeroUsize},
time::Duration,
};
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::sync::OnceCell;
use tower::ServiceExt;
static PG_S3: OnceCell<(DockerLocalStack, String, String)> = OnceCell::const_new();
async fn ensure_stack() -> (&'static str, &'static str) {
let (_, pg_url, s3_prefix) = PG_S3
.get_or_init(|| async {
let stack = DockerLocalStack::builder()
.with_postgres()
.with_minio()
.start()
.unwrap()
.expect("Docker stack: is docker available?");
let base = stack.postgres_url().unwrap();
let pg_url = format!("{base}?sslmode=disable");
let mut last_err = None;
for _ in 0..5 {
match sqlx::PgPool::connect(&pg_url).await {
Ok(pool) => {
shardline_server::apply_database_migrations(&pool)
.await
.unwrap();
pool.close().await;
let s3_prefix = stack.unique_s3_key_prefix("postgres-s3-e2e-http");
return (stack, pg_url, s3_prefix);
}
Err(e) => {
last_err = Some(e);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
}
}
panic!("Failed to connect to Postgres after 5 retries: {last_err:?}");
})
.await;
(pg_url, s3_prefix)
}
fn s3_config(key_prefix: &str) -> S3ObjectStoreConfig {
let raw = PG_S3
.get()
.expect("ensure_stack() not called yet")
.0
.s3_raw_config(Some(key_prefix))
.expect("MinIO should be configured");
S3ObjectStoreConfig::new(raw.bucket, raw.region)
.with_endpoint(raw.endpoint)
.with_credentials(
raw.access_key.map(SecretString::new),
raw.secret_key.map(SecretString::new),
raw.session_token.map(SecretString::new),
)
.with_key_prefix(raw.key_prefix.as_deref())
.with_allow_http(raw.allow_http)
}
const TEST_SIGNING_KEY: &[u8] = b"0123456789abcdef0123456789abcdef";
struct TestServer {
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
base_url: String,
token: String,
_tmp: TempDir,
}
impl TestServer {
async fn start(frontends: &[ServerFrontend]) -> Self {
let (pg_url, s3_prefix) = ensure_stack().await;
let tmp = TempDir::new().unwrap();
let chunk_size = NonZeroUsize::new(65536).unwrap();
let config = ServerConfig::new(
"127.0.0.1:0".parse().unwrap(),
"http://127.0.0.1:8080".to_owned(),
tmp.path().to_path_buf(),
chunk_size,
)
.with_server_role(ServerRole::All)
.with_server_frontends(frontends.to_vec())
.unwrap()
.with_token_signing_key(TEST_SIGNING_KEY.to_vec())
.unwrap()
.with_index_postgres_url(pg_url.to_owned())
.unwrap()
.with_reconstruction_cache_disabled()
.with_object_storage(ObjectStorageAdapter::S3, Some(s3_config(s3_prefix)));
config.validate_runtime_requirements().unwrap();
let app = app::router(config).await.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let base_url = format!("http://{addr}");
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async {
shutdown_rx.await.ok();
})
.await
.ok();
});
tokio::time::sleep(Duration::from_millis(100)).await;
let provider = LocalHmacProvider::new(TEST_SIGNING_KEY).unwrap();
let repo = RepositoryScope::new(RepositoryProvider::Generic, "test", "test", Some("main"))
.unwrap();
let claims =
TokenClaims::new("shardline", "test", TokenScope::Write, repo, u64::MAX).unwrap();
let token = provider.mint_token(&claims).unwrap();
Self {
shutdown: Some(shutdown_tx),
base_url,
token,
_tmp: tmp,
}
}
fn url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
fn auth_header(&self) -> &str {
&self.token
}
}
impl Drop for TestServer {
fn drop(&mut self) {
if let Some(tx) = self.shutdown.take() {
let _ = tx.send(());
}
}
}
struct TestServerBuilder {
frontends: Vec<ServerFrontend>,
provider_config: Option<(TempDir, std::path::PathBuf)>,
}
impl TestServerBuilder {
fn new(frontends: &[ServerFrontend]) -> Self {
Self {
frontends: frontends.to_vec(),
provider_config: None,
}
}
fn with_provider(mut self) -> Self {
let tmp = TempDir::new().unwrap();
let config_path = tmp.path().join("providers.json");
let provider_config = serde_json::json!({
"providers": [
{
"kind": "generic",
"integration_subject": "test-app",
"webhook_secret": "test-secret",
"repositories": [{
"owner": "test",
"name": "test",
"visibility": "private",
"default_revision": "main",
"clone_url": "https://example.com/test/test.git",
"read_subjects": ["test-user"],
"write_subjects": ["test-user"]
}]
},
{
"kind": "codeberg",
"integration_subject": "codeberg-app",
"webhook_secret": "codeberg-secret",
"repositories": [{
"owner": "codeberg-team",
"name": "codeberg-repo",
"visibility": "private",
"default_revision": "main",
"clone_url": "https://codeberg.example/codeberg-team/codeberg-repo.git",
"read_subjects": ["codeberg-user"],
"write_subjects": ["codeberg-user"]
}]
}
]
});
std::fs::write(&config_path, serde_json::to_vec(&provider_config).unwrap()).unwrap();
self.provider_config = Some((tmp, config_path));
self
}
async fn start(&mut self) -> TestServer {
let (pg_url, s3_prefix) = ensure_stack().await;
let tmp = TempDir::new().unwrap();
let chunk_size = NonZeroUsize::new(65536).unwrap();
let mut config = ServerConfig::new(
"127.0.0.1:0".parse().unwrap(),
"http://127.0.0.1:8080".to_owned(),
tmp.path().to_path_buf(),
chunk_size,
)
.with_server_role(ServerRole::All)
.with_server_frontends(self.frontends.clone())
.unwrap()
.with_token_signing_key(TEST_SIGNING_KEY.to_vec())
.unwrap()
.with_index_postgres_url(pg_url.to_owned())
.unwrap()
.with_reconstruction_cache_disabled()
.with_object_storage(ObjectStorageAdapter::S3, Some(s3_config(s3_prefix)));
if let Some((_provider_tmp, config_path)) = self.provider_config.as_ref() {
config = config
.with_provider_runtime(
config_path.clone(),
b"test-api-key".to_vec(),
"test-issuer".to_owned(),
NonZeroU64::new(3600).unwrap(),
)
.unwrap();
}
config.validate_runtime_requirements().unwrap();
let app = app::router(config).await.unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let base_url = format!("http://{addr}");
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async {
shutdown_rx.await.ok();
})
.await
.ok();
});
tokio::time::sleep(Duration::from_millis(100)).await;
let provider = LocalHmacProvider::new(TEST_SIGNING_KEY).unwrap();
let repo = RepositoryScope::new(RepositoryProvider::Generic, "test", "test", Some("main"))
.unwrap();
let claims =
TokenClaims::new("shardline", "test", TokenScope::Write, repo, u64::MAX).unwrap();
let token = provider.mint_token(&claims).unwrap();
TestServer {
shutdown: Some(shutdown_tx),
base_url,
token,
_tmp: tmp,
}
}
}
fn sha256_hex(data: &[u8]) -> String {
hex::encode(Sha256::digest(data))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_healthz_returns_200() {
let server = TestServer::start(&[ServerFrontend::Xet]).await;
let client = reqwest::Client::new();
let resp = client.get(server.url("/healthz")).send().await.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["status"], "ok");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_readyz_returns_postgres_and_s3_backends() {
let server = TestServer::start(&[ServerFrontend::Xet]).await;
let client = reqwest::Client::new();
let resp = client.get(server.url("/readyz")).send().await.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["status"], "ok");
assert_eq!(json["metadata_backend"], "postgres");
assert_eq!(json["object_backend"], "s3");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stats_without_auth_returns_401() {
let server = TestServer::start(&[ServerFrontend::Xet]).await;
let client = reqwest::Client::new();
let resp = client.get(server.url("/v1/stats")).send().await.unwrap();
assert_eq!(resp.status(), 401);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stats_with_auth_returns_200() {
let server = TestServer::start(&[ServerFrontend::Xet]).await;
let client = reqwest::Client::new();
let resp = client
.get(server.url("/v1/stats"))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["chunks"], 0);
assert_eq!(json["files"], 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lfs_put_and_get() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let content = b"Hello, LFS over HTTP!";
let oid = sha256_hex(content);
let put_resp = client
.put(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
assert!(
put_resp.status().is_success(),
"LFS PUT failed: {}",
put_resp.status()
);
let get_resp = client
.get(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(get_resp.status(), 200);
let body = get_resp.bytes().await.unwrap();
assert_eq!(body.as_ref(), content);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_bazel_cas_put_and_get() {
let server = TestServer::start(&[ServerFrontend::BazelHttp]).await;
let client = reqwest::Client::new();
let content = b"Bazel CAS content addressed!";
let hash = sha256_hex(content);
let put_resp = client
.put(server.url(&format!("/v1/bazel/cache/cas/{hash}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
assert!(
put_resp.status().is_success(),
"Bazel CAS PUT failed: {}",
put_resp.status()
);
let get_resp = client
.get(server.url(&format!("/v1/bazel/cache/cas/{hash}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(get_resp.status(), 200);
let body = get_resp.bytes().await.unwrap();
assert_eq!(body.as_ref(), content);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oci_v2_root_returns_200() {
let server = TestServer::start(&[ServerFrontend::Oci]).await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let resp = client
.get(server.url("/v2/"))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers()
.get("Docker-Distribution-API-Version")
.and_then(|v| v.to_str().ok()),
Some("registry/2.0")
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oci_upload_session() {
let content = b"OCI session upload content";
let digest = sha256_hex(content);
let repo = "test/test";
let (pg_url, s3_prefix) = ensure_stack().await;
let tmp = TempDir::new().unwrap();
let chunk_size = NonZeroUsize::new(65536).unwrap();
let config = ServerConfig::new(
"127.0.0.1:0".parse().unwrap(),
"http://127.0.0.1:8080".to_owned(),
tmp.path().to_path_buf(),
chunk_size,
)
.with_server_role(ServerRole::All)
.with_server_frontends([ServerFrontend::Oci])
.unwrap()
.with_token_signing_key(TEST_SIGNING_KEY.to_vec())
.unwrap()
.with_index_postgres_url(pg_url.to_owned())
.unwrap()
.with_reconstruction_cache_disabled()
.with_object_storage(ObjectStorageAdapter::S3, Some(s3_config(s3_prefix)));
config.validate_runtime_requirements().unwrap();
let app = app::router(config).await.unwrap();
let token = {
let provider = LocalHmacProvider::new(TEST_SIGNING_KEY).unwrap();
let repo = RepositoryScope::new(RepositoryProvider::Generic, "test", "test", Some("main"))
.unwrap();
let claims =
TokenClaims::new("shardline", "test", TokenScope::Write, repo, u64::MAX).unwrap();
provider.mint_token(&claims).unwrap()
};
let uri = format!("/v2/{repo}/blobs/uploads/?digest=sha256:{digest}");
let request = axum::http::Request::builder()
.method("POST")
.uri(&uri)
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/octet-stream")
.body(axum::body::Body::from(content.to_vec()))
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
status,
201,
"OCI monolithic upload (oneshot) failed: status={} body={:?}",
status,
String::from_utf8_lossy(&body)
);
let get_uri = format!("/v2/{repo}/blobs/sha256:{digest}");
let get_request = axum::http::Request::builder()
.method("GET")
.uri(&get_uri)
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap();
let get_response = app.oneshot(get_request).await.unwrap();
assert_eq!(get_response.status(), 200);
let get_body = axum::body::to_bytes(get_response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(get_body.as_ref(), content);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hub_whoami_returns_200() {
let server = TestServer::start(&[ServerFrontend::Hub]).await;
let client = reqwest::Client::new();
let resp = client
.get(server.url("/api/whoami-v2"))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_metrics_contains_expected_content() {
let server = TestServer::start(&[ServerFrontend::Xet]).await;
let client = reqwest::Client::new();
let resp = client.get(server.url("/metrics")).send().await.unwrap();
assert_eq!(resp.status(), 200);
let body = resp.text().await.unwrap();
assert!(
body.contains("shardline_up 1"),
"metrics should contain shardline_up gauge"
);
assert!(body.contains("# HELP"), "metrics should contain HELP lines");
assert!(body.contains("# TYPE"), "metrics should contain TYPE lines");
assert!(
body.contains("metadata_backend=\"postgres\""),
"metrics should reference postgres backend: {body}"
);
assert!(
body.contains("object_backend=\"s3\""),
"metrics should reference s3 backend: {body}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_auth_rejection_without_token() {
let server = TestServer::start(&[ServerFrontend::Xet]).await;
let client = reqwest::Client::new();
let endpoints = &[
"/v1/stats",
"/v1/reconstructions/nonexistent",
"/v1/chunks/default/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
];
for endpoint in endpoints {
let resp = client.get(server.url(endpoint)).send().await.unwrap();
assert_eq!(
resp.status(),
401,
"endpoint {endpoint} should return 401 without auth, got {}",
resp.status()
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_all_frontends_health_and_ready() {
let frontends = &[
ServerFrontend::Xet,
ServerFrontend::Lfs,
ServerFrontend::BazelHttp,
ServerFrontend::Oci,
ServerFrontend::Hub,
];
let server = TestServer::start(frontends).await;
let client = reqwest::Client::new();
let resp = client.get(server.url("/healthz")).send().await.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["status"], "ok");
let resp = client.get(server.url("/readyz")).send().await.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["status"], "ok");
assert_eq!(json["metadata_backend"], "postgres");
assert_eq!(json["object_backend"], "s3");
assert!(json["server_frontends"].as_array().unwrap().len() >= 5);
}
async fn oci_oneshot_app() -> (axum::Router, String) {
let (pg_url, s3_prefix) = ensure_stack().await;
let tmp = TempDir::new().unwrap();
let chunk_size = NonZeroUsize::new(65536).unwrap();
let config = ServerConfig::new(
"127.0.0.1:0".parse().unwrap(),
"http://127.0.0.1:8080".to_owned(),
tmp.path().to_path_buf(),
chunk_size,
)
.with_server_role(ServerRole::All)
.with_server_frontends([ServerFrontend::Oci])
.unwrap()
.with_token_signing_key(TEST_SIGNING_KEY.to_vec())
.unwrap()
.with_index_postgres_url(pg_url.to_owned())
.unwrap()
.with_reconstruction_cache_disabled()
.with_object_storage(ObjectStorageAdapter::S3, Some(s3_config(s3_prefix)));
config.validate_runtime_requirements().unwrap();
let app = app::router(config).await.unwrap();
let provider = LocalHmacProvider::new(TEST_SIGNING_KEY).unwrap();
let repo_s =
RepositoryScope::new(RepositoryProvider::Generic, "test", "test", Some("main")).unwrap();
let claims =
TokenClaims::new("shardline", "test", TokenScope::Write, repo_s, u64::MAX).unwrap();
let token = provider.mint_token(&claims).unwrap();
let _ = Box::new(tmp);
(app, token)
}
fn oci_digest_hex(data: &[u8]) -> String {
sha256_hex(data)
}
async fn oci_upload_blob_oneshot(
app: &axum::Router,
token: &str,
repo: &str,
data: &[u8],
) -> String {
let digest = oci_digest_hex(data);
let uri = format!("/v2/{repo}/blobs/uploads/?digest=sha256:{digest}");
let req = axum::http::Request::builder()
.method("POST")
.uri(&uri)
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/octet-stream")
.body(axum::body::Body::from(data.to_vec()))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 201, "blob upload: status={}", resp.status());
digest
}
fn oci_manifest_json(config_digest: &str, layer_digest: &str) -> String {
serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 0,
"digest": format!("sha256:{config_digest}")
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 0,
"digest": format!("sha256:{layer_digest}")
}
]
})
.to_string()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oci_manifest_push_and_get_by_tag() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let tag = "pg-s3-v0.0.1";
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = oci_upload_blob_oneshot(&app, &token, repo, config_data).await;
let layer_digest = oci_upload_blob_oneshot(&app, &token, repo, layer_data).await;
let manifest_body = oci_manifest_json(&config_digest, &layer_digest);
let manifest_digest = oci_digest_hex(manifest_body.as_bytes());
let put_uri = format!("/v2/{repo}/manifests/{tag}");
let put_req = axum::http::Request::builder()
.method("PUT")
.uri(&put_uri)
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/vnd.oci.image.manifest.v1+json")
.body(axum::body::Body::from(manifest_body))
.unwrap();
let put_resp = app.clone().oneshot(put_req).await.unwrap();
assert_eq!(put_resp.status(), 201);
let dh = put_resp
.headers()
.get("Docker-Content-Digest")
.and_then(|v| v.to_str().ok());
assert!(dh.is_some());
assert!(dh.unwrap().contains(&manifest_digest));
let get_req = axum::http::Request::builder()
.method("GET")
.uri(&format!("/v2/{repo}/manifests/{tag}"))
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap();
let get_resp = app.clone().oneshot(get_req).await.unwrap();
assert_eq!(get_resp.status(), 200);
let j: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(get_resp.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(j["schemaVersion"], 2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oci_manifest_get_by_digest() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let tag = "pg-s3-digest-ref";
let config_digest = oci_upload_blob_oneshot(&app, &token, repo, b"{}").await;
let layer_digest = oci_upload_blob_oneshot(&app, &token, repo, b"pg-s3-dl").await;
let manifest_body = oci_manifest_json(&config_digest, &layer_digest);
let manifest_digest = oci_digest_hex(manifest_body.as_bytes());
app.clone()
.oneshot(
axum::http::Request::builder()
.method("PUT")
.uri(&format!("/v2/{repo}/manifests/{tag}"))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/vnd.oci.image.manifest.v1+json")
.body(axum::body::Body::from(manifest_body))
.unwrap(),
)
.await
.unwrap();
let get_resp = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("GET")
.uri(&format!("/v2/{repo}/manifests/sha256:{manifest_digest}"))
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(get_resp.status(), 200);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oci_tags_list() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let tag = "pg-s3-tag";
let cd = oci_upload_blob_oneshot(&app, &token, repo, b"{}").await;
let ld = oci_upload_blob_oneshot(&app, &token, repo, b"pg-s3-tl").await;
app.clone()
.oneshot(
axum::http::Request::builder()
.method("PUT")
.uri(&format!("/v2/{repo}/manifests/{tag}"))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/vnd.oci.image.manifest.v1+json")
.body(axum::body::Body::from(oci_manifest_json(&cd, &ld)))
.unwrap(),
)
.await
.unwrap();
let list_resp = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("GET")
.uri(&format!("/v2/{repo}/tags/list"))
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(list_resp.status(), 200);
let j: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(list_resp.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(j["name"], repo);
assert!(j["tags"].as_array().unwrap().iter().any(|t| t == tag));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oci_blob_head() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let content = b"pg-s3-head-content";
let digest = oci_upload_blob_oneshot(&app, &token, repo, content).await;
let head_resp = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("HEAD")
.uri(&format!("/v2/{repo}/blobs/sha256:{digest}"))
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(head_resp.status(), 200);
let cl: u64 = head_resp
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
assert_eq!(cl, content.len() as u64);
assert!(head_resp.headers().get("Docker-Content-Digest").is_some());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lfs_batch_upload_existing() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let content = b"pg-s3-batch-test";
let oid = sha256_hex(content);
client
.put(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
let batch_resp = client.post(server.url("/v1/lfs/objects/batch"))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/vnd.git-lfs+json")
.json(&serde_json::json!({"operation":"upload","objects":[{"oid":oid,"size":content.len() as u64}]}))
.send().await.unwrap();
assert_eq!(batch_resp.status(), 200);
let j: serde_json::Value = batch_resp.json().await.unwrap();
assert_eq!(j["objects"][0]["oid"], oid);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lfs_batch_download_missing() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let batch_resp = client.post(server.url("/v1/lfs/objects/batch"))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/vnd.git-lfs+json")
.json(&serde_json::json!({"operation":"download","objects":[{"oid":"f".repeat(64),"size":42}]}))
.send().await.unwrap();
assert_eq!(batch_resp.status(), 200);
let j: serde_json::Value = batch_resp.json().await.unwrap();
assert!(j["objects"][0].get("error").is_some());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lfs_head_object() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let content = b"pg-s3-head-lfs";
let oid = sha256_hex(content);
client
.put(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
let head = client
.head(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(head.status(), 200);
let cl: u64 = head
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
assert_eq!(cl, content.len() as u64);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lfs_patch_object() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let c1 = b"pg-s3-patch-";
let c2 = b"final";
let full = [c1.to_vec(), c2.to_vec()].concat();
let oid = sha256_hex(&full);
let total = full.len() as u64;
client
.patch(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.header(
"Content-Range",
&format!("bytes 0-{}/{}", c1.len() as u64 - 1, total),
)
.body(c1.to_vec())
.send()
.await
.unwrap();
client
.patch(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.header(
"Content-Range",
&format!("bytes {}-{}/{}", c1.len(), total - 1, total),
)
.body(c2.to_vec())
.send()
.await
.unwrap();
let get = client
.get(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(get.status(), 200);
assert_eq!(get.bytes().await.unwrap().as_ref(), full.as_slice());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hub_create_repo_and_commit() {
let server = TestServer::start(&[ServerFrontend::Hub]).await;
let client = reqwest::Client::new();
let auth = || format!("Bearer {}", server.auth_header());
let (ns, name) = ("pgs3-team", "pgs3-model");
let mp = format!("{ns}/{name}");
assert_eq!(
client
.post(server.url("/api/repos/create"))
.header("Authorization", auth())
.header("Content-Type", "application/json")
.json(&serde_json::json!({"type":"model","name":&mp,"private":false}))
.send()
.await
.unwrap()
.status(),
201
);
let info = client
.get(server.url(&format!("/api/models/{ns}/{name}")))
.header("Authorization", auth())
.send()
.await
.unwrap();
assert_eq!(info.status(), 200);
assert_eq!(info.json::<serde_json::Value>().await.unwrap()["id"], mp);
let cb64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
b"PG+S3 e2e commit",
);
let ndjson = format!(
"{{\"header\":{{\"message\":\"pg-s3\",\"parentCommit\":\"\"}}}}\n{{\"file\":{{\"path\":\"f.txt\",\"content\":\"{cb64}\"}}}}"
);
assert_eq!(
client
.post(server.url(&format!("/api/models/{ns}/{name}/commit/main")))
.header("Authorization", auth())
.header("Content-Type", "application/x-ndjson")
.body(ndjson)
.send()
.await
.unwrap()
.status(),
200
);
assert_eq!(
client
.get(server.url(&format!("/api/models/{ns}/{name}/revisions")))
.header("Authorization", auth())
.send()
.await
.unwrap()
.status(),
200
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hub_upload_lfs_and_batch() {
let server = TestServer::start(&[ServerFrontend::Hub]).await;
let client = reqwest::Client::new();
let auth = || format!("Bearer {}", server.auth_header());
let content = b"pgs3-hub-lfs";
let oid = sha256_hex(content);
assert_eq!(
client
.put(server.url(&format!("/lfs/objects/{oid}")))
.header("Authorization", auth())
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap()
.status(),
200
);
let get = client
.get(server.url(&format!("/lfs/objects/{oid}")))
.header("Authorization", auth())
.send()
.await
.unwrap();
assert_eq!(get.status(), 200);
assert_eq!(get.bytes().await.unwrap().as_ref(), content);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hub_modelcard() {
let server = TestServer::start(&[ServerFrontend::Hub]).await;
let client = reqwest::Client::new();
let auth = || format!("Bearer {}", server.auth_header());
let (ns, name) = ("pgs3card-team", "pgs3card-model");
client
.post(server.url("/api/repos/create"))
.header("Authorization", auth())
.header("Content-Type", "application/json")
.json(&serde_json::json!({"type":"model","name":format!("{ns}/{name}"),"private":false}))
.send()
.await
.unwrap();
assert_eq!(
client
.get(server.url(&format!("/api/models/{ns}/{name}/modelcard")))
.header("Authorization", auth())
.send()
.await
.unwrap()
.status(),
404
);
let rb64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, b"# PG+S3 Model");
client.post(server.url(&format!("/api/models/{ns}/{name}/commit/main")))
.header("Authorization", auth()).header("Content-Type", "application/x-ndjson")
.body(format!("{{\"header\":{{\"message\":\"mc\",\"parentCommit\":\"\"}}}}\n{{\"file\":{{\"path\":\"README.md\",\"content\":\"{rb64}\"}}}}"))
.send().await.unwrap();
assert_eq!(
client
.get(server.url(&format!("/api/models/{ns}/{name}/modelcard")))
.header("Authorization", auth())
.send()
.await
.unwrap()
.status(),
200
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_error_no_auth_on_protected_endpoints() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
assert_eq!(
client
.post(server.url("/v1/lfs/objects/batch"))
.header("Content-Type", "application/vnd.git-lfs+json")
.json(&serde_json::json!({"operation":"download","objects":[]}))
.send()
.await
.unwrap()
.status(),
401
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_error_invalid_token() {
let server = TestServer::start(&[ServerFrontend::Xet]).await;
let client = reqwest::Client::new();
assert_eq!(
client
.get(server.url("/v1/stats"))
.header("Authorization", "Bearer invalid.token.here")
.send()
.await
.unwrap()
.status(),
401
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_error_oci_manifest_not_found() {
let (app, token) = oci_oneshot_app().await;
let resp = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("GET")
.uri("/v2/test/test/manifests/nonexistent-tag")
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), 404);
let j: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(j["errors"][0]["code"], "MANIFEST_UNKNOWN");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_metrics_after_lfs_upload() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let oid = sha256_hex(b"pgs3-metrics");
client
.put(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(b"pgs3-metrics".to_vec())
.send()
.await
.unwrap();
let m = client.get(server.url("/metrics")).send().await.unwrap();
assert_eq!(m.status(), 200);
assert!(m.text().await.unwrap().contains("shardline_up 1"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cross_protocol_lfs_to_oci() {
let server = TestServer::start(&[ServerFrontend::Lfs, ServerFrontend::Oci]).await;
let client = reqwest::Client::new();
let content = b"pgs3-cross";
let oid = sha256_hex(content);
client
.put(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
let get = client
.get(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(get.status(), 200);
assert_eq!(get.bytes().await.unwrap().as_ref(), content);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_provider_issue_token_with_valid_key() {
let mut builder =
TestServerBuilder::new(&[ServerFrontend::Xet, ServerFrontend::Oci]).with_provider();
let server = builder.start().await;
let client = reqwest::Client::new();
let resp = client
.post(server.url("/v1/providers/generic/tokens"))
.header("x-shardline-provider-key", "test-api-key")
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"subject": "test-user",
"owner": "test",
"repo": "test",
"revision": null,
"scope": "Read"
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert!(!json["token"].as_str().unwrap().is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_provider_issue_token_without_api_key_returns_401() {
let mut builder = TestServerBuilder::new(&[ServerFrontend::Xet]).with_provider();
let server = builder.start().await;
let client = reqwest::Client::new();
let resp = client
.post(server.url("/v1/providers/generic/tokens"))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"subject": "test-user",
"owner": "test",
"repo": "test",
"revision": null,
"scope": "Read"
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 401);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_provider_git_lfs_authenticate() {
let mut builder =
TestServerBuilder::new(&[ServerFrontend::Xet, ServerFrontend::Oci]).with_provider();
let server = builder.start().await;
let client = reqwest::Client::new();
let resp = client
.post(server.url("/v1/providers/generic/git-lfs-authenticate"))
.header("x-shardline-provider-key", "test-api-key")
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"subject": "test-user",
"owner": "test",
"repo": "test",
"revision": null,
"scope": "Read"
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert!(!json["href"].as_str().unwrap().is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_provider_xet_read_token() {
let mut builder = TestServerBuilder::new(&[ServerFrontend::Xet]).with_provider();
let server = builder.start().await;
let client = reqwest::Client::new();
let resp = client
.get(server.url("/api/generic/test/test/xet-read-token/main?subject=test-user"))
.header("x-shardline-provider-key", "test-api-key")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert!(!json["accessToken"].as_str().unwrap().is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_provider_xet_write_token() {
let mut builder = TestServerBuilder::new(&[ServerFrontend::Xet]).with_provider();
let server = builder.start().await;
let client = reqwest::Client::new();
let resp = client
.get(server.url("/api/generic/test/test/xet-write-token/main?subject=test-user"))
.header("x-shardline-provider-key", "test-api-key")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert!(!json["accessToken"].as_str().unwrap().is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_codeberg_webhook_push() {
let mut builder =
TestServerBuilder::new(&[ServerFrontend::Xet, ServerFrontend::Oci]).with_provider();
let server = builder.start().await;
let client = reqwest::Client::new();
let body =
br#"{"ref":"refs/heads/main","repository":{"full_name":"codeberg-team/codeberg-repo"}}"#;
use hmac::Mac;
let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(b"codeberg-secret").unwrap();
mac.update(&body[..]);
let signature = hex::encode(mac.finalize().into_bytes());
let resp = client
.post(server.url("/v1/providers/codeberg/webhooks"))
.header("x-codeberg-event", "push")
.header("x-codeberg-delivery", "delivery-pgs3-1")
.header("x-codeberg-signature", &signature)
.body(body.to_vec())
.send()
.await
.unwrap();
assert_eq!(resp.status(), 202);
let json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["provider"], "codeberg");
assert_eq!(json["event_kind"], "revision_pushed");
assert_eq!(json["owner"], "codeberg-team");
assert_eq!(json["repo"], "codeberg-repo");
assert_eq!(json["delivery_id"], "delivery-pgs3-1");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_oci_upload_invalid_digest_algorithm() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let req = axum::http::Request::builder()
.method("POST")
.uri(&format!(
"/v2/{repo}/blobs/uploads/?digest=sha512:{}",
sha256_hex(b"x")
))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/octet-stream")
.body(axum::body::Body::from(b"x".to_vec()))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 400);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_oci_upload_body_hash_mismatch() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let wrong_digest = sha256_hex(b"different");
let req = axum::http::Request::builder()
.method("POST")
.uri(&format!(
"/v2/{repo}/blobs/uploads/?digest=sha256:{wrong_digest}"
))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/octet-stream")
.body(axum::body::Body::from(b"actual".to_vec()))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 400);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_oci_patch_nonexistent_session() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let req = axum::http::Request::builder()
.method("PATCH")
.uri(&format!(
"/v2/{repo}/blobs/uploads/00000000-0000-0000-0000-000000000000"
))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/octet-stream")
.header("Content-Range", "0-63/64")
.body(axum::body::Body::from(b"data".to_vec()))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_oci_put_finalize_without_digest() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let req = axum::http::Request::builder()
.method("PUT")
.uri(&format!(
"/v2/{repo}/blobs/uploads/00000000-0000-0000-0000-000000000000"
))
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 400);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_oci_get_nonexistent_blob() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let req = axum::http::Request::builder()
.method("GET")
.uri(&format!("/v2/{repo}/blobs/sha256:{}", "a".repeat(64)))
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_oci_delete_nonexistent_manifest() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let req = axum::http::Request::builder()
.method("DELETE")
.uri(&format!("/v2/{repo}/manifests/nonexistent-tag"))
.header("Authorization", format!("Bearer {token}"))
.body(axum::body::Body::empty())
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_oci_push_manifest_with_nonexistent_blob() {
let (app, token) = oci_oneshot_app().await;
let repo = "test/test";
let manifest_body = serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 0,
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"layers": [{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}]
})
.to_string();
let req = axum::http::Request::builder()
.method("PUT")
.uri(&format!("/v2/{repo}/manifests/nonexistent-blob-ref"))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/vnd.oci.image.manifest.v1+json")
.body(axum::body::Body::from(manifest_body))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), 400);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_lfs_verify_object() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let content = b"pgs3-verify-content";
let oid = sha256_hex(content);
client
.put(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
let verify = client
.post(server.url(&format!("/v1/lfs/objects/{oid}/verify")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(verify.status(), 200);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_lfs_verify_nonexistent() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let verify = client
.post(server.url(&format!("/v1/lfs/objects/{}/verify", "f".repeat(64))))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(verify.status(), 404);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_lfs_batch_mixed() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let existing = b"pgs3-mixed-existing";
let eoid = sha256_hex(existing);
client
.put(server.url(&format!("/v1/lfs/objects/{eoid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(existing.to_vec())
.send()
.await
.unwrap();
let batch_resp = client
.post(server.url("/v1/lfs/objects/batch"))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/vnd.git-lfs+json")
.json(&serde_json::json!({"operation":"download","objects":[
{"oid":eoid,"size":existing.len() as u64},
{"oid":"d".repeat(64),"size":99}
]}))
.send()
.await
.unwrap();
assert_eq!(batch_resp.status(), 200);
let j: serde_json::Value = batch_resp.json().await.unwrap();
assert_eq!(j["objects"].as_array().unwrap().len(), 2);
assert!(j["objects"][1].get("error").is_some());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_lfs_upload_empty() {
let server = TestServer::start(&[ServerFrontend::Lfs]).await;
let client = reqwest::Client::new();
let content = b"";
let oid = sha256_hex(content);
client
.put(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
let get = client
.get(server.url(&format!("/v1/lfs/objects/{oid}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(get.status(), 200);
assert_eq!(get.bytes().await.unwrap().as_ref(), content);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_bazel_ac_put_and_get() {
let server = TestServer::start(&[ServerFrontend::BazelHttp]).await;
let client = reqwest::Client::new();
let content = b"pgs3-ac-content";
let hash = sha256_hex(content);
client
.put(server.url(&format!("/v1/bazel/cache/ac/{hash}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
let get = client
.get(server.url(&format!("/v1/bazel/cache/ac/{hash}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(get.status(), 200);
assert_eq!(get.bytes().await.unwrap().as_ref(), content);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_bazel_head_existing() {
let server = TestServer::start(&[ServerFrontend::BazelHttp]).await;
let client = reqwest::Client::new();
let content = b"pgs3-head-cas";
let hash = sha256_hex(content);
client
.put(server.url(&format!("/v1/bazel/cache/cas/{hash}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(content.to_vec())
.send()
.await
.unwrap();
let head = client
.head(server.url(&format!("/v1/bazel/cache/cas/{hash}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(head.status(), 200);
let cl: u64 = head
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
assert_eq!(cl, content.len() as u64);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_bazel_head_nonexistent() {
let server = TestServer::start(&[ServerFrontend::BazelHttp]).await;
let client = reqwest::Client::new();
let head = client
.head(server.url(&format!("/v1/bazel/cache/cas/{}", "f".repeat(64))))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.send()
.await
.unwrap();
assert_eq!(head.status(), 404);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pgs3_bazel_cas_content_hash_mismatch() {
let server = TestServer::start(&[ServerFrontend::BazelHttp]).await;
let client = reqwest::Client::new();
let wrong_hash = sha256_hex(b"different-body");
let put = client
.put(server.url(&format!("/v1/bazel/cache/cas/{wrong_hash}")))
.header("Authorization", format!("Bearer {}", server.auth_header()))
.header("Content-Type", "application/octet-stream")
.body(b"actual-body".to_vec())
.send()
.await
.unwrap();
assert_eq!(put.status(), 400);
}