krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

7fba8922e1540741240555560cc9504342bdb091

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-03-19T00:16:02Z

Guard authenticated text fetches to sr.ht hosts and add regression test
 Hutch.xcodeproj/project.pbxproj                    | 130 +++++++++++++++++++++
 .../xcshareddata/xcschemes/Hutch.xcscheme          |  96 +++++++++++++++
 .../xcshareddata/xcschemes/HutchTests.xcscheme     |  69 +++++++++++
 Hutch/Networking/SRHTClient.swift                  |  14 +++
 Hutch/Networking/SRHTError.swift                   |   4 +
 HutchTests.xctestplan                              |  30 +++++
 HutchTests/HutchTests.swift                        |  16 +++
 HutchTests/SRHTClientTests.swift                   |  27 +++++
 8 files changed, 386 insertions(+)

diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 8aab8bb..3cd0646 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -13,12 +13,25 @@
 		8B2F89682F69DEB900FC0253 /* SECURITY.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B2F89652F69DEB900FC0253 /* SECURITY.md */; };
 /* End PBXBuildFile section */
 
+/* Begin PBXContainerItemProxy section */
+		8B8182BB2F6B73F3000AE049 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 8B4B28C92F6704280045FA19 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 8B4B28D02F6704280045FA19;
+			remoteInfo = Hutch;
+		};
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXFileReference section */
 		8B2F89612F69DEA900FC0253 /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = "<group>"; };
 		8B2F89632F69DEB900FC0253 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
 		8B2F89642F69DEB900FC0253 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
 		8B2F89652F69DEB900FC0253 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
 		8B4B28D12F6704280045FA19 /* Hutch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hutch.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		8B8182B72F6B73F3000AE049 /* HutchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HutchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		8B8182C22F6B742B000AE049 /* HutchTests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HutchTests; sourceTree = "<group>"; };
+		8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = HutchTests.xctestplan; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -40,6 +53,11 @@
 			path = Hutch;
 			sourceTree = "<group>";
 		};
+		8B8182B82F6B73F3000AE049 /* HutchTests */ = {
+			isa = PBXFileSystemSynchronizedRootGroup;
+			path = HutchTests;
+			sourceTree = "<group>";
+		};
 /* End PBXFileSystemSynchronizedRootGroup section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -50,17 +68,27 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		8B8182B42F6B73F3000AE049 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
 		8B4B28C82F6704280045FA19 = {
 			isa = PBXGroup;
 			children = (
+				8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */,
+				8B8182C22F6B742B000AE049 /* HutchTests */,
 				8B2F89632F69DEB900FC0253 /* LICENSE */,
 				8B2F89642F69DEB900FC0253 /* README.md */,
 				8B2F89652F69DEB900FC0253 /* SECURITY.md */,
 				8B2F89612F69DEA900FC0253 /* TODO.md */,
 				8B4B28D32F6704280045FA19 /* Hutch */,
+				8B8182B82F6B73F3000AE049 /* HutchTests */,
 				8B4B28D22F6704280045FA19 /* Products */,
 			);
 			sourceTree = "<group>";
@@ -69,6 +97,7 @@
 			isa = PBXGroup;
 			children = (
 				8B4B28D12F6704280045FA19 /* Hutch.app */,
+				8B8182B72F6B73F3000AE049 /* HutchTests.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -98,6 +127,29 @@
 			productReference = 8B4B28D12F6704280045FA19 /* Hutch.app */;
 			productType = "com.apple.product-type.application";
 		};
+		8B8182B62F6B73F3000AE049 /* HutchTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 8B8182BF2F6B73F3000AE049 /* Build configuration list for PBXNativeTarget "HutchTests" */;
+			buildPhases = (
+				8B8182B32F6B73F3000AE049 /* Sources */,
+				8B8182B42F6B73F3000AE049 /* Frameworks */,
+				8B8182B52F6B73F3000AE049 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				8B8182BC2F6B73F3000AE049 /* PBXTargetDependency */,
+			);
+			fileSystemSynchronizedGroups = (
+				8B8182B82F6B73F3000AE049 /* HutchTests */,
+			);
+			name = HutchTests;
+			packageProductDependencies = (
+			);
+			productName = HutchTests;
+			productReference = 8B8182B72F6B73F3000AE049 /* HutchTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
@@ -111,6 +163,10 @@
 					8B4B28D02F6704280045FA19 = {
 						CreatedOnToolsVersion = 26.3;
 					};
+					8B8182B62F6B73F3000AE049 = {
+						CreatedOnToolsVersion = 26.3;
+						TestTargetID = 8B4B28D02F6704280045FA19;
+					};
 				};
 			};
 			buildConfigurationList = 8B4B28CC2F6704280045FA19 /* Build configuration list for PBXProject "Hutch" */;
@@ -128,6 +184,7 @@
 			projectRoot = "";
 			targets = (
 				8B4B28D02F6704280045FA19 /* Hutch */,
+				8B8182B62F6B73F3000AE049 /* HutchTests */,
 			);
 		};
 /* End PBXProject section */
@@ -144,6 +201,13 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		8B8182B52F6B73F3000AE049 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
@@ -154,8 +218,23 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		8B8182B32F6B73F3000AE049 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		8B8182BC2F6B73F3000AE049 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 8B4B28D02F6704280045FA19 /* Hutch */;
+			targetProxy = 8B8182BB2F6B73F3000AE049 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
 		8B4B28DA2F6704290045FA19 /* Debug */ = {
 			isa = XCBuildConfiguration;
@@ -350,6 +429,48 @@
 			};
 			name = Release;
 		};
+		8B8182BD2F6B73F3000AE049 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = ZCNAX3VL9D;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.HutchTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRING_CATALOG_GENERATE_SYMBOLS = NO;
+				SWIFT_APPROACHABLE_CONCURRENCY = YES;
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
+			};
+			name = Debug;
+		};
+		8B8182BE2F6B73F3000AE049 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = ZCNAX3VL9D;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.HutchTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRING_CATALOG_GENERATE_SYMBOLS = NO;
+				SWIFT_APPROACHABLE_CONCURRENCY = YES;
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
+			};
+			name = Release;
+		};
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
@@ -371,6 +492,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		8B8182BF2F6B73F3000AE049 /* Build configuration list for PBXNativeTarget "HutchTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				8B8182BD2F6B73F3000AE049 /* Debug */,
+				8B8182BE2F6B73F3000AE049 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 /* End XCConfigurationList section */
 	};
 	rootObject = 8B4B28C92F6704280045FA19 /* Project object */;
diff --git a/Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme b/Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme
new file mode 100644
index 0000000..4bb83be
--- /dev/null
+++ b/Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "2630"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+               BuildableName = "Hutch.app"
+               BlueprintName = "Hutch"
+               ReferencedContainer = "container:Hutch.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <TestPlans>
+         <TestPlanReference
+            reference = "container:HutchTests"
+            default = "YES">
+         </TestPlanReference>
+      </TestPlans>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "8B8182B62F6B73F3000AE049"
+               BuildableName = "HutchTests.xctest"
+               BlueprintName = "HutchTests"
+               ReferencedContainer = "container:Hutch.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+            BuildableName = "Hutch.app"
+            BlueprintName = "Hutch"
+            ReferencedContainer = "container:Hutch.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+            BuildableName = "Hutch.app"
+            BlueprintName = "Hutch"
+            ReferencedContainer = "container:Hutch.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme b/Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme
new file mode 100644
index 0000000..909ebd2
--- /dev/null
+++ b/Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "2630"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <TestPlans>
+         <TestPlanReference
+            reference = "container:HutchTests.xctestplan"
+            default = "YES">
+         </TestPlanReference>
+      </TestPlans>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "8B8182B62F6B73F3000AE049"
+               BuildableName = "HutchTests.xctest"
+               BlueprintName = "HutchTests"
+               ReferencedContainer = "container:Hutch.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+            BuildableName = "Hutch.app"
+            BlueprintName = "Hutch"
+            ReferencedContainer = "container:Hutch.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Hutch/Networking/SRHTClient.swift b/Hutch/Networking/SRHTClient.swift
index a24fe24..6032cab 100644
--- a/Hutch/Networking/SRHTClient.swift
+++ b/Hutch/Networking/SRHTClient.swift
@@ -435,6 +435,9 @@ final class SRHTClient: Sendable {
         guard let token = _token.withLock({ $0 }), !token.isEmpty else {
             throw SRHTError.unauthorized
         }
+        guard Self.isTrustedAuthenticatedTextURL(url) else {
+            throw SRHTError.invalidAuthenticatedURL(url)
+        }
 
         var request = URLRequest(url: url)
         request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
@@ -512,6 +515,17 @@ final class SRHTClient: Sendable {
 
 // MARK: - Data Helper
 
+private extension SRHTClient {
+    static func isTrustedAuthenticatedTextURL(_ url: URL) -> Bool {
+        guard url.scheme?.localizedCaseInsensitiveCompare("https") == .orderedSame,
+              let host = url.host?.lowercased() else {
+            return false
+        }
+
+        return host.hasSuffix(".sr.ht")
+    }
+}
+
 private extension Data {
     mutating func append(_ string: String) {
         if let data = string.data(using: .utf8) {
diff --git a/Hutch/Networking/SRHTError.swift b/Hutch/Networking/SRHTError.swift
index f148c00..f2da408 100644
--- a/Hutch/Networking/SRHTError.swift
+++ b/Hutch/Networking/SRHTError.swift
@@ -6,6 +6,8 @@ enum SRHTError: LocalizedError, Sendable {
     case graphQLErrors([GraphQLError])
     /// The HTTP response had a non-2xx status code.
     case httpError(Int)
+    /// The client refused to send credentials to an unexpected URL.
+    case invalidAuthenticatedURL(URL)
     /// The response data could not be decoded.
     case decodingError(any Error)
     /// A networking error from URLSession (timeout, DNS, connectivity, etc.).
@@ -20,6 +22,8 @@ enum SRHTError: LocalizedError, Sendable {
             return "GraphQL error: \(messages)"
         case .httpError(let code):
             return "Server returned HTTP \(code)."
+        case .invalidAuthenticatedURL(let url):
+            return "Refused to authenticate request to unexpected URL: \(url.absoluteString)"
         case .decodingError(let error):
             return "Failed to decode response: \(error.localizedDescription)"
         case .networkError(let error):
diff --git a/HutchTests.xctestplan b/HutchTests.xctestplan
new file mode 100644
index 0000000..8af3d1b
--- /dev/null
+++ b/HutchTests.xctestplan
@@ -0,0 +1,30 @@
+{
+  "configurations" : [
+    {
+      "id" : "06A589A5-80C7-41FE-911F-03518252CBDD",
+      "name" : "Test Scheme Action",
+      "options" : {
+
+      }
+    }
+  ],
+  "defaultOptions" : {
+    "performanceAntipatternCheckerEnabled" : true,
+    "targetForVariableExpansion" : {
+      "containerPath" : "container:Hutch.xcodeproj",
+      "identifier" : "8B4B28D02F6704280045FA19",
+      "name" : "Hutch"
+    }
+  },
+  "testTargets" : [
+    {
+      "parallelizable" : true,
+      "target" : {
+        "containerPath" : "container:Hutch.xcodeproj",
+        "identifier" : "8B8182B62F6B73F3000AE049",
+        "name" : "HutchTests"
+      }
+    }
+  ],
+  "version" : 1
+}
diff --git a/HutchTests/HutchTests.swift b/HutchTests/HutchTests.swift
new file mode 100644
index 0000000..c681efa
--- /dev/null
+++ b/HutchTests/HutchTests.swift
@@ -0,0 +1,16 @@
+//
+//  HutchTests.swift
+//  HutchTests
+//
+//  Created by cmc on 2026-03-18.
+//
+
+import Testing
+
+struct HutchTests {
+
+    @Test func example() async throws {
+        // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+    }
+
+}
diff --git a/HutchTests/SRHTClientTests.swift b/HutchTests/SRHTClientTests.swift
new file mode 100644
index 0000000..2ce2529
--- /dev/null
+++ b/HutchTests/SRHTClientTests.swift
@@ -0,0 +1,27 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+struct SRHTClientTests {
+
+    @Test
+    @MainActor
+    func fetchTextRejectsUnexpectedAuthenticatedURL() async throws {
+        let client = SRHTClient(token: "test-token")
+        let url = try #require(URL(string: "https://example.com/build-log"))
+
+        do {
+            _ = try await client.fetchText(url: url)
+            Issue.record("Expected fetchText(url:) to reject non-sr.ht URLs.")
+        } catch let error as SRHTError {
+            guard case .invalidAuthenticatedURL(let rejectedURL) = error else {
+                Issue.record("Expected invalidAuthenticatedURL error, got \(error).")
+                return
+            }
+
+            #expect(rejectedURL == url)
+        } catch {
+            Issue.record("Expected SRHTError.invalidAuthenticatedURL, got \(error).")
+        }
+    }
+}