import XCTest @testable import ConsentOSCore final class ConsentStateTests: XCTestCase { private let visitorId = "test-visitor-123" // MARK: - Initial State func test_newState_hasNoInteraction() { let state = ConsentState(visitorId: visitorId) XCTAssertFalse(state.hasInteracted) XCTAssertNil(state.consentedAt) XCTAssertTrue(state.accepted.isEmpty) XCTAssertTrue(state.rejected.isEmpty) } func test_newState_preservesVisitorId() { let state = ConsentState(visitorId: visitorId) XCTAssertEqual(state.visitorId, visitorId) } // MARK: - isGranted func test_necessary_isAlwaysGranted() { let state = ConsentState(visitorId: visitorId) // no interaction XCTAssertTrue(state.isGranted(.necessary)) } func test_optional_isNotGranted_whenNoInteraction() { let state = ConsentState(visitorId: visitorId) for category in ConsentCategory.allCases where category != .necessary { XCTAssertFalse(state.isGranted(category), "\(category) should not be granted by default") } } func test_accepted_category_isGranted() { let state = ConsentState( visitorId: visitorId, accepted: [.analytics], rejected: [], consentedAt: Date(), bannerVersion: nil ) XCTAssertTrue(state.isGranted(.analytics)) } func test_rejected_category_isNotGranted() { let state = ConsentState( visitorId: visitorId, accepted: [], rejected: [.analytics], consentedAt: Date(), bannerVersion: nil ) XCTAssertFalse(state.isGranted(.analytics)) } // MARK: - isDenied func test_necessary_isNeverDenied() { let state = ConsentState( visitorId: visitorId, accepted: [], rejected: [.analytics], consentedAt: Date(), bannerVersion: nil ) XCTAssertFalse(state.isDenied(.necessary)) } func test_rejected_category_isDenied() { let state = ConsentState( visitorId: visitorId, accepted: [], rejected: [.marketing], consentedAt: Date(), bannerVersion: nil ) XCTAssertTrue(state.isDenied(.marketing)) } // MARK: - acceptingAll() func test_acceptingAll_grantsAllOptionalCategories() { let state = ConsentState(visitorId: visitorId) let accepted = state.acceptingAll() let expected = Set(ConsentCategory.allCases.filter { $0.requiresConsent }) XCTAssertEqual(accepted.accepted, expected) XCTAssertTrue(accepted.rejected.isEmpty) } func test_acceptingAll_setsConsentedAt() { let before = Date() let state = ConsentState(visitorId: visitorId).acceptingAll() XCTAssertNotNil(state.consentedAt) XCTAssertGreaterThanOrEqual(state.consentedAt!, before) } func test_acceptingAll_preservesVisitorId() { let state = ConsentState(visitorId: visitorId).acceptingAll() XCTAssertEqual(state.visitorId, visitorId) } func test_acceptingAll_hasInteracted() { let state = ConsentState(visitorId: visitorId).acceptingAll() XCTAssertTrue(state.hasInteracted) } // MARK: - rejectingAll() func test_rejectingAll_emptiesAccepted() { let state = ConsentState(visitorId: visitorId) let rejected = state.rejectingAll() XCTAssertTrue(rejected.accepted.isEmpty) } func test_rejectingAll_rejectsAllOptionalCategories() { let state = ConsentState(visitorId: visitorId).rejectingAll() let expected = Set(ConsentCategory.allCases.filter { $0.requiresConsent }) XCTAssertEqual(state.rejected, expected) } func test_rejectingAll_setsConsentedAt() { let state = ConsentState(visitorId: visitorId).rejectingAll() XCTAssertNotNil(state.consentedAt) } // MARK: - accepting(categories:) func test_acceptingCategories_onlyAcceptsSpecified() { let state = ConsentState(visitorId: visitorId) let result = state.accepting(categories: [.analytics, .functional]) XCTAssertTrue(result.accepted.contains(.analytics)) XCTAssertTrue(result.accepted.contains(.functional)) XCTAssertFalse(result.accepted.contains(.marketing)) XCTAssertFalse(result.accepted.contains(.personalisation)) } func test_acceptingCategories_rejectsRemainder() { let state = ConsentState(visitorId: visitorId) let result = state.accepting(categories: [.analytics]) XCTAssertTrue(result.rejected.contains(.marketing)) XCTAssertTrue(result.rejected.contains(.functional)) XCTAssertTrue(result.rejected.contains(.personalisation)) } func test_acceptingCategories_ignoresNecessary() { let state = ConsentState(visitorId: visitorId) // Passing .necessary should not land in accepted/rejected sets let result = state.accepting(categories: [.necessary]) XCTAssertFalse(result.accepted.contains(.necessary)) } func test_acceptingEmptySet_rejectsAll() { let state = ConsentState(visitorId: visitorId) let result = state.accepting(categories: []) XCTAssertTrue(result.accepted.isEmpty) let expectedRejected = Set(ConsentCategory.allCases.filter { $0.requiresConsent }) XCTAssertEqual(result.rejected, expectedRejected) } // MARK: - Codable func test_state_roundTripsViaJSON() throws { let original = ConsentState( visitorId: visitorId, accepted: [.analytics, .functional], rejected: [.marketing], consentedAt: Date(timeIntervalSince1970: 1_700_000_000), bannerVersion: "v2" ) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let data = try encoder.encode(original) let decoded = try decoder.decode(ConsentState.self, from: data) XCTAssertEqual(decoded.visitorId, original.visitorId) XCTAssertEqual(decoded.accepted, original.accepted) XCTAssertEqual(decoded.rejected, original.rejected) XCTAssertEqual(decoded.bannerVersion, original.bannerVersion) // Date round-trip: allow 1-second tolerance for ISO8601 sub-second truncation XCTAssertEqual( decoded.consentedAt!.timeIntervalSince1970, original.consentedAt!.timeIntervalSince1970, accuracy: 1.0 ) } // MARK: - Equatable func test_twoStatesWithSameValues_areEqual() { let date = Date(timeIntervalSince1970: 1_000_000) let a = ConsentState(visitorId: visitorId, accepted: [.analytics], rejected: [], consentedAt: date, bannerVersion: "v1") let b = ConsentState(visitorId: visitorId, accepted: [.analytics], rejected: [], consentedAt: date, bannerVersion: "v1") XCTAssertEqual(a, b) } func test_twoStatesWithDifferentAccepted_areNotEqual() { let date = Date(timeIntervalSince1970: 1_000_000) let a = ConsentState(visitorId: visitorId, accepted: [.analytics], rejected: [], consentedAt: date, bannerVersion: nil) let b = ConsentState(visitorId: visitorId, accepted: [.marketing], rejected: [], consentedAt: date, bannerVersion: nil) XCTAssertNotEqual(a, b) } }