remora/crates/remora-object-source/tests/validation.rs

122 lines
3.8 KiB
Rust

use remora_domain::MemberPath;
use remora_object_source::{
DestinationResumeProof, ObjectResumeCheckpoint, ObjectSourceConfig, ObjectSourceError,
ObjectSourceIdentity, ObjectSourceRequest, ObjectValidator,
};
use remora_outbound::CanonicalUrl;
use remora_stream::{Sha256Accountant, SourceBody};
use remora_target::{CompletedPart, MultipartIdentity};
fn digest(bytes: &[u8]) -> remora_stream::IntegrityDigest {
let mut accountant = Sha256Accountant::new();
accountant.update(bytes);
accountant.finalize().digest().expect("digest").clone()
}
fn source_identity(size: u64) -> ObjectSourceIdentity {
ObjectSourceIdentity::from_parts(
CanonicalUrl::parse("https://objects.example.test").expect("endpoint"),
"source-bucket",
"private/key.bin",
size,
None,
Some(ObjectValidator::parse("\"validator\"").expect("validator")),
)
.expect("identity")
}
fn destination(size: u64) -> DestinationResumeProof {
DestinationResumeProof::new(
CanonicalUrl::parse("https://target.example.test").expect("target endpoint"),
MultipartIdentity {
target_store_id: "target-a".to_owned(),
bucket: "target-bucket".to_owned(),
key: "_remora/a/attempt/o/object".to_owned(),
upload_id: "upload-1".to_owned(),
},
vec![CompletedPart {
part_number: 1,
etag: "part-etag".to_owned(),
size: Some(size),
}],
None,
)
.expect("destination")
}
#[test]
fn checkpoint_requires_exact_source_and_destination_boundary() {
let path = MemberPath::parse("dir/cafe\u{301}.bin").expect("path");
let checkpoint = ObjectResumeCheckpoint::new(
source_identity(10),
path,
4,
digest(b"abcd"),
destination(4),
)
.expect("checkpoint");
assert_eq!(checkpoint.result_path().canonical(), "dir/café.bin");
assert_eq!(checkpoint.destination().completed_bytes(), 4);
assert_eq!(checkpoint.destination().completed_part_count(), 1);
assert_eq!(
ObjectResumeCheckpoint::new(
source_identity(10),
MemberPath::parse("member.bin").expect("path"),
5,
digest(b"abcde"),
destination(4),
),
Err(ObjectSourceError::InvalidRequest)
);
}
#[test]
fn restart_only_identity_cannot_form_checkpoint() {
let identity = ObjectSourceIdentity::from_parts(
CanonicalUrl::parse("https://objects.example.test").expect("endpoint"),
"source-bucket",
"key.bin",
10,
None,
None,
)
.expect("identity");
assert!(!identity.is_resumable());
assert_eq!(
ObjectResumeCheckpoint::new(
identity,
MemberPath::parse("member.bin").expect("path"),
4,
digest(b"abcd"),
destination(4),
),
Err(ObjectSourceError::InvalidRequest)
);
}
#[test]
fn validation_and_debug_surfaces_hide_keys_and_credentials() {
let config = ObjectSourceConfig::new(
"https://objects.example.test",
"bucket",
"region",
"access-secret",
"credential-secret",
)
.expect("config");
let request = ObjectSourceRequest::new("private/token/object", "member.bin").expect("request");
let combined = format!("{config:?} {request:?} {:?}", source_identity(10));
assert!(!combined.contains("private"));
assert!(!combined.contains("token"));
assert!(!combined.contains("access-secret"));
assert!(!combined.contains("credential-secret"));
assert!(ObjectSourceRequest::new("key", "../bad").is_err());
assert!(ObjectValidator::parse("bad\r\nvalidator").is_err());
}
fn assert_source_body<T: SourceBody>() {}
#[test]
fn source_body_implements_streaming_port() {
assert_source_body::<remora_object_source::ObjectSourceBody>();
}