aboutsummaryrefslogtreecommitdiff
path: root/DomainDigTests/DomainDataPortabilityServiceTests.swift
blob: ca2c72c7d2214eb07efc5b8adaaa6349d130935c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import XCTest
@testable import DomainDig

/// Characterization tests for the portability layer's deterministic core: the
/// CSV round-trip and the merge/dedup semantics that `load*/save*` apply. Storage
/// is exercised through an ephemeral `UserDefaults` suite so nothing touches the
/// real app domain.
final class DomainDataPortabilityServiceTests: XCTestCase {
    private let suiteName = "DomainDigTests.portability"
    private var defaults: UserDefaults!
    private let base = Date(timeIntervalSince1970: 1_700_000_000)

    override func setUp() {
        super.setUp()
        defaults = UserDefaults(suiteName: suiteName)
        defaults.removePersistentDomain(forName: suiteName)
    }

    override func tearDown() {
        defaults.removePersistentDomain(forName: suiteName)
        defaults = nil
        super.tearDown()
    }

    // MARK: - CSV round-trip

    func testTrackedDomainCSVRoundTripPreservesFields() throws {
        let original = TrackedDomain(
            domain: "csv.example",
            createdAt: base,
            updatedAt: base.addingTimeInterval(3_600),
            note: "keep an eye on this",
            isPinned: true,
            monitoringEnabled: false,
            lastKnownAvailability: .registered,
            certificateWarningLevel: .warning,
            certificateDaysRemaining: 12
        )

        let csv = DataPortabilityCSV.trackedDomains([original])
        let parsed = try DataPortabilityCSV.parseTrackedDomains(from: csv)

        XCTAssertEqual(parsed.count, 1)
        let restored = try XCTUnwrap(parsed.first)
        XCTAssertEqual(restored.domain, "csv.example")
        XCTAssertEqual(restored.note, "keep an eye on this")
        XCTAssertTrue(restored.isPinned)
        XCTAssertFalse(restored.monitoringEnabled)
        XCTAssertEqual(restored.lastKnownAvailability, .registered)
        XCTAssertEqual(restored.certificateWarningLevel, .warning)
        XCTAssertEqual(restored.certificateDaysRemaining, 12)
    }

    func testTrackedDomainCSVParseSkipsRowsWithoutDomain() throws {
        let csv = """
        domain,isPinned,monitoringEnabled
        ,true,true
        valid.example,false,true
        """

        let parsed = try DataPortabilityCSV.parseTrackedDomains(from: csv)

        XCTAssertEqual(parsed.map(\.domain), ["valid.example"])
    }

    // MARK: - Merge / dedup

    func testSaveLoadDeduplicatesSameDomainCaseInsensitively() {
        let older = TrackedDomain(
            domain: "example.com",
            createdAt: base.addingTimeInterval(-100),
            updatedAt: base.addingTimeInterval(-50),
            isPinned: false,
            monitoringEnabled: false
        )
        let newer = TrackedDomain(
            domain: "EXAMPLE.com",
            createdAt: base.addingTimeInterval(-80),
            updatedAt: base.addingTimeInterval(-10),
            isPinned: true,
            monitoringEnabled: false
        )

        DomainDataPortabilityService.saveTrackedDomains([older, newer], defaults: defaults)
        let loaded = DomainDataPortabilityService.loadTrackedDomains(defaults: defaults)

        XCTAssertEqual(loaded.count, 1, "same domain differing only in case must collapse to one entry")
        let merged = loaded[0]
        XCTAssertEqual(merged.domain, "example.com")
        XCTAssertTrue(merged.isPinned, "pinned state is OR-merged")
        XCTAssertFalse(merged.monitoringEnabled)
        XCTAssertEqual(merged.createdAt, base.addingTimeInterval(-100), "createdAt takes the earliest")
        XCTAssertEqual(merged.updatedAt, base.addingTimeInterval(-10), "updatedAt takes the latest")
    }

    func testSaveLoadKeepsDistinctDomainsSortedByRecency() {
        let domains = [
            TrackedDomain(domain: "old.example", updatedAt: base.addingTimeInterval(-300)),
            TrackedDomain(domain: "new.example", updatedAt: base.addingTimeInterval(-10)),
            TrackedDomain(domain: "mid.example", updatedAt: base.addingTimeInterval(-100))
        ]

        DomainDataPortabilityService.saveTrackedDomains(domains, defaults: defaults)
        let loaded = DomainDataPortabilityService.loadTrackedDomains(defaults: defaults)

        XCTAssertEqual(loaded.map(\.domain), ["new.example", "mid.example", "old.example"])
    }

    func testLoadTrackedDomainsEmptyWhenUnset() {
        XCTAssertTrue(DomainDataPortabilityService.loadTrackedDomains(defaults: defaults).isEmpty)
    }

    // MARK: - Recent searches

    func testRecentSearchesRoundTripAndTruncateToTwenty() {
        let values = (0..<30).map { "domain\($0).example" }

        DomainDataPortabilityService.saveRecentSearches(values, defaults: defaults)
        let loaded = DomainDataPortabilityService.loadRecentSearches(defaults: defaults)

        XCTAssertEqual(loaded.count, 20, "recent searches are capped at 20")
        XCTAssertEqual(loaded, Array(values.prefix(20)), "order is preserved")
    }
}