Commit a7d154e1db
Unsigned
Layout: unified · split
.github/workflows/build.yml +4 −2
| @@ -6,8 +6,10 @@ name: Build | ||
| 6 | 6 | # |
| 7 | 7 | # This runs `xcodebuild test`, which also compiles the app, the widget, and the |
| 8 | 8 | # share extension (the DomainDig scheme's build action pulls both in as |
| 9 | # dependencies). The test target is DomainDigUITests — an accessibility audit | |
| 10 | # suite; see DomainDigUITests/AccessibilityAuditHarness.swift. | |
| 9 | # dependencies). The scheme has two test targets: DomainDigUITests — an | |
| 10 | # accessibility audit suite; see DomainDigUITests/AccessibilityAuditHarness.swift | |
| 11 | # — and DomainDigTests — unit coverage of the deterministic core (report builder, | |
| 12 | # exporter, diff, portability dedup). | |
| 11 | 13 | # |
| 12 | 14 | # WHAT THIS JOB IS FOR, given the audit also runs locally: |
| 13 | 15 | # a clean checkout of the merge result. A local hook runs against the working |
DomainDig.xcodeproj/project.pbxproj +171 −8
| @@ -7,6 +7,9 @@ | ||
| 7 | 7 | objects = { |
| 8 | 8 | |
| 9 | 9 | /* Begin PBXBuildFile section */ |
| 10 | 38316D90539394C2CC7C12BE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE8B269A01CC5E593DA3DFC2 /* Foundation.framework */; }; | |
| 11 | 47CD3BB1AE143733A73E0E5B /* DomainReportExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D7C7D97A4006831572F468 /* DomainReportExporterTests.swift */; }; | |
| 12 | 81359F63C7A23454B8FA0141 /* DomainReportBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82320955416797CFE59464A /* DomainReportBuilderTests.swift */; }; | |
| 10 | 13 | 8BBFEF092F9874AE00E8E144 /* DomainInspectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF032F9874AE00E8E144 /* DomainInspectionService.swift */; }; |
| 11 | 14 | 8BBFEF0A2F9874AE00E8E144 /* DomainReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */; }; |
| 12 | 15 | 8BBFEF0B2F9874AE00E8E144 /* DomainReportExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */; }; |
| @@ -16,9 +19,19 @@ | ||
| 16 | 19 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; |
| 17 | 20 | 8BDB00010000000000000002 /* DomainDigShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8BDB00010000000000000001 /* DomainDigShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; |
| 18 | 21 | 8BF9DA872F9B13FB00EF41D5 /* DomainDataPortabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */; }; |
| 22 | A5AF921BC1C2E6E940CC05DC /* SnapshotFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA789D3607B55E3612E6FFA /* SnapshotFixture.swift */; }; | |
| 23 | C7CA9E02B0DC2708DE7A8563 /* DomainDataPortabilityServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC948A02EC0184228BC4630E /* DomainDataPortabilityServiceTests.swift */; }; | |
| 24 | E959C4D24DAAB3CB80D854B2 /* DiffServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A25DF2B8BB52589D49986B /* DiffServiceTests.swift */; }; | |
| 19 | 25 | /* End PBXBuildFile section */ |
| 20 | 26 | |
| 21 | 27 | /* Begin PBXContainerItemProxy section */ |
| 28 | 57D61F819B6EA1C654237C8F /* PBXContainerItemProxy */ = { | |
| 29 | isa = PBXContainerItemProxy; | |
| 30 | containerPortal = 8B7800612F6090E300933221 /* Project object */; | |
| 31 | proxyType = 1; | |
| 32 | remoteGlobalIDString = 8B7800682F6090E300933221; | |
| 33 | remoteInfo = DomainDig; | |
| 34 | }; | |
| 22 | 35 | 8BDA00010000000000000003 /* PBXContainerItemProxy */ = { |
| 23 | 36 | isa = PBXContainerItemProxy; |
| 24 | 37 | containerPortal = 8B7800612F6090E300933221 /* Project object */; |
| @@ -58,6 +71,9 @@ | ||
| 58 | 71 | /* End PBXCopyFilesBuildPhase section */ |
| 59 | 72 | |
| 60 | 73 | /* Begin PBXFileReference section */ |
| 74 | 15A25DF2B8BB52589D49986B /* DiffServiceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DiffServiceTests.swift; sourceTree = "<group>"; }; | |
| 75 | 54D7C7D97A4006831572F468 /* DomainReportExporterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DomainReportExporterTests.swift; sourceTree = "<group>"; }; | |
| 76 | 5CA789D3607B55E3612E6FFA /* SnapshotFixture.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnapshotFixture.swift; sourceTree = "<group>"; }; | |
| 61 | 77 | 8B6472EF300EBFA30018E10A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; sourceTree = "<group>"; }; |
| 62 | 78 | 8B7800692F6090E300933221 /* DomainDig.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DomainDig.app; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 63 | 79 | 8BBFEF032F9874AE00E8E144 /* DomainInspectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainInspectionService.swift; sourceTree = "<group>"; }; |
| @@ -70,6 +86,10 @@ | ||
| 70 | 86 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 71 | 87 | 8BDC00010000000000000001 /* DomainDigUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DomainDigUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 72 | 88 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; }; |
| 89 | A77F7ED29E7C6669F5E61C04 /* DomainDigTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DomainDigTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; | |
| 90 | CC948A02EC0184228BC4630E /* DomainDataPortabilityServiceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityServiceTests.swift; sourceTree = "<group>"; }; | |
| 91 | DE8B269A01CC5E593DA3DFC2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; | |
| 92 | E82320955416797CFE59464A /* DomainReportBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DomainReportBuilderTests.swift; sourceTree = "<group>"; }; | |
| 73 | 93 | /* End PBXFileReference section */ |
| 74 | 94 | |
| 75 | 95 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| @@ -109,6 +129,8 @@ | ||
| 109 | 129 | }; |
| 110 | 130 | 8BDA0001000000000000000D /* Shared */ = { |
| 111 | 131 | isa = PBXFileSystemSynchronizedRootGroup; |
| 132 | exceptions = ( | |
| 133 | ); | |
| 112 | 134 | path = Shared; |
| 113 | 135 | sourceTree = "<group>"; |
| 114 | 136 | }; |
| @@ -130,12 +152,22 @@ | ||
| 130 | 152 | }; |
| 131 | 153 | 8BDC0001000000000000000E /* DomainDigUITests */ = { |
| 132 | 154 | isa = PBXFileSystemSynchronizedRootGroup; |
| 155 | exceptions = ( | |
| 156 | ); | |
| 133 | 157 | path = DomainDigUITests; |
| 134 | 158 | sourceTree = "<group>"; |
| 135 | 159 | }; |
| 136 | 160 | /* End PBXFileSystemSynchronizedRootGroup section */ |
| 137 | 161 | |
| 138 | 162 | /* Begin PBXFrameworksBuildPhase section */ |
| 163 | 68EB2A07F42245EE1B09B018 /* Frameworks */ = { | |
| 164 | isa = PBXFrameworksBuildPhase; | |
| 165 | buildActionMask = 2147483647; | |
| 166 | files = ( | |
| 167 | 38316D90539394C2CC7C12BE /* Foundation.framework in Frameworks */, | |
| 168 | ); | |
| 169 | runOnlyForDeploymentPostprocessing = 0; | |
| 170 | }; | |
| 139 | 171 | 8B7800662F6090E300933221 /* Frameworks */ = { |
| 140 | 172 | isa = PBXFrameworksBuildPhase; |
| 141 | 173 | buildActionMask = 2147483647; |
| @@ -167,6 +199,36 @@ | ||
| 167 | 199 | /* End PBXFrameworksBuildPhase section */ |
| 168 | 200 | |
| 169 | 201 | /* Begin PBXGroup section */ |
| 202 | 12026CD045DFB45E9E37D207 /* Frameworks */ = { | |
| 203 | isa = PBXGroup; | |
| 204 | children = ( | |
| 205 | E4CAAB98268DE269071F04E3 /* iOS */, | |
| 206 | ); | |
| 207 | name = Frameworks; | |
| 208 | sourceTree = "<group>"; | |
| 209 | }; | |
| 210 | 2B31A5F9F390F512E966D4B1 /* Fixtures */ = { | |
| 211 | isa = PBXGroup; | |
| 212 | children = ( | |
| 213 | 5CA789D3607B55E3612E6FFA /* SnapshotFixture.swift */, | |
| 214 | ); | |
| 215 | name = Fixtures; | |
| 216 | path = Fixtures; | |
| 217 | sourceTree = "<group>"; | |
| 218 | }; | |
| 219 | 7CC29A88387286DE8B4D17B3 /* DomainDigTests */ = { | |
| 220 | isa = PBXGroup; | |
| 221 | children = ( | |
| 222 | 2B31A5F9F390F512E966D4B1 /* Fixtures */, | |
| 223 | 15A25DF2B8BB52589D49986B /* DiffServiceTests.swift */, | |
| 224 | E82320955416797CFE59464A /* DomainReportBuilderTests.swift */, | |
| 225 | 54D7C7D97A4006831572F468 /* DomainReportExporterTests.swift */, | |
| 226 | CC948A02EC0184228BC4630E /* DomainDataPortabilityServiceTests.swift */, | |
| 227 | ); | |
| 228 | name = DomainDigTests; | |
| 229 | path = DomainDigTests; | |
| 230 | sourceTree = "<group>"; | |
| 231 | }; | |
| 170 | 232 | 8B7800602F6090E300933221 = { |
| 171 | 233 | isa = PBXGroup; |
| 172 | 234 | children = ( |
| @@ -184,6 +246,8 @@ | ||
| 184 | 246 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */, |
| 185 | 247 | 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */, |
| 186 | 248 | 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */, |
| 249 | 12026CD045DFB45E9E37D207 /* Frameworks */, | |
| 250 | 7CC29A88387286DE8B4D17B3 /* DomainDigTests */, | |
| 187 | 251 | ); |
| 188 | 252 | sourceTree = "<group>"; |
| 189 | 253 | }; |
| @@ -194,13 +258,40 @@ | ||
| 194 | 258 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, |
| 195 | 259 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */, |
| 196 | 260 | 8BDC00010000000000000001 /* DomainDigUITests.xctest */, |
| 261 | A77F7ED29E7C6669F5E61C04 /* DomainDigTests.xctest */, | |
| 197 | 262 | ); |
| 198 | 263 | name = Products; |
| 199 | 264 | sourceTree = "<group>"; |
| 200 | 265 | }; |
| 266 | E4CAAB98268DE269071F04E3 /* iOS */ = { | |
| 267 | isa = PBXGroup; | |
| 268 | children = ( | |
| 269 | DE8B269A01CC5E593DA3DFC2 /* Foundation.framework */, | |
| 270 | ); | |
| 271 | name = iOS; | |
| 272 | sourceTree = "<group>"; | |
| 273 | }; | |
| 201 | 274 | /* End PBXGroup section */ |
| 202 | 275 | |
| 203 | 276 | /* Begin PBXNativeTarget section */ |
| 277 | 77CB8BAE320E7119E3283122 /* DomainDigTests */ = { | |
| 278 | isa = PBXNativeTarget; | |
| 279 | buildConfigurationList = 108E5397EABF84F5A56080A9 /* Build configuration list for PBXNativeTarget "DomainDigTests" */; | |
| 280 | buildPhases = ( | |
| 281 | 34C44EAF4B4DA8C10B33208D /* Sources */, | |
| 282 | 68EB2A07F42245EE1B09B018 /* Frameworks */, | |
| 283 | A5B47E96AE4B6A13198132C2 /* Resources */, | |
| 284 | ); | |
| 285 | buildRules = ( | |
| 286 | ); | |
| 287 | dependencies = ( | |
| 288 | D17881CF5E934733B8E99DE8 /* PBXTargetDependency */, | |
| 289 | ); | |
| 290 | name = DomainDigTests; | |
| 291 | productName = DomainDigTests; | |
| 292 | productReference = A77F7ED29E7C6669F5E61C04 /* DomainDigTests.xctest */; | |
| 293 | productType = "com.apple.product-type.bundle.unit-test"; | |
| 294 | }; | |
| 204 | 295 | 8B7800682F6090E300933221 /* DomainDig */ = { |
| 205 | 296 | isa = PBXNativeTarget; |
| 206 | 297 | buildConfigurationList = 8B7800742F6090E400933221 /* Build configuration list for PBXNativeTarget "DomainDig" */; |
| @@ -221,8 +312,6 @@ | ||
| 221 | 312 | 8BDA0001000000000000000D /* Shared */, |
| 222 | 313 | ); |
| 223 | 314 | name = DomainDig; |
| 224 | packageProductDependencies = ( | |
| 225 | ); | |
| 226 | 315 | productName = DomainDig; |
| 227 | 316 | productReference = 8B7800692F6090E300933221 /* DomainDig.app */; |
| 228 | 317 | productType = "com.apple.product-type.application"; |
| @@ -244,8 +333,6 @@ | ||
| 244 | 333 | 8BDA0001000000000000000E /* DomainDigWidget */, |
| 245 | 334 | ); |
| 246 | 335 | name = DomainDigWidgetExtension; |
| 247 | packageProductDependencies = ( | |
| 248 | ); | |
| 249 | 336 | productName = DomainDigWidgetExtension; |
| 250 | 337 | productReference = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; |
| 251 | 338 | productType = "com.apple.product-type.app-extension"; |
| @@ -267,8 +354,6 @@ | ||
| 267 | 354 | 8BDB0001000000000000000E /* DomainDigShareExtension */, |
| 268 | 355 | ); |
| 269 | 356 | name = DomainDigShareExtension; |
| 270 | packageProductDependencies = ( | |
| 271 | ); | |
| 272 | 357 | productName = DomainDigShareExtension; |
| 273 | 358 | productReference = 8BDB00010000000000000001 /* DomainDigShareExtension.appex */; |
| 274 | 359 | productType = "com.apple.product-type.app-extension"; |
| @@ -290,8 +375,6 @@ | ||
| 290 | 375 | 8BDC0001000000000000000E /* DomainDigUITests */, |
| 291 | 376 | ); |
| 292 | 377 | name = DomainDigUITests; |
| 293 | packageProductDependencies = ( | |
| 294 | ); | |
| 295 | 378 | productName = DomainDigUITests; |
| 296 | 379 | productReference = 8BDC00010000000000000001 /* DomainDigUITests.xctest */; |
| 297 | 380 | productType = "com.apple.product-type.bundle.ui-testing"; |
| @@ -344,6 +427,7 @@ | ||
| 344 | 427 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */, |
| 345 | 428 | 8BDB00010000000000000006 /* DomainDigShareExtension */, |
| 346 | 429 | 8BDC00010000000000000006 /* DomainDigUITests */, |
| 430 | 77CB8BAE320E7119E3283122 /* DomainDigTests */, | |
| 347 | 431 | ); |
| 348 | 432 | }; |
| 349 | 433 | /* End PBXProject section */ |
| @@ -377,9 +461,28 @@ | ||
| 377 | 461 | ); |
| 378 | 462 | runOnlyForDeploymentPostprocessing = 0; |
| 379 | 463 | }; |
| 464 | A5B47E96AE4B6A13198132C2 /* Resources */ = { | |
| 465 | isa = PBXResourcesBuildPhase; | |
| 466 | buildActionMask = 2147483647; | |
| 467 | files = ( | |
| 468 | ); | |
| 469 | runOnlyForDeploymentPostprocessing = 0; | |
| 470 | }; | |
| 380 | 471 | /* End PBXResourcesBuildPhase section */ |
| 381 | 472 | |
| 382 | 473 | /* Begin PBXSourcesBuildPhase section */ |
| 474 | 34C44EAF4B4DA8C10B33208D /* Sources */ = { | |
| 475 | isa = PBXSourcesBuildPhase; | |
| 476 | buildActionMask = 2147483647; | |
| 477 | files = ( | |
| 478 | E959C4D24DAAB3CB80D854B2 /* DiffServiceTests.swift in Sources */, | |
| 479 | 81359F63C7A23454B8FA0141 /* DomainReportBuilderTests.swift in Sources */, | |
| 480 | 47CD3BB1AE143733A73E0E5B /* DomainReportExporterTests.swift in Sources */, | |
| 481 | C7CA9E02B0DC2708DE7A8563 /* DomainDataPortabilityServiceTests.swift in Sources */, | |
| 482 | A5AF921BC1C2E6E940CC05DC /* SnapshotFixture.swift in Sources */, | |
| 483 | ); | |
| 484 | runOnlyForDeploymentPostprocessing = 0; | |
| 485 | }; | |
| 383 | 486 | 8B7800652F6090E300933221 /* Sources */ = { |
| 384 | 487 | isa = PBXSourcesBuildPhase; |
| 385 | 488 | buildActionMask = 2147483647; |
| @@ -433,9 +536,38 @@ | ||
| 433 | 536 | target = 8B7800682F6090E300933221 /* DomainDig */; |
| 434 | 537 | targetProxy = 8BDC00010000000000000003 /* PBXContainerItemProxy */; |
| 435 | 538 | }; |
| 539 | D17881CF5E934733B8E99DE8 /* PBXTargetDependency */ = { | |
| 540 | isa = PBXTargetDependency; | |
| 541 | name = DomainDig; | |
| 542 | target = 8B7800682F6090E300933221 /* DomainDig */; | |
| 543 | targetProxy = 57D61F819B6EA1C654237C8F /* PBXContainerItemProxy */; | |
| 544 | }; | |
| 436 | 545 | /* End PBXTargetDependency section */ |
| 437 | 546 | |
| 438 | 547 | /* Begin XCBuildConfiguration section */ |
| 548 | 87FD0A9B1630561EDC7F2A3D /* Release */ = { | |
| 549 | isa = XCBuildConfiguration; | |
| 550 | buildSettings = { | |
| 551 | BUNDLE_LOADER = "$(TEST_HOST)"; | |
| 552 | CLANG_ENABLE_OBJC_WEAK = NO; | |
| 553 | CODE_SIGN_STYLE = Automatic; | |
| 554 | CURRENT_PROJECT_VERSION = 44; | |
| 555 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | |
| 556 | GENERATE_INFOPLIST_FILE = YES; | |
| 557 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | |
| 558 | MARKETING_VERSION = 4.9.0; | |
| 559 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigTests; | |
| 560 | PRODUCT_NAME = "$(TARGET_NAME)"; | |
| 561 | SDKROOT = iphoneos; | |
| 562 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | |
| 563 | SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | |
| 564 | SWIFT_VERSION = 5.0; | |
| 565 | TARGETED_DEVICE_FAMILY = "1,2"; | |
| 566 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DomainDig.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DomainDig"; | |
| 567 | VALIDATE_PRODUCT = YES; | |
| 568 | }; | |
| 569 | name = Release; | |
| 570 | }; | |
| 439 | 571 | 8B7800722F6090E400933221 /* Debug */ = { |
| 440 | 572 | isa = XCBuildConfiguration; |
| 441 | 573 | buildSettings = { |
| @@ -787,9 +919,40 @@ | ||
| 787 | 919 | }; |
| 788 | 920 | name = Release; |
| 789 | 921 | }; |
| 922 | A6C716A4AC78E316F1378CBB /* Debug */ = { | |
| 923 | isa = XCBuildConfiguration; | |
| 924 | buildSettings = { | |
| 925 | BUNDLE_LOADER = "$(TEST_HOST)"; | |
| 926 | CLANG_ENABLE_OBJC_WEAK = NO; | |
| 927 | CODE_SIGN_STYLE = Automatic; | |
| 928 | CURRENT_PROJECT_VERSION = 44; | |
| 929 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | |
| 930 | GENERATE_INFOPLIST_FILE = YES; | |
| 931 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | |
| 932 | MARKETING_VERSION = 4.9.0; | |
| 933 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigTests; | |
| 934 | PRODUCT_NAME = "$(TARGET_NAME)"; | |
| 935 | SDKROOT = iphoneos; | |
| 936 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | |
| 937 | SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | |
| 938 | SWIFT_VERSION = 5.0; | |
| 939 | TARGETED_DEVICE_FAMILY = "1,2"; | |
| 940 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DomainDig.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DomainDig"; | |
| 941 | }; | |
| 942 | name = Debug; | |
| 943 | }; | |
| 790 | 944 | /* End XCBuildConfiguration section */ |
| 791 | 945 | |
| 792 | 946 | /* Begin XCConfigurationList section */ |
| 947 | 108E5397EABF84F5A56080A9 /* Build configuration list for PBXNativeTarget "DomainDigTests" */ = { | |
| 948 | isa = XCConfigurationList; | |
| 949 | buildConfigurations = ( | |
| 950 | 87FD0A9B1630561EDC7F2A3D /* Release */, | |
| 951 | A6C716A4AC78E316F1378CBB /* Debug */, | |
| 952 | ); | |
| 953 | defaultConfigurationIsVisible = 0; | |
| 954 | defaultConfigurationName = Release; | |
| 955 | }; | |
| 793 | 956 | 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */ = { |
| 794 | 957 | isa = XCConfigurationList; |
| 795 | 958 | buildConfigurations = ( |
DomainDig.xcodeproj/xcshareddata/xcschemes/DomainDig.xcscheme +10
| @@ -40,6 +40,16 @@ | ||
| 40 | 40 | ReferencedContainer = "container:DomainDig.xcodeproj"> |
| 41 | 41 | </BuildableReference> |
| 42 | 42 | </TestableReference> |
| 43 | <TestableReference | |
| 44 | skipped = "NO"> | |
| 45 | <BuildableReference | |
| 46 | BuildableIdentifier = "primary" | |
| 47 | BlueprintIdentifier = "77CB8BAE320E7119E3283122" | |
| 48 | BuildableName = "DomainDigTests.xctest" | |
| 49 | BlueprintName = "DomainDigTests" | |
| 50 | ReferencedContainer = "container:DomainDig.xcodeproj"> | |
| 51 | </BuildableReference> | |
| 52 | </TestableReference> | |
| 43 | 53 | </Testables> |
| 44 | 54 | </TestAction> |
| 45 | 55 | <LaunchAction |
DomainDigTests/DiffServiceTests.swift added +163
| @@ -0,0 +1,163 @@ | ||
| 1 | import XCTest | |
| 2 | @testable import DomainDig | |
| 3 | ||
| 4 | /// Characterization tests for the field-level diff engine. `DiffService` is a | |
| 5 | /// pure function over two `DomainReport`s, so every case here pins observable | |
| 6 | /// behavior (change classification, normalization, summary phrasing) against a | |
| 7 | /// deterministic fixture. | |
| 8 | final class DiffServiceTests: XCTestCase { | |
| 9 | ||
| 10 | func testIdenticalReportsProduceNoChanges() { | |
| 11 | let report = SnapshotFixture.report( | |
| 12 | availability: .registered, | |
| 13 | ownership: DomainOwnership(registrar: "Example Registrar") | |
| 14 | ) | |
| 15 | ||
| 16 | let diff = DiffService.compare(from: report, to: report) | |
| 17 | ||
| 18 | XCTAssertEqual(diff.changeCount, 0) | |
| 19 | XCTAssertTrue(diff.changedSectionTitles.isEmpty) | |
| 20 | XCTAssertFalse(diff.sections.contains { $0.hasChanges }) | |
| 21 | } | |
| 22 | ||
| 23 | func testAvailabilityTransitionIsReportedAsChanged() { | |
| 24 | let old = SnapshotFixture.report(availability: .available) | |
| 25 | let new = SnapshotFixture.report(availability: .registered) | |
| 26 | ||
| 27 | let diff = DiffService.compare(from: old, to: new) | |
| 28 | ||
| 29 | let availability = try? XCTUnwrap(diff.sections.first { $0.id == "availability" }) | |
| 30 | let item = availability?.items.first { $0.id == "availability" } | |
| 31 | XCTAssertEqual(item?.changeType, .changed) | |
| 32 | XCTAssertEqual(item?.oldValue, "Available") | |
| 33 | XCTAssertEqual(item?.newValue, "Registered") | |
| 34 | XCTAssertTrue(diff.changedSectionTitles.contains("Domain / Availability")) | |
| 35 | } | |
| 36 | ||
| 37 | func testPrimaryIPChangeSurfacesInAvailabilitySection() { | |
| 38 | let old = SnapshotFixture.report( | |
| 39 | availability: .registered, | |
| 40 | dnsSections: [SnapshotFixture.dnsSection(type: .A, values: ["203.0.113.10"])] | |
| 41 | ) | |
| 42 | let new = SnapshotFixture.report( | |
| 43 | availability: .registered, | |
| 44 | dnsSections: [SnapshotFixture.dnsSection(type: .A, values: ["203.0.113.20"])] | |
| 45 | ) | |
| 46 | ||
| 47 | let diff = DiffService.compare(from: old, to: new) | |
| 48 | let item = diff.sections | |
| 49 | .first { $0.id == "availability" }? | |
| 50 | .items.first { $0.id == "primary-ip" } | |
| 51 | ||
| 52 | XCTAssertEqual(item?.changeType, .changed) | |
| 53 | XCTAssertEqual(item?.oldValue, "203.0.113.10") | |
| 54 | XCTAssertEqual(item?.newValue, "203.0.113.20") | |
| 55 | } | |
| 56 | ||
| 57 | func testCaseAndWhitespaceDifferencesAreNotChanges() { | |
| 58 | let old = SnapshotFixture.report(ownership: DomainOwnership(registrar: "GoDaddy")) | |
| 59 | let new = SnapshotFixture.report(ownership: DomainOwnership(registrar: " godaddy ")) | |
| 60 | ||
| 61 | let diff = DiffService.compare(from: old, to: new) | |
| 62 | let item = diff.sections | |
| 63 | .first { $0.id == "ownership" }? | |
| 64 | .items.first { $0.id == "registrar" } | |
| 65 | ||
| 66 | XCTAssertEqual(item?.changeType, .unchanged, "case- and whitespace-only differences must not register as changes") | |
| 67 | } | |
| 68 | ||
| 69 | func testAddedAndRemovedOwnershipFields() { | |
| 70 | let absent = SnapshotFixture.report(ownership: nil) | |
| 71 | let present = SnapshotFixture.report(ownership: DomainOwnership(registrar: "Example Registrar")) | |
| 72 | ||
| 73 | let added = DiffService.compare(from: absent, to: present) | |
| 74 | .sections.first { $0.id == "ownership" }? | |
| 75 | .items.first { $0.id == "registrar" } | |
| 76 | XCTAssertEqual(added?.changeType, .added) | |
| 77 | XCTAssertNil(added?.oldValue) | |
| 78 | XCTAssertEqual(added?.newValue, "Example Registrar") | |
| 79 | ||
| 80 | let removed = DiffService.compare(from: present, to: absent) | |
| 81 | .sections.first { $0.id == "ownership" }? | |
| 82 | .items.first { $0.id == "registrar" } | |
| 83 | XCTAssertEqual(removed?.changeType, .removed) | |
| 84 | XCTAssertEqual(removed?.oldValue, "Example Registrar") | |
| 85 | XCTAssertNil(removed?.newValue) | |
| 86 | } | |
| 87 | ||
| 88 | func testDNSRecordValueChangeIsDetected() { | |
| 89 | let old = SnapshotFixture.report( | |
| 90 | dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["ns1.example.com", "ns2.example.com"])] | |
| 91 | ) | |
| 92 | let new = SnapshotFixture.report( | |
| 93 | dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["ns1.example.com", "ns3.example.com"])] | |
| 94 | ) | |
| 95 | ||
| 96 | let dns = DiffService.compare(from: old, to: new).sections.first { $0.id == "dns" } | |
| 97 | let records = dns?.items.first { $0.id == "dns-ns-records" } | |
| 98 | XCTAssertEqual(records?.changeType, .changed) | |
| 99 | } | |
| 100 | ||
| 101 | func testDNSRecordReorderIsNotAChange() { | |
| 102 | // Values are normalized (sorted, lowercased) before comparison, so a pure | |
| 103 | // reorder must diff as unchanged. | |
| 104 | let old = SnapshotFixture.report( | |
| 105 | dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["ns1.example.com", "ns2.example.com"])] | |
| 106 | ) | |
| 107 | let new = SnapshotFixture.report( | |
| 108 | dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["NS2.example.com", "NS1.example.com"])] | |
| 109 | ) | |
| 110 | ||
| 111 | let records = DiffService.compare(from: old, to: new) | |
| 112 | .sections.first { $0.id == "dns" }? | |
| 113 | .items.first { $0.id == "dns-ns-records" } | |
| 114 | XCTAssertEqual(records?.changeType, .unchanged) | |
| 115 | } | |
| 116 | ||
| 117 | func testSummaryMessagePhrasing() { | |
| 118 | XCTAssertEqual(DiffService.summaryMessage(from: [], changeCount: 0), "No meaningful changes") | |
| 119 | XCTAssertEqual(DiffService.summaryMessage(from: ["DNS"], changeCount: 1), "DNS changed") | |
| 120 | XCTAssertEqual( | |
| 121 | DiffService.summaryMessage(from: ["DNS", "Ownership"], changeCount: 3), | |
| 122 | "DNS and ownership changed (3 items)" | |
| 123 | ) | |
| 124 | } | |
| 125 | ||
| 126 | func testContextNoteFlagsDifferentResolvers() { | |
| 127 | let old = SnapshotFixture.report(resolverURLString: "https://one.example/dns-query") | |
| 128 | let new = SnapshotFixture.report(resolverURLString: "https://two.example/dns-query") | |
| 129 | ||
| 130 | let note = DiffService.comparisonContextNote(from: old, to: new) | |
| 131 | XCTAssertEqual(note, "Compared snapshots used different DNS resolvers.") | |
| 132 | } | |
| 133 | ||
| 134 | func testContextNoteNilWhenResolversMatch() { | |
| 135 | let report = SnapshotFixture.report() | |
| 136 | XCTAssertNil(DiffService.comparisonContextNote(from: report, to: report)) | |
| 137 | } | |
| 138 | ||
| 139 | func testCertificateWarningLevelThresholds() { | |
| 140 | func level(daysUntilExpiry days: Int?) -> CertificateWarningLevel { | |
| 141 | let ssl = days.map { SnapshotFixture.certificate(daysUntilExpiry: $0) } | |
| 142 | return DiffService.certificateWarningLevel(for: SnapshotFixture.snapshot(sslInfo: ssl)) | |
| 143 | } | |
| 144 | ||
| 145 | XCTAssertEqual(level(daysUntilExpiry: nil), .none) | |
| 146 | XCTAssertEqual(level(daysUntilExpiry: 45), .none) | |
| 147 | XCTAssertEqual(level(daysUntilExpiry: 29), .warning) | |
| 148 | XCTAssertEqual(level(daysUntilExpiry: 14), .warning) | |
| 149 | XCTAssertEqual(level(daysUntilExpiry: 13), .critical) | |
| 150 | XCTAssertEqual(level(daysUntilExpiry: 0), .critical) | |
| 151 | } | |
| 152 | ||
| 153 | func testCrossDomainComparisonPairsBothDomains() { | |
| 154 | let a = SnapshotFixture.report(domain: "alpha.example", availability: .registered) | |
| 155 | let b = SnapshotFixture.report(domain: "beta.example", availability: .available) | |
| 156 | ||
| 157 | let result = DiffService.compare(domainA: a, domainB: b) | |
| 158 | ||
| 159 | XCTAssertEqual(result.domainA, "alpha.example") | |
| 160 | XCTAssertEqual(result.domainB, "beta.example") | |
| 161 | XCTAssertTrue(result.changeCount > 0) | |
| 162 | } | |
| 163 | } | |
DomainDigTests/DomainDataPortabilityServiceTests.swift added +124
| @@ -0,0 +1,124 @@ | ||
| 1 | import XCTest | |
| 2 | @testable import DomainDig | |
| 3 | ||
| 4 | /// Characterization tests for the portability layer's deterministic core: the | |
| 5 | /// CSV round-trip and the merge/dedup semantics that `load*/save*` apply. Storage | |
| 6 | /// is exercised through an ephemeral `UserDefaults` suite so nothing touches the | |
| 7 | /// real app domain. | |
| 8 | final class DomainDataPortabilityServiceTests: XCTestCase { | |
| 9 | private let suiteName = "DomainDigTests.portability" | |
| 10 | private var defaults: UserDefaults! | |
| 11 | private let base = Date(timeIntervalSince1970: 1_700_000_000) | |
| 12 | ||
| 13 | override func setUp() { | |
| 14 | super.setUp() | |
| 15 | defaults = UserDefaults(suiteName: suiteName) | |
| 16 | defaults.removePersistentDomain(forName: suiteName) | |
| 17 | } | |
| 18 | ||
| 19 | override func tearDown() { | |
| 20 | defaults.removePersistentDomain(forName: suiteName) | |
| 21 | defaults = nil | |
| 22 | super.tearDown() | |
| 23 | } | |
| 24 | ||
| 25 | // MARK: - CSV round-trip | |
| 26 | ||
| 27 | func testTrackedDomainCSVRoundTripPreservesFields() throws { | |
| 28 | let original = TrackedDomain( | |
| 29 | domain: "csv.example", | |
| 30 | createdAt: base, | |
| 31 | updatedAt: base.addingTimeInterval(3_600), | |
| 32 | note: "keep an eye on this", | |
| 33 | isPinned: true, | |
| 34 | monitoringEnabled: false, | |
| 35 | lastKnownAvailability: .registered, | |
| 36 | certificateWarningLevel: .warning, | |
| 37 | certificateDaysRemaining: 12 | |
| 38 | ) | |
| 39 | ||
| 40 | let csv = DataPortabilityCSV.trackedDomains([original]) | |
| 41 | let parsed = try DataPortabilityCSV.parseTrackedDomains(from: csv) | |
| 42 | ||
| 43 | XCTAssertEqual(parsed.count, 1) | |
| 44 | let restored = try XCTUnwrap(parsed.first) | |
| 45 | XCTAssertEqual(restored.domain, "csv.example") | |
| 46 | XCTAssertEqual(restored.note, "keep an eye on this") | |
| 47 | XCTAssertTrue(restored.isPinned) | |
| 48 | XCTAssertFalse(restored.monitoringEnabled) | |
| 49 | XCTAssertEqual(restored.lastKnownAvailability, .registered) | |
| 50 | XCTAssertEqual(restored.certificateWarningLevel, .warning) | |
| 51 | XCTAssertEqual(restored.certificateDaysRemaining, 12) | |
| 52 | } | |
| 53 | ||
| 54 | func testTrackedDomainCSVParseSkipsRowsWithoutDomain() throws { | |
| 55 | let csv = """ | |
| 56 | domain,isPinned,monitoringEnabled | |
| 57 | ,true,true | |
| 58 | valid.example,false,true | |
| 59 | """ | |
| 60 | ||
| 61 | let parsed = try DataPortabilityCSV.parseTrackedDomains(from: csv) | |
| 62 | ||
| 63 | XCTAssertEqual(parsed.map(\.domain), ["valid.example"]) | |
| 64 | } | |
| 65 | ||
| 66 | // MARK: - Merge / dedup | |
| 67 | ||
| 68 | func testSaveLoadDeduplicatesSameDomainCaseInsensitively() { | |
| 69 | let older = TrackedDomain( | |
| 70 | domain: "example.com", | |
| 71 | createdAt: base.addingTimeInterval(-100), | |
| 72 | updatedAt: base.addingTimeInterval(-50), | |
| 73 | isPinned: false, | |
| 74 | monitoringEnabled: false | |
| 75 | ) | |
| 76 | let newer = TrackedDomain( | |
| 77 | domain: "EXAMPLE.com", | |
| 78 | createdAt: base.addingTimeInterval(-80), | |
| 79 | updatedAt: base.addingTimeInterval(-10), | |
| 80 | isPinned: true, | |
| 81 | monitoringEnabled: false | |
| 82 | ) | |
| 83 | ||
| 84 | DomainDataPortabilityService.saveTrackedDomains([older, newer], defaults: defaults) | |
| 85 | let loaded = DomainDataPortabilityService.loadTrackedDomains(defaults: defaults) | |
| 86 | ||
| 87 | XCTAssertEqual(loaded.count, 1, "same domain differing only in case must collapse to one entry") | |
| 88 | let merged = loaded[0] | |
| 89 | XCTAssertEqual(merged.domain, "example.com") | |
| 90 | XCTAssertTrue(merged.isPinned, "pinned state is OR-merged") | |
| 91 | XCTAssertFalse(merged.monitoringEnabled) | |
| 92 | XCTAssertEqual(merged.createdAt, base.addingTimeInterval(-100), "createdAt takes the earliest") | |
| 93 | XCTAssertEqual(merged.updatedAt, base.addingTimeInterval(-10), "updatedAt takes the latest") | |
| 94 | } | |
| 95 | ||
| 96 | func testSaveLoadKeepsDistinctDomainsSortedByRecency() { | |
| 97 | let domains = [ | |
| 98 | TrackedDomain(domain: "old.example", updatedAt: base.addingTimeInterval(-300)), | |
| 99 | TrackedDomain(domain: "new.example", updatedAt: base.addingTimeInterval(-10)), | |
| 100 | TrackedDomain(domain: "mid.example", updatedAt: base.addingTimeInterval(-100)) | |
| 101 | ] | |
| 102 | ||
| 103 | DomainDataPortabilityService.saveTrackedDomains(domains, defaults: defaults) | |
| 104 | let loaded = DomainDataPortabilityService.loadTrackedDomains(defaults: defaults) | |
| 105 | ||
| 106 | XCTAssertEqual(loaded.map(\.domain), ["new.example", "mid.example", "old.example"]) | |
| 107 | } | |
| 108 | ||
| 109 | func testLoadTrackedDomainsEmptyWhenUnset() { | |
| 110 | XCTAssertTrue(DomainDataPortabilityService.loadTrackedDomains(defaults: defaults).isEmpty) | |
| 111 | } | |
| 112 | ||
| 113 | // MARK: - Recent searches | |
| 114 | ||
| 115 | func testRecentSearchesRoundTripAndTruncateToTwenty() { | |
| 116 | let values = (0..<30).map { "domain\($0).example" } | |
| 117 | ||
| 118 | DomainDataPortabilityService.saveRecentSearches(values, defaults: defaults) | |
| 119 | let loaded = DomainDataPortabilityService.loadRecentSearches(defaults: defaults) | |
| 120 | ||
| 121 | XCTAssertEqual(loaded.count, 20, "recent searches are capped at 20") | |
| 122 | XCTAssertEqual(loaded, Array(values.prefix(20)), "order is preserved") | |
| 123 | } | |
| 124 | } | |
DomainDigTests/DomainReportBuilderTests.swift added +130
| @@ -0,0 +1,130 @@ | ||
| 1 | import XCTest | |
| 2 | @testable import DomainDig | |
| 3 | ||
| 4 | /// Characterization tests for the snapshot → report projection. These lock the | |
| 5 | /// field-mapping and derivation rules the export/diff contracts depend on. | |
| 6 | final class DomainReportBuilderTests: XCTestCase { | |
| 7 | private let builder = DomainReportBuilder() | |
| 8 | ||
| 9 | func testCoreIdentityFieldsPassThrough() { | |
| 10 | let report = builder.build( | |
| 11 | from: SnapshotFixture.snapshot( | |
| 12 | domain: "mapped.example", | |
| 13 | resolverURLString: "https://r.example/dns-query" | |
| 14 | ), | |
| 15 | deriveChangeSummary: false | |
| 16 | ) | |
| 17 | ||
| 18 | XCTAssertEqual(report.domain, "mapped.example") | |
| 19 | XCTAssertEqual(report.timestamp, SnapshotFixture.referenceDate) | |
| 20 | XCTAssertEqual(report.resolverURLString, "https://r.example/dns-query") | |
| 21 | XCTAssertFalse(report.metadata.schemaVersion.isEmpty) | |
| 22 | } | |
| 23 | ||
| 24 | func testAvailabilityDefaultsToUnknownWhenAbsent() { | |
| 25 | let unknown = builder.build(from: SnapshotFixture.snapshot(availability: nil), deriveChangeSummary: false) | |
| 26 | XCTAssertEqual(unknown.availability, .unknown) | |
| 27 | ||
| 28 | let registered = builder.build(from: SnapshotFixture.snapshot(availability: .registered), deriveChangeSummary: false) | |
| 29 | XCTAssertEqual(registered.availability, .registered) | |
| 30 | } | |
| 31 | ||
| 32 | func testPrimaryIPComesFromFirstARecord() { | |
| 33 | let report = builder.build( | |
| 34 | from: SnapshotFixture.snapshot( | |
| 35 | dnsSections: [ | |
| 36 | SnapshotFixture.dnsSection(type: .A, values: ["198.51.100.7", "198.51.100.8"]), | |
| 37 | SnapshotFixture.dnsSection(type: .AAAA, values: ["2001:db8::1"]) | |
| 38 | ] | |
| 39 | ), | |
| 40 | deriveChangeSummary: false | |
| 41 | ) | |
| 42 | ||
| 43 | XCTAssertEqual(report.dns.primaryIP, "198.51.100.7") | |
| 44 | XCTAssertEqual(report.network.primaryIP, "198.51.100.7") | |
| 45 | } | |
| 46 | ||
| 47 | func testPrimaryIPNilWithoutARecord() { | |
| 48 | let report = builder.build( | |
| 49 | from: SnapshotFixture.snapshot( | |
| 50 | dnsSections: [SnapshotFixture.dnsSection(type: .MX, values: ["mail.example.com"])] | |
| 51 | ), | |
| 52 | deriveChangeSummary: false | |
| 53 | ) | |
| 54 | XCTAssertNil(report.dns.primaryIP) | |
| 55 | } | |
| 56 | ||
| 57 | func testDNSSECDerivedFromSections() { | |
| 58 | let signed = builder.build( | |
| 59 | from: SnapshotFixture.snapshot( | |
| 60 | dnsSections: [SnapshotFixture.dnsSection(type: .A, values: ["203.0.113.1"], dnssecSigned: true)] | |
| 61 | ), | |
| 62 | deriveChangeSummary: false | |
| 63 | ) | |
| 64 | XCTAssertEqual(signed.dns.dnssecSigned, true) | |
| 65 | ||
| 66 | let unknown = builder.build( | |
| 67 | from: SnapshotFixture.snapshot( | |
| 68 | dnsSections: [SnapshotFixture.dnsSection(type: .A, values: ["203.0.113.1"])] | |
| 69 | ), | |
| 70 | deriveChangeSummary: false | |
| 71 | ) | |
| 72 | XCTAssertNil(unknown.dns.dnssecSigned) | |
| 73 | } | |
| 74 | ||
| 75 | func testTLSStatusReflectsCertificatePresence() { | |
| 76 | let valid = builder.build( | |
| 77 | from: SnapshotFixture.snapshot(sslInfo: SnapshotFixture.certificate()), | |
| 78 | deriveChangeSummary: false | |
| 79 | ) | |
| 80 | XCTAssertEqual(valid.web.tlsStatus, "valid") | |
| 81 | ||
| 82 | let missing = builder.build(from: SnapshotFixture.snapshot(sslInfo: nil), deriveChangeSummary: false) | |
| 83 | XCTAssertEqual(missing.web.tlsStatus, "unavailable") | |
| 84 | } | |
| 85 | ||
| 86 | func testWebHeaderCountAndFinalURL() { | |
| 87 | let report = builder.build( | |
| 88 | from: SnapshotFixture.snapshot( | |
| 89 | httpHeaders: [ | |
| 90 | HTTPHeader(name: "Content-Type", value: "text/html"), | |
| 91 | HTTPHeader(name: "Strict-Transport-Security", value: "max-age=63072000") | |
| 92 | ], | |
| 93 | httpStatusCode: 200 | |
| 94 | ), | |
| 95 | deriveChangeSummary: false | |
| 96 | ) | |
| 97 | ||
| 98 | XCTAssertEqual(report.web.headerCount, 2) | |
| 99 | XCTAssertEqual(report.web.statusCode, 200) | |
| 100 | XCTAssertNil(report.web.finalURL, "no redirect chain means no final URL") | |
| 101 | } | |
| 102 | ||
| 103 | func testPartialSnapshotAndValidationIssuesPropagate() { | |
| 104 | let report = builder.build( | |
| 105 | from: SnapshotFixture.snapshot( | |
| 106 | isPartialSnapshot: true, | |
| 107 | validationIssues: ["missing DNS", "stale WHOIS"] | |
| 108 | ), | |
| 109 | deriveChangeSummary: false | |
| 110 | ) | |
| 111 | ||
| 112 | XCTAssertTrue(report.isPartialSnapshot) | |
| 113 | XCTAssertEqual(report.validationIssues, ["missing DNS", "stale WHOIS"]) | |
| 114 | XCTAssertTrue(report.metadata.isPartialSnapshot) | |
| 115 | XCTAssertEqual(report.metadata.validationIssues, ["missing DNS", "stale WHOIS"]) | |
| 116 | } | |
| 117 | ||
| 118 | func testRecordSectionsArePreserved() { | |
| 119 | let sections = [ | |
| 120 | SnapshotFixture.dnsSection(type: .A, values: ["203.0.113.1"]), | |
| 121 | SnapshotFixture.dnsSection(type: .MX, values: ["mail.example.com"]) | |
| 122 | ] | |
| 123 | let report = builder.build( | |
| 124 | from: SnapshotFixture.snapshot(dnsSections: sections), | |
| 125 | deriveChangeSummary: false | |
| 126 | ) | |
| 127 | ||
| 128 | XCTAssertEqual(Set(report.dns.recordSections.map(\.recordType)), [.A, .MX]) | |
| 129 | } | |
| 130 | } | |
DomainDigTests/DomainReportExporterTests.swift added +107
| @@ -0,0 +1,107 @@ | ||
| 1 | import XCTest | |
| 2 | @testable import DomainDig | |
| 3 | ||
| 4 | /// Characterization tests for the export surface. These pin the format dispatch, | |
| 5 | /// the JSON round-trip (the canonical machine contract), and the structural | |
| 6 | /// invariants of the human-readable formats. | |
| 7 | final class DomainReportExporterTests: XCTestCase { | |
| 8 | ||
| 9 | private let decoder: JSONDecoder = { | |
| 10 | let decoder = JSONDecoder() | |
| 11 | decoder.dateDecodingStrategy = .iso8601 | |
| 12 | return decoder | |
| 13 | }() | |
| 14 | ||
| 15 | func testEveryFormatProducesNonEmptyData() throws { | |
| 16 | let report = SnapshotFixture.report(availability: .registered) | |
| 17 | for format in DomainExportFormat.allCases { | |
| 18 | let data = try DomainReportExporter.data(for: report, format: format) | |
| 19 | XCTAssertFalse(data.isEmpty, "\(format.rawValue) export was empty") | |
| 20 | } | |
| 21 | } | |
| 22 | ||
| 23 | func testJSONExportRoundTripsToReport() throws { | |
| 24 | let report = SnapshotFixture.report(domain: "roundtrip.example", availability: .registered) | |
| 25 | ||
| 26 | let data = try DomainReportExporter.data(for: report, format: .json) | |
| 27 | let decoded = try decoder.decode(DomainReport.self, from: data) | |
| 28 | ||
| 29 | XCTAssertEqual(decoded.domain, "roundtrip.example") | |
| 30 | XCTAssertEqual(decoded.availability, .registered) | |
| 31 | XCTAssertEqual(decoded.timestamp, report.timestamp) | |
| 32 | } | |
| 33 | ||
| 34 | func testBatchJSONExportRoundTripsToReportArray() throws { | |
| 35 | let reports = [ | |
| 36 | SnapshotFixture.report(domain: "one.example"), | |
| 37 | SnapshotFixture.report(domain: "two.example") | |
| 38 | ] | |
| 39 | ||
| 40 | let data = try DomainReportExporter.data(for: reports, format: .json, title: "Batch") | |
| 41 | let decoded = try decoder.decode([DomainReport].self, from: data) | |
| 42 | ||
| 43 | XCTAssertEqual(decoded.map(\.domain), ["one.example", "two.example"]) | |
| 44 | } | |
| 45 | ||
| 46 | func testCSVHasHeaderAndOneRowPerReport() { | |
| 47 | let reports = [ | |
| 48 | SnapshotFixture.report(domain: "alpha.example"), | |
| 49 | SnapshotFixture.report(domain: "beta.example") | |
| 50 | ] | |
| 51 | ||
| 52 | let csv = DomainReportExporter.csv(for: reports) | |
| 53 | let lines = csv.split(separator: "\n", omittingEmptySubsequences: false) | |
| 54 | ||
| 55 | XCTAssertEqual(lines.count, 3, "one header line plus one row per report") | |
| 56 | XCTAssertTrue(lines[0].contains("\"domain\"")) | |
| 57 | XCTAssertTrue(csv.contains("\"alpha.example\"")) | |
| 58 | XCTAssertTrue(csv.contains("\"beta.example\"")) | |
| 59 | } | |
| 60 | ||
| 61 | func testMarkdownIsHeadedAndNamesTheDomain() { | |
| 62 | let markdown = DomainReportExporter.markdown(for: SnapshotFixture.report(domain: "md.example")) | |
| 63 | ||
| 64 | XCTAssertTrue(markdown.hasPrefix("# "), "markdown export should open with an H1") | |
| 65 | XCTAssertTrue(markdown.contains("md.example")) | |
| 66 | } | |
| 67 | ||
| 68 | func testTextExportNamesTheDomain() { | |
| 69 | let text = DomainReportExporter.text(for: SnapshotFixture.report(domain: "txt.example")) | |
| 70 | XCTAssertTrue(text.contains("txt.example")) | |
| 71 | } | |
| 72 | ||
| 73 | func testBatchExportsCarryTitleAndEveryDomain() { | |
| 74 | let reports = [ | |
| 75 | SnapshotFixture.report(domain: "first.example"), | |
| 76 | SnapshotFixture.report(domain: "second.example") | |
| 77 | ] | |
| 78 | ||
| 79 | let markdown = DomainReportExporter.batchMarkdown(for: reports, title: "Portfolio Sweep") | |
| 80 | XCTAssertTrue(markdown.contains("Portfolio Sweep")) | |
| 81 | XCTAssertTrue(markdown.contains("first.example")) | |
| 82 | XCTAssertTrue(markdown.contains("second.example")) | |
| 83 | ||
| 84 | let text = DomainReportExporter.batchText(for: reports, title: "Portfolio Sweep") | |
| 85 | XCTAssertTrue(text.contains("Portfolio Sweep")) | |
| 86 | XCTAssertTrue(text.contains("first.example")) | |
| 87 | XCTAssertTrue(text.contains("second.example")) | |
| 88 | } | |
| 89 | ||
| 90 | func testPDFExportHasPDFSignature() throws { | |
| 91 | let data = try DomainReportExporter.data(for: SnapshotFixture.report(), format: .pdf) | |
| 92 | XCTAssertTrue(data.starts(with: Array("%PDF".utf8)), "PDF export should begin with the %PDF signature") | |
| 93 | } | |
| 94 | ||
| 95 | func testTimelineTextNamesTheDomain() { | |
| 96 | let reports = [ | |
| 97 | SnapshotFixture.report(domain: "timeline.example", timestamp: SnapshotFixture.referenceDate), | |
| 98 | SnapshotFixture.report( | |
| 99 | domain: "timeline.example", | |
| 100 | timestamp: SnapshotFixture.referenceDate.addingTimeInterval(86_400) | |
| 101 | ) | |
| 102 | ] | |
| 103 | ||
| 104 | let text = DomainReportExporter.timelineText(for: reports, domain: "timeline.example", includeDiffSummary: false) | |
| 105 | XCTAssertTrue(text.contains("timeline.example")) | |
| 106 | } | |
| 107 | } | |
DomainDigTests/Fixtures/SnapshotFixture.swift added +191
| @@ -0,0 +1,191 @@ | ||
| 1 | import Foundation | |
| 2 | @testable import DomainDig | |
| 3 | ||
| 4 | /// Deterministic builders for the deep `LookupSnapshot` / `DomainReport` models | |
| 5 | /// so the core unit tests can construct inputs without wiring up every one of the | |
| 6 | /// ~75 snapshot fields at each call site. | |
| 7 | /// | |
| 8 | /// `snapshot(...)` exposes only the fields the tests actually vary; everything | |
| 9 | /// else defaults to an empty/absent value. `report(...)` runs a snapshot through | |
| 10 | /// the real `DomainReportBuilder`, which is both the natural constructor for the | |
| 11 | /// otherwise-unconstructable `DomainReport` and, for the builder tests, the unit | |
| 12 | /// under test. | |
| 13 | enum SnapshotFixture { | |
| 14 | /// Fixed instant so timestamp-derived output (diff ranges, export headers) is | |
| 15 | /// stable across runs. | |
| 16 | static let referenceDate = Date(timeIntervalSince1970: 1_700_000_000) | |
| 17 | ||
| 18 | static let defaultResolverURL = "https://resolver.example/dns-query" | |
| 19 | ||
| 20 | static func snapshot( | |
| 21 | domain: String = "example.com", | |
| 22 | timestamp: Date = referenceDate, | |
| 23 | resolverURLString: String = defaultResolverURL, | |
| 24 | resultSource: LookupResultSource = .live, | |
| 25 | availability: DomainAvailabilityStatus? = nil, | |
| 26 | ownership: DomainOwnership? = nil, | |
| 27 | dnsSections: [DNSSection] = [], | |
| 28 | ptrRecord: String? = nil, | |
| 29 | sslInfo: SSLCertificateInfo? = nil, | |
| 30 | httpHeaders: [HTTPHeader] = [], | |
| 31 | httpStatusCode: Int? = nil, | |
| 32 | httpSecurityGrade: String? = nil, | |
| 33 | subdomains: [DiscoveredSubdomain] = [], | |
| 34 | extendedSubdomains: [DiscoveredSubdomain] = [], | |
| 35 | isPartialSnapshot: Bool = false, | |
| 36 | validationIssues: [String] = [], | |
| 37 | changeSummary: DomainChangeSummary? = nil | |
| 38 | ) -> LookupSnapshot { | |
| 39 | LookupSnapshot( | |
| 40 | historyEntryID: nil, | |
| 41 | domain: domain, | |
| 42 | timestamp: timestamp, | |
| 43 | trackedDomainID: nil, | |
| 44 | note: nil, | |
| 45 | appVersion: "test", | |
| 46 | resolverDisplayName: "Test Resolver", | |
| 47 | resolverURLString: resolverURLString, | |
| 48 | dataSources: [], | |
| 49 | provenanceBySection: [:], | |
| 50 | availabilityConfidence: nil, | |
| 51 | ownershipConfidence: nil, | |
| 52 | subdomainConfidence: nil, | |
| 53 | emailSecurityConfidence: nil, | |
| 54 | geolocationConfidence: nil, | |
| 55 | errorDetails: [:], | |
| 56 | isPartialSnapshot: isPartialSnapshot, | |
| 57 | validationIssues: validationIssues, | |
| 58 | totalLookupDurationMs: nil, | |
| 59 | snapshotIndex: nil, | |
| 60 | previousSnapshotID: nil, | |
| 61 | changeCount: 0, | |
| 62 | severitySummary: nil, | |
| 63 | dnsSections: dnsSections, | |
| 64 | dnsError: nil, | |
| 65 | availabilityResult: availability.map { DomainAvailabilityResult(domain: domain, status: $0) }, | |
| 66 | suggestions: [], | |
| 67 | sslInfo: sslInfo, | |
| 68 | sslError: nil, | |
| 69 | hstsPreloaded: nil, | |
| 70 | httpHeaders: httpHeaders, | |
| 71 | httpSecurityGrade: httpSecurityGrade, | |
| 72 | httpStatusCode: httpStatusCode, | |
| 73 | httpResponseTimeMs: nil, | |
| 74 | httpProtocol: nil, | |
| 75 | http3Advertised: false, | |
| 76 | httpHeadersError: nil, | |
| 77 | reachabilityResults: [], | |
| 78 | reachabilityError: nil, | |
| 79 | ipGeolocation: nil, | |
| 80 | ipGeolocationError: nil, | |
| 81 | emailSecurity: nil, | |
| 82 | emailSecurityError: nil, | |
| 83 | ownership: ownership, | |
| 84 | ownershipError: nil, | |
| 85 | ownershipHistory: [], | |
| 86 | ownershipHistoryError: nil, | |
| 87 | inferredProvider: nil, | |
| 88 | priorProviders: [], | |
| 89 | domainClassification: nil, | |
| 90 | ownershipTransitions: [], | |
| 91 | hostingTransitions: [], | |
| 92 | subdomainHistory: [], | |
| 93 | riskSignals: [], | |
| 94 | intelligenceTimeline: [], | |
| 95 | ptrRecord: ptrRecord, | |
| 96 | ptrError: nil, | |
| 97 | redirectChain: [], | |
| 98 | redirectChainError: nil, | |
| 99 | subdomains: subdomains, | |
| 100 | subdomainsError: nil, | |
| 101 | extendedSubdomains: extendedSubdomains, | |
| 102 | extendedSubdomainsError: nil, | |
| 103 | dnsHistory: [], | |
| 104 | dnsHistoryError: nil, | |
| 105 | domainPricing: nil, | |
| 106 | domainPricingError: nil, | |
| 107 | reputation: nil, | |
| 108 | reputationError: nil, | |
| 109 | portScanResults: [], | |
| 110 | portScanError: nil, | |
| 111 | changeSummary: changeSummary, | |
| 112 | resultSource: resultSource, | |
| 113 | cachedSections: [], | |
| 114 | statusMessage: nil | |
| 115 | ) | |
| 116 | } | |
| 117 | ||
| 118 | static func report( | |
| 119 | domain: String = "example.com", | |
| 120 | timestamp: Date = referenceDate, | |
| 121 | resolverURLString: String = defaultResolverURL, | |
| 122 | resultSource: LookupResultSource = .live, | |
| 123 | availability: DomainAvailabilityStatus? = nil, | |
| 124 | ownership: DomainOwnership? = nil, | |
| 125 | dnsSections: [DNSSection] = [], | |
| 126 | ptrRecord: String? = nil, | |
| 127 | sslInfo: SSLCertificateInfo? = nil, | |
| 128 | httpHeaders: [HTTPHeader] = [], | |
| 129 | httpStatusCode: Int? = nil, | |
| 130 | httpSecurityGrade: String? = nil, | |
| 131 | subdomains: [DiscoveredSubdomain] = [], | |
| 132 | extendedSubdomains: [DiscoveredSubdomain] = [], | |
| 133 | isPartialSnapshot: Bool = false, | |
| 134 | validationIssues: [String] = [] | |
| 135 | ) -> DomainReport { | |
| 136 | DomainReportBuilder().build( | |
| 137 | from: snapshot( | |
| 138 | domain: domain, | |
| 139 | timestamp: timestamp, | |
| 140 | resolverURLString: resolverURLString, | |
| 141 | resultSource: resultSource, | |
| 142 | availability: availability, | |
| 143 | ownership: ownership, | |
| 144 | dnsSections: dnsSections, | |
| 145 | ptrRecord: ptrRecord, | |
| 146 | sslInfo: sslInfo, | |
| 147 | httpHeaders: httpHeaders, | |
| 148 | httpStatusCode: httpStatusCode, | |
| 149 | httpSecurityGrade: httpSecurityGrade, | |
| 150 | subdomains: subdomains, | |
| 151 | extendedSubdomains: extendedSubdomains, | |
| 152 | isPartialSnapshot: isPartialSnapshot, | |
| 153 | validationIssues: validationIssues | |
| 154 | ), | |
| 155 | deriveChangeSummary: false | |
| 156 | ) | |
| 157 | } | |
| 158 | ||
| 159 | // MARK: - Nested model conveniences | |
| 160 | ||
| 161 | static func dnsSection( | |
| 162 | type: DNSRecordType, | |
| 163 | values: [String], | |
| 164 | ttl: Int = 300, | |
| 165 | dnssecSigned: Bool? = nil, | |
| 166 | wildcards: [String] = [] | |
| 167 | ) -> DNSSection { | |
| 168 | DNSSection( | |
| 169 | recordType: type, | |
| 170 | records: values.map { DNSRecord(value: $0, ttl: ttl) }, | |
| 171 | wildcardRecords: wildcards.map { DNSRecord(value: $0, ttl: ttl) }, | |
| 172 | dnssecSigned: dnssecSigned | |
| 173 | ) | |
| 174 | } | |
| 175 | ||
| 176 | static func certificate( | |
| 177 | commonName: String = "example.com", | |
| 178 | issuer: String = "Test CA", | |
| 179 | daysUntilExpiry: Int = 90 | |
| 180 | ) -> SSLCertificateInfo { | |
| 181 | SSLCertificateInfo( | |
| 182 | commonName: commonName, | |
| 183 | subjectAltNames: [commonName], | |
| 184 | issuer: issuer, | |
| 185 | validFrom: referenceDate, | |
| 186 | validUntil: referenceDate.addingTimeInterval(Double(daysUntilExpiry) * 86_400), | |
| 187 | daysUntilExpiry: daysUntilExpiry, | |
| 188 | chainDepth: 1 | |
| 189 | ) | |
| 190 | } | |
| 191 | } | |