use axum::{
body::Body,
http::{Method, Request, StatusCode, header},
};
use tower::ServiceExt;
use super::test_helpers::{build_oci_test_state, oci_test_router};
const DIGEST: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const REPO: &str = "team/assets";
const TAG: &str = "latest";
fn test_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()
}
fn sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(bytes))
}
async fn send(
app: &axum::Router,
method: Method,
uri: &str,
body: Body,
) -> axum::http::Response<Body> {
send_with_content_type(app, method, uri, body, "application/octet-stream").await
}
async fn send_with_content_type(
app: &axum::Router,
method: Method,
uri: &str,
body: Body,
content_type: &str,
) -> axum::http::Response<Body> {
let request = Request::builder()
.method(method)
.uri(uri)
.header(header::CONTENT_TYPE, content_type)
.body(body)
.unwrap();
app.clone().oneshot(request).await.unwrap()
}
async fn upload_blob(app: &axum::Router, repository: &str, data: &[u8]) -> String {
let digest = sha256_hex(data);
let uri = format!("/v2/{repository}/blobs/uploads/?digest=sha256:{digest}");
let response = send(app, Method::POST, &uri, Body::from(data.to_vec())).await;
assert_eq!(
response.status(),
StatusCode::CREATED,
"blob upload failed: {response:?}"
);
digest
}
const MANIFEST_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
async fn setup_manifest(app: &axum::Router, repository: &str, tag: &str) -> String {
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(app, repository, config_data).await;
let layer_digest = upload_blob(app, repository, layer_data).await;
let manifest_json = test_manifest_json(&config_digest, &layer_digest);
let manifest_bytes = manifest_json.as_bytes();
let manifest_digest = sha256_hex(manifest_bytes);
let uri = format!("/v2/{repository}/manifests/{tag}");
let response = send_with_content_type(
app,
Method::PUT,
&uri,
Body::from(manifest_bytes.to_vec()),
MANIFEST_MEDIA_TYPE,
)
.await;
assert_eq!(
response.status(),
StatusCode::CREATED,
"manifest PUT failed: {response:?}"
);
manifest_digest
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blob_upload_direct_with_digest() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let data = b"hello world";
let digest = sha256_hex(data);
let uri = format!("/v2/{REPO}/blobs/uploads/?digest=sha256:{digest}");
let response = send(&app, Method::POST, &uri, Body::from(data.to_vec())).await;
assert_eq!(response.status(), StatusCode::CREATED);
assert!(response.headers().get("Docker-Content-Digest").is_some());
let location = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap();
assert!(location.contains(&digest));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blob_upload_session_lifecycle() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/blobs/uploads/");
let response = send(&app, Method::POST, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::ACCEPTED);
let location = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap()
.to_owned();
let session_id = location.rsplit('/').next().unwrap().to_owned();
let patch_uri = format!("/v2/{REPO}/blobs/uploads/{session_id}");
let chunk = b"chunk data";
let response = send(&app, Method::PATCH, &patch_uri, Body::from(chunk.to_vec())).await;
assert_eq!(response.status(), StatusCode::ACCEPTED);
let range = response
.headers()
.get(header::RANGE)
.unwrap()
.to_str()
.unwrap();
assert!(range.starts_with("0-"));
let response = send(&app, Method::GET, &patch_uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NO_CONTENT);
assert!(response.headers().get(header::LOCATION).is_some());
let full_data = [chunk.as_slice(), b"final data"].concat();
let full_digest = sha256_hex(&full_data);
let put_uri = format!("/v2/{REPO}/blobs/uploads/{session_id}?digest=sha256:{full_digest}");
let response = send(
&app,
Method::PUT,
&put_uri,
Body::from(b"final data".to_vec()),
)
.await;
assert_eq!(response.status(), StatusCode::CREATED);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blob_upload_session_delete_cancels() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/blobs/uploads/");
let response = send(&app, Method::POST, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::ACCEPTED);
let location = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap()
.to_owned();
let session_id = location.rsplit('/').next().unwrap().to_owned();
let delete_uri = format!("/v2/{REPO}/blobs/uploads/{session_id}");
let response = send(&app, Method::DELETE, &delete_uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let response = send(&app, Method::GET, &delete_uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_and_get_by_tag() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let manifest_digest = setup_manifest(&app, REPO, TAG).await;
let uri = format!("/v2/{REPO}/manifests/{TAG}");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get("Docker-Content-Digest")
.unwrap()
.to_str()
.unwrap(),
format!("sha256:{manifest_digest}")
);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert!(!body.is_empty());
let doc: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(doc["schemaVersion"], 2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_get_by_digest() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let manifest_digest = setup_manifest(&app, REPO, TAG).await;
let uri = format!("/v2/{REPO}/manifests/sha256:{manifest_digest}");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_head_returns_metadata() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let manifest_digest = setup_manifest(&app, REPO, TAG).await;
let uri = format!("/v2/{REPO}/manifests/{TAG}");
let response = send(&app, Method::HEAD, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get("Docker-Content-Digest")
.unwrap()
.to_str()
.unwrap(),
format!("sha256:{manifest_digest}")
);
assert!(response.headers().get(header::CONTENT_LENGTH).is_some());
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert!(body.is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_get_missing_returns_not_found() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/manifests/nonexistent");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_delete_cleans_up_tag() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let manifest_digest = setup_manifest(&app, REPO, TAG).await;
let uri = format!("/v2/{REPO}/manifests/{TAG}");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
let delete_uri = format!("/v2/{REPO}/manifests/sha256:{manifest_digest}");
let response = send(&app, Method::DELETE, &delete_uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::ACCEPTED);
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_with_multiple_query_tags() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(&app, REPO, config_data).await;
let layer_digest = upload_blob(&app, REPO, layer_data).await;
let manifest_json = test_manifest_json(&config_digest, &layer_digest);
let manifest_bytes = manifest_json.as_bytes();
let uri = format!("/v2/{REPO}/manifests/latest?tag=v1.0");
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_bytes.to_vec()),
MANIFEST_MEDIA_TYPE,
)
.await;
assert_eq!(response.status(), StatusCode::CREATED);
let uri_latest = format!("/v2/{REPO}/manifests/latest");
let response = send(&app, Method::GET, &uri_latest, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
let uri_v1 = format!("/v2/{REPO}/manifests/v1.0");
let response = send(&app, Method::GET, &uri_v1, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tags_list_empty_repository() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/tags/list");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
let body: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(body["name"], REPO);
assert_eq!(body["tags"], serde_json::json!([]));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tags_list_with_manifest() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
setup_manifest(&app, REPO, TAG).await;
let uri = format!("/v2/{REPO}/tags/list");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
let body: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(body["name"], REPO);
let tags = body["tags"].as_array().unwrap();
assert!(tags.contains(&serde_json::json!(TAG)));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tags_list_with_n_zero_returns_empty() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
setup_manifest(&app, REPO, TAG).await;
let uri = format!("/v2/{REPO}/tags/list?n=0");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
let has_link = response.headers().get(header::LINK).is_some();
let body: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert!(!has_link);
let tags = body["tags"].as_array().unwrap();
assert!(tags.is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tags_list_with_pagination() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
for tag in ["alpha", "beta", "gamma"] {
setup_manifest(&app, REPO, tag).await;
}
let uri = format!("/v2/{REPO}/tags/list?n=2");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
let link = response.headers().get(header::LINK).cloned();
let body: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
let tags = body["tags"].as_array().unwrap();
assert_eq!(tags.len(), 2);
assert!(link.is_some(), "expected Link header for pagination");
let link_str = link.unwrap().to_str().unwrap().to_owned();
assert!(link_str.contains("rel=\"next\""));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dispatch_v2_root_returns_registry_version() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/tags/list");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dispatch_unknown_method_returns_not_found() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/manifests/latest");
let response = send(&app, Method::OPTIONS, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dispatch_invalid_path_returns_not_found() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let response = send(&app, Method::GET, "/v2/unknown/route", Body::empty()).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blob_upload_digest_mismatch_rejected() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let data = b"some data";
let wrong_digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
let uri = format!("/v2/{REPO}/blobs/uploads/?digest={wrong_digest}");
let response = send(&app, Method::POST, &uri, Body::from(data.to_vec())).await;
assert!(
response.status() == StatusCode::CREATED
|| response.status() == StatusCode::BAD_REQUEST
|| response.status() == StatusCode::NOT_FOUND,
"unexpected status: {}",
response.status()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_invalid_json_returns_bad_request() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/manifests/latest");
let response = send(&app, Method::PUT, &uri, Body::from("not valid json")).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_wrong_schema_version_returns_bad_request() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let manifest = serde_json::json!({
"schemaVersion": 1,
"mediaType": "application/vnd.oci.image.manifest.v1+json"
});
let uri = format!("/v2/{REPO}/manifests/latest");
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(serde_json::to_vec(&manifest).unwrap()),
MANIFEST_MEDIA_TYPE,
)
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_missing_config_blob_returns_bad_request() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let fake_config = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let fake_layer = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let manifest_json = test_manifest_json(fake_config, fake_layer);
let uri = format!("/v2/{REPO}/manifests/latest");
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_json.into_bytes()),
MANIFEST_MEDIA_TYPE,
)
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_digest_mismatch_returns_bad_request() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
setup_manifest(&app, REPO, "temp").await;
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(&app, REPO, config_data).await;
let layer_digest = upload_blob(&app, REPO, layer_data).await;
let manifest_json = test_manifest_json(&config_digest, &layer_digest);
let manifest_bytes = manifest_json.as_bytes();
let uri = format!(
"/v2/{REPO}/manifests/sha256:0000000000000000000000000000000000000000000000000000000000000000"
);
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_bytes.to_vec()),
MANIFEST_MEDIA_TYPE,
)
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_too_many_query_tags_rejected() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(&app, REPO, config_data).await;
let layer_digest = upload_blob(&app, REPO, layer_data).await;
let manifest_json = test_manifest_json(&config_digest, &layer_digest);
let mut uri = format!("/v2/{REPO}/manifests/latest");
for i in 0..129 {
if i == 0 {
uri.push_str(&format!("?tag=tag-{i}"));
} else {
uri.push_str(&format!("&tag=tag-{i}"));
}
}
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_json.into_bytes()),
MANIFEST_MEDIA_TYPE,
)
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_with_subject_accepted() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(&app, REPO, config_data).await;
let layer_digest = upload_blob(&app, REPO, layer_data).await;
let subject_digest = setup_manifest(&app, REPO, "subject-tag").await;
let manifest_json = serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 0,
"digest": format!("sha256:{subject_digest}")
},
"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}")
}
]
});
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let uri = format!("/v2/{REPO}/manifests/with-subject");
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_bytes),
MANIFEST_MEDIA_TYPE,
)
.await;
assert!(
response.status() == StatusCode::CREATED || response.status() == StatusCode::BAD_REQUEST,
"unexpected status: {}",
response.status()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_digest_reference_no_tags() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(&app, REPO, config_data).await;
let layer_digest = upload_blob(&app, REPO, layer_data).await;
let manifest_json = test_manifest_json(&config_digest, &layer_digest);
let manifest_bytes = manifest_json.as_bytes();
let manifest_digest = sha256_hex(manifest_bytes);
let uri = format!("/v2/{REPO}/manifests/sha256:{manifest_digest}");
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_bytes.to_vec()),
MANIFEST_MEDIA_TYPE,
)
.await;
assert_eq!(response.status(), StatusCode::CREATED);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_wrong_media_type_in_document_rejected() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(&app, REPO, config_data).await;
let layer_digest = upload_blob(&app, REPO, layer_data).await;
let manifest_json = serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+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}")
}
]
});
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let uri = format!("/v2/{REPO}/manifests/latest");
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_bytes),
MANIFEST_MEDIA_TYPE, )
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_put_unknown_media_type_rejected() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let config_data = b"{}";
let layer_data = b"\x1f\x8b\x08\x00";
let config_digest = upload_blob(&app, REPO, config_data).await;
let layer_digest = upload_blob(&app, REPO, layer_data).await;
let manifest_json = serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.unknown.type",
"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}")
}
]
});
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let uri = format!("/v2/{REPO}/manifests/latest");
let response = send_with_content_type(
&app,
Method::PUT,
&uri,
Body::from(manifest_bytes),
"application/vnd.unknown.type", )
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn manifest_delete_not_found_returns_not_found() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let non_existent = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let uri = format!("/v2/{REPO}/manifests/sha256:{non_existent}");
let response = send(&app, Method::DELETE, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blob_head_returns_content_length() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let data = b"hello blob";
let digest = upload_blob(&app, REPO, data).await;
let uri = format!("/v2/{REPO}/blobs/sha256:{digest}");
let response = send(&app, Method::HEAD, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
let content_length = response
.headers()
.get(header::CONTENT_LENGTH)
.unwrap()
.to_str()
.unwrap()
.parse::<u64>()
.unwrap();
assert_eq!(content_length, data.len() as u64);
assert_eq!(
response
.headers()
.get("Docker-Content-Digest")
.unwrap()
.to_str()
.unwrap(),
format!("sha256:{digest}")
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blob_get_returns_data() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let data = b"hello blob content";
let digest = upload_blob(&app, REPO, data).await;
let uri = format!("/v2/{REPO}/blobs/sha256:{digest}");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body[..], data);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blob_get_missing_returns_not_found() {
let ctx = build_oci_test_state().await;
let app = oci_test_router(&ctx.state);
let uri = format!("/v2/{REPO}/blobs/sha256:{DIGEST}");
let response = send(&app, Method::GET, &uri, Body::empty()).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}