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:
James Cottrill
2026-04-13 14:20:15 +00:00
commit fbf26453f2
341 changed files with 62807 additions and 0 deletions

View File

@@ -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)
}
}