import XCTest
import Foundation
@testable import ForgeOffload
final class MHLTests: XCTestCase {
var root: String!
override func setUpWithError() throws {
root = NSTemporaryDirectory() + "forge-mhl-\(UUID().uuidString)"
try FileManager.default.createDirectory(atPath: root + "/CLIPS", withIntermediateDirectories: true)
try Data("clip one contents".utf8).write(to: URL(fileURLWithPath: root + "/CLIPS/C001.mov"))
try Data("clip two contents, longer".utf8).write(to: URL(fileURLWithPath: root + "/CLIPS/C002.mov"))
}
override func tearDown() {
try? FileManager.default.removeItem(atPath: root)
}
// MHL XML structure: hashlist root, creatorinfo, hash entries with file/size/xxh64.
func testWriteStructure() throws {
let xml = try MHL.generate(root: root, creator: "Forge 0.1", files: ["CLIPS/C001.mov", "CLIPS/C002.mov"])
XCTAssertTrue(xml.hasPrefix("\n"))
XCTAssertTrue(xml.contains(""))
XCTAssertTrue(xml.contains("Forge 0.1"))
XCTAssertTrue(xml.contains("CLIPS/C001.mov"))
XCTAssertTrue(xml.contains("17"))
XCTAssertTrue(xml.contains(""))
XCTAssertTrue(xml.hasSuffix("\n"))
}
// Verify: clean tree passes.
func testVerifyClean() throws {
let xml = try MHL.generate(root: root, creator: "Forge", files: ["CLIPS/C001.mov", "CLIPS/C002.mov"])
let result = try MHL.verify(manifest: xml, root: root)
XCTAssertTrue(result.passed)
XCTAssertEqual(result.checked, 2)
XCTAssertTrue(result.failures.isEmpty)
}
// Tamper detection: flipped byte fails that file.
func testVerifyTamperDetected() throws {
let xml = try MHL.generate(root: root, creator: "Forge", files: ["CLIPS/C001.mov", "CLIPS/C002.mov"])
try Data("clip one CONTENTS".utf8).write(to: URL(fileURLWithPath: root + "/CLIPS/C001.mov"))
let result = try MHL.verify(manifest: xml, root: root)
XCTAssertFalse(result.passed)
XCTAssertEqual(result.failures.count, 1)
XCTAssertEqual(result.failures[0].path, "CLIPS/C001.mov")
XCTAssertEqual(result.failures[0].reason, .hashMismatch)
}
// Missing file fails verify.
func testVerifyMissingFile() throws {
let xml = try MHL.generate(root: root, creator: "Forge", files: ["CLIPS/C001.mov", "CLIPS/C002.mov"])
try FileManager.default.removeItem(atPath: root + "/CLIPS/C002.mov")
let result = try MHL.verify(manifest: xml, root: root)
XCTAssertFalse(result.passed)
XCTAssertEqual(result.failures[0].reason, .missing)
}
// Round trip through file on disk.
func testManifestFileRoundTrip() throws {
let mhlPath = root + "/A001.mhl"
try MHL.write(root: root!, creator: "Forge", files: ["CLIPS/C001.mov"], to: mhlPath)
let loaded = try String(contentsOfFile: mhlPath, encoding: .utf8)
let result = try MHL.verify(manifest: loaded, root: root)
XCTAssertTrue(result.passed)
}
// Malformed manifest rejected.
func testMalformedManifestRejected() {
XCTAssertThrowsError(try MHL.verify(manifest: "", root: root))
}
// XML escaping in paths.
func testPathEscaping() throws {
try Data("x".utf8).write(to: URL(fileURLWithPath: root + "/a&b.mov"))
let xml = try MHL.generate(root: root, creator: "Forge", files: ["a&b.mov"])
XCTAssertTrue(xml.contains("a&b.mov"))
let result = try MHL.verify(manifest: xml, root: root)
XCTAssertTrue(result.passed)
}
}