Test iOS UUID parsing and normalization on a cloud Mac

Test iOS UUID parsing and normalization on a cloud Mac

Test iOS UUID parsing and normalization on a cloud Mac

An API changes a record’s UUID from uppercase to lowercase, and the client inserts a second record. The problem usually is not the random number generator. It is the use of a raw string as a primary key, with comparison, caching, and persistence each applying different rules. On a cloud Mac, you can first capture this boundary in a standalone Swift acceptance script, then bring it into your iOS project’s tests—without launching a simulator.

Define the input contract first

This article accepts the standard 36-byte text representation: hexadecimal characters in five groups of 8, 4, 4, 4, and 12, separated by ASCII hyphens. Input may mix uppercase and lowercase letters; output is always lowercase. Leading or trailing whitespace, braces, and representations without hyphens are rejected.

This does not mean that all other representations are invalid. It means choosing a single accepted form for the API boundary. If a legacy API accepts another form, handle it in an explicit compatibility layer rather than letting the core parser silently repair it.

Keep syntax and business rules separate. The all-zero UUID is structurally valid, but it may be prohibited for order or document identifiers. Any restriction on UUID versions also belongs in the API contract; do not reject a value merely because the first character of the third group is not a particular digit.

A UUID is an identifier, not an access credential. Even when its format is correct, the server must still check whether the current user is authorized to read the corresponding record.

Write an executable strict parser

Run xcrun --find swift on the node to confirm that the current toolchain provides Swift. Save the following as uuid-contract.swift. The code checks the byte structure before passing the input to Foundation, so it does not depend on how permissively a particular system version handles input.

import Foundation

enum IdentifierError: Error {
    case invalidSyntax
    case zeroNotAllowed
}

func parseIdentifier(
    _ text: String,
    allowZero: Bool = false
) throws -> UUID {
    let bytes = Array(text.utf8)
    let separators: Set<Int> = [8, 13, 18, 23]

    guard bytes.count == 36 else {
        throw IdentifierError.invalidSyntax
    }

    for (index, byte) in bytes.enumerated() {
        if separators.contains(index) {
            guard byte == 45 else {
                throw IdentifierError.invalidSyntax
            }
        } else {
            let isHex = (48...57).contains(byte)
                || (65...70).contains(byte)
                || (97...102).contains(byte)
            guard isHex else {
                throw IdentifierError.invalidSyntax
            }
        }
    }

    guard let value = UUID(uuidString: text) else {
        throw IdentifierError.invalidSyntax
    }

    let zero = UUID(
        uuid: (0, 0, 0, 0, 0, 0, 0, 0,
               0, 0, 0, 0, 0, 0, 0, 0)
    )
    guard allowZero || value != zero else {
        throw IdentifierError.zeroNotAllowed
    }
    return value
}

func canonical(_ value: UUID) -> String {
    value.uuidString.lowercased()
}

Checking UTF-8 bytes rather than the number of visible characters explicitly excludes full-width characters and Unicode symbols that resemble hyphens. Here, lowercased() only processes UUID output that has already been validated. It does not convert arbitrary user input before allowing it through.

Test boundary cases with fixed fixtures

Append the tests below to the same file. The valid cases check value equality, normalization, and idempotence; the invalid cases cover whitespace, character substitutions, and changes in length. Do not replace fixed negative examples with random UUIDs, or failures will be harder to reproduce.

let upper = "550E8400-E29B-41D4-A716-446655440000"
let lower = upper.lowercased()
let value = try parseIdentifier(upper)

precondition(value == (try parseIdentifier(lower)))
precondition(canonical(value) == lower)
let reparsed = try parseIdentifier(canonical(value))
precondition(canonical(reparsed) == lower)

let invalid = [
    "",
    " " + lower,
    lower + "\n",
    "{" + lower + "}",
    lower.replacingOccurrences(of: "-", with: ""),
    lower.replacingOccurrences(of: "-", with: "-"),
    lower.replacingOccurrences(of: "5", with: "g"),
    lower + "0"
]

for text in invalid {
    do {
        _ = try parseIdentifier(text)
        fatalError("Invalid UUID accepted")
    } catch IdentifierError.invalidSyntax {
    }
}

let zero = "00000000-0000-0000-0000-000000000000"
do {
    _ = try parseIdentifier(zero)
    fatalError("Zero UUID accepted")
} catch IdentifierError.zeroNotAllowed {
}
_ = try parseIdentifier(zero, allowZero: true)

Run xcrun swift uuid-contract.swift. A successful run produces no output; a failure exits with a nonzero status. The empty catch blocks mean that the expected error was encountered. Other errors are not swallowed and will still cause the script to fail. When moving these tests into XCTest, use assertions for specific errors to preserve this distinction.

Normalize at the data boundary

Parse identifiers where network responses, deep-link parameters, or imported files enter the domain model. Keep a UUID in the domain layer, and call canonical only when sending a request or writing a text field. Do not let the UI and cache layers each perform their own string replacements.

Take particular care with storage migrations. First, map legacy keys to their canonical forms in a read-only copy and count cases where multiple legacy keys map to the same canonical form. Resolve collisions according to the business rules for merging records before updating foreign keys and unique constraints. A bulk lowercase conversion could collapse two historical records into one.

Do not treat UUID text order as chronological order

The lexicographic order of random UUIDs does not indicate creation order. If a list needs a stable order, use an explicit creation timestamp and add the normalized identifier as a secondary sort key when timestamps are equal. If you use a UUID scheme with time semantics, specify its version and sorting rules separately.

Add the checks to your cloud Mac acceptance workflow

Before running the script on an ArmVNC node, record the commit identifier, sw_vers, and xcrun swift --version. This information helps explain toolchain differences and contains no business data. Use fixed test values for failure cases rather than archiving actual user records.

Once the checks are part of continuous integration, verify at least these four points:

  • Standard text parses successfully, and different letter cases produce the same value.
  • Whitespace, incorrect separators, and non-hexadecimal characters must fail.
  • The all-zero value follows an explicit policy and is not hidden behind a generic syntax error.
  • Legacy key migrations check for collisions first, and the number of related records remains unchanged after migration.

Finally, add an integration test: submit the same entity twice, once with an uppercase UUID and once with a lowercase UUID. Storage must still contain only one record, with the update applied to the original. The parser ensures a consistent entry point; only the integration test proves that caching, the database, and business updates actually honor the contract.

Frequently asked questions

Are UUID strings with different casing different identifiers?

No. Parse them into UUID values before comparison and use a consistent lowercase standard representation for text storage. Check existing string keys for normalization collisions before migrating.

Must an all-zero UUID always be rejected?

No. It is structurally valid, but an entity identifier contract may prohibit it. Make that policy explicit and test both acceptance and rejection.

Hand build tasks to a dedicated cloud Mac

Choose an Apple Silicon physical host and get the entire machine to yourself, with native macOS GUI and command-line access. The model, rental period, and location are confirmed with your order.