Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 30 additions & 31 deletions src/utils/source_bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ fn url_to_bundle_path(url: &str) -> Result<String> {

#[cfg(test)]
mod tests {
use sha1_smol::Sha1;
use symbolic::debuginfo::sourcebundle::SourceFileType;

use crate::utils::file_upload::SourceFile;
Expand Down Expand Up @@ -229,37 +228,37 @@ mod tests {

#[test]
fn build_deterministic() {
let projects_slice = &["wat-project".into()];
let context = BundleContext {
org: "wat-org",
projects: Some(projects_slice.into()),
release: None,
dist: None,
note: None,
let make_bundle = || {
let projects_slice = &["wat-project".into()];
let context = BundleContext {
org: "wat-org",
projects: Some(projects_slice.into()),
release: None,
dist: None,
note: None,
};

let source_files = ["bundle.min.js.map", "vendor.min.js.map"]
.into_iter()
.map(|name| SourceFile {
url: format!("~/{name}"),
path: format!("tests/integration/_fixtures/{name}").into(),
contents: std::fs::read(format!("tests/integration/_fixtures/{name}"))
.unwrap()
.into(),
ty: SourceFileType::SourceMap,
headers: Default::default(),
messages: Default::default(),
already_uploaded: false,
})
.collect::<Vec<_>>();

let file = build(context, &source_files, None).unwrap();
std::fs::read(file.path()).unwrap()
};

let source_files = ["bundle.min.js.map", "vendor.min.js.map"]
.into_iter()
.map(|name| SourceFile {
url: format!("~/{name}"),
path: format!("tests/integration/_fixtures/{name}").into(),
contents: std::fs::read(format!("tests/integration/_fixtures/{name}"))
.unwrap()
.into(),
ty: SourceFileType::SourceMap,
headers: Default::default(),
messages: Default::default(),
already_uploaded: false,
})
.collect::<Vec<_>>();

let file = build(context, &source_files, None).unwrap();

let buf = std::fs::read(file.path()).unwrap();
let hash = Sha1::from(buf);
assert_eq!(
hash.digest().to_string(),
"f0e25ae149b711c510148e022ebc883ad62c7c4c"
);
let first = make_bundle();
let second = make_bundle();
assert_eq!(first, second, "bundle output should be deterministic");
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
73 changes: 30 additions & 43 deletions tests/integration/build/upload.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use crate::integration::{AssertCommand, MockEndpointBuilder, TestManager};
use regex::bytes::Regex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::LazyLock;
use std::{fs, str};

use crate::integration::test_utils::chunk_upload;
use crate::integration::{AssertCommand, MockEndpointBuilder, TestManager};

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
#[test]
Expand Down Expand Up @@ -105,26 +104,13 @@ fn command_build_upload_apk_invlid_sha() {
TestManager::new().register_trycmd_test("build/build-invalid-*-sha.trycmd");
}

/// This regex is used to extract the boundary from the content-type header.
/// We need to match the boundary, since it changes with each request.
/// The regex matches the format as specified in
/// https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html.
static CONTENT_TYPE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"^multipart\/form-data; boundary=(?<boundary>[\w'\(\)+,\-\.\/:=? ]{0,69}[\w'\(\)+,\-\.\/:=?])$"#
)
.expect("Regex is valid")
});

#[test]
/// This test simulates a full chunk upload (with only one chunk).
/// It verifies that the Sentry CLI makes the expected API calls to the chunk upload endpoint
/// and that the data sent to the chunk upload endpoint is exactly as expected.
/// It also verifies that the correct calls are made to the assemble endpoint.
fn command_build_upload_apk_chunked() {
let is_first_assemble_call = AtomicBool::new(true);
let expected_chunk_body = fs::read("tests/integration/_expected_requests/build/apk_chunk.bin")
.expect("expected chunk body file should be present");

TestManager::new()
.mock_endpoint(
Expand All @@ -134,34 +120,35 @@ fn command_build_upload_apk_chunked() {
.mock_endpoint(
MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/chunk-upload/")
.with_response_fn(move |request| {
let content_type_headers = request.header("content-type");
let boundary = chunk_upload::boundary_from_request(request)
.expect("content-type header should be a valid multipart/form-data header");

let body = request.body().expect("body should be readable");

let decompressed = chunk_upload::decompress_chunks(body, boundary)
.expect("chunks should be valid gzip data");

assert_eq!(decompressed.len(), 1, "expected exactly one chunk");

// The CLI wraps the APK in a zip bundle with metadata.
// Verify the bundle is a valid zip containing the APK.
let chunk = decompressed.iter().next().unwrap();
let cursor = std::io::Cursor::new(chunk);
let mut archive =
zip::ZipArchive::new(cursor).expect("chunk should be a valid zip");
let apk_entry = archive
.by_name("apk.apk")
.expect("bundle should contain the APK file");
let expected_size =
std::fs::metadata("tests/integration/_fixtures/build/apk.apk")
.expect("fixture file should exist")
.len();
assert_eq!(
content_type_headers.len(),
1,
"content-type header should be present exactly once, found {} times",
content_type_headers.len()
apk_entry.size(),
expected_size,
"APK size in bundle should match the fixture"
);
let content_type = content_type_headers[0].as_bytes();
let boundary = CONTENT_TYPE_REGEX
.captures(content_type)
.expect("content-type should match regex")
.name("boundary")
.expect("boundary should be present")
.as_bytes();
let boundary_str = str::from_utf8(boundary).expect("boundary should be valid utf-8");
let boundary_escaped = regex::escape(boundary_str);
let body_regex = Regex::new(&format!(
r#"^--{boundary_escaped}(?<chunk_body>(?s-u:.)*?)--{boundary_escaped}--\s*$"#
))
.expect("regex should be valid");
let body = request.body().expect("body should be readable");
let chunk_body = body_regex
.captures(body)
.expect("body should match regex")
.name("chunk_body")
.expect("chunk_body section should be present")
.as_bytes();
assert_eq!(chunk_body, expected_chunk_body);

vec![] // Client does not expect a response body
}),
)
Expand Down
Loading