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,137 @@
|
||||
import XCTest
|
||||
@testable import ConsentOSCore
|
||||
|
||||
final class TCFStringEncoderTests: XCTestCase {
|
||||
|
||||
// MARK: - Encoding Returns nil Before Interaction
|
||||
|
||||
func test_encode_returnsNil_whenStateHasNoInteraction() {
|
||||
let state = ConsentState(visitorId: "v1") // consentedAt is nil
|
||||
let config = makeSampleConfig()
|
||||
XCTAssertNil(TCFStringEncoder.encode(state: state, config: config))
|
||||
}
|
||||
|
||||
// MARK: - Encoding Returns a Non-Empty String After Interaction
|
||||
|
||||
func test_encode_returnsNonEmptyString_afterAcceptAll() {
|
||||
let state = ConsentState(visitorId: "v1").acceptingAll()
|
||||
let config = makeSampleConfig()
|
||||
let result = TCFStringEncoder.encode(state: state, config: config)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertFalse(result!.isEmpty)
|
||||
}
|
||||
|
||||
func test_encode_returnsNonEmptyString_afterRejectAll() {
|
||||
let state = ConsentState(visitorId: "v1").rejectingAll()
|
||||
let config = makeSampleConfig()
|
||||
let result = TCFStringEncoder.encode(state: state, config: config)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertFalse(result!.isEmpty)
|
||||
}
|
||||
|
||||
// MARK: - Base64url Format
|
||||
|
||||
func test_encode_producesValidBase64url() {
|
||||
let state = ConsentState(visitorId: "v1").acceptingAll()
|
||||
let config = makeSampleConfig()
|
||||
let tcString = TCFStringEncoder.encode(state: state, config: config)!
|
||||
|
||||
// Base64url must not contain standard Base64 characters that were replaced
|
||||
XCTAssertFalse(tcString.contains("+"), "TC string must not contain '+'")
|
||||
XCTAssertFalse(tcString.contains("/"), "TC string must not contain '/'")
|
||||
XCTAssertFalse(tcString.contains("="), "TC string must not contain padding '='")
|
||||
}
|
||||
|
||||
func test_encode_producesDecodableBase64() {
|
||||
let state = ConsentState(visitorId: "v1").acceptingAll()
|
||||
let config = makeSampleConfig()
|
||||
let tcString = TCFStringEncoder.encode(state: state, config: config)!
|
||||
|
||||
// Convert back to standard Base64 for decoding
|
||||
var base64 = tcString
|
||||
.replacingOccurrences(of: "-", with: "+")
|
||||
.replacingOccurrences(of: "_", with: "/")
|
||||
// Pad to multiple of 4
|
||||
let remainder = base64.count % 4
|
||||
if remainder != 0 {
|
||||
base64 += String(repeating: "=", count: 4 - remainder)
|
||||
}
|
||||
|
||||
let data = Data(base64Encoded: base64)
|
||||
XCTAssertNotNil(data, "TC string should be valid Base64")
|
||||
XCTAssertGreaterThan(data!.count, 0)
|
||||
}
|
||||
|
||||
// MARK: - Determinism
|
||||
|
||||
func test_encode_producesConsistentOutput_forSameInput() {
|
||||
// The encoder uses a fixed timestamp, so two calls with the same state
|
||||
// should produce strings of the same length (timestamps differ slightly).
|
||||
let consentDate = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
let state = ConsentState(
|
||||
visitorId: "v1",
|
||||
accepted: [.analytics, .marketing],
|
||||
rejected: [.functional, .personalisation],
|
||||
consentedAt: consentDate,
|
||||
bannerVersion: "v1"
|
||||
)
|
||||
let config = makeSampleConfig()
|
||||
|
||||
let result1 = TCFStringEncoder.encode(state: state, config: config)
|
||||
let result2 = TCFStringEncoder.encode(state: state, config: config)
|
||||
|
||||
XCTAssertEqual(result1, result2)
|
||||
}
|
||||
|
||||
// MARK: - Different States Produce Different Strings
|
||||
|
||||
func test_encode_producesDistinctStrings_forAcceptAllVsRejectAll() {
|
||||
let consentDate = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
let acceptedState = ConsentState(
|
||||
visitorId: "v1",
|
||||
accepted: Set(ConsentCategory.allCases.filter { $0.requiresConsent }),
|
||||
rejected: [],
|
||||
consentedAt: consentDate,
|
||||
bannerVersion: nil
|
||||
)
|
||||
let rejectedState = ConsentState(
|
||||
visitorId: "v1",
|
||||
accepted: [],
|
||||
rejected: Set(ConsentCategory.allCases.filter { $0.requiresConsent }),
|
||||
consentedAt: consentDate,
|
||||
bannerVersion: nil
|
||||
)
|
||||
let config = makeSampleConfig()
|
||||
|
||||
let tcAccepted = TCFStringEncoder.encode(state: acceptedState, config: config)
|
||||
let tcRejected = TCFStringEncoder.encode(state: rejectedState, config: config)
|
||||
|
||||
XCTAssertNotEqual(tcAccepted, tcRejected)
|
||||
}
|
||||
|
||||
// MARK: - Minimum Length
|
||||
|
||||
func test_encode_producesStringOfReasonableLength() {
|
||||
let state = ConsentState(visitorId: "v1").acceptingAll()
|
||||
let config = makeSampleConfig()
|
||||
let tcString = TCFStringEncoder.encode(state: state, config: config)!
|
||||
|
||||
// A valid core TC string serialises to at least ~20 bytes before Base64url encoding
|
||||
// (the first 6 bits alone carry the version). 28 chars is a reasonable lower bound.
|
||||
XCTAssertGreaterThan(tcString.count, 28, "TC string appears too short")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makeSampleConfig() -> ConsentConfig {
|
||||
ConsentConfig(
|
||||
siteId: "site-001",
|
||||
siteName: "Test Site",
|
||||
blockingMode: .optIn,
|
||||
consentExpiryDays: 365,
|
||||
bannerVersion: "v1",
|
||||
bannerConfig: ConsentConfig.BannerConfig(),
|
||||
categories: []
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user