From 0f980834918f62876ee577d6778b376cf97a8b2e Mon Sep 17 00:00:00 2001 From: Fayner Brack Date: Sat, 4 Jul 2026 23:08:28 +1000 Subject: [PATCH] test(ios-readplace): assert exact values instead of existence/negatives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace assertions that pass for the wrong reason with ones that pin the actual value, per the test-driven-design conventions: - SirenDate: assert the parsed instant equals a known UTC time (and that the fractional variant is exactly 123 ms later) instead of `XCTAssertNotNil`, and assert the fixture's `savedAt` parses to its exact instant — a parser that returned the wrong time previously passed. - PKCE: assert the challenge's exact length (43) and that every character is in the base64url alphabet, replacing three `contains("+"/"/"/"=")` negatives that a wrong-length or wrong-alphabet challenge would still satisfy. - Login/Signup authorize tests: derive the expected host from `AppConfig.serverBaseURL` instead of the literal "readplace.com", so the assertion is environment-agnostic (a prerequisite for running the full suite under the STAGING build). --- .../ios-readplace/Tests/LoginFlowTests.swift | 2 +- projects/ios-readplace/Tests/PKCETests.swift | 13 ++++++----- .../ios-readplace/Tests/SignupFlowTests.swift | 4 ++-- .../Tests/SirenDecodingTests.swift | 22 +++++++++++++++---- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/projects/ios-readplace/Tests/LoginFlowTests.swift b/projects/ios-readplace/Tests/LoginFlowTests.swift index a01c04d5a..98ce81547 100644 --- a/projects/ios-readplace/Tests/LoginFlowTests.swift +++ b/projects/ios-readplace/Tests/LoginFlowTests.swift @@ -23,7 +23,7 @@ final class LoginFlowTests: XCTestCase { let request = makeService(store: TestSupport.loggedInStore()).makeNativeLoginAuthorizationRequest() let components = URLComponents(url: request.url, resolvingAgainstBaseURL: false)! - XCTAssertEqual(components.host, "readplace.com") + XCTAssertEqual(components.host, URL(string: AppConfig.serverBaseURL)?.host) XCTAssertEqual(components.path, "/oauth/authorize") let items = Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value ?? "") }) XCTAssertEqual(items["client_id"], "ios-app") diff --git a/projects/ios-readplace/Tests/PKCETests.swift b/projects/ios-readplace/Tests/PKCETests.swift index 71c89d799..ee0d93515 100644 --- a/projects/ios-readplace/Tests/PKCETests.swift +++ b/projects/ios-readplace/Tests/PKCETests.swift @@ -23,11 +23,14 @@ final class PKCETests: XCTestCase { XCTAssertNotEqual(PKCE.makeCodeVerifier(), PKCE.makeCodeVerifier()) } - func testChallengeIsURLSafe() { + func testChallengeIsBase64URLAndCorrectLength() { let challenge = PKCE.challenge(for: PKCE.makeCodeVerifier()) - XCTAssertFalse(challenge.contains("+")) - XCTAssertFalse(challenge.contains("/")) - XCTAssertFalse(challenge.contains("=")) - XCTAssertFalse(challenge.isEmpty) + XCTAssertEqual(challenge.count, 43, "a SHA-256 digest base64url-encodes to 43 chars") + let allowed = CharacterSet(charactersIn: + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") + XCTAssertTrue( + challenge.unicodeScalars.allSatisfy(allowed.contains), + "every character is in the base64url alphabet (no +, /, or =)" + ) } } diff --git a/projects/ios-readplace/Tests/SignupFlowTests.swift b/projects/ios-readplace/Tests/SignupFlowTests.swift index cbd3b0eee..686aac663 100644 --- a/projects/ios-readplace/Tests/SignupFlowTests.swift +++ b/projects/ios-readplace/Tests/SignupFlowTests.swift @@ -21,7 +21,7 @@ final class SignupFlowTests: XCTestCase { let request = makeService(store: TestSupport.loggedInStore()).makeSignupAuthorizationRequest() let components = URLComponents(url: request.url, resolvingAgainstBaseURL: false)! - XCTAssertEqual(components.host, "readplace.com") + XCTAssertEqual(components.host, URL(string: AppConfig.serverBaseURL)?.host) XCTAssertEqual(components.path, "/oauth/authorize") let items = Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value ?? "") }) XCTAssertEqual(items["client_id"], "ios-app") @@ -83,7 +83,7 @@ final class SignupFlowTests: XCTestCase { XCTAssertEqual(probed?.scheme, "googlechromes", "Chrome availability is probed with the https scheme variant") let components = URLComponents(url: chrome, resolvingAgainstBaseURL: false)! XCTAssertEqual(components.scheme, "googlechromes") - XCTAssertEqual(components.host, "readplace.com") + XCTAssertEqual(components.host, URL(string: AppConfig.serverBaseURL)?.host) XCTAssertEqual(components.path, "/oauth/authorize") XCTAssertEqual(components.percentEncodedQuery, "client_id=ios-app&state=abc") } diff --git a/projects/ios-readplace/Tests/SirenDecodingTests.swift b/projects/ios-readplace/Tests/SirenDecodingTests.swift index 499c1d5a9..78fa5ab5b 100644 --- a/projects/ios-readplace/Tests/SirenDecodingTests.swift +++ b/projects/ios-readplace/Tests/SirenDecodingTests.swift @@ -2,6 +2,16 @@ import XCTest @testable import Readplace final class SirenDecodingTests: XCTestCase { + /// A fixed UTC instant, for asserting a parsed date equals a known point in + /// time rather than merely being non-nil. + static func utc(_ year: Int, _ month: Int, _ day: Int, _ hour: Int, _ minute: Int, _ second: Int) -> Date { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(identifier: "UTC")! + return calendar.date(from: DateComponents( + year: year, month: month, day: day, hour: hour, minute: minute, second: second + ))! + } + private func decodeCollection(_ json: String) throws -> SirenCollection { try JSONDecoder().decode(SirenCollection.self, from: Data(json.utf8)) } @@ -27,7 +37,8 @@ final class SirenDecodingTests: XCTestCase { XCTAssertEqual(updateStatus.type, "application/x-www-form-urlencoded") XCTAssertEqual(updateStatus.fields?.first?.name, "status") XCTAssertEqual(article.readHref, "/queue/a1/view") - XCTAssertNotNil(article.savedAt) + XCTAssertEqual(article.savedAt, SirenDecodingTests.utc(2026, 5, 30, 10, 0, 0), + "the fixture's savedAt (2026-05-30T10:00:00.000Z) parses to that exact instant") } func testArticleAffordancesIterateAdvertisedActionsInWireOrder() throws { @@ -315,9 +326,12 @@ final class SirenDecodingTests: XCTestCase { XCTAssertEqual(page.warning?.message, "Cannot save that link.") } - func testSirenDateParsesWithAndWithoutFractionalSeconds() { - XCTAssertNotNil(SirenDate.parse("2026-05-30T10:00:00.123Z")) - XCTAssertNotNil(SirenDate.parse("2026-05-30T10:00:00Z")) + func testSirenDateParsesWithAndWithoutFractionalSeconds() throws { + let base = SirenDecodingTests.utc(2026, 5, 30, 10, 0, 0) + XCTAssertEqual(try XCTUnwrap(SirenDate.parse("2026-05-30T10:00:00Z")), base) + let fractional = try XCTUnwrap(SirenDate.parse("2026-05-30T10:00:00.123Z")) + XCTAssertEqual(fractional.timeIntervalSince(base), 0.123, accuracy: 0.0005, + "the fractional variant is 123 ms after the whole-second instant") XCTAssertNil(SirenDate.parse("not-a-date")) XCTAssertNil(SirenDate.parse("")) }