|
| 1 | +use std::sync::atomic::{AtomicBool, Ordering}; |
| 2 | +use std::sync::LazyLock; |
| 3 | +use std::{fs, str}; |
| 4 | + |
1 | 5 | use assert_cmd::Command;
|
| 6 | +use regex::bytes::Regex; |
2 | 7 |
|
3 | 8 | use crate::integration::{test_utils::env, MockEndpointBuilder, TestManager};
|
4 | 9 |
|
| 10 | +/// This regex is used to extract the boundary from the content-type header. |
| 11 | +/// We need to match the boundary, since it changes with each request. |
| 12 | +/// The regex matches the format as specified in |
| 13 | +/// https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html. |
| 14 | +static CONTENT_TYPE_REGEX: LazyLock<Regex> = LazyLock::new(|| { |
| 15 | + Regex::new( |
| 16 | + r#"^multipart\/form-data; boundary=(?<boundary>[\w'\(\)+,\-\.\/:=? ]{0,69}[\w'\(\)+,\-\.\/:=?])$"# |
| 17 | + ) |
| 18 | + .expect("Regex is valid") |
| 19 | +}); |
| 20 | + |
5 | 21 | #[test]
|
6 | 22 | fn command_debug_files_upload() {
|
7 | 23 | TestManager::new()
|
@@ -189,3 +205,110 @@ fn ensure_correct_assemble_call() {
|
189 | 205 | manager.assert_mock_endpoints();
|
190 | 206 | command_result.success();
|
191 | 207 | }
|
| 208 | + |
| 209 | +#[test] |
| 210 | +/// This test simulates a full chunk upload (with only one chunk). |
| 211 | +/// It verifies that the Sentry CLI makes the expected API calls to the chunk upload endpoint |
| 212 | +/// and that the data sent to the chunk upload endpoint is exactly as expected. |
| 213 | +/// It also verifies that the correct calls are made to the assemble endpoint. |
| 214 | +fn ensure_correct_chunk_upload() { |
| 215 | + let is_first_assemble_call = AtomicBool::new(true); |
| 216 | + let expected_chunk_body = |
| 217 | + fs::read("tests/integration/_expected_requests/debug_files/upload/chunk_upload.bin") |
| 218 | + .expect("expected chunk body file should be present"); |
| 219 | + |
| 220 | + let manager = TestManager::new() |
| 221 | + .mock_endpoint( |
| 222 | + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") |
| 223 | + .with_response_file("debug_files/get-chunk-upload.json"), |
| 224 | + ) |
| 225 | + .mock_endpoint( |
| 226 | + MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/chunk-upload/") |
| 227 | + .with_response_fn(move |request| { |
| 228 | + let content_type_headers = request.header("content-type"); |
| 229 | + assert_eq!( |
| 230 | + content_type_headers.len(), |
| 231 | + 1, |
| 232 | + "content-type header should be present exactly once, found {} times", |
| 233 | + content_type_headers.len() |
| 234 | + ); |
| 235 | + |
| 236 | + let content_type = content_type_headers[0].as_bytes(); |
| 237 | + |
| 238 | + let boundary = CONTENT_TYPE_REGEX |
| 239 | + .captures(content_type) |
| 240 | + .expect("content-type should match regex") |
| 241 | + .name("boundary") |
| 242 | + .expect("boundary should be present") |
| 243 | + .as_bytes(); |
| 244 | + |
| 245 | + let boundary_str = str::from_utf8(boundary).expect("boundary should be valid utf-8"); |
| 246 | + |
| 247 | + let boundary_escaped = regex::escape(boundary_str); |
| 248 | + |
| 249 | + let body_regex = Regex::new(&format!( |
| 250 | + r#"^--{boundary_escaped}(?<chunk_body>(?s-u:.)*?)--{boundary_escaped}--\s*$"# |
| 251 | + )) |
| 252 | + .expect("regex should be valid"); |
| 253 | + |
| 254 | + let body = request.body().expect("body should be readable"); |
| 255 | + |
| 256 | + let chunk_body = body_regex |
| 257 | + .captures(body) |
| 258 | + .expect("body should match regex") |
| 259 | + .name("chunk_body") |
| 260 | + .expect("chunk_body section should be present") |
| 261 | + .as_bytes(); |
| 262 | + |
| 263 | + assert_eq!(chunk_body, expected_chunk_body); |
| 264 | + |
| 265 | + vec![] // Client does not expect a response body |
| 266 | + }), |
| 267 | + ) |
| 268 | + .mock_endpoint( |
| 269 | + MockEndpointBuilder::new( |
| 270 | + "POST", |
| 271 | + "/api/0/projects/wat-org/wat-project/files/difs/assemble/", |
| 272 | + ) |
| 273 | + .with_header_matcher("content-type", "application/json") |
| 274 | + .with_matcher(r#"{"21b76b717dbbd8c89e42d92b29667ac87aa3c124":{"name":"SrcGenSampleApp.pdb","debug_id":"c02651ae-cd6f-492d-bc33-0b83111e7106-8d8e7c60","chunks":["21b76b717dbbd8c89e42d92b29667ac87aa3c124"]}}"#) |
| 275 | + .with_response_fn(move |_| { |
| 276 | + if is_first_assemble_call.swap(false, Ordering::Relaxed) { |
| 277 | + r#"{ |
| 278 | + "21b76b717dbbd8c89e42d92b29667ac87aa3c124": { |
| 279 | + "state": "not_found", |
| 280 | + "missingChunks": ["21b76b717dbbd8c89e42d92b29667ac87aa3c124"] |
| 281 | + } |
| 282 | + }"# |
| 283 | + } else { |
| 284 | + r#"{ |
| 285 | + "21b76b717dbbd8c89e42d92b29667ac87aa3c124": { |
| 286 | + "state": "created", |
| 287 | + "missingChunks": [] |
| 288 | + } |
| 289 | + }"# |
| 290 | + } |
| 291 | + .into() |
| 292 | + }) |
| 293 | + .expect(2), |
| 294 | + ); |
| 295 | + |
| 296 | + let mut command = Command::cargo_bin("sentry-cli").expect("sentry-cli should be available"); |
| 297 | + |
| 298 | + command.args( |
| 299 | + "debug-files upload --include-sources tests/integration/_fixtures/SrcGenSampleApp.pdb" |
| 300 | + .split(' '), |
| 301 | + ); |
| 302 | + |
| 303 | + env::set_all(manager.server_info(), |k, v| { |
| 304 | + command.env(k, v.as_ref()); |
| 305 | + }); |
| 306 | + |
| 307 | + let command_result = command.assert(); |
| 308 | + |
| 309 | + // First assert the mock was called as expected, then that the command was successful. |
| 310 | + // This is because failure with the mock assertion can cause the command to fail, and |
| 311 | + // the mock assertion failure is likely more interesting in this case. |
| 312 | + manager.assert_mock_endpoints(); |
| 313 | + command_result.success(); |
| 314 | +} |
0 commit comments