| 1 | // vtest build: !docker-ubuntu-musl && !windows // !windows: fasthttp.Server.run not implemented; !docker-ubuntu-musl: readonly-permission assertions can fail there |
| 2 | import veb |
| 3 | import net.http |
| 4 | import os |
| 5 | import time |
| 6 | import compress.gzip |
| 7 | import compress.zstd |
| 8 | |
| 9 | const port = 14013 |
| 10 | const port_no_auto = 14014 // Port for static_compression_max_size = 0 test |
| 11 | const port_gzip_only = 14015 // Port for enable_static_gzip only test |
| 12 | const port_zstd_only = 14016 // Port for enable_static_zstd only test |
| 13 | const port_filtered_mimes = 14017 // Port for static_compression_mime_types test |
| 14 | |
| 15 | const localserver = 'http://127.0.0.1:${port}' |
| 16 | const localserver_no_auto = 'http://127.0.0.1:${port_no_auto}' |
| 17 | const localserver_gzip_only = 'http://127.0.0.1:${port_gzip_only}' |
| 18 | const localserver_zstd_only = 'http://127.0.0.1:${port_zstd_only}' |
| 19 | const localserver_filtered_mimes = 'http://127.0.0.1:${port_filtered_mimes}' |
| 20 | |
| 21 | const exit_after = time.second * 30 |
| 22 | |
| 23 | const test_file_content = 'This is a test file for gzip compression. It contains enough text to make compression worthwhile. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' |
| 24 | const filtered_css_content = 'body{margin:0;padding:0;color:#123456;background:#fafafa;}' |
| 25 | const filtered_svg_content = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><rect width="2" height="2" fill="#fff"/></svg>' |
| 26 | |
| 27 | pub struct App { |
| 28 | veb.StaticHandler |
| 29 | veb.Middleware[Context] |
| 30 | mut: |
| 31 | started chan bool |
| 32 | } |
| 33 | |
| 34 | pub fn (mut app App) before_accept_loop() { |
| 35 | app.started <- true |
| 36 | } |
| 37 | |
| 38 | pub fn (mut app App) index(mut ctx Context) veb.Result { |
| 39 | return ctx.text('Hello V!') |
| 40 | } |
| 41 | |
| 42 | pub struct Context { |
| 43 | veb.Context |
| 44 | } |
| 45 | |
| 46 | fn sanitize_cache_path_component(component string) string { |
| 47 | mut sanitized := component.trim_space() |
| 48 | if sanitized == '' { |
| 49 | return 'unknown' |
| 50 | } |
| 51 | for invalid_char in ['/', '\\', ':', '*', '?', '"', '<', '>', '|'] { |
| 52 | sanitized = sanitized.replace(invalid_char, '_') |
| 53 | } |
| 54 | return sanitized |
| 55 | } |
| 56 | |
| 57 | fn static_cache_root_for_tests() string { |
| 58 | app_dir_name := sanitize_cache_path_component(os.base(os.getwd())) |
| 59 | return os.join_path(os.cache_dir(), 'veb', 'static_compression', app_dir_name) |
| 60 | } |
| 61 | |
| 62 | fn reset_test_static_cache() { |
| 63 | os.rmdir_all(static_cache_root_for_tests()) or {} |
| 64 | } |
| 65 | |
| 66 | fn find_cached_static_file(file_name string, ext string) string { |
| 67 | cache_root := static_cache_root_for_tests() |
| 68 | if !os.exists(cache_root) { |
| 69 | return '' |
| 70 | } |
| 71 | for path in os.walk_ext(cache_root, ext) { |
| 72 | normalized := path.replace('\\', '/') |
| 73 | if normalized.contains('/${file_name}.') { |
| 74 | return path |
| 75 | } |
| 76 | } |
| 77 | return '' |
| 78 | } |
| 79 | |
| 80 | fn testsuite_begin() { |
| 81 | os.chdir(os.dir(@FILE))! |
| 82 | reset_test_static_cache() |
| 83 | |
| 84 | // Create test directory and files |
| 85 | os.mkdir_all('testdata_compression')! |
| 86 | os.write_file('testdata_compression/test.txt', test_file_content)! |
| 87 | os.write_file('testdata_compression/large.txt', test_file_content.repeat(100))! |
| 88 | |
| 89 | // Create readonly directory and file for readonly filesystem test |
| 90 | os.mkdir_all('testdata_compression/readonly')! |
| 91 | os.write_file('testdata_compression/readonly/readonly.txt', 'This is a readonly file test')! |
| 92 | |
| 93 | // Create pre-compressed file for manual .gz test |
| 94 | large_content := 'X'.repeat(2000) |
| 95 | os.write_file('testdata_compression/precompressed.txt', large_content)! |
| 96 | compressed_gz := gzip.compress(large_content.bytes()) or { panic(err) } |
| 97 | os.write_file('testdata_compression/precompressed.txt.gz', compressed_gz.bytestr())! |
| 98 | |
| 99 | // Create pre-compressed file for manual .zst test |
| 100 | os.write_file('testdata_compression/precompressed_zstd.txt', large_content)! |
| 101 | compressed_zst := zstd.compress(large_content.bytes()) or { panic(err) } |
| 102 | os.write_file('testdata_compression/precompressed_zstd.txt.zst', compressed_zst.bytestr())! |
| 103 | |
| 104 | // Create test file for zstd auto-compression |
| 105 | os.write_file('testdata_compression/zstd_test.txt', test_file_content)! |
| 106 | |
| 107 | // Create file for testing max_size = 0 (no auto-compression) |
| 108 | os.write_file('testdata_compression/no_auto.txt', 'This file should not be auto-compressed')! |
| 109 | |
| 110 | // Create files for gzip-only and zstd-only tests |
| 111 | os.write_file('testdata_compression/gzip_only_test.txt', test_file_content)! |
| 112 | os.write_file('testdata_compression/zstd_only_test.txt', test_file_content)! |
| 113 | os.write_file('testdata_compression/filtered.css', filtered_css_content)! |
| 114 | os.write_file('testdata_compression/filtered.svg', filtered_svg_content)! |
| 115 | filtered_svg_gz := gzip.compress(filtered_svg_content.bytes()) or { panic(err) } |
| 116 | os.write_file('testdata_compression/filtered.svg.gz', filtered_svg_gz.bytestr())! |
| 117 | |
| 118 | spawn fn () { |
| 119 | time.sleep(exit_after) |
| 120 | assert true == false, 'timeout reached!' |
| 121 | exit(1) |
| 122 | }() |
| 123 | |
| 124 | run_app_test() |
| 125 | run_no_auto_compression_test() |
| 126 | run_gzip_only_test() |
| 127 | run_zstd_only_test() |
| 128 | run_filtered_mime_test() |
| 129 | } |
| 130 | |
| 131 | fn testsuite_end() { |
| 132 | // Clean up test files |
| 133 | os.rmdir_all('testdata_compression') or {} |
| 134 | reset_test_static_cache() |
| 135 | } |
| 136 | |
| 137 | fn run_app_test() { |
| 138 | mut app := &App{} |
| 139 | |
| 140 | // Enable static compression (zstd/gzip) |
| 141 | app.enable_static_compression = true |
| 142 | app.static_compression_max_size = 1048576 // 1MB |
| 143 | |
| 144 | app.handle_static('testdata_compression', true) or { panic(err) } |
| 145 | |
| 146 | // Add compression middleware (gzip for this test app) |
| 147 | app.use(veb.encode_gzip[Context]()) |
| 148 | |
| 149 | spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 25, family: .ip) |
| 150 | _ := <-app.started |
| 151 | } |
| 152 | |
| 153 | fn run_no_auto_compression_test() { |
| 154 | mut app := &App{} |
| 155 | |
| 156 | // Enable static compression but disable auto-compression (max_size = 0) |
| 157 | app.enable_static_compression = true |
| 158 | app.static_compression_max_size = 0 // Disable auto-compression |
| 159 | |
| 160 | app.handle_static('testdata_compression', true) or { panic(err) } |
| 161 | |
| 162 | // Add compression middleware (gzip for this test app) |
| 163 | app.use(veb.encode_gzip[Context]()) |
| 164 | |
| 165 | spawn veb.run_at[App, Context](mut app, |
| 166 | port: port_no_auto |
| 167 | timeout_in_seconds: 25 |
| 168 | family: .ip |
| 169 | ) |
| 170 | _ := <-app.started |
| 171 | } |
| 172 | |
| 173 | fn run_gzip_only_test() { |
| 174 | mut app := &App{} |
| 175 | |
| 176 | // Enable ONLY gzip compression (not zstd, not auto) |
| 177 | app.enable_static_gzip = true |
| 178 | app.static_compression_max_size = 1048576 // 1MB |
| 179 | |
| 180 | app.handle_static('testdata_compression', true) or { panic(err) } |
| 181 | |
| 182 | spawn veb.run_at[App, Context](mut app, |
| 183 | port: port_gzip_only |
| 184 | timeout_in_seconds: 25 |
| 185 | family: .ip |
| 186 | ) |
| 187 | _ := <-app.started |
| 188 | } |
| 189 | |
| 190 | fn run_zstd_only_test() { |
| 191 | mut app := &App{} |
| 192 | |
| 193 | // Enable ONLY zstd compression (not gzip, not auto) |
| 194 | app.enable_static_zstd = true |
| 195 | app.static_compression_max_size = 1048576 // 1MB |
| 196 | |
| 197 | app.handle_static('testdata_compression', true) or { panic(err) } |
| 198 | |
| 199 | spawn veb.run_at[App, Context](mut app, |
| 200 | port: port_zstd_only |
| 201 | timeout_in_seconds: 25 |
| 202 | family: .ip |
| 203 | ) |
| 204 | _ := <-app.started |
| 205 | } |
| 206 | |
| 207 | fn run_filtered_mime_test() { |
| 208 | mut app := &App{} |
| 209 | |
| 210 | app.enable_static_compression = true |
| 211 | app.static_compression_max_size = 1048576 |
| 212 | app.static_compression_mime_types = [veb.mime_types['.css'], veb.mime_types['.js']] |
| 213 | |
| 214 | app.handle_static('testdata_compression', true) or { panic(err) } |
| 215 | |
| 216 | spawn veb.run_at[App, Context](mut app, |
| 217 | port: port_filtered_mimes |
| 218 | timeout_in_seconds: 25 |
| 219 | family: .ip |
| 220 | ) |
| 221 | _ := <-app.started |
| 222 | } |
| 223 | |
| 224 | fn test_gzip_compression_with_accept_encoding() { |
| 225 | // Request with Accept-Encoding: gzip |
| 226 | mut req := http.new_request(.get, '${localserver}/test.txt', '') |
| 227 | req.add_header(.accept_encoding, 'gzip') |
| 228 | x := req.do()! |
| 229 | |
| 230 | assert x.status() == .ok |
| 231 | assert x.header.get(.content_encoding)! == 'gzip' |
| 232 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 233 | |
| 234 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 235 | assert x.body == test_file_content |
| 236 | } |
| 237 | |
| 238 | fn test_no_compression_without_accept_encoding() { |
| 239 | // Request without Accept-Encoding header |
| 240 | x := http.get('${localserver}/test.txt')! |
| 241 | |
| 242 | assert x.status() == .ok |
| 243 | // Should not have content-encoding header when client doesn't accept gzip |
| 244 | _ := x.header.get(.content_encoding) or { |
| 245 | // Expected: no content-encoding header |
| 246 | assert x.body == test_file_content |
| 247 | return |
| 248 | } |
| 249 | assert false, 'should not compress without Accept-Encoding: gzip' |
| 250 | } |
| 251 | |
| 252 | fn test_gz_file_cache_creation() { |
| 253 | // First request creates a veb-managed .gz cache file. |
| 254 | mut req := http.new_request(.get, '${localserver}/test.txt', '') |
| 255 | req.add_header(.accept_encoding, 'gzip') |
| 256 | _ := req.do()! |
| 257 | |
| 258 | // Cache files should not be created beside the source file. |
| 259 | source_gz_path := 'testdata_compression/test.txt.gz' |
| 260 | assert !os.exists(source_gz_path), '.gz cache file should not be created beside the source file' |
| 261 | gz_path := find_cached_static_file('test.txt', '.gz') |
| 262 | assert gz_path != '', '.gz cache file should be created in veb cache dir' |
| 263 | |
| 264 | // Second request should use cached .gz file |
| 265 | y := req.do()! |
| 266 | assert y.status() == .ok |
| 267 | assert y.header.get(.content_encoding)! == 'gzip' |
| 268 | |
| 269 | // Verify Content-Length matches .gz file size (tests os.file_size() code path) |
| 270 | gz_file_size := os.file_size(gz_path) |
| 271 | content_length := y.header.get(.content_length)!.u64() |
| 272 | assert content_length == gz_file_size, 'Content-Length should match .gz file size' |
| 273 | } |
| 274 | |
| 275 | fn test_large_file_not_auto_compressed() { |
| 276 | // Configure app with very small max size to test threshold |
| 277 | // The large.txt file is ~20KB (200 chars * 100), which exceeds a 1KB threshold |
| 278 | // But we set it to 1MB, so it should still be compressed |
| 279 | // Let's test by checking if it gets compressed |
| 280 | |
| 281 | mut req := http.new_request(.get, '${localserver}/large.txt', '') |
| 282 | req.add_header(.accept_encoding, 'gzip') |
| 283 | x := req.do()! |
| 284 | |
| 285 | assert x.status() == .ok |
| 286 | // File should be compressed as it's under 1MB threshold |
| 287 | assert x.header.get(.content_encoding)! == 'gzip' |
| 288 | } |
| 289 | |
| 290 | fn test_already_compressed_flag() { |
| 291 | // Request a file that will be compressed and cached |
| 292 | mut req := http.new_request(.get, '${localserver}/test.txt', '') |
| 293 | req.add_header(.accept_encoding, 'gzip') |
| 294 | x := req.do()! |
| 295 | |
| 296 | assert x.status() == .ok |
| 297 | // The file should be compressed only once (in send_file, not by middleware) |
| 298 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 299 | assert x.body == test_file_content |
| 300 | } |
| 301 | |
| 302 | fn test_readonly_filesystem_fallback() { |
| 303 | // Test that compression works when source files are in readonly directories. |
| 304 | // Skip on Windows as readonly permissions work differently (ACL vs chmod) |
| 305 | $if windows { |
| 306 | eprintln('Skipping readonly filesystem test on Windows') |
| 307 | return |
| 308 | } |
| 309 | |
| 310 | // Make readonly directory readonly (no write permissions) |
| 311 | readonly_dir := 'testdata_compression/readonly' |
| 312 | readonly_file := '${readonly_dir}/readonly.txt' |
| 313 | |
| 314 | os.chmod(readonly_dir, 0o555)! // r-xr-xr-x |
| 315 | |
| 316 | mut req := http.new_request(.get, '${localserver}/readonly/readonly.txt', '') |
| 317 | req.add_header(.accept_encoding, 'gzip') |
| 318 | x := req.do()! |
| 319 | |
| 320 | // Restore permissions before assertions (for cleanup) |
| 321 | os.chmod(readonly_dir, 0o755) or {} // rwxr-xr-x |
| 322 | |
| 323 | assert x.status() == .ok |
| 324 | // Should still be compressed. |
| 325 | assert x.header.get(.content_encoding)! == 'gzip' |
| 326 | |
| 327 | // Verify that .gz file was NOT created beside the source file. |
| 328 | gz_path := '${readonly_file}.gz' |
| 329 | assert !os.exists(gz_path), '.gz cache file should not be created beside readonly source files' |
| 330 | |
| 331 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 332 | assert x.body == 'This is a readonly file test' |
| 333 | } |
| 334 | |
| 335 | fn test_readonly_filesystem_fallback_zstd() { |
| 336 | // Test that zstd compression works when source files are in readonly directories. |
| 337 | // Skip on Windows as readonly permissions work differently (ACL vs chmod) |
| 338 | $if windows { |
| 339 | eprintln('Skipping readonly filesystem test on Windows') |
| 340 | return |
| 341 | } |
| 342 | |
| 343 | // Make readonly directory readonly (no write permissions) |
| 344 | readonly_dir := 'testdata_compression/readonly' |
| 345 | readonly_file := '${readonly_dir}/readonly.txt' |
| 346 | |
| 347 | os.chmod(readonly_dir, 0o555)! // r-xr-xr-x |
| 348 | |
| 349 | mut req := http.new_request(.get, '${localserver}/readonly/readonly.txt', '') |
| 350 | req.add_header(.accept_encoding, 'zstd') |
| 351 | x := req.do()! |
| 352 | |
| 353 | // Restore permissions before assertions (for cleanup) |
| 354 | os.chmod(readonly_dir, 0o755) or {} // rwxr-xr-x |
| 355 | |
| 356 | assert x.status() == .ok |
| 357 | // Should still be compressed. |
| 358 | assert x.header.get(.content_encoding)! == 'zstd' |
| 359 | |
| 360 | // Verify that .zst file was NOT created beside the source file. |
| 361 | zst_path := '${readonly_file}.zst' |
| 362 | assert !os.exists(zst_path), '.zst cache file should not be created beside readonly source files' |
| 363 | |
| 364 | // Verify content is valid zstd |
| 365 | decompressed := zstd.decompress(x.body.bytes()) or { |
| 366 | assert false, 'response should be valid zstd even on readonly fs: ${err}' |
| 367 | return |
| 368 | } |
| 369 | assert decompressed.bytestr() == 'This is a readonly file test' |
| 370 | } |
| 371 | |
| 372 | fn test_precompressed_gz_file_served() { |
| 373 | // Test that manually pre-compressed .gz files are always served |
| 374 | // This validates the manual pre-compression workflow (useful with static_compression_max_size = 0) |
| 375 | |
| 376 | // Request the pre-compressed file |
| 377 | mut req := http.new_request(.get, '${localserver}/precompressed.txt', '') |
| 378 | req.add_header(.accept_encoding, 'gzip') |
| 379 | x := req.do()! |
| 380 | |
| 381 | assert x.status() == .ok |
| 382 | // Should serve the manually pre-compressed .gz file |
| 383 | assert x.header.get(.content_encoding)! == 'gzip' |
| 384 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 385 | |
| 386 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 387 | large_content := 'X'.repeat(2000) |
| 388 | assert x.body == large_content |
| 389 | } |
| 390 | |
| 391 | fn test_no_auto_compression_with_max_size_zero() { |
| 392 | // Test that static_compression_max_size = 0 disables auto-compression |
| 393 | // but still serves manually pre-compressed .gz files |
| 394 | |
| 395 | // 1. Verify manually pre-compressed .gz files are still served |
| 396 | mut req1 := http.new_request(.get, '${localserver_no_auto}/precompressed.txt', '') |
| 397 | req1.add_header(.accept_encoding, 'gzip') |
| 398 | x := req1.do()! |
| 399 | |
| 400 | assert x.status() == .ok |
| 401 | // Should serve the manually pre-compressed .gz file |
| 402 | assert x.header.get(.content_encoding)! == 'gzip' |
| 403 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 404 | |
| 405 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 406 | large_content := 'X'.repeat(2000) |
| 407 | assert x.body == large_content |
| 408 | |
| 409 | // 2. Verify auto-compression is disabled for files without .gz |
| 410 | mut req2 := http.new_request(.get, '${localserver_no_auto}/no_auto.txt', '') |
| 411 | req2.add_header(.accept_encoding, 'gzip') |
| 412 | y := req2.do()! |
| 413 | |
| 414 | assert y.status() == .ok |
| 415 | // Should NOT have content-encoding header (no auto-compression) |
| 416 | _ := y.header.get(.content_encoding) or { |
| 417 | // Expected: no content-encoding header |
| 418 | assert y.body == 'This file should not be auto-compressed' |
| 419 | return |
| 420 | } |
| 421 | assert false, 'should not auto-compress with static_compression_max_size = 0' |
| 422 | |
| 423 | // 3. Verify that .gz file was NOT created |
| 424 | gz_path := 'testdata_compression/no_auto.txt.gz' |
| 425 | assert !os.exists(gz_path), '.gz cache file should not be created with max_size = 0' |
| 426 | cache_gz_path := find_cached_static_file('no_auto.txt', '.gz') |
| 427 | assert cache_gz_path == '', '.gz cache file should not be created in veb cache with max_size = 0' |
| 428 | } |
| 429 | |
| 430 | // Zstd tests |
| 431 | |
| 432 | fn test_zstd_preferred_over_gzip() { |
| 433 | // When client supports both zstd and gzip, zstd should be preferred |
| 434 | mut req := http.new_request(.get, '${localserver}/zstd_test.txt', '') |
| 435 | req.add_header(.accept_encoding, 'gzip, zstd, br') |
| 436 | x := req.do()! |
| 437 | |
| 438 | assert x.status() == .ok |
| 439 | assert x.header.get(.content_encoding)! == 'zstd', 'zstd should be preferred over gzip' |
| 440 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 441 | |
| 442 | // Verify the body is valid zstd |
| 443 | decompressed := zstd.decompress(x.body.bytes()) or { |
| 444 | assert false, 'failed to decompress zstd response: ${err}' |
| 445 | return |
| 446 | } |
| 447 | assert decompressed.bytestr() == test_file_content |
| 448 | } |
| 449 | |
| 450 | fn test_zst_file_cache_creation() { |
| 451 | // First request should create a veb-managed .zst cache file. |
| 452 | mut req := http.new_request(.get, '${localserver}/zstd_test.txt', '') |
| 453 | req.add_header(.accept_encoding, 'zstd') |
| 454 | _ := req.do()! |
| 455 | |
| 456 | // Cache files should not be created beside the source file. |
| 457 | source_zst_path := 'testdata_compression/zstd_test.txt.zst' |
| 458 | assert !os.exists(source_zst_path), '.zst cache file should not be created beside the source file' |
| 459 | zst_path := find_cached_static_file('zstd_test.txt', '.zst') |
| 460 | assert zst_path != '', '.zst cache file should be created in veb cache dir' |
| 461 | |
| 462 | // Second request should use cached .zst file |
| 463 | y := req.do()! |
| 464 | assert y.status() == .ok |
| 465 | assert y.header.get(.content_encoding)! == 'zstd' |
| 466 | |
| 467 | // Verify Content-Length matches .zst file size |
| 468 | zst_file_size := os.file_size(zst_path) |
| 469 | content_length := y.header.get(.content_length)!.u64() |
| 470 | assert content_length == zst_file_size, 'Content-Length should match .zst file size' |
| 471 | } |
| 472 | |
| 473 | fn test_precompressed_zst_file_served() { |
| 474 | // Test that manually pre-compressed .zst files are served |
| 475 | mut req := http.new_request(.get, '${localserver}/precompressed_zstd.txt', '') |
| 476 | req.add_header(.accept_encoding, 'zstd') |
| 477 | x := req.do()! |
| 478 | |
| 479 | assert x.status() == .ok |
| 480 | assert x.header.get(.content_encoding)! == 'zstd' |
| 481 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 482 | |
| 483 | // Verify it's the pre-compressed content |
| 484 | large_content := 'X'.repeat(2000) |
| 485 | decompressed := zstd.decompress(x.body.bytes()) or { |
| 486 | assert false, 'manual .zst should be valid: ${err}' |
| 487 | return |
| 488 | } |
| 489 | assert decompressed.bytestr() == large_content |
| 490 | } |
| 491 | |
| 492 | fn test_gzip_fallback_when_zstd_not_supported() { |
| 493 | // When client only supports gzip, gzip should be used |
| 494 | mut req := http.new_request(.get, '${localserver}/zstd_test.txt', '') |
| 495 | req.add_header(.accept_encoding, 'gzip') |
| 496 | x := req.do()! |
| 497 | |
| 498 | assert x.status() == .ok |
| 499 | assert x.header.get(.content_encoding)! == 'gzip', 'should fallback to gzip when zstd not supported' |
| 500 | |
| 501 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 502 | assert x.body == test_file_content |
| 503 | } |
| 504 | |
| 505 | // Tests for enable_static_gzip only (backward compatibility) |
| 506 | |
| 507 | fn test_gzip_only_serves_gzip() { |
| 508 | // Test that enable_static_gzip alone works (backward compatibility) |
| 509 | mut req := http.new_request(.get, '${localserver_gzip_only}/gzip_only_test.txt', '') |
| 510 | req.add_header(.accept_encoding, 'gzip') |
| 511 | x := req.do()! |
| 512 | |
| 513 | assert x.status() == .ok |
| 514 | assert x.header.get(.content_encoding)! == 'gzip', 'gzip-only mode should serve gzip' |
| 515 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 516 | |
| 517 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 518 | assert x.body == test_file_content |
| 519 | } |
| 520 | |
| 521 | fn test_gzip_only_ignores_zstd_request() { |
| 522 | // Test that enable_static_gzip does NOT serve zstd even if client supports it |
| 523 | mut req := http.new_request(.get, '${localserver_gzip_only}/gzip_only_test.txt', '') |
| 524 | req.add_header(.accept_encoding, 'zstd, gzip') |
| 525 | x := req.do()! |
| 526 | |
| 527 | assert x.status() == .ok |
| 528 | // Should serve gzip, NOT zstd (because only enable_static_gzip is set) |
| 529 | assert x.header.get(.content_encoding)! == 'gzip', 'gzip-only mode should serve gzip even when client supports zstd' |
| 530 | |
| 531 | // HTTP client auto-decompresses gzip, so verify the content directly |
| 532 | assert x.body == test_file_content |
| 533 | } |
| 534 | |
| 535 | fn test_gzip_only_no_compression_without_gzip_header() { |
| 536 | // Test that enable_static_gzip does not compress when client doesn't accept gzip |
| 537 | mut req := http.new_request(.get, '${localserver_gzip_only}/gzip_only_test.txt', '') |
| 538 | req.add_header(.accept_encoding, 'zstd') // Only zstd, no gzip |
| 539 | x := req.do()! |
| 540 | |
| 541 | assert x.status() == .ok |
| 542 | // Should not have content-encoding header (no compression) |
| 543 | _ := x.header.get(.content_encoding) or { |
| 544 | // Expected: no content-encoding header |
| 545 | assert x.body == test_file_content |
| 546 | return |
| 547 | } |
| 548 | assert false, 'gzip-only mode should not compress when client only accepts zstd' |
| 549 | } |
| 550 | |
| 551 | // Tests for enable_static_zstd only |
| 552 | |
| 553 | fn test_zstd_only_serves_zstd() { |
| 554 | // Test that enable_static_zstd alone works |
| 555 | mut req := http.new_request(.get, '${localserver_zstd_only}/zstd_only_test.txt', '') |
| 556 | req.add_header(.accept_encoding, 'zstd') |
| 557 | x := req.do()! |
| 558 | |
| 559 | assert x.status() == .ok |
| 560 | assert x.header.get(.content_encoding)! == 'zstd', 'zstd-only mode should serve zstd' |
| 561 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 562 | |
| 563 | // Verify the body is valid zstd |
| 564 | decompressed := zstd.decompress(x.body.bytes()) or { |
| 565 | assert false, 'failed to decompress zstd response: ${err}' |
| 566 | return |
| 567 | } |
| 568 | assert decompressed.bytestr() == test_file_content |
| 569 | } |
| 570 | |
| 571 | fn test_zstd_only_ignores_gzip_request() { |
| 572 | // Test that enable_static_zstd does not serve gzip even if client supports it |
| 573 | mut req := http.new_request(.get, '${localserver_zstd_only}/zstd_only_test.txt', '') |
| 574 | req.add_header(.accept_encoding, 'gzip, zstd') |
| 575 | x := req.do()! |
| 576 | |
| 577 | assert x.status() == .ok |
| 578 | // Should serve zstd, not gzip (because only enable_static_zstd is set) |
| 579 | assert x.header.get(.content_encoding)! == 'zstd', 'zstd-only mode should serve zstd even when client supports gzip' |
| 580 | |
| 581 | decompressed := zstd.decompress(x.body.bytes()) or { |
| 582 | assert false, 'failed to decompress zstd response: ${err}' |
| 583 | return |
| 584 | } |
| 585 | assert decompressed.bytestr() == test_file_content |
| 586 | } |
| 587 | |
| 588 | fn test_zstd_only_no_compression_without_zstd_header() { |
| 589 | // Test that enable_static_zstd does not compress when client doesn't accept zstd |
| 590 | mut req := http.new_request(.get, '${localserver_zstd_only}/zstd_only_test.txt', '') |
| 591 | req.add_header(.accept_encoding, 'gzip') // Only gzip, no zstd |
| 592 | x := req.do()! |
| 593 | |
| 594 | assert x.status() == .ok |
| 595 | // Should not have content-encoding header (no compression) |
| 596 | _ := x.header.get(.content_encoding) or { |
| 597 | // Expected: no content-encoding header |
| 598 | assert x.body == test_file_content |
| 599 | return |
| 600 | } |
| 601 | assert false, 'zstd-only mode should not compress when client only accepts gzip' |
| 602 | } |
| 603 | |
| 604 | fn test_static_compression_mime_filter_allows_matching_types() { |
| 605 | mut req := http.new_request(.get, '${localserver_filtered_mimes}/filtered.css', '') |
| 606 | req.add_header(.accept_encoding, 'gzip') |
| 607 | x := req.do()! |
| 608 | |
| 609 | assert x.status() == .ok |
| 610 | assert x.header.get(.content_encoding)! == 'gzip' |
| 611 | assert x.header.get(.vary)! == 'Accept-Encoding' |
| 612 | assert x.body == filtered_css_content |
| 613 | |
| 614 | gz_path := find_cached_static_file('filtered.css', '.gz') |
| 615 | assert gz_path != '', 'allowed MIME type should create a cached compressed file' |
| 616 | } |
| 617 | |
| 618 | fn test_static_compression_mime_filter_skips_excluded_types() { |
| 619 | mut req := http.new_request(.get, '${localserver_filtered_mimes}/filtered.svg', '') |
| 620 | req.add_header(.accept_encoding, 'gzip') |
| 621 | x := req.do()! |
| 622 | |
| 623 | assert x.status() == .ok |
| 624 | _ := x.header.get(.content_encoding) or { |
| 625 | assert x.body == filtered_svg_content |
| 626 | return |
| 627 | } |
| 628 | assert false, 'excluded MIME type should not be served from compressed content' |
| 629 | } |
| 630 | |