Skip to content

Commit d0ae59d

Browse files
test: add integration tests covering reference-tool workflows
Adds 89 new integration tests across five crates, modeled on the bootstrap and extraction workflows used by TACTSharp, wow.export, BuildBackup, CASCHost, CascLib, and tactmon. Test files added: - cascette-formats/tests/blte_fixtures.rs (25 tests) Covers BLTE parse, decompress, round-trip, MD5 content-key verification, LZ4 codec, and error rejection using binary fixtures. - cascette-formats/tests/bpsv_fixtures.rs (21 tests) Covers BPSV versions/cdns parse, field validation, seqn extraction, round-trip via CascFormat trait, and error cases using text fixtures. - cascette-installation/tests/mirror_tests.rs (14 tests) Covers MirrorConfig endpoint ordering for current vs historic builds, fallback flags, HTTPS scheme enforcement, and backoff constants. - cascette-import/tests/listfile_tests.rs (18 tests) Covers TactKeyStore TXT and CSV loading, 0x prefix parsing, comment skipping, deduplication, size limits, and ListfileProvider construction. - cascette-protocol/tests/cdn_bootstrap_tests.rs (11 tests) Covers TactClient querying BPSV endpoints via wiremock, error propagation (404, 503, invalid body), v1/products prefix rewriting, and the full two-step versions→cdns bootstrap workflow. Test fixtures added: - test_fixtures/blte/: 5 binary BLTE files (single/multi-chunk, none/zlib compression, known-content with pinned MD5) - test_fixtures/bpsv/: 3 BPSV text files matching real Blizzard CDN wire format (wow_classic_era, wow_retail with pinned build hashes) Also fixes a type mismatch in integration.rs where the public API passed Option<&TactKeyStore> to archive methods expecting Option<&dyn TactKeyProvider>. Fixed with zero-cost coercion at the two internal call sites. Workspace: 2069 tests passing.
1 parent 269a305 commit d0ae59d

14 files changed

Lines changed: 1612 additions & 3 deletions

File tree

73 Bytes
Binary file not shown.
194 Bytes
Binary file not shown.
1.57 KB
Binary file not shown.
65 Bytes
Binary file not shown.
46 Bytes
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0
2+
## seqn = 6226474
3+
us|tpr/wow_classic|blzddist1-a.akamaihd.net blzddist2-a.akamaihd.net|http://us.patch.battle.net:1119/wow_classic|tpr/configs/data
4+
eu|tpr/wow_classic|eu.cdn.blizzard.com level3.blizzard.com|http://eu.patch.battle.net:1119/wow_classic|tpr/configs/data
5+
kr|tpr/wow_classic|kr.cdn.blizzard.com level3.blizzard.com|http://kr.patch.battle.net:1119/wow_classic|tpr/configs/data
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Region!STRING:0|BuildConfig!HEX:16|CDNConfig!HEX:16|KeyRing!HEX:16|BuildId!DEC:4|VersionsName!String:0|ProductConfig!HEX:16
2+
## seqn = 6226474
3+
us|2b4e7dc7462f9ca5d2e6b5b0ce9cf1d3|aa3a0d72d7dfc97d4d3e2ecba86a9b31||6226474|1.15.5.56260|
4+
eu|2b4e7dc7462f9ca5d2e6b5b0ce9cf1d3|aa3a0d72d7dfc97d4d3e2ecba86a9b31||6226474|1.15.5.56260|
5+
kr|2b4e7dc7462f9ca5d2e6b5b0ce9cf1d3|aa3a0d72d7dfc97d4d3e2ecba86a9b31||6226474|1.15.5.56260|
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Region!STRING:0|BuildConfig!HEX:16|CDNConfig!HEX:16|KeyRing!HEX:16|BuildId!DEC:4|VersionsName!String:0|ProductConfig!HEX:16
2+
## seqn = 57212
3+
us|43a001a23efd4193a96266be43fe67d8|c67fdeccf96e2a0ddf205e0a7e8f1927||57212|11.0.7.57212|
4+
eu|43a001a23efd4193a96266be43fe67d8|c67fdeccf96e2a0ddf205e0a7e8f1927||57212|11.0.7.57212|
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
2+
//! Integration tests for BLTE format parsing using pre-built binary fixtures.
3+
//!
4+
//! Each fixture was generated by a Python script (`/tmp/gen_blte_fixtures.py`)
5+
//! that constructs valid BLTE containers matching the format used by
6+
//! Blizzard's agent.exe. The tests mirror the extraction workflow in TACTSharp
7+
//! and wow.export: fetch BLTE-wrapped data from CDN → parse → decompress →
8+
//! verify content matches expected content key (MD5).
9+
10+
use cascette_formats::CascFormat;
11+
use cascette_formats::blte::{BlteFile, CompressionMode};
12+
use std::path::Path;
13+
14+
fn fixtures_dir() -> &'static Path {
15+
Path::new(env!("CARGO_MANIFEST_DIR"))
16+
.join("test_fixtures/blte")
17+
.leak()
18+
}
19+
20+
fn read_fixture(name: &str) -> Vec<u8> {
21+
let path = fixtures_dir().join(name);
22+
std::fs::read(&path)
23+
.unwrap_or_else(|e| panic!("Failed to read fixture {}: {}", path.display(), e))
24+
}
25+
26+
// --- Single-chunk uncompressed ---
27+
28+
#[test]
29+
fn blte_single_chunk_none_parse() {
30+
let data = read_fixture("single_chunk_none.blte");
31+
let blte = BlteFile::parse(&data).expect("Single-chunk uncompressed BLTE should parse");
32+
33+
assert!(blte.header.is_single_chunk(), "Should be single-chunk");
34+
assert_eq!(blte.chunks.len(), 1);
35+
assert_eq!(blte.chunks[0].mode, CompressionMode::None);
36+
}
37+
38+
#[test]
39+
fn blte_single_chunk_none_decompress() {
40+
let data = read_fixture("single_chunk_none.blte");
41+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
42+
let decompressed = blte.decompress().expect("Decompress should succeed");
43+
44+
let expected = b"Hello from BLTE! This is uncompressed single-chunk data.";
45+
assert_eq!(decompressed, expected);
46+
}
47+
48+
#[test]
49+
fn blte_single_chunk_none_round_trip() {
50+
let data = read_fixture("single_chunk_none.blte");
51+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
52+
let rebuilt = blte.build().expect("Build should succeed");
53+
54+
// The rebuilt bytes must re-parse and decompress to the same content
55+
let reparsed = BlteFile::parse(&rebuilt).expect("Re-parse should succeed");
56+
let original_content = blte.decompress().expect("Decompress should succeed");
57+
let reparsed_content = reparsed
58+
.decompress()
59+
.expect("Reparsed decompress should succeed");
60+
assert_eq!(original_content, reparsed_content);
61+
}
62+
63+
// --- Single-chunk zlib ---
64+
65+
#[test]
66+
fn blte_single_chunk_zlib_parse() {
67+
let data = read_fixture("single_chunk_zlib.blte");
68+
let blte = BlteFile::parse(&data).expect("Single-chunk zlib BLTE should parse");
69+
70+
assert!(blte.header.is_single_chunk());
71+
assert_eq!(blte.chunks.len(), 1);
72+
assert_eq!(blte.chunks[0].mode, CompressionMode::ZLib);
73+
}
74+
75+
#[test]
76+
fn blte_single_chunk_zlib_decompress() {
77+
let data = read_fixture("single_chunk_zlib.blte");
78+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
79+
let decompressed = blte.decompress().expect("Decompress should succeed");
80+
81+
// The fixture contains "ZLib compressed content. " repeated 20 times
82+
let expected = b"ZLib compressed content. ".repeat(20);
83+
assert_eq!(decompressed, expected);
84+
}
85+
86+
#[test]
87+
fn blte_single_chunk_zlib_compressed_smaller_than_raw() {
88+
let data = read_fixture("single_chunk_zlib.blte");
89+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
90+
let decompressed = blte.decompress().expect("Decompress should succeed");
91+
92+
// Compressed file should be smaller than decompressed content for repetitive data
93+
assert!(
94+
data.len() < decompressed.len(),
95+
"BLTE file ({} bytes) should be smaller than decompressed content ({} bytes) for repetitive data",
96+
data.len(),
97+
decompressed.len()
98+
);
99+
}
100+
101+
#[test]
102+
fn blte_single_chunk_zlib_round_trip() {
103+
let data = read_fixture("single_chunk_zlib.blte");
104+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
105+
let content = blte.decompress().expect("Decompress should succeed");
106+
107+
// Re-compress and verify the content survives
108+
let rebuilt = BlteFile::single_chunk(content.clone(), CompressionMode::ZLib)
109+
.expect("Single-chunk creation should succeed");
110+
let reparsed = BlteFile::parse(&rebuilt.build().expect("Build should succeed"))
111+
.expect("Re-parse should succeed");
112+
let round_tripped = reparsed.decompress().expect("Re-decompress should succeed");
113+
114+
assert_eq!(content, round_tripped);
115+
}
116+
117+
// --- Multi-chunk uncompressed ---
118+
119+
#[test]
120+
fn blte_multi_chunk_none_parse() {
121+
let data = read_fixture("multi_chunk_none.blte");
122+
let blte = BlteFile::parse(&data).expect("Multi-chunk uncompressed BLTE should parse");
123+
124+
assert!(!blte.header.is_single_chunk(), "Should be multi-chunk");
125+
assert_eq!(blte.chunks.len(), 3, "Should have 3 chunks");
126+
127+
for chunk in &blte.chunks {
128+
assert_eq!(chunk.mode, CompressionMode::None);
129+
}
130+
}
131+
132+
#[test]
133+
fn blte_multi_chunk_none_decompress_concatenates() {
134+
let data = read_fixture("multi_chunk_none.blte");
135+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
136+
let decompressed = blte.decompress().expect("Decompress should succeed");
137+
138+
// All three chunks concatenated
139+
let expected = b"First chunk of the multi-part file. \
140+
Second chunk continues the content. \
141+
Third and final chunk of the file.\n";
142+
assert_eq!(decompressed, expected);
143+
}
144+
145+
#[test]
146+
fn blte_multi_chunk_none_chunk_info_consistency() {
147+
let data = read_fixture("multi_chunk_none.blte");
148+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
149+
150+
// Extended header chunk infos must match actual chunk sizes
151+
let extended = blte
152+
.header
153+
.extended
154+
.as_ref()
155+
.expect("Should have extended header");
156+
assert_eq!(extended.chunk_infos.len(), blte.chunks.len());
157+
158+
for (info, chunk) in extended.chunk_infos.iter().zip(&blte.chunks) {
159+
assert_eq!(
160+
info.compressed_size as usize,
161+
chunk.compressed_size(),
162+
"ChunkInfo compressed_size must match actual chunk size"
163+
);
164+
}
165+
}
166+
167+
#[test]
168+
fn blte_multi_chunk_none_round_trip() {
169+
let data = read_fixture("multi_chunk_none.blte");
170+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
171+
let original_content = blte.decompress().expect("Decompress should succeed");
172+
173+
let rebuilt = blte.build().expect("Build should succeed");
174+
let reparsed = BlteFile::parse(&rebuilt).expect("Re-parse should succeed");
175+
let round_tripped = reparsed.decompress().expect("Re-decompress should succeed");
176+
177+
assert_eq!(original_content, round_tripped);
178+
}
179+
180+
// --- Multi-chunk zlib (simulates encoding/root file wrapping) ---
181+
182+
#[test]
183+
fn blte_multi_chunk_zlib_parse() {
184+
let data = read_fixture("multi_chunk_zlib.blte");
185+
let blte = BlteFile::parse(&data).expect("Multi-chunk zlib BLTE should parse");
186+
187+
assert!(!blte.header.is_single_chunk());
188+
assert_eq!(blte.chunks.len(), 5, "Should have 5 chunks");
189+
190+
for chunk in &blte.chunks {
191+
assert_eq!(chunk.mode, CompressionMode::ZLib);
192+
}
193+
}
194+
195+
#[test]
196+
fn blte_multi_chunk_zlib_decompress_all() {
197+
let data = read_fixture("multi_chunk_zlib.blte");
198+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
199+
let decompressed = blte.decompress().expect("Decompress should succeed");
200+
201+
// Total decompressed size = 5 chunks × (chunk header + 256 bytes × 4)
202+
// Each chunk: "Chunk N: " (9 bytes) + 256*4=1024 bytes = 1033 bytes
203+
assert_eq!(
204+
decompressed.len(),
205+
5 * 1033,
206+
"Total decompressed size mismatch"
207+
);
208+
}
209+
210+
#[test]
211+
fn blte_multi_chunk_zlib_header_decompressed_sizes() {
212+
let data = read_fixture("multi_chunk_zlib.blte");
213+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
214+
215+
let extended = blte
216+
.header
217+
.extended
218+
.as_ref()
219+
.expect("Should have extended header");
220+
221+
// Each chunk decompresses to 1033 bytes
222+
for (i, info) in extended.chunk_infos.iter().enumerate() {
223+
assert_eq!(
224+
info.decompressed_size, 1033,
225+
"Chunk {i} decompressed_size should be 1033"
226+
);
227+
}
228+
}
229+
230+
#[test]
231+
fn blte_multi_chunk_zlib_round_trip() {
232+
let data = read_fixture("multi_chunk_zlib.blte");
233+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
234+
let original = blte.decompress().expect("Decompress should succeed");
235+
236+
let rebuilt = blte.build().expect("Build should succeed");
237+
let reparsed = BlteFile::parse(&rebuilt).expect("Re-parse should succeed");
238+
let round_tripped = reparsed.decompress().expect("Re-decompress should succeed");
239+
240+
assert_eq!(original, round_tripped);
241+
}
242+
243+
// --- Known-content fixture (MD5 verification, mirrors TACTSharp ExtractionTests) ---
244+
245+
#[test]
246+
fn blte_known_content_decompress_and_verify_md5() {
247+
// This mirrors the TACTSharp ExtractionTests workflow:
248+
// fetch file by CKey (MD5 of decompressed content), decompress BLTE, verify MD5.
249+
let data = read_fixture("known_content.blte");
250+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
251+
let decompressed = blte.decompress().expect("Decompress should succeed");
252+
253+
// MD5 of the decompressed content must match the known CKey
254+
let content_key = cascette_crypto::md5::ContentKey::from_data(&decompressed);
255+
let expected_md5 = "2bf0c207b4943701ca5923e027c66474";
256+
assert_eq!(
257+
format!("{content_key}"),
258+
expected_md5,
259+
"Decompressed content MD5 must match known CKey"
260+
);
261+
}
262+
263+
#[test]
264+
fn blte_known_content_payload() {
265+
let data = read_fixture("known_content.blte");
266+
let blte = BlteFile::parse(&data).expect("Parse should succeed");
267+
let decompressed = blte.decompress().expect("Decompress should succeed");
268+
269+
assert_eq!(
270+
decompressed,
271+
b"WoW Classic Era build data - pinned content for extraction tests"
272+
);
273+
}
274+
275+
// --- Header structure tests ---
276+
277+
#[test]
278+
fn blte_magic_bytes_present() {
279+
// All fixtures must start with BLTE magic
280+
for name in &[
281+
"single_chunk_none.blte",
282+
"single_chunk_zlib.blte",
283+
"multi_chunk_none.blte",
284+
"multi_chunk_zlib.blte",
285+
"known_content.blte",
286+
] {
287+
let data = read_fixture(name);
288+
assert!(data.len() >= 4, "{name}: file too short");
289+
assert_eq!(&data[0..4], b"BLTE", "{name}: missing BLTE magic");
290+
}
291+
}
292+
293+
#[test]
294+
fn blte_single_chunk_header_size_zero() {
295+
for name in &[
296+
"single_chunk_none.blte",
297+
"single_chunk_zlib.blte",
298+
"known_content.blte",
299+
] {
300+
let data = read_fixture(name);
301+
// Bytes 4..8 are header_size as big-endian u32; 0 = single-chunk
302+
let header_size = u32::from_be_bytes(data[4..8].try_into().expect("Slice is 4 bytes"));
303+
assert_eq!(header_size, 0, "{name}: single-chunk header_size must be 0");
304+
}
305+
}
306+
307+
#[test]
308+
fn blte_multi_chunk_header_size_nonzero() {
309+
for name in &["multi_chunk_none.blte", "multi_chunk_zlib.blte"] {
310+
let data = read_fixture(name);
311+
let header_size = u32::from_be_bytes(data[4..8].try_into().expect("Slice is 4 bytes"));
312+
assert!(
313+
header_size > 0,
314+
"{name}: multi-chunk header_size must be > 0"
315+
);
316+
}
317+
}
318+
319+
// --- Error handling ---
320+
321+
#[test]
322+
fn blte_wrong_magic_rejected() {
323+
let data = b"NOPE\x00\x00\x00\x00NHello";
324+
let result = BlteFile::parse(data);
325+
assert!(result.is_err(), "Wrong magic bytes must be rejected");
326+
}
327+
328+
#[test]
329+
fn blte_empty_data_rejected() {
330+
let result = BlteFile::parse(b"");
331+
assert!(result.is_err(), "Empty data must be rejected");
332+
}
333+
334+
#[test]
335+
fn blte_truncated_header_rejected() {
336+
let result = BlteFile::parse(b"BLT"); // Only 3 bytes
337+
assert!(result.is_err(), "Truncated magic must be rejected");
338+
}
339+
340+
// --- LZ4 round-trip (no fixture file, generated inline) ---
341+
342+
#[test]
343+
fn blte_lz4_single_chunk_round_trip() {
344+
// LZ4 is not in our CDN fixtures (Blizzard rarely uses it in practice)
345+
// but the codec must work end-to-end.
346+
let original = b"LZ4 test data that is long enough to actually compress. ".repeat(10);
347+
let blte = BlteFile::single_chunk(original.clone(), CompressionMode::LZ4)
348+
.expect("LZ4 single-chunk creation should succeed");
349+
350+
let built = blte.build().expect("Build should succeed");
351+
let parsed = BlteFile::parse(&built).expect("Parse should succeed");
352+
let decompressed = parsed.decompress().expect("Decompress should succeed");
353+
354+
assert_eq!(decompressed, original.as_slice());
355+
}
356+
357+
#[test]
358+
fn blte_lz4_multi_chunk_round_trip() {
359+
let chunks: Vec<Vec<u8>> = (0..3)
360+
.map(|i| {
361+
format!("LZ4 multi-chunk test data chunk {i}. ")
362+
.repeat(20)
363+
.into_bytes()
364+
})
365+
.collect();
366+
367+
let total_expected: Vec<u8> = chunks.iter().flatten().copied().collect();
368+
369+
let blte = BlteFile::compress(&total_expected, 512, CompressionMode::LZ4)
370+
.expect("LZ4 auto-chunking should succeed");
371+
372+
let built = blte.build().expect("Build should succeed");
373+
let parsed = BlteFile::parse(&built).expect("Parse should succeed");
374+
let decompressed = parsed.decompress().expect("Decompress should succeed");
375+
376+
assert_eq!(decompressed, total_expected);
377+
}

0 commit comments

Comments
 (0)