NYTPuzzleFetcherTests.swift (2219B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 /// Pins down the response bounds on the NYT download path: the byte budget 7 /// applied while streaming, the `Content-Length` preflight, and the 8 /// content-type gate. 9 @Suite("NYTPuzzleFetcher limits") 10 struct NYTPuzzleFetcherTests { 11 12 private func byteStream(_ data: Data) -> AsyncStream<UInt8> { 13 AsyncStream { continuation in 14 for byte in data { continuation.yield(byte) } 15 continuation.finish() 16 } 17 } 18 19 @Test("A body exactly at the byte budget is accepted") 20 func bodyAtLimitIsAccepted() async throws { 21 let data = try await NYTPuzzleFetcher.boundedResponseData( 22 from: byteStream(Data(count: 100)), 23 limit: 100, 24 expectedLength: -1 25 ) 26 #expect(data.count == 100) 27 } 28 29 @Test("A body over the byte budget is rejected mid-stream") 30 func bodyOverLimitIsRejected() async { 31 await #expect(throws: NYTFetchError.self) { 32 _ = try await NYTPuzzleFetcher.boundedResponseData( 33 from: byteStream(Data(count: 101)), 34 limit: 100, 35 expectedLength: -1 36 ) 37 } 38 } 39 40 @Test("An oversized Content-Length is rejected before reading") 41 func oversizedContentLengthIsRejected() async { 42 await #expect(throws: NYTFetchError.self) { 43 _ = try await NYTPuzzleFetcher.boundedResponseData( 44 from: byteStream(Data()), 45 limit: 100, 46 expectedLength: 101 47 ) 48 } 49 } 50 51 @Test("JSON content types pass; others are rejected; missing is allowed") 52 func contentTypeGate() { 53 #expect(NYTPuzzleFetcher.isJSONContentType(nil)) 54 #expect(NYTPuzzleFetcher.isJSONContentType("application/json")) 55 #expect(NYTPuzzleFetcher.isJSONContentType("application/json; charset=utf-8")) 56 #expect(NYTPuzzleFetcher.isJSONContentType("application/vnd.api+json")) 57 #expect(!NYTPuzzleFetcher.isJSONContentType("text/html")) 58 #expect(!NYTPuzzleFetcher.isJSONContentType("text/html; charset=utf-8")) 59 #expect(!NYTPuzzleFetcher.isJSONContentType("application/octet-stream")) 60 } 61 }