122 lines
3.8 KiB
Rust
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>();
|
||
|
|
}
|