Files
consentos/sdks/ios/ConsentOS/Tests/ConsentOSCoreTests/ConsentOSTests.swift
James Cottrill fbf26453f2 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.
2026-04-14 09:18:18 +00:00

293 lines
9.5 KiB
Swift

import XCTest
@testable import ConsentOSCore
// MARK: - Mock Storage
final class MockConsentStorage: ConsentStorageProtocol, @unchecked Sendable {
var storedState: ConsentState?
var storedCachedConfig: CachedConfig?
var storedVisitorId: String = UUID().uuidString
private(set) var saveStateCallCount = 0
private(set) var clearStateCallCount = 0
func loadState() -> ConsentState? { storedState }
func saveState(_ state: ConsentState) {
saveStateCallCount += 1
storedState = state
}
func clearState() {
clearStateCallCount += 1
storedState = nil
}
func loadCachedConfig() -> CachedConfig? { storedCachedConfig }
func saveCachedConfig(_ cached: CachedConfig) {
storedCachedConfig = cached
}
func clearCachedConfig() {
storedCachedConfig = nil
}
func visitorId() -> String { storedVisitorId }
}
// MARK: - Tests
final class ConsentOSTests: XCTestCase {
private var sdk: ConsentOS!
private var mockStorage: MockConsentStorage!
private var mockAPI: MockConsentAPI!
override func setUp() {
super.setUp()
mockStorage = MockConsentStorage()
mockAPI = MockConsentAPI()
sdk = ConsentOS(storage: mockStorage, api: mockAPI)
}
// MARK: - Configuration
func test_configure_setsIsConfigured() {
XCTAssertFalse(sdk.isConfigured)
sdk.configure(
siteId: "site-001",
apiBase: URL(string: "https://api.example.com")!
)
XCTAssertTrue(sdk.isConfigured)
}
func test_configure_setsSiteId() {
sdk.configure(siteId: "my-site", apiBase: URL(string: "https://api.example.com")!)
XCTAssertEqual(sdk.siteId, "my-site")
}
func test_configure_restoresPersistedState() {
let existing = ConsentState(
visitorId: mockStorage.storedVisitorId,
accepted: [.analytics],
rejected: [],
consentedAt: Date(),
bannerVersion: "v1"
)
mockStorage.storedState = existing
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
XCTAssertEqual(sdk.consentState?.accepted, [.analytics])
}
func test_configure_createsNewState_whenNoPersistedState() {
mockStorage.storedState = nil
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
XCTAssertNotNil(sdk.consentState)
XCTAssertFalse(sdk.consentState!.hasInteracted)
}
// MARK: - shouldShowBanner
func test_shouldShowBanner_returnsTrue_whenNoInteraction() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
// State has no interaction (consentedAt is nil)
let shouldShow = await sdk.shouldShowBanner()
XCTAssertTrue(shouldShow)
}
func test_shouldShowBanner_returnsFalse_whenRecentConsentExists() async {
mockAPI.configToReturn = makeSampleConfig(expiryDays: 365, bannerVersion: "v1")
let recent = ConsentState(
visitorId: "v1",
accepted: [.analytics],
rejected: [.marketing, .functional, .personalisation],
consentedAt: Date(), // just now
bannerVersion: "v1"
)
mockStorage.storedState = recent
// Pre-load cached config so shouldShowBanner sees it
mockStorage.storedCachedConfig = CachedConfig(
config: makeSampleConfig(expiryDays: 365, bannerVersion: "v1"),
fetchedAt: Date()
)
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
let shouldShow = await sdk.shouldShowBanner()
XCTAssertFalse(shouldShow)
}
func test_shouldShowBanner_returnsTrue_whenBannerVersionChanged() async {
mockAPI.configToReturn = makeSampleConfig(expiryDays: 365, bannerVersion: "v2")
let state = ConsentState(
visitorId: "v1",
accepted: [.analytics],
rejected: [],
consentedAt: Date(),
bannerVersion: "v1" // old version
)
mockStorage.storedState = state
mockStorage.storedCachedConfig = CachedConfig(
config: makeSampleConfig(expiryDays: 365, bannerVersion: "v2"),
fetchedAt: Date()
)
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
let shouldShow = await sdk.shouldShowBanner()
XCTAssertTrue(shouldShow)
}
func test_shouldShowBanner_returnsTrue_whenConsentExpired() async {
let expiryDays = 30
let consentedAt = Date(timeIntervalSinceNow: -Double(expiryDays * 86_400 + 1))
let state = ConsentState(
visitorId: "v1",
accepted: [.analytics],
rejected: [],
consentedAt: consentedAt,
bannerVersion: "v1"
)
mockStorage.storedState = state
mockStorage.storedCachedConfig = CachedConfig(
config: makeSampleConfig(expiryDays: expiryDays, bannerVersion: "v1"),
fetchedAt: Date()
)
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
let shouldShow = await sdk.shouldShowBanner()
XCTAssertTrue(shouldShow)
}
// MARK: - acceptAll
func test_acceptAll_updatesConsentState() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.acceptAll()
let state = sdk.consentState
XCTAssertNotNil(state)
XCTAssertTrue(state!.hasInteracted)
XCTAssertFalse(state!.accepted.isEmpty)
}
func test_acceptAll_grantsAllOptionalCategories() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.acceptAll()
for category in ConsentCategory.allCases where category.requiresConsent {
XCTAssertTrue(sdk.getConsentStatus(for: category), "\(category) should be granted")
}
}
func test_acceptAll_persistsToStorage() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.acceptAll()
XCTAssertGreaterThan(mockStorage.saveStateCallCount, 0)
XCTAssertNotNil(mockStorage.storedState)
}
func test_acceptAll_postsConsentToAPI() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.acceptAll()
XCTAssertEqual(mockAPI.postConsentCallCount, 1)
XCTAssertEqual(mockAPI.lastPostedPayload?.platform, "ios")
}
// MARK: - rejectAll
func test_rejectAll_updatesConsentState() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.rejectAll()
let state = sdk.consentState
XCTAssertNotNil(state)
XCTAssertTrue(state!.hasInteracted)
XCTAssertTrue(state!.accepted.isEmpty)
}
func test_rejectAll_deniesAllOptionalCategories() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.rejectAll()
for category in ConsentCategory.allCases where category.requiresConsent {
XCTAssertFalse(sdk.getConsentStatus(for: category), "\(category) should be denied")
}
}
// MARK: - acceptCategories
func test_acceptCategories_onlyGrantsSpecifiedCategories() async {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.acceptCategories([.analytics, .functional])
XCTAssertTrue(sdk.getConsentStatus(for: .analytics))
XCTAssertTrue(sdk.getConsentStatus(for: .functional))
XCTAssertFalse(sdk.getConsentStatus(for: .marketing))
XCTAssertFalse(sdk.getConsentStatus(for: .personalisation))
}
// MARK: - getConsentStatus
func test_getConsentStatus_returnsTrue_forNecessary_always() {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
XCTAssertTrue(sdk.getConsentStatus(for: .necessary))
}
func test_getConsentStatus_returnsFalse_forOptional_beforeInteraction() {
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
XCTAssertFalse(sdk.getConsentStatus(for: .analytics))
}
// MARK: - Delegate
func test_delegate_isNotifiedAfterAcceptAll() async {
class MockDelegate: ConsentOSDelegate {
var didChangeCalled = false
var receivedState: ConsentState?
func consentDidChange(_ state: ConsentState) {
didChangeCalled = true
receivedState = state
}
}
let delegate = MockDelegate()
sdk.delegate = delegate
sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
await sdk.acceptAll()
// Allow the MainActor dispatch to complete
await Task.yield()
await Task.yield()
XCTAssertTrue(delegate.didChangeCalled)
XCTAssertNotNil(delegate.receivedState)
}
// MARK: - Helpers
private func makeSampleConfig(expiryDays: Int = 365, bannerVersion: String = "v1") -> ConsentConfig {
ConsentConfig(
siteId: "site-001",
siteName: "Test",
blockingMode: .optIn,
consentExpiryDays: expiryDays,
bannerVersion: bannerVersion,
bannerConfig: ConsentConfig.BannerConfig(),
categories: []
)
}
}