feat: initial public release
ConsentOS — a privacy-first cookie consent management platform. Self-hosted, source-available alternative to OneTrust, Cookiebot, and CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant architecture with role-based access, configuration cascade (system → org → group → site → region), dark-pattern detection in the scanner, and a tamper-evident consent record audit trail. This is the initial public release. Prior development history is retained internally. See README.md for the feature list, architecture overview, and quick-start instructions. Licensed under the Elastic Licence 2.0 — self-host freely; do not resell as a managed service.
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user