Commit 1bc0c44013
Verified · cmc
Layout: unified · split
.gitignore added +3
| @@ -0,0 +1,3 @@ | ||
| 1 | ||
| 2 | # Xcode per-user state | |
| 3 | xcuserdata/ | |
DomainDig.xcodeproj/project.pbxproj +4
| @@ -12,6 +12,7 @@ | ||
| 12 | 12 | 47CD3BB1AE143733A73E0E5B /* DomainReportExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D7C7D97A4006831572F468 /* DomainReportExporterTests.swift */; }; |
| 13 | 13 | 4F96CEB875EC3501E784CE39 /* DataMigrationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435AFB3F99D579D3D7B58279 /* DataMigrationServiceTests.swift */; }; |
| 14 | 14 | 81359F63C7A23454B8FA0141 /* DomainReportBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82320955416797CFE59464A /* DomainReportBuilderTests.swift */; }; |
| 15 | A1B2C3D40000000000000102 /* HistoryEntryCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000000000000101 /* HistoryEntryCodableTests.swift */; }; | |
| 15 | 16 | 8BBFEF092F9874AE00E8E144 /* DomainInspectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF032F9874AE00E8E144 /* DomainInspectionService.swift */; }; |
| 16 | 17 | 8BBFEF0A2F9874AE00E8E144 /* DomainReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */; }; |
| 17 | 18 | 8BBFEF0B2F9874AE00E8E144 /* DomainReportExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */; }; |
| @@ -98,6 +99,7 @@ | ||
| 98 | 99 | CC948A02EC0184228BC4630E /* DomainDataPortabilityServiceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityServiceTests.swift; sourceTree = "<group>"; }; |
| 99 | 100 | 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; }; |
| 100 | 101 | E82320955416797CFE59464A /* DomainReportBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DomainReportBuilderTests.swift; sourceTree = "<group>"; }; |
| 102 | A1B2C3D40000000000000101 /* HistoryEntryCodableTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryEntryCodableTests.swift; sourceTree = "<group>"; }; | |
| 101 | 103 | /* End PBXFileReference section */ |
| 102 | 104 | |
| 103 | 105 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| @@ -235,6 +237,7 @@ | ||
| 235 | 237 | 0D85A44C5F315A1644AC9073 /* LocalAPIContractTests.swift */, |
| 236 | 238 | 435AFB3F99D579D3D7B58279 /* DataMigrationServiceTests.swift */, |
| 237 | 239 | 499B99A1866999A38AC475F5 /* AppInfoTests.swift */, |
| 240 | A1B2C3D40000000000000101 /* HistoryEntryCodableTests.swift */, | |
| 238 | 241 | ); |
| 239 | 242 | name = DomainDigTests; |
| 240 | 243 | path = DomainDigTests; |
| @@ -489,6 +492,7 @@ | ||
| 489 | 492 | files = ( |
| 490 | 493 | E959C4D24DAAB3CB80D854B2 /* DiffServiceTests.swift in Sources */, |
| 491 | 494 | 81359F63C7A23454B8FA0141 /* DomainReportBuilderTests.swift in Sources */, |
| 495 | A1B2C3D40000000000000102 /* HistoryEntryCodableTests.swift in Sources */, | |
| 492 | 496 | 47CD3BB1AE143733A73E0E5B /* DomainReportExporterTests.swift in Sources */, |
| 493 | 497 | C7CA9E02B0DC2708DE7A8563 /* DomainDataPortabilityServiceTests.swift in Sources */, |
| 494 | 498 | A5AF921BC1C2E6E940CC05DC /* SnapshotFixture.swift in Sources */, |
DomainDig/DNSLookupService.swift +1 −1
| @@ -364,7 +364,7 @@ struct DNSLookupService { | ||
| 364 | 364 | answers.append(.init( |
| 365 | 365 | name: name, |
| 366 | 366 | type: type, |
| 367 | TTL: ttl, | |
| 367 | ttl: ttl, | |
| 368 | 368 | data: parsedValue |
| 369 | 369 | )) |
| 370 | 370 | } |
DomainDig/DomainMonitoringService.swift +74 −60
| @@ -520,66 +520,80 @@ final class DomainMonitoringService { | ||
| 520 | 520 | } |
| 521 | 521 | |
| 522 | 522 | let entry = HistoryEntry( |
| 523 | domain: snapshot.domain, | |
| 524 | timestamp: snapshot.timestamp, | |
| 525 | trackedDomainID: trackedDomainID, | |
| 526 | note: trackedDomains.first(where: { $0.id == trackedDomainID })?.note, | |
| 527 | dnsSections: snapshot.dnsSections, | |
| 528 | sslInfo: snapshot.sslInfo, | |
| 529 | httpHeaders: snapshot.httpHeaders, | |
| 530 | reachabilityResults: snapshot.reachabilityResults, | |
| 531 | ipGeolocation: snapshot.ipGeolocation, | |
| 532 | emailSecurity: snapshot.emailSecurity, | |
| 533 | mtaSts: snapshot.emailSecurity?.mtaSts, | |
| 534 | ownership: snapshot.ownership, | |
| 535 | ownershipHistory: snapshot.ownershipHistory, | |
| 536 | ptrRecord: snapshot.ptrRecord, | |
| 537 | redirectChain: snapshot.redirectChain, | |
| 538 | subdomains: snapshot.subdomains, | |
| 539 | extendedSubdomains: snapshot.extendedSubdomains, | |
| 540 | dnsHistory: snapshot.dnsHistory, | |
| 541 | domainPricing: snapshot.domainPricing, | |
| 542 | reputation: snapshot.reputation, | |
| 543 | portScanResults: snapshot.portScanResults, | |
| 544 | hstsPreloaded: snapshot.hstsPreloaded, | |
| 545 | availabilityResult: snapshot.availabilityResult, | |
| 546 | suggestions: snapshot.suggestions, | |
| 547 | appVersion: snapshot.appVersion, | |
| 548 | resultSource: snapshot.resultSource, | |
| 549 | dataSources: snapshot.dataSources, | |
| 550 | provenanceBySection: snapshot.provenanceBySection, | |
| 551 | availabilityConfidence: snapshot.availabilityConfidence, | |
| 552 | ownershipConfidence: snapshot.ownershipConfidence, | |
| 553 | subdomainConfidence: snapshot.subdomainConfidence, | |
| 554 | emailSecurityConfidence: snapshot.emailSecurityConfidence, | |
| 555 | geolocationConfidence: snapshot.geolocationConfidence, | |
| 556 | errorDetails: snapshot.errorDetails, | |
| 557 | isPartialSnapshot: snapshot.isPartialSnapshot, | |
| 558 | validationIssues: snapshot.validationIssues, | |
| 559 | resolverDisplayName: snapshot.resolverDisplayName, | |
| 560 | resolverURLString: snapshot.resolverURLString, | |
| 561 | totalLookupDurationMs: snapshot.totalLookupDurationMs, | |
| 562 | primaryIP: Self.primaryIPAddress(from: snapshot), | |
| 563 | finalRedirectURL: snapshot.redirectChain.last?.url, | |
| 564 | tlsStatusSummary: Self.tlsSummary(from: snapshot), | |
| 565 | emailSecuritySummary: Self.emailSummary(from: snapshot), | |
| 566 | httpGradeSummary: snapshot.httpSecurityGrade ?? snapshot.httpHeadersError, | |
| 567 | changeSummary: changeSummary, | |
| 568 | sslError: snapshot.sslError, | |
| 569 | httpHeadersError: snapshot.httpHeadersError, | |
| 570 | reachabilityError: snapshot.reachabilityError, | |
| 571 | ipGeolocationError: snapshot.ipGeolocationError, | |
| 572 | emailSecurityError: snapshot.emailSecurityError, | |
| 573 | ownershipError: snapshot.ownershipError, | |
| 574 | ownershipHistoryError: snapshot.ownershipHistoryError, | |
| 575 | ptrError: snapshot.ptrError, | |
| 576 | redirectChainError: snapshot.redirectChainError, | |
| 577 | subdomainsError: snapshot.subdomainsError, | |
| 578 | extendedSubdomainsError: snapshot.extendedSubdomainsError, | |
| 579 | dnsHistoryError: snapshot.dnsHistoryError, | |
| 580 | domainPricingError: snapshot.domainPricingError, | |
| 581 | reputationError: snapshot.reputationError, | |
| 582 | portScanError: snapshot.portScanError | |
| 523 | identity: HistoryEntry.Identity( | |
| 524 | domain: snapshot.domain, | |
| 525 | timestamp: snapshot.timestamp, | |
| 526 | trackedDomainID: trackedDomainID, | |
| 527 | note: trackedDomains.first(where: { $0.id == trackedDomainID })?.note | |
| 528 | ), | |
| 529 | inspection: HistoryEntry.Inspection( | |
| 530 | dnsSections: snapshot.dnsSections, | |
| 531 | sslInfo: snapshot.sslInfo, | |
| 532 | httpHeaders: snapshot.httpHeaders, | |
| 533 | reachabilityResults: snapshot.reachabilityResults, | |
| 534 | ipGeolocation: snapshot.ipGeolocation, | |
| 535 | emailSecurity: snapshot.emailSecurity, | |
| 536 | mtaSts: snapshot.emailSecurity?.mtaSts, | |
| 537 | ptrRecord: snapshot.ptrRecord, | |
| 538 | redirectChain: snapshot.redirectChain, | |
| 539 | subdomains: snapshot.subdomains, | |
| 540 | extendedSubdomains: snapshot.extendedSubdomains, | |
| 541 | dnsHistory: snapshot.dnsHistory, | |
| 542 | portScanResults: snapshot.portScanResults, | |
| 543 | hstsPreloaded: snapshot.hstsPreloaded, | |
| 544 | availabilityResult: snapshot.availabilityResult, | |
| 545 | suggestions: snapshot.suggestions | |
| 546 | ), | |
| 547 | registration: HistoryEntry.Registration( | |
| 548 | ownership: snapshot.ownership, | |
| 549 | ownershipHistory: snapshot.ownershipHistory, | |
| 550 | domainPricing: snapshot.domainPricing | |
| 551 | ), | |
| 552 | intelligence: HistoryEntry.Intelligence( | |
| 553 | reputation: snapshot.reputation | |
| 554 | ), | |
| 555 | provenance: HistoryEntry.Provenance( | |
| 556 | appVersion: snapshot.appVersion, | |
| 557 | resultSource: snapshot.resultSource, | |
| 558 | dataSources: snapshot.dataSources, | |
| 559 | provenanceBySection: snapshot.provenanceBySection, | |
| 560 | availabilityConfidence: snapshot.availabilityConfidence, | |
| 561 | ownershipConfidence: snapshot.ownershipConfidence, | |
| 562 | subdomainConfidence: snapshot.subdomainConfidence, | |
| 563 | emailSecurityConfidence: snapshot.emailSecurityConfidence, | |
| 564 | geolocationConfidence: snapshot.geolocationConfidence, | |
| 565 | isPartialSnapshot: snapshot.isPartialSnapshot, | |
| 566 | validationIssues: snapshot.validationIssues, | |
| 567 | resolverDisplayName: snapshot.resolverDisplayName, | |
| 568 | resolverURLString: snapshot.resolverURLString, | |
| 569 | totalLookupDurationMs: snapshot.totalLookupDurationMs | |
| 570 | ), | |
| 571 | summary: HistoryEntry.Summary( | |
| 572 | primaryIP: Self.primaryIPAddress(from: snapshot), | |
| 573 | finalRedirectURL: snapshot.redirectChain.last?.url, | |
| 574 | tlsStatusSummary: Self.tlsSummary(from: snapshot), | |
| 575 | emailSecuritySummary: Self.emailSummary(from: snapshot), | |
| 576 | httpGradeSummary: snapshot.httpSecurityGrade ?? snapshot.httpHeadersError, | |
| 577 | changeSummary: changeSummary | |
| 578 | ), | |
| 579 | failures: HistoryEntry.Failures( | |
| 580 | errorDetails: snapshot.errorDetails, | |
| 581 | sslError: snapshot.sslError, | |
| 582 | httpHeadersError: snapshot.httpHeadersError, | |
| 583 | reachabilityError: snapshot.reachabilityError, | |
| 584 | ipGeolocationError: snapshot.ipGeolocationError, | |
| 585 | emailSecurityError: snapshot.emailSecurityError, | |
| 586 | ownershipError: snapshot.ownershipError, | |
| 587 | ownershipHistoryError: snapshot.ownershipHistoryError, | |
| 588 | ptrError: snapshot.ptrError, | |
| 589 | redirectChainError: snapshot.redirectChainError, | |
| 590 | subdomainsError: snapshot.subdomainsError, | |
| 591 | extendedSubdomainsError: snapshot.extendedSubdomainsError, | |
| 592 | dnsHistoryError: snapshot.dnsHistoryError, | |
| 593 | domainPricingError: snapshot.domainPricingError, | |
| 594 | reputationError: snapshot.reputationError, | |
| 595 | portScanError: snapshot.portScanError | |
| 596 | ) | |
| 583 | 597 | ) |
| 584 | 598 | |
| 585 | 599 | history.insert(entry, at: 0) |
DomainDig/DomainViewModel.swift +86 −72
| @@ -1936,78 +1936,92 @@ final class DomainViewModel { | ||
| 1936 | 1936 | } |
| 1937 | 1937 | |
| 1938 | 1938 | let entry = HistoryEntry( |
| 1939 | domain: snapshot.domain, | |
| 1940 | timestamp: snapshot.timestamp, | |
| 1941 | trackedDomainID: trackedDomainID, | |
| 1942 | note: currentHistoryEntry?.note, | |
| 1943 | dnsSections: snapshot.dnsSections, | |
| 1944 | sslInfo: snapshot.sslInfo, | |
| 1945 | httpHeaders: snapshot.httpHeaders, | |
| 1946 | reachabilityResults: snapshot.reachabilityResults, | |
| 1947 | ipGeolocation: snapshot.ipGeolocation, | |
| 1948 | emailSecurity: snapshot.emailSecurity, | |
| 1949 | mtaSts: snapshot.emailSecurity?.mtaSts, | |
| 1950 | ownership: snapshot.ownership, | |
| 1951 | ownershipHistory: snapshot.ownershipHistory, | |
| 1952 | inferredProvider: intelligence.inferredProvider, | |
| 1953 | priorProviders: intelligence.priorProviders, | |
| 1954 | domainClassification: intelligence.domainClassification, | |
| 1955 | ownershipTransitions: intelligence.ownershipTransitions, | |
| 1956 | hostingTransitions: intelligence.hostingTransitions, | |
| 1957 | subdomainHistory: intelligence.subdomainHistory, | |
| 1958 | riskSignals: intelligence.riskSignals, | |
| 1959 | intelligenceTimeline: intelligence.timelineEvents, | |
| 1960 | ptrRecord: snapshot.ptrRecord, | |
| 1961 | redirectChain: snapshot.redirectChain, | |
| 1962 | subdomains: snapshot.subdomains, | |
| 1963 | extendedSubdomains: snapshot.extendedSubdomains, | |
| 1964 | dnsHistory: snapshot.dnsHistory, | |
| 1965 | domainPricing: snapshot.domainPricing, | |
| 1966 | reputation: snapshot.reputation, | |
| 1967 | portScanResults: snapshot.portScanResults, | |
| 1968 | hstsPreloaded: snapshot.hstsPreloaded, | |
| 1969 | availabilityResult: snapshot.availabilityResult, | |
| 1970 | suggestions: snapshot.suggestions, | |
| 1971 | appVersion: snapshot.appVersion, | |
| 1972 | resultSource: snapshot.resultSource, | |
| 1973 | dataSources: snapshot.dataSources, | |
| 1974 | provenanceBySection: snapshot.provenanceBySection, | |
| 1975 | availabilityConfidence: snapshot.availabilityConfidence, | |
| 1976 | ownershipConfidence: snapshot.ownershipConfidence, | |
| 1977 | subdomainConfidence: snapshot.subdomainConfidence, | |
| 1978 | emailSecurityConfidence: snapshot.emailSecurityConfidence, | |
| 1979 | geolocationConfidence: snapshot.geolocationConfidence, | |
| 1980 | errorDetails: snapshot.errorDetails, | |
| 1981 | isPartialSnapshot: snapshot.isPartialSnapshot, | |
| 1982 | validationIssues: snapshot.validationIssues, | |
| 1983 | resolverDisplayName: snapshot.resolverDisplayName, | |
| 1984 | resolverURLString: snapshot.resolverURLString, | |
| 1985 | totalLookupDurationMs: snapshot.totalLookupDurationMs, | |
| 1986 | primaryIP: Self.primaryIPAddress(from: snapshot), | |
| 1987 | finalRedirectURL: Self.finalRedirectTarget(from: snapshot), | |
| 1988 | tlsStatusSummary: Self.httpsSummary(from: snapshot), | |
| 1989 | emailSecuritySummary: Self.emailSummary(from: snapshot), | |
| 1990 | httpGradeSummary: snapshot.httpSecurityGrade ?? snapshot.httpHeadersError, | |
| 1991 | changeSummary: changeSummary, | |
| 1992 | snapshotIndex: nextSnapshotIndex, | |
| 1993 | previousSnapshotID: previousSnapshotID, | |
| 1994 | changeCount: domainDiff?.changeCount ?? changeSummary?.changedSections.count ?? 0, | |
| 1995 | severitySummary: changeSummary?.severity, | |
| 1996 | sslError: snapshot.sslError, | |
| 1997 | httpHeadersError: snapshot.httpHeadersError, | |
| 1998 | reachabilityError: snapshot.reachabilityError, | |
| 1999 | ipGeolocationError: snapshot.ipGeolocationError, | |
| 2000 | emailSecurityError: snapshot.emailSecurityError, | |
| 2001 | ownershipError: snapshot.ownershipError, | |
| 2002 | ownershipHistoryError: snapshot.ownershipHistoryError, | |
| 2003 | ptrError: snapshot.ptrError, | |
| 2004 | redirectChainError: snapshot.redirectChainError, | |
| 2005 | subdomainsError: snapshot.subdomainsError, | |
| 2006 | extendedSubdomainsError: snapshot.extendedSubdomainsError, | |
| 2007 | dnsHistoryError: snapshot.dnsHistoryError, | |
| 2008 | domainPricingError: snapshot.domainPricingError, | |
| 2009 | reputationError: snapshot.reputationError, | |
| 2010 | portScanError: snapshot.portScanError | |
| 1939 | identity: HistoryEntry.Identity( | |
| 1940 | domain: snapshot.domain, | |
| 1941 | timestamp: snapshot.timestamp, | |
| 1942 | trackedDomainID: trackedDomainID, | |
| 1943 | note: currentHistoryEntry?.note | |
| 1944 | ), | |
| 1945 | inspection: HistoryEntry.Inspection( | |
| 1946 | dnsSections: snapshot.dnsSections, | |
| 1947 | sslInfo: snapshot.sslInfo, | |
| 1948 | httpHeaders: snapshot.httpHeaders, | |
| 1949 | reachabilityResults: snapshot.reachabilityResults, | |
| 1950 | ipGeolocation: snapshot.ipGeolocation, | |
| 1951 | emailSecurity: snapshot.emailSecurity, | |
| 1952 | mtaSts: snapshot.emailSecurity?.mtaSts, | |
| 1953 | ptrRecord: snapshot.ptrRecord, | |
| 1954 | redirectChain: snapshot.redirectChain, | |
| 1955 | subdomains: snapshot.subdomains, | |
| 1956 | extendedSubdomains: snapshot.extendedSubdomains, | |
| 1957 | dnsHistory: snapshot.dnsHistory, | |
| 1958 | portScanResults: snapshot.portScanResults, | |
| 1959 | hstsPreloaded: snapshot.hstsPreloaded, | |
| 1960 | availabilityResult: snapshot.availabilityResult, | |
| 1961 | suggestions: snapshot.suggestions | |
| 1962 | ), | |
| 1963 | registration: HistoryEntry.Registration( | |
| 1964 | ownership: snapshot.ownership, | |
| 1965 | ownershipHistory: snapshot.ownershipHistory, | |
| 1966 | inferredProvider: intelligence.inferredProvider, | |
| 1967 | priorProviders: intelligence.priorProviders, | |
| 1968 | domainClassification: intelligence.domainClassification, | |
| 1969 | ownershipTransitions: intelligence.ownershipTransitions, | |
| 1970 | hostingTransitions: intelligence.hostingTransitions, | |
| 1971 | domainPricing: snapshot.domainPricing | |
| 1972 | ), | |
| 1973 | intelligence: HistoryEntry.Intelligence( | |
| 1974 | subdomainHistory: intelligence.subdomainHistory, | |
| 1975 | riskSignals: intelligence.riskSignals, | |
| 1976 | intelligenceTimeline: intelligence.timelineEvents, | |
| 1977 | reputation: snapshot.reputation | |
| 1978 | ), | |
| 1979 | provenance: HistoryEntry.Provenance( | |
| 1980 | appVersion: snapshot.appVersion, | |
| 1981 | resultSource: snapshot.resultSource, | |
| 1982 | dataSources: snapshot.dataSources, | |
| 1983 | provenanceBySection: snapshot.provenanceBySection, | |
| 1984 | availabilityConfidence: snapshot.availabilityConfidence, | |
| 1985 | ownershipConfidence: snapshot.ownershipConfidence, | |
| 1986 | subdomainConfidence: snapshot.subdomainConfidence, | |
| 1987 | emailSecurityConfidence: snapshot.emailSecurityConfidence, | |
| 1988 | geolocationConfidence: snapshot.geolocationConfidence, | |
| 1989 | isPartialSnapshot: snapshot.isPartialSnapshot, | |
| 1990 | validationIssues: snapshot.validationIssues, | |
| 1991 | resolverDisplayName: snapshot.resolverDisplayName, | |
| 1992 | resolverURLString: snapshot.resolverURLString, | |
| 1993 | totalLookupDurationMs: snapshot.totalLookupDurationMs | |
| 1994 | ), | |
| 1995 | summary: HistoryEntry.Summary( | |
| 1996 | primaryIP: Self.primaryIPAddress(from: snapshot), | |
| 1997 | finalRedirectURL: Self.finalRedirectTarget(from: snapshot), | |
| 1998 | tlsStatusSummary: Self.httpsSummary(from: snapshot), | |
| 1999 | emailSecuritySummary: Self.emailSummary(from: snapshot), | |
| 2000 | httpGradeSummary: snapshot.httpSecurityGrade ?? snapshot.httpHeadersError, | |
| 2001 | changeSummary: changeSummary, | |
| 2002 | snapshotIndex: nextSnapshotIndex, | |
| 2003 | previousSnapshotID: previousSnapshotID, | |
| 2004 | changeCount: domainDiff?.changeCount ?? changeSummary?.changedSections.count ?? 0, | |
| 2005 | severitySummary: changeSummary?.severity | |
| 2006 | ), | |
| 2007 | failures: HistoryEntry.Failures( | |
| 2008 | errorDetails: snapshot.errorDetails, | |
| 2009 | sslError: snapshot.sslError, | |
| 2010 | httpHeadersError: snapshot.httpHeadersError, | |
| 2011 | reachabilityError: snapshot.reachabilityError, | |
| 2012 | ipGeolocationError: snapshot.ipGeolocationError, | |
| 2013 | emailSecurityError: snapshot.emailSecurityError, | |
| 2014 | ownershipError: snapshot.ownershipError, | |
| 2015 | ownershipHistoryError: snapshot.ownershipHistoryError, | |
| 2016 | ptrError: snapshot.ptrError, | |
| 2017 | redirectChainError: snapshot.redirectChainError, | |
| 2018 | subdomainsError: snapshot.subdomainsError, | |
| 2019 | extendedSubdomainsError: snapshot.extendedSubdomainsError, | |
| 2020 | dnsHistoryError: snapshot.dnsHistoryError, | |
| 2021 | domainPricingError: snapshot.domainPricingError, | |
| 2022 | reputationError: snapshot.reputationError, | |
| 2023 | portScanError: snapshot.portScanError | |
| 2024 | ) | |
| 2011 | 2025 | ) |
| 2012 | 2026 | |
| 2013 | 2027 | if updateCurrentState { |
DomainDig/Models.swift +190 −107
| @@ -2399,113 +2399,196 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2399 | 2399 | var reputationError: String? |
| 2400 | 2400 | var portScanError: String? |
| 2401 | 2401 | |
| 2402 | init(domain: String, timestamp: Date, trackedDomainID: UUID? = nil, note: String? = nil, dnsSections: [DNSSection], | |
| 2403 | sslInfo: SSLCertificateInfo?, httpHeaders: [HTTPHeader], | |
| 2404 | reachabilityResults: [PortReachability], ipGeolocation: IPGeolocation?, | |
| 2405 | emailSecurity: EmailSecurityResult? = nil, mtaSts: MTASTSResult? = nil, ownership: DomainOwnership? = nil, | |
| 2406 | ownershipHistory: [DomainOwnershipHistoryEvent] = [], | |
| 2407 | inferredProvider: InferredProviderFingerprint? = nil, priorProviders: [String] = [], | |
| 2408 | domainClassification: DomainClassificationSummary? = nil, | |
| 2409 | ownershipTransitions: [OwnershipTransitionEvent] = [], | |
| 2410 | hostingTransitions: [HostingTransitionEvent] = [], | |
| 2411 | subdomainHistory: [SubdomainHistoryEntry] = [], | |
| 2412 | riskSignals: [IntelligenceRiskSignal] = [], | |
| 2413 | intelligenceTimeline: [IntelligenceTimelineEvent] = [], | |
| 2414 | ptrRecord: String? = nil, redirectChain: [RedirectHop] = [], subdomains: [DiscoveredSubdomain] = [], | |
| 2415 | extendedSubdomains: [DiscoveredSubdomain] = [], dnsHistory: [DNSHistoryEvent] = [], | |
| 2416 | domainPricing: DomainPricingInsight? = nil, | |
| 2417 | reputation: DomainReputationResult? = nil, | |
| 2418 | portScanResults: [PortScanResult] = [], | |
| 2419 | hstsPreloaded: Bool? = nil, availabilityResult: DomainAvailabilityResult? = nil, | |
| 2420 | suggestions: [DomainSuggestionResult] = [], appVersion: String = "2.7.0", | |
| 2421 | resultSource: LookupResultSource = .snapshot, dataSources: [String] = [], | |
| 2422 | provenanceBySection: [LookupSectionKind: SectionProvenance] = [:], | |
| 2423 | availabilityConfidence: ConfidenceLevel? = nil, ownershipConfidence: ConfidenceLevel? = nil, | |
| 2424 | subdomainConfidence: ConfidenceLevel? = nil, emailSecurityConfidence: ConfidenceLevel? = nil, | |
| 2425 | geolocationConfidence: ConfidenceLevel? = nil, | |
| 2426 | errorDetails: [LookupSectionKind: InspectionFailure] = [:], isPartialSnapshot: Bool = false, | |
| 2427 | validationIssues: [String] = [], resolverDisplayName: String, resolverURLString: String, | |
| 2428 | totalLookupDurationMs: Int? = nil, primaryIP: String? = nil, finalRedirectURL: String? = nil, | |
| 2429 | tlsStatusSummary: String? = nil, emailSecuritySummary: String? = nil, httpGradeSummary: String? = nil, | |
| 2430 | changeSummary: DomainChangeSummary? = nil, snapshotIndex: Int? = nil, previousSnapshotID: UUID? = nil, | |
| 2431 | changeCount: Int = 0, severitySummary: ChangeSeverity? = nil, sslError: String? = nil, httpHeadersError: String? = nil, | |
| 2432 | reachabilityError: String? = nil, ipGeolocationError: String? = nil, | |
| 2433 | emailSecurityError: String? = nil, ownershipError: String? = nil, ownershipHistoryError: String? = nil, | |
| 2434 | ptrError: String? = nil, redirectChainError: String? = nil, subdomainsError: String? = nil, | |
| 2435 | extendedSubdomainsError: String? = nil, dnsHistoryError: String? = nil, | |
| 2436 | domainPricingError: String? = nil, reputationError: String? = nil, portScanError: String? = nil) { | |
| 2437 | self.domain = domain | |
| 2438 | self.timestamp = timestamp | |
| 2439 | self.trackedDomainID = trackedDomainID | |
| 2440 | self.note = note | |
| 2441 | self.dnsSections = dnsSections | |
| 2442 | self.sslInfo = sslInfo | |
| 2443 | self.httpHeaders = httpHeaders | |
| 2444 | self.reachabilityResults = reachabilityResults | |
| 2445 | self.ipGeolocation = ipGeolocation | |
| 2446 | self.emailSecurity = emailSecurity | |
| 2447 | self.mtaSts = mtaSts ?? emailSecurity?.mtaSts | |
| 2448 | self.ownership = ownership | |
| 2449 | self.ownershipHistory = ownershipHistory | |
| 2450 | self.inferredProvider = inferredProvider | |
| 2451 | self.priorProviders = priorProviders | |
| 2452 | self.domainClassification = domainClassification | |
| 2453 | self.ownershipTransitions = ownershipTransitions | |
| 2454 | self.hostingTransitions = hostingTransitions | |
| 2455 | self.subdomainHistory = subdomainHistory | |
| 2456 | self.riskSignals = riskSignals | |
| 2457 | self.intelligenceTimeline = intelligenceTimeline | |
| 2458 | self.ptrRecord = ptrRecord | |
| 2459 | self.redirectChain = redirectChain | |
| 2460 | self.subdomains = subdomains | |
| 2461 | self.extendedSubdomains = extendedSubdomains | |
| 2462 | self.dnsHistory = dnsHistory | |
| 2463 | self.domainPricing = domainPricing | |
| 2464 | self.reputation = reputation | |
| 2465 | self.portScanResults = portScanResults | |
| 2466 | self.hstsPreloaded = hstsPreloaded | |
| 2467 | self.availabilityResult = availabilityResult | |
| 2468 | self.suggestions = suggestions | |
| 2469 | self.appVersion = appVersion | |
| 2470 | self.resultSource = resultSource | |
| 2471 | self.dataSources = dataSources | |
| 2472 | self.provenanceBySection = provenanceBySection | |
| 2473 | self.availabilityConfidence = availabilityConfidence | |
| 2474 | self.ownershipConfidence = ownershipConfidence | |
| 2475 | self.subdomainConfidence = subdomainConfidence | |
| 2476 | self.emailSecurityConfidence = emailSecurityConfidence | |
| 2477 | self.geolocationConfidence = geolocationConfidence | |
| 2478 | self.errorDetails = errorDetails | |
| 2479 | self.isPartialSnapshot = isPartialSnapshot | |
| 2480 | self.validationIssues = validationIssues | |
| 2481 | self.resolverDisplayName = resolverDisplayName | |
| 2482 | self.resolverURLString = resolverURLString | |
| 2483 | self.totalLookupDurationMs = totalLookupDurationMs | |
| 2484 | self.primaryIP = primaryIP | |
| 2485 | self.finalRedirectURL = finalRedirectURL | |
| 2486 | self.tlsStatusSummary = tlsStatusSummary | |
| 2487 | self.emailSecuritySummary = emailSecuritySummary | |
| 2488 | self.httpGradeSummary = httpGradeSummary | |
| 2489 | self.changeSummary = changeSummary | |
| 2490 | self.snapshotIndex = snapshotIndex | |
| 2491 | self.previousSnapshotID = previousSnapshotID | |
| 2492 | self.changeCount = changeCount | |
| 2493 | self.severitySummary = severitySummary | |
| 2494 | self.sslError = sslError | |
| 2495 | self.httpHeadersError = httpHeadersError | |
| 2496 | self.reachabilityError = reachabilityError | |
| 2497 | self.ipGeolocationError = ipGeolocationError | |
| 2498 | self.emailSecurityError = emailSecurityError | |
| 2499 | self.ownershipError = ownershipError | |
| 2500 | self.ownershipHistoryError = ownershipHistoryError | |
| 2501 | self.ptrError = ptrError | |
| 2502 | self.redirectChainError = redirectChainError | |
| 2503 | self.subdomainsError = subdomainsError | |
| 2504 | self.extendedSubdomainsError = extendedSubdomainsError | |
| 2505 | self.dnsHistoryError = dnsHistoryError | |
| 2506 | self.domainPricingError = domainPricingError | |
| 2507 | self.reputationError = reputationError | |
| 2508 | self.portScanError = portScanError | |
| 2402 | // A full inspection produces 73 stored properties, and passing them as one | |
| 2403 | // flat argument list made a 72-parameter initializer no call site could read | |
| 2404 | // (SonarCloud swift:S107). They are grouped below by what they describe. | |
| 2405 | // | |
| 2406 | // The stored properties stay flat, deliberately. This type is persisted to | |
| 2407 | // UserDefaults and read back out of backup files, and both decode paths drop | |
| 2408 | // entries that will not parse rather than raising — so nesting a value would | |
| 2409 | // change the encoded shape and silently discard history written by an older | |
| 2410 | // build. These groups exist at the call boundary only; the JSON is unchanged, | |
| 2411 | // which HistoryEntryCodableTests pins. | |
| 2412 | ||
| 2413 | /// What was inspected, and when. | |
| 2414 | struct Identity { | |
| 2415 | var domain: String | |
| 2416 | var timestamp: Date | |
| 2417 | var trackedDomainID: UUID? = nil | |
| 2418 | var note: String? = nil | |
| 2419 | } | |
| 2420 | ||
| 2421 | /// The live probe results for this snapshot. | |
| 2422 | struct Inspection { | |
| 2423 | var dnsSections: [DNSSection] | |
| 2424 | var sslInfo: SSLCertificateInfo? | |
| 2425 | var httpHeaders: [HTTPHeader] | |
| 2426 | var reachabilityResults: [PortReachability] | |
| 2427 | var ipGeolocation: IPGeolocation? | |
| 2428 | var emailSecurity: EmailSecurityResult? = nil | |
| 2429 | var mtaSts: MTASTSResult? = nil | |
| 2430 | var ptrRecord: String? = nil | |
| 2431 | var redirectChain: [RedirectHop] = [] | |
| 2432 | var subdomains: [DiscoveredSubdomain] = [] | |
| 2433 | var extendedSubdomains: [DiscoveredSubdomain] = [] | |
| 2434 | var dnsHistory: [DNSHistoryEvent] = [] | |
| 2435 | var portScanResults: [PortScanResult] = [] | |
| 2436 | var hstsPreloaded: Bool? = nil | |
| 2437 | var availabilityResult: DomainAvailabilityResult? = nil | |
| 2438 | var suggestions: [DomainSuggestionResult] = [] | |
| 2439 | } | |
| 2440 | ||
| 2441 | /// Who owns the domain and where it is hosted. | |
| 2442 | struct Registration { | |
| 2443 | var ownership: DomainOwnership? = nil | |
| 2444 | var ownershipHistory: [DomainOwnershipHistoryEvent] = [] | |
| 2445 | var inferredProvider: InferredProviderFingerprint? = nil | |
| 2446 | var priorProviders: [String] = [] | |
| 2447 | var domainClassification: DomainClassificationSummary? = nil | |
| 2448 | var ownershipTransitions: [OwnershipTransitionEvent] = [] | |
| 2449 | var hostingTransitions: [HostingTransitionEvent] = [] | |
| 2450 | var domainPricing: DomainPricingInsight? = nil | |
| 2451 | } | |
| 2452 | ||
| 2453 | /// Derived signals rather than direct observations. | |
| 2454 | struct Intelligence { | |
| 2455 | var subdomainHistory: [SubdomainHistoryEntry] = [] | |
| 2456 | var riskSignals: [IntelligenceRiskSignal] = [] | |
| 2457 | var intelligenceTimeline: [IntelligenceTimelineEvent] = [] | |
| 2458 | var reputation: DomainReputationResult? = nil | |
| 2459 | } | |
| 2460 | ||
| 2461 | /// Where the data came from, and how much to trust it. | |
| 2462 | struct Provenance { | |
| 2463 | var appVersion: String = "2.7.0" | |
| 2464 | var resultSource: LookupResultSource = .snapshot | |
| 2465 | var dataSources: [String] = [] | |
| 2466 | var provenanceBySection: [LookupSectionKind: SectionProvenance] = [:] | |
| 2467 | var availabilityConfidence: ConfidenceLevel? = nil | |
| 2468 | var ownershipConfidence: ConfidenceLevel? = nil | |
| 2469 | var subdomainConfidence: ConfidenceLevel? = nil | |
| 2470 | var emailSecurityConfidence: ConfidenceLevel? = nil | |
| 2471 | var geolocationConfidence: ConfidenceLevel? = nil | |
| 2472 | var isPartialSnapshot: Bool = false | |
| 2473 | var validationIssues: [String] = [] | |
| 2474 | var resolverDisplayName: String | |
| 2475 | var resolverURLString: String | |
| 2476 | var totalLookupDurationMs: Int? = nil | |
| 2477 | } | |
| 2478 | ||
| 2479 | /// Precomputed display values and change tracking. | |
| 2480 | struct Summary { | |
| 2481 | var primaryIP: String? = nil | |
| 2482 | var finalRedirectURL: String? = nil | |
| 2483 | var tlsStatusSummary: String? = nil | |
| 2484 | var emailSecuritySummary: String? = nil | |
| 2485 | var httpGradeSummary: String? = nil | |
| 2486 | var changeSummary: DomainChangeSummary? = nil | |
| 2487 | var snapshotIndex: Int? = nil | |
| 2488 | var previousSnapshotID: UUID? = nil | |
| 2489 | var changeCount: Int = 0 | |
| 2490 | var severitySummary: ChangeSeverity? = nil | |
| 2491 | } | |
| 2492 | ||
| 2493 | /// Per-section failures: a snapshot records what it could not collect. | |
| 2494 | struct Failures { | |
| 2495 | var errorDetails: [LookupSectionKind: InspectionFailure] = [:] | |
| 2496 | var sslError: String? = nil | |
| 2497 | var httpHeadersError: String? = nil | |
| 2498 | var reachabilityError: String? = nil | |
| 2499 | var ipGeolocationError: String? = nil | |
| 2500 | var emailSecurityError: String? = nil | |
| 2501 | var ownershipError: String? = nil | |
| 2502 | var ownershipHistoryError: String? = nil | |
| 2503 | var ptrError: String? = nil | |
| 2504 | var redirectChainError: String? = nil | |
| 2505 | var subdomainsError: String? = nil | |
| 2506 | var extendedSubdomainsError: String? = nil | |
| 2507 | var dnsHistoryError: String? = nil | |
| 2508 | var domainPricingError: String? = nil | |
| 2509 | var reputationError: String? = nil | |
| 2510 | var portScanError: String? = nil | |
| 2511 | } | |
| 2512 | ||
| 2513 | init(identity: Identity, | |
| 2514 | inspection: Inspection, | |
| 2515 | registration: Registration = Registration(), | |
| 2516 | intelligence: Intelligence = Intelligence(), | |
| 2517 | provenance: Provenance, | |
| 2518 | summary: Summary = Summary(), | |
| 2519 | failures: Failures = Failures()) { | |
| 2520 | self.domain = identity.domain | |
| 2521 | self.timestamp = identity.timestamp | |
| 2522 | self.trackedDomainID = identity.trackedDomainID | |
| 2523 | self.note = identity.note | |
| 2524 | self.dnsSections = inspection.dnsSections | |
| 2525 | self.sslInfo = inspection.sslInfo | |
| 2526 | self.httpHeaders = inspection.httpHeaders | |
| 2527 | self.reachabilityResults = inspection.reachabilityResults | |
| 2528 | self.ipGeolocation = inspection.ipGeolocation | |
| 2529 | self.emailSecurity = inspection.emailSecurity | |
| 2530 | self.mtaSts = inspection.mtaSts ?? inspection.emailSecurity?.mtaSts | |
| 2531 | self.ptrRecord = inspection.ptrRecord | |
| 2532 | self.redirectChain = inspection.redirectChain | |
| 2533 | self.subdomains = inspection.subdomains | |
| 2534 | self.extendedSubdomains = inspection.extendedSubdomains | |
| 2535 | self.dnsHistory = inspection.dnsHistory | |
| 2536 | self.portScanResults = inspection.portScanResults | |
| 2537 | self.hstsPreloaded = inspection.hstsPreloaded | |
| 2538 | self.availabilityResult = inspection.availabilityResult | |
| 2539 | self.suggestions = inspection.suggestions | |
| 2540 | self.ownership = registration.ownership | |
| 2541 | self.ownershipHistory = registration.ownershipHistory | |
| 2542 | self.inferredProvider = registration.inferredProvider | |
| 2543 | self.priorProviders = registration.priorProviders | |
| 2544 | self.domainClassification = registration.domainClassification | |
| 2545 | self.ownershipTransitions = registration.ownershipTransitions | |
| 2546 | self.hostingTransitions = registration.hostingTransitions | |
| 2547 | self.domainPricing = registration.domainPricing | |
| 2548 | self.subdomainHistory = intelligence.subdomainHistory | |
| 2549 | self.riskSignals = intelligence.riskSignals | |
| 2550 | self.intelligenceTimeline = intelligence.intelligenceTimeline | |
| 2551 | self.reputation = intelligence.reputation | |
| 2552 | self.appVersion = provenance.appVersion | |
| 2553 | self.resultSource = provenance.resultSource | |
| 2554 | self.dataSources = provenance.dataSources | |
| 2555 | self.provenanceBySection = provenance.provenanceBySection | |
| 2556 | self.availabilityConfidence = provenance.availabilityConfidence | |
| 2557 | self.ownershipConfidence = provenance.ownershipConfidence | |
| 2558 | self.subdomainConfidence = provenance.subdomainConfidence | |
| 2559 | self.emailSecurityConfidence = provenance.emailSecurityConfidence | |
| 2560 | self.geolocationConfidence = provenance.geolocationConfidence | |
| 2561 | self.isPartialSnapshot = provenance.isPartialSnapshot | |
| 2562 | self.validationIssues = provenance.validationIssues | |
| 2563 | self.resolverDisplayName = provenance.resolverDisplayName | |
| 2564 | self.resolverURLString = provenance.resolverURLString | |
| 2565 | self.totalLookupDurationMs = provenance.totalLookupDurationMs | |
| 2566 | self.primaryIP = summary.primaryIP | |
| 2567 | self.finalRedirectURL = summary.finalRedirectURL | |
| 2568 | self.tlsStatusSummary = summary.tlsStatusSummary | |
| 2569 | self.emailSecuritySummary = summary.emailSecuritySummary | |
| 2570 | self.httpGradeSummary = summary.httpGradeSummary | |
| 2571 | self.changeSummary = summary.changeSummary | |
| 2572 | self.snapshotIndex = summary.snapshotIndex | |
| 2573 | self.previousSnapshotID = summary.previousSnapshotID | |
| 2574 | self.changeCount = summary.changeCount | |
| 2575 | self.severitySummary = summary.severitySummary | |
| 2576 | self.errorDetails = failures.errorDetails | |
| 2577 | self.sslError = failures.sslError | |
| 2578 | self.httpHeadersError = failures.httpHeadersError | |
| 2579 | self.reachabilityError = failures.reachabilityError | |
| 2580 | self.ipGeolocationError = failures.ipGeolocationError | |
| 2581 | self.emailSecurityError = failures.emailSecurityError | |
| 2582 | self.ownershipError = failures.ownershipError | |
| 2583 | self.ownershipHistoryError = failures.ownershipHistoryError | |
| 2584 | self.ptrError = failures.ptrError | |
| 2585 | self.redirectChainError = failures.redirectChainError | |
| 2586 | self.subdomainsError = failures.subdomainsError | |
| 2587 | self.extendedSubdomainsError = failures.extendedSubdomainsError | |
| 2588 | self.dnsHistoryError = failures.dnsHistoryError | |
| 2589 | self.domainPricingError = failures.domainPricingError | |
| 2590 | self.reputationError = failures.reputationError | |
| 2591 | self.portScanError = failures.portScanError | |
| 2509 | 2592 | } |
| 2510 | 2593 | |
| 2511 | 2594 | init(from decoder: Decoder) throws { |
DomainDigTests/HistoryEntryCodableTests.swift added +129
| @@ -0,0 +1,129 @@ | ||
| 1 | import XCTest | |
| 2 | @testable import DomainDig | |
| 3 | ||
| 4 | /// Characterization tests pinning `HistoryEntry`'s encoded shape. | |
| 5 | /// | |
| 6 | /// This type is not merely an in-memory model: `DomainDataPortabilityService` | |
| 7 | /// persists it to `UserDefaults` as JSON and reads the same shape back out of | |
| 8 | /// backup files. Both decode paths swallow failures — `loadHistoryEntries` uses | |
| 9 | /// `try?` and drops anything that will not decode — so a change to the encoded | |
| 10 | /// keys does not raise, it silently discards a user's saved history. | |
| 11 | /// | |
| 12 | /// These tests exist so that any such change fails here first, loudly, instead | |
| 13 | /// of in someone's app. | |
| 14 | final class HistoryEntryCodableTests: XCTestCase { | |
| 15 | /// Every key `HistoryEntry` is expected to encode. Adding a stored property | |
| 16 | /// is a format change: extend this list deliberately, and only once you have | |
| 17 | /// decided what happens to history written by an older build. | |
| 18 | private static let expectedKeys: Set<String> = [ | |
| 19 | "id", "domain", "timestamp", "trackedDomainID", "note", "dnsSections", | |
| 20 | "sslInfo", "httpHeaders", "reachabilityResults", "ipGeolocation", | |
| 21 | "emailSecurity", "mtaSts", "ownership", "ownershipHistory", | |
| 22 | "inferredProvider", "priorProviders", "domainClassification", | |
| 23 | "ownershipTransitions", "hostingTransitions", "subdomainHistory", | |
| 24 | "riskSignals", "intelligenceTimeline", "ptrRecord", "redirectChain", | |
| 25 | "subdomains", "extendedSubdomains", "dnsHistory", "domainPricing", | |
| 26 | "reputation", "portScanResults", "hstsPreloaded", "availabilityResult", | |
| 27 | "suggestions", "appVersion", "resultSource", "dataSources", | |
| 28 | "provenanceBySection", "availabilityConfidence", "ownershipConfidence", | |
| 29 | "subdomainConfidence", "emailSecurityConfidence", "geolocationConfidence", | |
| 30 | "errorDetails", "isPartialSnapshot", "validationIssues", | |
| 31 | "resolverDisplayName", "resolverURLString", "totalLookupDurationMs", | |
| 32 | "primaryIP", "finalRedirectURL", "tlsStatusSummary", | |
| 33 | "emailSecuritySummary", "httpGradeSummary", "changeSummary", | |
| 34 | "snapshotIndex", "previousSnapshotID", "changeCount", "severitySummary", | |
| 35 | "sslError", "httpHeadersError", "reachabilityError", "ipGeolocationError", | |
| 36 | "emailSecurityError", "ownershipError", "ownershipHistoryError", | |
| 37 | "ptrError", "redirectChainError", "subdomainsError", | |
| 38 | "extendedSubdomainsError", "dnsHistoryError", "domainPricingError", | |
| 39 | "reputationError", "portScanError", | |
| 40 | ] | |
| 41 | ||
| 42 | private func makeEntry() -> HistoryEntry { | |
| 43 | HistoryEntry( | |
| 44 | identity: .init( | |
| 45 | domain: "example.com", | |
| 46 | timestamp: Date(timeIntervalSince1970: 1_700_000_000) | |
| 47 | ), | |
| 48 | inspection: .init( | |
| 49 | dnsSections: [], | |
| 50 | sslInfo: nil, | |
| 51 | httpHeaders: [], | |
| 52 | reachabilityResults: [], | |
| 53 | ipGeolocation: nil | |
| 54 | ), | |
| 55 | provenance: .init( | |
| 56 | resolverDisplayName: "Test Resolver", | |
| 57 | resolverURLString: "https://resolver.example/dns-query" | |
| 58 | ) | |
| 59 | ) | |
| 60 | } | |
| 61 | ||
| 62 | private func encoder() -> JSONEncoder { | |
| 63 | let encoder = JSONEncoder() | |
| 64 | encoder.outputFormatting = .sortedKeys | |
| 65 | return encoder | |
| 66 | } | |
| 67 | ||
| 68 | /// The set of top-level keys is the persisted contract. Optionals that are | |
| 69 | /// nil are omitted by the synthesized encoder, so this asserts containment | |
| 70 | /// rather than equality — no key may appear that is not accounted for. | |
| 71 | func testEncodedKeysAreAllAccountedFor() throws { | |
| 72 | let data = try encoder().encode(makeEntry()) | |
| 73 | let object = try XCTUnwrap( | |
| 74 | JSONSerialization.jsonObject(with: data) as? [String: Any] | |
| 75 | ) | |
| 76 | ||
| 77 | let unexpected = Set(object.keys).subtracting(Self.expectedKeys) | |
| 78 | XCTAssertTrue( | |
| 79 | unexpected.isEmpty, | |
| 80 | "HistoryEntry encoded keys not in the pinned set: \(unexpected.sorted()). " | |
| 81 | + "This changes the persisted format; older history will not decode." | |
| 82 | ) | |
| 83 | } | |
| 84 | ||
| 85 | /// A populated entry must survive encode → decode → encode unchanged. This | |
| 86 | /// is the guard that a refactor which regroups the initializer has not also | |
| 87 | /// moved a value into a different place in the JSON. | |
| 88 | func testRoundTripIsStable() throws { | |
| 89 | let first = try encoder().encode(makeEntry()) | |
| 90 | let decoded = try JSONDecoder().decode(HistoryEntry.self, from: first) | |
| 91 | let second = try encoder().encode(decoded) | |
| 92 | ||
| 93 | XCTAssertEqual( | |
| 94 | first, second, | |
| 95 | "HistoryEntry did not survive a JSON round trip unchanged" | |
| 96 | ) | |
| 97 | } | |
| 98 | ||
| 99 | /// The values a caller supplies must land under the keys the persisted format | |
| 100 | /// already uses, not merely somewhere in the document. | |
| 101 | func testRequiredValuesEncodeAtTheTopLevel() throws { | |
| 102 | let data = try encoder().encode(makeEntry()) | |
| 103 | let object = try XCTUnwrap( | |
| 104 | JSONSerialization.jsonObject(with: data) as? [String: Any] | |
| 105 | ) | |
| 106 | ||
| 107 | XCTAssertEqual(object["domain"] as? String, "example.com") | |
| 108 | XCTAssertEqual(object["resolverDisplayName"] as? String, "Test Resolver") | |
| 109 | XCTAssertEqual( | |
| 110 | object["resolverURLString"] as? String, | |
| 111 | "https://resolver.example/dns-query" | |
| 112 | ) | |
| 113 | } | |
| 114 | ||
| 115 | /// What `loadHistoryEntries` actually does with a stored blob, end to end: | |
| 116 | /// anything that fails to decode is dropped without error, so this pins that | |
| 117 | /// a current-format entry survives the real read path. | |
| 118 | func testSurvivesTheRealPersistencePath() throws { | |
| 119 | let suite = "DomainDigTests.historyEntryCodable" | |
| 120 | let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) | |
| 121 | defer { defaults.removePersistentDomain(forName: suite) } | |
| 122 | ||
| 123 | DomainDataPortabilityService.saveHistoryEntries([makeEntry()], defaults: defaults) | |
| 124 | let loaded = DomainDataPortabilityService.loadHistoryEntries(defaults: defaults) | |
| 125 | ||
| 126 | XCTAssertEqual(loaded.count, 1, "entry was silently dropped by the load path") | |
| 127 | XCTAssertEqual(loaded.first?.domain, "example.com") | |
| 128 | } | |
| 129 | } | |