crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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 }