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,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: []
)
}
}