PuzzleFileReaderTests.swift (3645B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 /// Pins down the streaming byte cap on filesystem puzzle ingress. The reader 7 /// never consults the reported file size, so a URL with no size metadata is 8 /// bounded the same way as any other — the cap is enforced by how many bytes 9 /// are actually read. 10 @Suite("PuzzleFileReader") 11 struct PuzzleFileReaderTests { 12 13 private static let validXD = """ 14 Title: Test Puzzle 15 Author: Alice 16 17 18 ABC 19 D#E 20 FGH 21 22 23 A1. Across 1 ~ ABC 24 A4. Across 4 ~ DE 25 A5. Across 5 ~ FGH 26 D1. Down 1 ~ ADF 27 D2. Down 2 ~ BG 28 D3. Down 3 ~ CEH 29 """ 30 31 private func writeTemp(_ data: Data, ext: String) throws -> URL { 32 let url = FileManager.default.temporaryDirectory 33 .appendingPathComponent("puzzle-reader-\(UUID().uuidString)") 34 .appendingPathExtension(ext) 35 try data.write(to: url) 36 return url 37 } 38 39 @Test("A valid .xd source is returned unchanged") 40 func readsValidXD() throws { 41 let url = try writeTemp(Data(Self.validXD.utf8), ext: "xd") 42 defer { try? FileManager.default.removeItem(at: url) } 43 44 #expect(try PuzzleFileReader.readSource(at: url) == Self.validXD) 45 } 46 47 @Test("A file exactly at the cap is read in full") 48 func boundedDataAtCap() throws { 49 let url = try writeTemp(Data(count: 1_000), ext: "bin") 50 defer { try? FileManager.default.removeItem(at: url) } 51 52 let data = try PuzzleFileReader.boundedData(at: url, limit: 1_000) 53 #expect(data.count == 1_000) 54 } 55 56 @Test("A file one byte over the cap is rejected") 57 func boundedDataOverCap() throws { 58 let url = try writeTemp(Data(count: 1_001), ext: "bin") 59 defer { try? FileManager.default.removeItem(at: url) } 60 61 #expect(throws: PuzzleFileReader.ReadError.oversized(limit: 1_000)) { 62 _ = try PuzzleFileReader.boundedData(at: url, limit: 1_000) 63 } 64 } 65 66 @Test("An oversized .xd is rejected at the source cap") 67 func oversizedXDIsRejected() throws { 68 let url = try writeTemp( 69 Data(repeating: UInt8(ascii: "A"), count: XD.maxSourceBytes + 1), 70 ext: "xd" 71 ) 72 defer { try? FileManager.default.removeItem(at: url) } 73 74 #expect(throws: PuzzleFileReader.ReadError.oversized(limit: XD.maxSourceBytes)) { 75 _ = try PuzzleFileReader.readSource(at: url) 76 } 77 } 78 79 @Test("An oversized .puz is rejected before conversion") 80 func oversizedPUZIsRejected() throws { 81 let url = try writeTemp( 82 Data(count: PUZToXDConverter.maxSourceBytes + 1), 83 ext: "puz" 84 ) 85 defer { try? FileManager.default.removeItem(at: url) } 86 87 #expect(throws: PuzzleFileReader.ReadError.oversized(limit: PUZToXDConverter.maxSourceBytes)) { 88 _ = try PuzzleFileReader.readSource(at: url) 89 } 90 } 91 92 @Test("Non-UTF-8 .xd bytes are rejected") 93 func nonUTF8XDIsRejected() throws { 94 let url = try writeTemp(Data([0xFF, 0xFE, 0x41, 0x42]), ext: "xd") 95 defer { try? FileManager.default.removeItem(at: url) } 96 97 #expect(throws: PuzzleFileReader.ReadError.notUTF8) { 98 _ = try PuzzleFileReader.readSource(at: url) 99 } 100 } 101 102 @Test("An unsupported extension is rejected") 103 func unsupportedExtensionIsRejected() throws { 104 let url = try writeTemp(Data(Self.validXD.utf8), ext: "txt") 105 defer { try? FileManager.default.removeItem(at: url) } 106 107 #expect(throws: PuzzleFileReader.ReadError.unsupportedType("txt")) { 108 _ = try PuzzleFileReader.readSource(at: url) 109 } 110 } 111 }