1use crate::audit::AuditReport;
15use crate::error::SsgError;
16
17pub fn format(report: &AuditReport) -> Result<String, SsgError> {
34 fail_point!("audit::json-format", |_| {
35 Err(SsgError::Io {
36 path: std::path::PathBuf::from("<audit-report>"),
37 source: std::io::Error::other("injected: audit::json-format"),
38 })
39 });
40 serde_json::to_string_pretty(report).map_err(serialize_error)
41}
42
43fn serialize_error(e: serde_json::Error) -> SsgError {
46 SsgError::Io {
47 path: std::path::PathBuf::from("<audit-report>"),
48 source: std::io::Error::other(e),
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use crate::audit::{Finding, GateResult, Severity, SeverityCounts};
56
57 #[test]
64 #[serial_test::parallel]
65 fn json_round_trips() {
66 let report = AuditReport {
67 gates: vec![GateResult {
68 name: "g".to_string(),
69 skipped: false,
70 skip_reason: None,
71 severity_counts: SeverityCounts {
72 info: 0,
73 warn: 1,
74 error: 0,
75 },
76 findings: vec![Finding::new("g", Severity::Warn, "msg")
77 .with_code("X")
78 .with_path("a.html")],
79 }],
80 };
81 let s = format(&report).unwrap();
82 let v: serde_json::Value = serde_json::from_str(&s).unwrap();
83 assert_eq!(v["gates"][0]["name"], "g");
84 assert_eq!(v["gates"][0]["severity_counts"]["warn"], 1);
85 assert_eq!(v["gates"][0]["findings"][0]["code"], "X");
86 assert_eq!(v["gates"][0]["findings"][0]["path"], "a.html");
87 assert_eq!(v["gates"][0]["findings"][0]["severity"], "warn");
88 }
89
90 #[test]
91 fn serialize_error_maps_to_io_variant() {
92 let e = serde_json::from_str::<serde_json::Value>("{")
93 .expect_err("truncated JSON must fail");
94 match serialize_error(e) {
95 SsgError::Io { path, source } => {
96 assert_eq!(path, std::path::PathBuf::from("<audit-report>"));
97 assert_eq!(source.kind(), std::io::ErrorKind::Other);
98 }
99 other => panic!("expected SsgError::Io, got {other:?}"),
100 }
101 }
102
103 #[test]
104 #[serial_test::parallel]
105 fn json_schema_has_stable_shape() {
106 let report = AuditReport { gates: vec![] };
107 let s = format(&report).unwrap();
108 assert!(s.contains("\"gates\""));
109 }
110}
111
112#[cfg(all(test, feature = "test-fault-injection"))]
113mod fault_tests {
114 use super::*;
115 use serial_test::serial;
116
117 struct FailGuard(&'static str);
119
120 impl Drop for FailGuard {
121 fn drop(&mut self) {
122 let _ = fail::cfg(self.0, "off");
123 }
124 }
125
126 #[test]
131 #[serial]
132 fn format_propagates_injected_io_error() {
133 let _guard = FailGuard("audit::json-format");
134 fail::cfg("audit::json-format", "return").expect("activate failpoint");
135
136 let report = AuditReport { gates: vec![] };
137 let err = format(&report).unwrap_err();
138 match err {
139 SsgError::Io { path, source } => {
140 assert_eq!(path, std::path::PathBuf::from("<audit-report>"));
141 assert_eq!(source.kind(), std::io::ErrorKind::Other);
142 assert!(
143 format!("{source}").contains("audit::json-format"),
144 "got: {source}"
145 );
146 }
147 other => panic!("expected SsgError::Io, got {other:?}"),
148 }
149 }
150}