From ac2e5b49e23cf3c9afb0d67de6419f9fdeda75db Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 10 Oct 2025 19:16:01 +0200 Subject: [PATCH] chore(test): Rework image upload roundtrip test Add explicit md5 checksum handling hoping to catch devstack issue. --- Cargo.lock | 15 +-- openstack_cli/Cargo.toml | 3 +- .../tests/image/v2/image/file/roundtrip.rs | 116 ++++++++++++++---- 3 files changed, 103 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be6b3b673..ab10f94b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1044,12 +1044,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "file_diff" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" - [[package]] name = "find-msvc-tools" version = "0.1.2" @@ -1969,6 +1963,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "mdbook" version = "0.4.52" @@ -2239,11 +2239,12 @@ dependencies = [ "dialoguer", "dirs", "eyre", - "file_diff", + "futures", "http", "indicatif", "itertools 0.14.0", "json-patch", + "md5", "openstack_sdk", "openstack_types", "owo-colors", diff --git a/openstack_cli/Cargo.toml b/openstack_cli/Cargo.toml index d3c0dd25a..3fd42a8cd 100644 --- a/openstack_cli/Cargo.toml +++ b/openstack_cli/Cargo.toml @@ -95,7 +95,8 @@ webauthn-rs-proto = { workspace = true, optional = true } [dev-dependencies] assert_cmd = "^2.0" -file_diff = "^1.0" +futures.workspace = true +md5 = "^0.7" rand = "^0.9" tempfile = { workspace = true } diff --git a/openstack_cli/tests/image/v2/image/file/roundtrip.rs b/openstack_cli/tests/image/v2/image/file/roundtrip.rs index d2a16bca1..1d79128d3 100644 --- a/openstack_cli/tests/image/v2/image/file/roundtrip.rs +++ b/openstack_cli/tests/image/v2/image/file/roundtrip.rs @@ -13,14 +13,77 @@ // SPDX-License-Identifier: Apache-2.0 use assert_cmd::prelude::*; -use file_diff::diff_files; +use futures::StreamExt; +use md5::Context; use rand::distr::{Alphanumeric, SampleString}; +use reqwest::{Client, header}; use serde_json::Value; -use std::fs::File; -use std::io::Cursor; -use std::io::copy; use std::process::Command; -use tempfile::Builder; +use std::{error::Error, path::PathBuf}; +use tempfile::{Builder, TempDir}; +use tokio::{fs::File, io::AsyncReadExt, io::AsyncWriteExt}; + +/// Downloads a file, saving it with the filename provided by the server. +/// Returns (final_path, md5_checksum). +pub async fn download_with_md5_and_filename( + url: &str, + tmp_dir: &TempDir, +) -> Result<(PathBuf, String), Box> { + let client = Client::new(); + let response = client.get(url).send().await?; + response.error_for_status_ref()?; // fail fast on HTTP errors + + // Try to extract filename from Content-Disposition or fallback to URL + let filename = response + .headers() + .get(header::CONTENT_DISPOSITION) + .and_then(|val| val.to_str().ok()) + .and_then(parse_filename_from_content_disposition) + .or_else(|| extract_filename_from_url(url)) + .unwrap_or_else(|| "download.bin".to_string()); + + let path = tmp_dir.path().join(&filename); + let mut file = File::create(&path).await?; + let mut context = Context::new(); + + let mut stream = response.bytes_stream(); + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result?; + file.write_all(&chunk).await?; + context.consume(&chunk); + } + + file.flush().await?; + let digest = context.compute(); + let checksum = format!("{:x}", digest); + + Ok((path, checksum)) +} + +/// Parse filename from a Content-Disposition header value. +fn parse_filename_from_content_disposition(header_value: &str) -> Option { + // Simple extraction for headers like: attachment; filename="example.txt" + header_value.split(';').find_map(|part| { + let part = part.trim(); + if part.starts_with("filename=") { + Some( + part.trim_start_matches("filename=") + .trim_matches('"') + .to_string(), + ) + } else { + None + } + }) +} + +/// Extracts filename from the URL path (fallback) +fn extract_filename_from_url(url: &str) -> Option { + url.split('/') + .filter(|s| !s.is_empty()) + .last() + .map(|s| s.to_string()) +} #[tokio::test] async fn image_upload_download_roundtrip() -> Result<(), Box> { @@ -30,20 +93,13 @@ async fn image_upload_download_roundtrip() -> Result<(), Box Result<(), Box Result<(), Box