use crate::state::AppState;
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Purpose {
Marketing,
Analytics,
ThirdPartySharing,
DataProcessing,
Personalization,
LocationTracking,
Profiling,
CrossDeviceTracking,
Advertising,
Research,
DoNotSell,
Custom(u32),
}
impl Purpose {
pub fn description(&self) -> &'static str {
match self {
Purpose::Marketing => "Marketing communications and promotional materials",
Purpose::Analytics => "Analytics and usage tracking",
Purpose::ThirdPartySharing => "Sharing data with third parties",
Purpose::DataProcessing => "General data processing for service operation",
Purpose::Personalization => "Personalization and recommendations",
Purpose::LocationTracking => "Location tracking and geolocation services",
Purpose::Profiling => "Profiling for automated decision-making",
Purpose::CrossDeviceTracking => "Cross-device tracking",
Purpose::Advertising => "Advertising and targeted ads",
Purpose::Research => "Research and statistical purposes",
Purpose::DoNotSell => "CCPA: Do Not Sell My Personal Information",
Purpose::Custom(_) => "Custom purpose",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"marketing" => Some(Purpose::Marketing),
"analytics" => Some(Purpose::Analytics),
"third_party_sharing" | "thirdpartysharing" => Some(Purpose::ThirdPartySharing),
"data_processing" | "dataprocessing" => Some(Purpose::DataProcessing),
"personalization" => Some(Purpose::Personalization),
"location_tracking" | "locationtracking" => Some(Purpose::LocationTracking),
"profiling" => Some(Purpose::Profiling),
"cross_device_tracking" | "crossdevicetracking" => Some(Purpose::CrossDeviceTracking),
"advertising" => Some(Purpose::Advertising),
"research" => Some(Purpose::Research),
"do_not_sell" | "donotsell" => Some(Purpose::DoNotSell),
s if s.starts_with("custom:") => s
.strip_prefix("custom:")
.and_then(|id| id.parse().ok())
.map(Purpose::Custom),
_ => None,
}
}
pub fn to_str(&self) -> String {
match self {
Purpose::Marketing => "marketing".to_string(),
Purpose::Analytics => "analytics".to_string(),
Purpose::ThirdPartySharing => "third_party_sharing".to_string(),
Purpose::DataProcessing => "data_processing".to_string(),
Purpose::Personalization => "personalization".to_string(),
Purpose::LocationTracking => "location_tracking".to_string(),
Purpose::Profiling => "profiling".to_string(),
Purpose::CrossDeviceTracking => "cross_device_tracking".to_string(),
Purpose::Advertising => "advertising".to_string(),
Purpose::Research => "research".to_string(),
Purpose::DoNotSell => "do_not_sell".to_string(),
Purpose::Custom(id) => format!("custom:{}", id),
}
}
}
impl std::fmt::Display for Purpose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConsentSource {
WebForm,
MobileApp,
Api,
Import,
PaperForm,
Email,
Verbal,
SystemDefault,
Custom(String),
}
impl ConsentSource {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"web_form" | "webform" | "web" => ConsentSource::WebForm,
"mobile_app" | "mobileapp" | "mobile" => ConsentSource::MobileApp,
"api" => ConsentSource::Api,
"import" => ConsentSource::Import,
"paper_form" | "paperform" | "paper" => ConsentSource::PaperForm,
"email" => ConsentSource::Email,
"verbal" => ConsentSource::Verbal,
"system_default" | "systemdefault" | "system" => ConsentSource::SystemDefault,
other => ConsentSource::Custom(other.to_string()),
}
}
}
impl std::fmt::Display for ConsentSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConsentSource::WebForm => write!(f, "web_form"),
ConsentSource::MobileApp => write!(f, "mobile_app"),
ConsentSource::Api => write!(f, "api"),
ConsentSource::Import => write!(f, "import"),
ConsentSource::PaperForm => write!(f, "paper_form"),
ConsentSource::Email => write!(f, "email"),
ConsentSource::Verbal => write!(f, "verbal"),
ConsentSource::SystemDefault => write!(f, "system_default"),
ConsentSource::Custom(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsentRecord {
pub id: String,
pub subject_id: String,
pub purpose: Purpose,
pub granted: bool,
pub timestamp: u64,
pub source: ConsentSource,
pub expires_at: Option<u64>,
pub version: String,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
impl ConsentRecord {
pub fn new(
subject_id: &str,
purpose: Purpose,
granted: bool,
source: ConsentSource,
version: &str,
) -> Self {
Self {
id: generate_consent_id(),
subject_id: subject_id.to_string(),
purpose,
granted,
timestamp: now_timestamp_ms(),
source,
expires_at: None,
version: version.to_string(),
metadata: HashMap::new(),
}
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
now_timestamp_ms() > expires_at
} else {
false
}
}
pub fn is_valid(&self) -> bool {
self.granted && !self.is_expired()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsentHistoryEntry {
pub record: ConsentRecord,
pub action: ConsentAction,
pub actor: String,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConsentAction {
Grant,
Deny,
Withdraw,
Renew,
Update,
Expire,
}
pub struct ConsentManager {
records: RwLock<HashMap<String, HashMap<Purpose, ConsentRecord>>>,
history: RwLock<HashMap<String, Vec<ConsentHistoryEntry>>>,
do_not_sell_list: RwLock<std::collections::HashSet<String>>,
record_counter: RwLock<u64>,
data_dir: Option<PathBuf>,
}
#[derive(Serialize, Deserialize)]
struct ConsentSnapshot {
records: HashMap<String, HashMap<Purpose, ConsentRecord>>,
history: HashMap<String, Vec<ConsentHistoryEntry>>,
do_not_sell_list: std::collections::HashSet<String>,
record_counter: u64,
}
impl ConsentManager {
pub fn new() -> Self {
Self::with_data_dir(None)
}
pub fn with_data_dir(data_dir: Option<PathBuf>) -> Self {
if let Some(ref dir) = data_dir {
let path = dir.join("consent.json");
if path.exists() {
match std::fs::read_to_string(&path) {
Ok(contents) => match serde_json::from_str::<ConsentSnapshot>(&contents) {
Ok(snapshot) => {
tracing::info!("Loaded consent data from {}", path.display());
return Self {
records: RwLock::new(snapshot.records),
history: RwLock::new(snapshot.history),
do_not_sell_list: RwLock::new(snapshot.do_not_sell_list),
record_counter: RwLock::new(snapshot.record_counter),
data_dir,
};
}
Err(e) => {
tracing::error!(
"Failed to deserialize consent data from {}: {}",
path.display(),
e
);
}
},
Err(e) => {
tracing::error!(
"Failed to read consent data from {}: {}",
path.display(),
e
);
}
}
}
}
Self {
records: RwLock::new(HashMap::new()),
history: RwLock::new(HashMap::new()),
do_not_sell_list: RwLock::new(std::collections::HashSet::new()),
record_counter: RwLock::new(0),
data_dir,
}
}
fn flush_to_disk(&self) {
let Some(ref dir) = self.data_dir else {
return;
};
let snapshot = ConsentSnapshot {
records: self.records.read().clone(),
history: self.history.read().clone(),
do_not_sell_list: self.do_not_sell_list.read().clone(),
record_counter: *self.record_counter.read(),
};
if let Err(e) = std::fs::create_dir_all(dir) {
tracing::error!(
"Failed to create consent data directory {}: {}",
dir.display(),
e
);
return;
}
let path = dir.join("consent.json");
match serde_json::to_string_pretty(&snapshot) {
Ok(json) => {
if let Err(e) = std::fs::write(&path, json) {
tracing::error!("Failed to write consent data to {}: {}", path.display(), e);
}
}
Err(e) => {
tracing::error!("Failed to serialize consent data: {}", e);
}
}
}
pub fn record_consent(
&self,
subject_id: &str,
purpose: Purpose,
granted: bool,
source: ConsentSource,
version: &str,
expires_at: Option<u64>,
metadata: Option<HashMap<String, String>>,
actor: &str,
) -> ConsentRecord {
let mut record = ConsentRecord::new(subject_id, purpose, granted, source, version);
record.expires_at = expires_at;
if let Some(meta) = metadata {
record.metadata = meta;
}
let action = {
let records = self.records.read();
if let Some(subject_records) = records.get(subject_id) {
if let Some(existing) = subject_records.get(&purpose) {
if granted && !existing.granted {
ConsentAction::Grant
} else if !granted && existing.granted {
ConsentAction::Withdraw
} else if granted {
ConsentAction::Renew
} else {
ConsentAction::Deny
}
} else if granted {
ConsentAction::Grant
} else {
ConsentAction::Deny
}
} else if granted {
ConsentAction::Grant
} else {
ConsentAction::Deny
}
};
{
let mut records = self.records.write();
let subject_records = records.entry(subject_id.to_string()).or_default();
subject_records.insert(purpose, record.clone());
}
if purpose == Purpose::DoNotSell {
let mut dns_list = self.do_not_sell_list.write();
if granted {
dns_list.insert(subject_id.to_string());
} else {
dns_list.remove(subject_id);
}
}
{
let mut history = self.history.write();
let subject_history = history.entry(subject_id.to_string()).or_default();
subject_history.push(ConsentHistoryEntry {
record: record.clone(),
action,
actor: actor.to_string(),
reason: None,
});
if subject_history.len() > 1000 {
let excess = subject_history.len() - 1000;
subject_history.drain(..excess);
}
}
tracing::info!(
"Consent recorded: subject={}, purpose={}, granted={}, action={:?}",
subject_id,
purpose,
granted,
action
);
self.flush_to_disk();
record
}
pub fn get_consent(&self, subject_id: &str) -> Vec<ConsentRecord> {
let records = self.records.read();
records
.get(subject_id)
.map(|r| r.values().cloned().collect())
.unwrap_or_default()
}
pub fn get_consent_for_purpose(
&self,
subject_id: &str,
purpose: Purpose,
) -> Option<ConsentRecord> {
let records = self.records.read();
records
.get(subject_id)
.and_then(|r| r.get(&purpose))
.cloned()
}
pub fn check_consent(&self, subject_id: &str, purpose: Purpose) -> bool {
let records = self.records.read();
if let Some(subject_records) = records.get(subject_id) {
if let Some(record) = subject_records.get(&purpose) {
return record.is_valid();
}
}
false
}
pub fn withdraw_consent(
&self,
subject_id: &str,
purpose: Purpose,
actor: &str,
reason: Option<&str>,
) -> Result<ConsentRecord, String> {
let existing = {
let records = self.records.read();
records
.get(subject_id)
.and_then(|r| r.get(&purpose))
.cloned()
};
let existing = existing.ok_or_else(|| {
format!(
"No consent record found for subject {} and purpose {}",
subject_id, purpose
)
})?;
let mut record = ConsentRecord::new(
subject_id,
purpose,
false,
existing.source.clone(),
&existing.version,
);
record.metadata = existing.metadata.clone();
record
.metadata
.insert("withdrawn_from".to_string(), existing.id.clone());
{
let mut records = self.records.write();
if let Some(subject_records) = records.get_mut(subject_id) {
subject_records.insert(purpose, record.clone());
}
}
if purpose == Purpose::DoNotSell {
let mut dns_list = self.do_not_sell_list.write();
dns_list.remove(subject_id);
}
{
let mut history = self.history.write();
let subject_history = history.entry(subject_id.to_string()).or_default();
subject_history.push(ConsentHistoryEntry {
record: record.clone(),
action: ConsentAction::Withdraw,
actor: actor.to_string(),
reason: reason.map(String::from),
});
}
tracing::info!(
"Consent withdrawn: subject={}, purpose={}, actor={}",
subject_id,
purpose,
actor
);
self.flush_to_disk();
Ok(record)
}
pub fn get_history(&self, subject_id: &str) -> Vec<ConsentHistoryEntry> {
let history = self.history.read();
history.get(subject_id).cloned().unwrap_or_default()
}
pub fn get_history_for_purpose(
&self,
subject_id: &str,
purpose: Purpose,
) -> Vec<ConsentHistoryEntry> {
let history = self.history.read();
history
.get(subject_id)
.map(|h| {
h.iter()
.filter(|e| e.record.purpose == purpose)
.cloned()
.collect()
})
.unwrap_or_default()
}
pub fn is_on_do_not_sell_list(&self, subject_id: &str) -> bool {
self.do_not_sell_list.read().contains(subject_id)
}
pub fn get_do_not_sell_list(&self) -> Vec<String> {
self.do_not_sell_list.read().iter().cloned().collect()
}
pub fn export_subject_data(&self, subject_id: &str) -> SubjectConsentExport {
let records = self.get_consent(subject_id);
let history = self.get_history(subject_id);
let on_dns_list = self.is_on_do_not_sell_list(subject_id);
SubjectConsentExport {
subject_id: subject_id.to_string(),
export_timestamp: now_timestamp_ms(),
current_consents: records,
consent_history: history,
on_do_not_sell_list: on_dns_list,
}
}
pub fn delete_subject_data(&self, subject_id: &str, actor: &str) -> bool {
let had_records = {
let mut records = self.records.write();
records.remove(subject_id).is_some()
};
let had_history = {
let mut history = self.history.write();
history.remove(subject_id).is_some()
};
{
let mut dns_list = self.do_not_sell_list.write();
dns_list.remove(subject_id);
}
if had_records || had_history {
tracing::info!(
"Subject consent data deleted: subject={}, actor={}",
subject_id,
actor
);
self.flush_to_disk();
}
had_records || had_history
}
pub fn get_stats(&self) -> ConsentStats {
let records = self.records.read();
let history = self.history.read();
let dns_list = self.do_not_sell_list.read();
let total_subjects = records.len();
let total_records: usize = records.values().map(|r| r.len()).sum();
let total_history_entries: usize = history.values().map(|h| h.len()).sum();
let mut consents_by_purpose: HashMap<String, (usize, usize)> = HashMap::new();
for subject_records in records.values() {
for (purpose, record) in subject_records {
let entry = consents_by_purpose
.entry(purpose.to_str())
.or_insert((0, 0));
if record.is_valid() {
entry.0 += 1;
} else {
entry.1 += 1;
}
}
}
ConsentStats {
total_subjects,
total_records,
total_history_entries,
do_not_sell_count: dns_list.len(),
consents_by_purpose,
}
}
}
impl Default for ConsentManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubjectConsentExport {
pub subject_id: String,
pub export_timestamp: u64,
pub current_consents: Vec<ConsentRecord>,
pub consent_history: Vec<ConsentHistoryEntry>,
pub on_do_not_sell_list: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsentStats {
pub total_subjects: usize,
pub total_records: usize,
pub total_history_entries: usize,
pub do_not_sell_count: usize,
pub consents_by_purpose: HashMap<String, (usize, usize)>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RecordConsentRequest {
pub subject_id: String,
pub purpose: String,
pub granted: bool,
#[serde(default = "default_source")]
pub source: String,
#[serde(default = "default_version")]
pub version: String,
pub expires_at: Option<u64>,
#[serde(default)]
pub metadata: Option<HashMap<String, String>>,
}
fn default_source() -> String {
"api".to_string()
}
fn default_version() -> String {
"1.0".to_string()
}
#[derive(Debug, Clone, Serialize)]
pub struct ConsentResponse {
pub success: bool,
pub record: Option<ConsentRecord>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConsentStatusResponse {
pub subject_id: String,
pub consents: Vec<ConsentRecord>,
pub on_do_not_sell_list: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WithdrawConsentRequest {
pub reason: Option<String>,
}
pub async fn record_consent(
State(state): State<AppState>,
Json(request): Json<RecordConsentRequest>,
) -> impl IntoResponse {
let purpose = match Purpose::from_str(&request.purpose) {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ConsentResponse {
success: false,
record: None,
error: Some(format!("Invalid purpose: {}", request.purpose)),
}),
);
}
};
let source = ConsentSource::from_str(&request.source);
let record = state.consent_manager.record_consent(
&request.subject_id,
purpose,
request.granted,
source,
&request.version,
request.expires_at,
request.metadata,
"api", );
state.activity.log_write(
&format!(
"Consent recorded: subject={}, purpose={}, granted={}",
request.subject_id, purpose, request.granted
),
None,
);
(
StatusCode::CREATED,
Json(ConsentResponse {
success: true,
record: Some(record),
error: None,
}),
)
}
pub async fn get_consent_status(
State(state): State<AppState>,
Path(subject_id): Path<String>,
) -> Json<ConsentStatusResponse> {
let consents = state.consent_manager.get_consent(&subject_id);
let on_dns_list = state.consent_manager.is_on_do_not_sell_list(&subject_id);
Json(ConsentStatusResponse {
subject_id,
consents,
on_do_not_sell_list: on_dns_list,
})
}
pub async fn withdraw_consent(
State(state): State<AppState>,
Path((subject_id, purpose_str)): Path<(String, String)>,
Json(request): Json<WithdrawConsentRequest>,
) -> impl IntoResponse {
let purpose = match Purpose::from_str(&purpose_str) {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ConsentResponse {
success: false,
record: None,
error: Some(format!("Invalid purpose: {}", purpose_str)),
}),
);
}
};
match state.consent_manager.withdraw_consent(
&subject_id,
purpose,
"api", request.reason.as_deref(),
) {
Ok(record) => {
state.activity.log_write(
&format!(
"Consent withdrawn: subject={}, purpose={}",
subject_id, purpose
),
None,
);
(
StatusCode::OK,
Json(ConsentResponse {
success: true,
record: Some(record),
error: None,
}),
)
}
Err(e) => (
StatusCode::NOT_FOUND,
Json(ConsentResponse {
success: false,
record: None,
error: Some(e),
}),
),
}
}
pub async fn get_consent_history(
State(state): State<AppState>,
Path(subject_id): Path<String>,
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> Json<Vec<ConsentHistoryEntry>> {
let history = if let Some(purpose_str) = params.get("purpose") {
if let Some(purpose) = Purpose::from_str(purpose_str) {
state
.consent_manager
.get_history_for_purpose(&subject_id, purpose)
} else {
state.consent_manager.get_history(&subject_id)
}
} else {
state.consent_manager.get_history(&subject_id)
};
Json(history)
}
pub async fn check_consent_status(
State(state): State<AppState>,
Path((subject_id, purpose_str)): Path<(String, String)>,
) -> impl IntoResponse {
let purpose = match Purpose::from_str(&purpose_str) {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": format!("Invalid purpose: {}", purpose_str)
})),
);
}
};
let granted = state.consent_manager.check_consent(&subject_id, purpose);
(
StatusCode::OK,
Json(serde_json::json!({
"subject_id": subject_id,
"purpose": purpose.to_str(),
"granted": granted
})),
)
}
pub async fn export_consent_data(
State(state): State<AppState>,
Path(subject_id): Path<String>,
) -> Json<SubjectConsentExport> {
let export = state.consent_manager.export_subject_data(&subject_id);
Json(export)
}
pub async fn delete_consent_data(
State(state): State<AppState>,
Path(subject_id): Path<String>,
) -> impl IntoResponse {
let deleted = state
.consent_manager
.delete_subject_data(&subject_id, "api");
state.activity.log(
crate::activity::ActivityType::Delete,
&format!("Consent data deleted for subject: {}", subject_id),
);
if deleted {
(
StatusCode::OK,
Json(serde_json::json!({
"success": true,
"message": format!("Consent data deleted for subject: {}", subject_id)
})),
)
} else {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"success": false,
"error": format!("No consent data found for subject: {}", subject_id)
})),
)
}
}
pub async fn get_consent_stats(State(state): State<AppState>) -> Json<ConsentStats> {
Json(state.consent_manager.get_stats())
}
pub async fn get_do_not_sell_list(State(state): State<AppState>) -> Json<Vec<String>> {
Json(state.consent_manager.get_do_not_sell_list())
}
pub fn check_consent(state: &AppState, subject_id: &str, purpose: Purpose) -> bool {
state.consent_manager.check_consent(subject_id, purpose)
}
pub fn check_all_consents(state: &AppState, subject_id: &str, purposes: &[Purpose]) -> bool {
purposes
.iter()
.all(|p| state.consent_manager.check_consent(subject_id, *p))
}
pub fn check_any_consent(state: &AppState, subject_id: &str, purposes: &[Purpose]) -> bool {
purposes
.iter()
.any(|p| state.consent_manager.check_consent(subject_id, *p))
}
fn now_timestamp_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn generate_consent_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let count = COUNTER.fetch_add(1, Ordering::SeqCst);
let timestamp = now_timestamp_ms();
format!("consent-{}-{:06}", timestamp, count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_consent_record_creation() {
let record = ConsentRecord::new(
"user123",
Purpose::Marketing,
true,
ConsentSource::WebForm,
"1.0",
);
assert_eq!(record.subject_id, "user123");
assert_eq!(record.purpose, Purpose::Marketing);
assert!(record.granted);
assert!(record.is_valid());
}
#[test]
fn test_consent_expiration() {
let mut record = ConsentRecord::new(
"user123",
Purpose::Analytics,
true,
ConsentSource::Api,
"1.0",
);
record.expires_at = Some(1000);
assert!(record.is_expired());
assert!(!record.is_valid());
}
#[test]
fn test_consent_manager_basic_operations() {
let manager = ConsentManager::new();
let record = manager.record_consent(
"user123",
Purpose::Marketing,
true,
ConsentSource::WebForm,
"1.0",
None,
None,
"test",
);
assert!(record.granted);
assert!(manager.check_consent("user123", Purpose::Marketing));
assert!(!manager.check_consent("user123", Purpose::Analytics));
assert!(!manager.check_consent("user456", Purpose::Marketing));
}
#[test]
fn test_consent_withdrawal() {
let manager = ConsentManager::new();
manager.record_consent(
"user123",
Purpose::Marketing,
true,
ConsentSource::WebForm,
"1.0",
None,
None,
"test",
);
assert!(manager.check_consent("user123", Purpose::Marketing));
let result =
manager.withdraw_consent("user123", Purpose::Marketing, "test", Some("User request"));
assert!(result.is_ok());
assert!(!manager.check_consent("user123", Purpose::Marketing));
}
#[test]
fn test_consent_history() {
let manager = ConsentManager::new();
manager.record_consent(
"user123",
Purpose::Marketing,
true,
ConsentSource::WebForm,
"1.0",
None,
None,
"test",
);
manager.record_consent(
"user123",
Purpose::Marketing,
false,
ConsentSource::WebForm,
"1.0",
None,
None,
"test",
);
let history = manager.get_history("user123");
assert_eq!(history.len(), 2);
assert_eq!(history[0].action, ConsentAction::Grant);
assert_eq!(history[1].action, ConsentAction::Withdraw);
}
#[test]
fn test_do_not_sell_list() {
let manager = ConsentManager::new();
manager.record_consent(
"user123",
Purpose::DoNotSell,
true,
ConsentSource::WebForm,
"1.0",
None,
None,
"test",
);
assert!(manager.is_on_do_not_sell_list("user123"));
assert!(!manager.is_on_do_not_sell_list("user456"));
manager.record_consent(
"user123",
Purpose::DoNotSell,
false,
ConsentSource::WebForm,
"1.0",
None,
None,
"test",
);
assert!(!manager.is_on_do_not_sell_list("user123"));
}
#[test]
fn test_purpose_parsing() {
assert_eq!(Purpose::from_str("marketing"), Some(Purpose::Marketing));
assert_eq!(Purpose::from_str("ANALYTICS"), Some(Purpose::Analytics));
assert_eq!(
Purpose::from_str("third_party_sharing"),
Some(Purpose::ThirdPartySharing)
);
assert_eq!(Purpose::from_str("do_not_sell"), Some(Purpose::DoNotSell));
assert_eq!(Purpose::from_str("custom:42"), Some(Purpose::Custom(42)));
assert_eq!(Purpose::from_str("unknown"), None);
}
#[test]
fn test_consent_stats() {
let manager = ConsentManager::new();
manager.record_consent(
"user1",
Purpose::Marketing,
true,
ConsentSource::Api,
"1.0",
None,
None,
"test",
);
manager.record_consent(
"user1",
Purpose::Analytics,
false,
ConsentSource::Api,
"1.0",
None,
None,
"test",
);
manager.record_consent(
"user2",
Purpose::Marketing,
true,
ConsentSource::Api,
"1.0",
None,
None,
"test",
);
let stats = manager.get_stats();
assert_eq!(stats.total_subjects, 2);
assert_eq!(stats.total_records, 3);
let marketing_stats = stats.consents_by_purpose.get("marketing").unwrap();
assert_eq!(marketing_stats.0, 2); assert_eq!(marketing_stats.1, 0); }
#[test]
fn test_subject_data_export() {
let manager = ConsentManager::new();
manager.record_consent(
"user123",
Purpose::Marketing,
true,
ConsentSource::Api,
"1.0",
None,
None,
"test",
);
manager.record_consent(
"user123",
Purpose::Analytics,
true,
ConsentSource::Api,
"1.0",
None,
None,
"test",
);
let export = manager.export_subject_data("user123");
assert_eq!(export.subject_id, "user123");
assert_eq!(export.current_consents.len(), 2);
assert_eq!(export.consent_history.len(), 2);
}
#[test]
fn test_subject_data_deletion() {
let manager = ConsentManager::new();
manager.record_consent(
"user123",
Purpose::Marketing,
true,
ConsentSource::Api,
"1.0",
None,
None,
"test",
);
assert!(manager.delete_subject_data("user123", "test"));
assert!(manager.get_consent("user123").is_empty());
assert!(manager.get_history("user123").is_empty());
assert!(!manager.delete_subject_data("user123", "test"));
}
}