use chrono::{DateTime, Utc};
use serde::Serialize;
use serde_json::Value;
use crate::models::Course;
pub fn mask_course(c: &Course) -> Course {
let mut out = c.clone();
out.provider_id = None;
for inst in &mut out.instances {
inst.instructor_ids.clear();
inst.instructor_names = inst
.instructor_names
.iter()
.map(|_| MASK_LABEL.to_string())
.collect();
}
out
}
const MASK_LABEL: &str = "[REDACTED]";
#[derive(Debug, Clone, Serialize)]
pub struct CourseExport<'a> {
pub exported_at: DateTime<Utc>,
pub source: &'static str,
pub schema: &'static str,
pub course: &'a Course,
}
pub fn export_course(c: &Course) -> Value {
let envelope = CourseExport {
exported_at: Utc::now(),
source: "course-service",
schema: "https://schema.org/Course",
course: c,
};
serde_json::to_value(envelope).unwrap_or(Value::Null)
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
use crate::models::{CourseInstance, CourseInstanceStatus};
fn instance(course_id: Uuid) -> CourseInstance {
CourseInstance {
id: Uuid::new_v4(),
course_id,
name: Some("Spring 2026".into()),
course_mode: None,
status: CourseInstanceStatus::Scheduled,
schedule: None,
in_language: vec!["en".into()],
location: None,
location_id: None,
instructor_ids: vec![Uuid::new_v4(), Uuid::new_v4()],
instructor_names: vec!["Prof. Smith".into(), "Prof. Jones".into()],
maximum_attendee_capacity: Some(30),
enrolled_count: Some(12),
enrollment_opens: None,
enrollment_closes: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn masking_clears_provider_and_instructor_refs() {
let mut c = Course::new("Intro to CS");
c.provider_id = Some(Uuid::new_v4());
c.instances.push(instance(c.id));
let masked = mask_course(&c);
assert!(masked.provider_id.is_none());
let inst = &masked.instances[0];
assert!(inst.instructor_ids.is_empty());
assert_eq!(inst.instructor_names, vec![MASK_LABEL, MASK_LABEL]);
}
#[test]
fn masking_leaves_non_sensitive_fields_intact() {
let mut c = Course::new("Intro to CS");
c.course_code = Some("CS101".into());
c.keywords = vec!["programming".into()];
let masked = mask_course(&c);
assert_eq!(masked.name, "Intro to CS");
assert_eq!(masked.course_code.as_deref(), Some("CS101"));
assert_eq!(masked.keywords, vec!["programming".to_string()]);
}
#[test]
fn masking_does_not_mutate_input() {
let mut c = Course::new("Intro to CS");
let provider = Uuid::new_v4();
c.provider_id = Some(provider);
let _masked = mask_course(&c);
assert_eq!(c.provider_id, Some(provider));
}
#[test]
fn export_envelope_carries_metadata_and_record() {
let c = Course::new("Intro to CS");
let v = export_course(&c);
assert_eq!(v["source"], "course-service");
assert_eq!(v["schema"], "https://schema.org/Course");
assert_eq!(v["course"]["name"], "Intro to CS");
assert!(v["exported_at"].is_string());
}
}