1use chrono::{DateTime, Utc};
5use serde::Serialize;
6use uuid::Uuid;
7
8#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
9pub struct User {
10 pub id: Uuid,
11 pub username: String,
12 pub display_name: String,
13 pub email: Option<String>,
14 pub auth_source: String,
15 pub role: String,
16 pub active: bool,
17 pub created_at: DateTime<Utc>,
18 pub last_login_at: Option<DateTime<Utc>>,
19 pub skin: String,
20}
21
22pub const USER_COLUMNS: &str =
23 "id, username, display_name, email, auth_source, role, active, created_at, last_login_at, skin";
24
25#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
26pub struct ApiToken {
27 pub id: Uuid,
28 pub name: String,
29 pub token_prefix: String,
30 pub created_at: DateTime<Utc>,
31 pub last_used_at: Option<DateTime<Utc>>,
32 pub expires_at: Option<DateTime<Utc>>,
33 pub scope: String,
35 pub idle_days: Option<i32>,
37}
38
39pub const TOKEN_COLUMNS: &str =
40 "id, name, token_prefix, created_at, last_used_at, expires_at, scope, idle_days";
41
42#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
43pub struct Project {
44 pub id: Uuid,
45 pub code: String,
46 pub name: String,
47 pub description: String,
48 pub active: bool,
49 pub created_at: DateTime<Utc>,
50 pub oidc_group: Option<String>,
52 pub member_count: i64,
53 pub created_by: Option<Uuid>,
56 pub number_scheme: Option<String>,
59 pub visibility: String,
61}
62
63impl Project {
64 pub fn is_restricted(&self) -> bool {
65 self.visibility == crate::domain::ProjectVisibility::Restricted.as_str()
66 }
67}
68
69#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
71pub struct ProjectMember {
72 pub user_id: Uuid,
73 pub username: String,
74 pub display_name: String,
75 pub role: String,
76 pub active: bool,
77 pub source: String,
79 pub added_at: DateTime<Utc>,
80 pub manager: bool,
82 pub checkouts: i64,
84}
85
86#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
88pub struct ItemSummary {
89 pub id: Uuid,
90 pub number: String,
91 pub item_type: String,
92 pub unit: String,
93 pub project_code: Option<String>,
94 pub created_at: DateTime<Utc>,
95 pub latest_revision_id: Uuid,
96 pub latest_code: String,
97 pub latest_state: String,
98 pub name: String,
99 pub released_code: Option<String>,
101 pub matched_file: Option<String>,
103 pub classification: String,
105 pub attributes: serde_json::Value,
107}
108
109#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
110pub struct Item {
111 pub id: Uuid,
112 pub number: String,
113 pub item_type: String,
114 pub unit: String,
115 pub project_id: Option<Uuid>,
116 pub project_code: Option<String>,
117 pub created_by: Uuid,
118 pub created_by_name: String,
119 pub created_at: DateTime<Utc>,
120 pub owner_id: Uuid,
122 pub owner_name: String,
123 pub classification: String,
125}
126
127#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
129pub struct AttributeDef {
130 pub id: Uuid,
131 pub key: String,
132 pub label: String,
133 pub data_type: String,
135 pub unit: String,
136 pub choices: Vec<String>,
137 pub item_types: Vec<String>,
139 pub position: i32,
140 pub active: bool,
141 pub created_at: DateTime<Utc>,
142}
143
144#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
146pub struct Revision {
147 pub id: Uuid,
148 pub item_id: Uuid,
149 pub item_number: String,
150 pub item_type: String,
151 pub unit: String,
152 pub project_id: Option<Uuid>,
153 pub project_code: Option<String>,
154 pub seq: i32,
155 pub code: String,
156 pub state: String,
157 pub name: String,
158 pub description: String,
159 pub material: String,
160 pub mass_kg: Option<f64>,
161 pub unit_cost: Option<f64>,
162 pub attributes: serde_json::Value,
164 pub classification: String,
166 pub checked_out_by: Option<Uuid>,
167 pub checked_out_by_name: Option<String>,
168 pub checked_out_at: Option<DateTime<Utc>>,
169 pub created_by: Uuid,
170 pub created_by_name: String,
171 pub created_at: DateTime<Utc>,
172 pub updated_at: DateTime<Utc>,
173 pub released_at: Option<DateTime<Utc>>,
174}
175
176#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
177pub struct FileRecord {
178 pub id: Uuid,
179 pub revision_id: Uuid,
180 pub filename: String,
181 pub content_type: String,
182 pub size_bytes: i64,
183 pub sha256: String,
184 pub uploaded_by: Uuid,
185 pub uploaded_by_name: String,
186 pub uploaded_at: DateTime<Utc>,
187 pub version: i32,
189 pub format: String,
191 pub folder: String,
194 pub retention: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
200 pub solver_summary: Option<serde_json::Value>,
201 #[sqlx(skip)]
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub solver_line: Option<String>,
205 #[sqlx(skip)]
207 pub format_info: Option<crate::cadformat::FormatInfo>,
208}
209
210#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
212pub struct FileVersion {
213 pub id: Uuid,
214 pub file_id: Uuid,
215 pub version: i32,
216 pub sha256: String,
217 pub size_bytes: i64,
218 pub content_type: String,
219 pub comment: String,
220 pub uploaded_by: Uuid,
221 pub uploaded_by_name: String,
222 pub uploaded_at: DateTime<Utc>,
223}
224
225#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
227pub struct BomLine {
228 pub id: Uuid,
229 pub parent_revision_id: Uuid,
230 pub child_item_id: Uuid,
231 pub child_number: String,
232 pub child_type: String,
233 pub child_unit: String,
234 pub find_number: i32,
235 pub quantity: f64,
236 pub notes: String,
237 pub child_latest_revision_id: Uuid,
239 pub child_latest_code: String,
240 pub child_latest_state: String,
241 pub child_name: String,
242 pub released_child_revision_id: Option<Uuid>,
244 pub released_child_code: Option<String>,
245 #[sqlx(default)]
247 #[serde(skip)]
248 pub child_project_id: Option<Uuid>,
249 #[sqlx(default)]
253 pub restricted: bool,
254}
255
256impl BomLine {
257 pub fn mask(&mut self) {
259 let label = crate::services::access::RESTRICTED_LABEL;
260 self.child_item_id = Uuid::nil();
261 self.child_number = label.into();
262 self.child_type = String::new();
263 self.child_unit = String::new();
264 self.child_latest_revision_id = Uuid::nil();
265 self.child_latest_code = String::new();
266 self.child_latest_state = String::new();
267 self.child_name = String::new();
268 self.notes = String::new();
269 self.released_child_revision_id = None;
270 self.released_child_code = None;
271 self.child_project_id = None;
272 self.restricted = true;
273 }
274}
275
276#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
278pub struct WhereUsed {
279 pub line_id: Uuid,
280 pub parent_revision_id: Uuid,
281 pub parent_item_id: Uuid,
282 pub parent_number: String,
283 pub parent_code: String,
284 pub parent_name: String,
285 pub parent_state: String,
286 pub find_number: i32,
287 pub quantity: f64,
288 pub released_child_code: Option<String>,
290}
291
292#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
295pub struct WhereUsedLevel {
296 pub level: i32,
297 pub child_number: String,
299 #[sqlx(flatten)]
300 #[serde(flatten)]
301 pub usage: WhereUsed,
302}
303
304#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
307pub struct ItemReference {
308 pub id: Uuid,
309 pub from_revision_id: Uuid,
310 pub to_item_id: Uuid,
311 pub to_number: String,
312 pub to_type: String,
313 pub kind: String,
314 pub note: String,
315 pub to_latest_revision_id: Uuid,
316 pub to_latest_code: String,
317 pub to_latest_state: String,
318 pub to_name: String,
319 pub released_to_revision_id: Option<Uuid>,
321 pub released_to_code: Option<String>,
322 pub created_at: DateTime<Utc>,
323 #[sqlx(default)]
325 #[serde(skip)]
326 pub to_project_id: Option<Uuid>,
327 #[sqlx(default)]
329 pub restricted: bool,
330}
331
332impl ItemReference {
333 pub fn mask(&mut self) {
336 self.to_item_id = Uuid::nil();
337 self.note = String::new();
338 self.to_number = crate::services::access::RESTRICTED_LABEL.into();
339 self.to_type = String::new();
340 self.to_latest_revision_id = Uuid::nil();
341 self.to_latest_code = String::new();
342 self.to_latest_state = String::new();
343 self.to_name = String::new();
344 self.released_to_revision_id = None;
345 self.released_to_code = None;
346 self.to_project_id = None;
347 self.restricted = true;
348 }
349}
350
351#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
354pub struct Referrer {
355 pub reference_id: Uuid,
356 pub kind: String,
357 pub note: String,
358 pub from_revision_id: Uuid,
359 pub from_item_id: Uuid,
360 pub from_number: String,
361 pub from_code: String,
362 pub from_name: String,
363 pub from_state: String,
364 pub released_to_code: Option<String>,
365}
366
367#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
370pub struct Impact {
371 pub id: Uuid,
372 pub item_id: Uuid,
374 pub item_number: String,
375 pub project_id: Option<Uuid>,
376 pub project_code: Option<String>,
377 pub revision_id: Uuid,
378 pub revision_code: String,
379 pub revision_state: String,
380 pub item_name: String,
381 pub source_item_id: Uuid,
383 pub source_number: String,
384 pub source_revision_id: Uuid,
385 pub source_code: String,
386 pub source_name: String,
387 pub event: String,
389 pub relation: String,
391 pub detail: String,
392 pub change_id: Option<Uuid>,
393 pub change_number: Option<String>,
394 pub created_at: DateTime<Utc>,
395 pub resolved_at: Option<DateTime<Utc>>,
396 pub resolution: Option<String>,
398 pub resolved_by_name: Option<String>,
399 pub note: String,
400 #[sqlx(default)]
402 #[serde(skip)]
403 pub source_project_id: Option<Uuid>,
404 #[sqlx(default)]
408 pub source_restricted: bool,
409}
410
411impl Impact {
412 pub fn hide_change(&mut self) {
414 self.change_id = None;
415 self.change_number = None;
416 }
417
418 pub fn redact(mut self, actor: &crate::auth::Actor) -> Self {
421 if !actor.can_see_project(self.source_project_id) {
422 self.source_item_id = Uuid::nil();
423 self.source_number = crate::services::access::RESTRICTED_LABEL.into();
424 self.source_revision_id = Uuid::nil();
425 self.source_code = String::new();
426 self.source_name = String::new();
427 self.detail = String::new();
428 self.change_id = None;
429 self.change_number = None;
430 self.source_project_id = None;
431 self.source_restricted = true;
432 }
433 self
434 }
435}
436
437#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
438pub struct Change {
439 pub id: Uuid,
440 pub number: String,
441 pub title: String,
442 pub description: String,
443 pub priority: String,
444 pub status: String,
445 pub created_by: Uuid,
446 pub created_by_name: String,
447 pub created_at: DateTime<Utc>,
448 pub updated_at: DateTime<Utc>,
449 pub submitted_at: Option<DateTime<Utc>>,
450 pub closed_at: Option<DateTime<Utc>>,
451}
452
453#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
456pub struct ChangeItem {
457 pub revision_id: Uuid,
458 pub item_id: Uuid,
459 pub item_number: String,
460 pub code: String,
461 pub name: String,
462 pub state: String,
463 pub checked_out_by_name: Option<String>,
464 pub action: String,
467 pub successor_number: Option<String>,
469 #[sqlx(default)]
471 #[serde(skip)]
472 pub successor_project_id: Option<Uuid>,
473 #[sqlx(default)]
475 #[serde(skip)]
476 pub project_id: Option<Uuid>,
477 #[sqlx(default)]
479 pub restricted: bool,
480}
481
482impl ChangeItem {
483 pub fn mask(&mut self) {
485 self.revision_id = Uuid::nil();
486 self.item_id = Uuid::nil();
487 self.item_number = crate::services::access::RESTRICTED_LABEL.into();
488 self.code = String::new();
489 self.name = String::new();
490 self.state = String::new();
491 self.checked_out_by_name = None;
492 self.action = String::new();
493 self.successor_number = None;
494 self.successor_project_id = None;
495 self.project_id = None;
496 self.restricted = true;
497 }
498}
499
500#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
501pub struct ChangeReview {
502 pub reviewer_id: Uuid,
503 pub reviewer_username: String,
504 pub reviewer_name: String,
505 pub decision: String,
506 pub comment: String,
507 pub decided_at: Option<DateTime<Utc>>,
508 #[sqlx(default)]
510 pub sign_off_method: Option<String>,
511 #[sqlx(default)]
513 pub sign_off_meaning: String,
514 #[sqlx(default)]
517 pub absent: Option<String>,
518}
519
520#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
522pub struct ChangeReviewRound {
523 pub round: i32,
524 pub reviewer_id: Uuid,
525 pub reviewer_username: String,
526 pub reviewer_name: String,
527 pub decision: String,
528 pub comment: String,
529 pub decided_at: Option<DateTime<Utc>>,
530 pub closed_at: DateTime<Utc>,
531 #[sqlx(default)]
532 pub sign_off_method: Option<String>,
533 #[sqlx(default)]
534 pub sign_off_meaning: String,
535}
536
537#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
539pub struct ChangeRequest {
540 pub id: Uuid,
541 pub number: String,
542 pub title: String,
543 pub description: String,
544 pub reason: String,
545 pub priority: String,
546 pub status: String,
547 pub requested_by: Uuid,
548 pub requested_by_name: String,
549 pub assignee: Option<Uuid>,
550 pub assignee_name: Option<String>,
551 pub assignee_username: Option<String>,
552 pub decision_comment: String,
553 pub decided_by: Option<Uuid>,
554 pub decided_by_name: Option<String>,
555 pub decided_at: Option<DateTime<Utc>>,
556 pub due_date: Option<chrono::NaiveDate>,
557 pub created_at: DateTime<Utc>,
558 pub updated_at: DateTime<Utc>,
559 pub closed_at: Option<DateTime<Utc>>,
560 pub item_count: i64,
562}
563
564#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
566pub struct RequestItem {
567 pub item_id: Uuid,
568 pub item_number: String,
569 pub item_type: String,
570 pub project_id: Option<Uuid>,
571 pub project_code: Option<String>,
572 pub revision_id: Option<Uuid>,
574 pub revision_code: Option<String>,
575 pub revision_state: Option<String>,
576 pub name: String,
578 pub latest_revision_id: Uuid,
579 pub latest_code: String,
580 pub latest_state: String,
581 pub note: String,
582 #[sqlx(default)]
584 pub restricted: bool,
585}
586
587impl RequestItem {
588 pub fn mask(&mut self) {
590 self.item_id = Uuid::nil();
591 self.item_number = crate::services::access::RESTRICTED_LABEL.into();
592 self.item_type = String::new();
593 self.project_id = None;
594 self.project_code = None;
595 self.revision_id = None;
596 self.revision_code = None;
597 self.revision_state = None;
598 self.name = String::new();
599 self.latest_revision_id = Uuid::nil();
600 self.latest_code = String::new();
601 self.latest_state = String::new();
602 self.note = String::new();
604 self.restricted = true;
605 }
606}
607
608#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
609pub struct RequestComment {
610 pub id: Uuid,
611 pub author_id: Uuid,
612 pub author_name: String,
613 pub body: String,
614 pub created_at: DateTime<Utc>,
615}
616
617#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
618pub struct RequestAttachment {
619 pub id: Uuid,
620 pub request_id: Uuid,
621 pub filename: String,
622 pub content_type: String,
623 pub size_bytes: i64,
624 pub sha256: String,
625 pub uploaded_by: Uuid,
626 pub uploaded_by_name: String,
627 pub uploaded_at: DateTime<Utc>,
628}
629
630#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
631pub struct AuditEntry {
632 pub id: i64,
633 pub at: DateTime<Utc>,
634 pub user_id: Option<Uuid>,
635 pub username: String,
636 pub action: String,
637 pub entity_type: String,
638 pub entity_id: Option<Uuid>,
639 pub summary: String,
640 pub details: serde_json::Value,
641 #[sqlx(default)]
643 #[serde(skip)]
644 pub related_hidden: bool,
645 #[sqlx(default)]
647 #[serde(skip)]
648 pub mentions_hidden: bool,
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::auth::Actor;
655 use crate::domain::{ProjectVisibility, Role};
656 use crate::services::access::RESTRICTED_LABEL;
657
658 const SECRET: &str = "TOPSECRET";
661
662 fn id() -> Uuid {
663 Uuid::from_u128(0x1234_5678)
664 }
665
666 fn actor(hidden: Vec<Uuid>) -> Actor {
667 Actor {
668 id: Uuid::from_u128(1),
669 username: "reader".into(),
670 display_name: "Reader".into(),
671 role: Role::Viewer,
672 skin: String::new(),
673 preferred_cad: String::new(),
674 projects: Vec::new(),
675 hidden_projects: hidden,
676 }
677 }
678
679 fn secret(name: &str) -> String {
680 format!("{SECRET}-{name}")
681 }
682
683 fn bom_line() -> BomLine {
684 BomLine {
685 id: Uuid::from_u128(77),
686 parent_revision_id: Uuid::from_u128(78),
687 child_item_id: id(),
688 child_number: secret("number"),
689 child_type: secret("type"),
690 child_unit: secret("unit"),
691 find_number: 30,
692 quantity: 4.5,
693 notes: secret("notes"),
694 child_latest_revision_id: id(),
695 child_latest_code: secret("code"),
696 child_latest_state: secret("state"),
697 child_name: secret("name"),
698 released_child_revision_id: Some(id()),
699 released_child_code: Some(secret("released")),
700 child_project_id: Some(id()),
701 restricted: false,
702 }
703 }
704
705 fn reference() -> ItemReference {
706 ItemReference {
707 id: Uuid::from_u128(79),
708 from_revision_id: Uuid::from_u128(80),
709 to_item_id: id(),
710 to_number: secret("number"),
711 to_type: secret("type"),
712 kind: "drawing".into(),
713 note: secret("note"),
714 to_latest_revision_id: id(),
715 to_latest_code: secret("code"),
716 to_latest_state: secret("state"),
717 to_name: secret("name"),
718 released_to_revision_id: Some(id()),
719 released_to_code: Some(secret("released")),
720 created_at: Utc::now(),
721 to_project_id: Some(id()),
722 restricted: false,
723 }
724 }
725
726 fn impact(project: Option<Uuid>) -> Impact {
727 Impact {
728 id: Uuid::from_u128(81),
729 item_id: Uuid::from_u128(82),
730 item_number: "DEP-000001".into(),
731 project_id: None,
732 project_code: None,
733 revision_id: Uuid::from_u128(83),
734 revision_code: "A".into(),
735 revision_state: "draft".into(),
736 item_name: "Dependent".into(),
737 source_item_id: id(),
738 source_number: secret("number"),
739 source_revision_id: id(),
740 source_code: secret("code"),
741 source_name: secret("name"),
742 event: "released".into(),
743 relation: "bom".into(),
744 detail: secret("detail"),
745 change_id: Some(id()),
746 change_number: Some(secret("change")),
747 created_at: Utc::now(),
748 resolved_at: None,
749 resolution: None,
750 resolved_by_name: None,
751 note: String::new(),
752 source_project_id: project,
753 source_restricted: false,
754 }
755 }
756
757 fn change_item() -> ChangeItem {
758 ChangeItem {
759 revision_id: id(),
760 item_id: id(),
761 item_number: secret("number"),
762 code: secret("code"),
763 name: secret("name"),
764 state: secret("state"),
765 checked_out_by_name: Some(secret("user")),
766 action: secret("supersede"),
767 successor_number: Some(secret("successor")),
768 successor_project_id: Some(id()),
769 project_id: Some(id()),
770 restricted: false,
771 }
772 }
773
774 fn request_item() -> RequestItem {
775 RequestItem {
776 item_id: id(),
777 item_number: secret("number"),
778 item_type: secret("type"),
779 project_id: Some(id()),
780 project_code: Some(secret("project")),
781 revision_id: Some(id()),
782 revision_code: Some(secret("code")),
783 revision_state: Some(secret("state")),
784 name: secret("name"),
785 latest_revision_id: id(),
786 latest_code: secret("latest"),
787 latest_state: secret("state"),
788 note: secret("note"),
789 restricted: false,
790 }
791 }
792
793 fn json<T: Serialize>(value: &T) -> String {
794 serde_json::to_string(value).unwrap()
795 }
796
797 #[test]
798 fn masked_bom_line_leaks_nothing_but_position_and_quantity() {
799 let mut line = bom_line();
800 assert!(
801 json(&line).contains(SECRET),
802 "the fixture must carry the marker"
803 );
804 line.mask();
805 let out = json(&line);
806 assert!(!out.contains(SECRET), "leaked: {out}");
807 assert!(!out.contains(&id().to_string()), "leaked an id: {out}");
808 assert!(line.restricted);
809 assert_eq!(line.child_number, RESTRICTED_LABEL);
810 assert_eq!(line.child_item_id, Uuid::nil());
811 assert_eq!(line.find_number, 30);
812 assert_eq!(line.quantity, 4.5);
813 assert!(line.child_project_id.is_none());
814 }
815
816 #[test]
817 fn masked_reference_keeps_only_its_kind() {
818 let mut r = reference();
819 r.mask();
820 let out = json(&r);
821 assert!(!out.contains(SECRET), "leaked: {out}");
822 assert!(!out.contains(&id().to_string()), "leaked an id: {out}");
823 assert!(r.restricted);
824 assert_eq!(r.kind, "drawing");
825 assert_eq!(r.to_number, RESTRICTED_LABEL);
826 assert_eq!(r.to_item_id, Uuid::nil());
827 }
828
829 #[test]
830 fn masked_change_item_leaks_nothing() {
831 let mut c = change_item();
832 c.mask();
833 let out = json(&c);
834 assert!(!out.contains(SECRET), "leaked: {out}");
835 assert!(!out.contains(&id().to_string()), "leaked an id: {out}");
836 assert!(c.restricted);
837 assert_eq!(c.item_number, RESTRICTED_LABEL);
838 assert!(c.checked_out_by_name.is_none());
839 }
840
841 #[test]
842 fn masked_request_item_leaks_nothing_including_the_note() {
843 let mut r = request_item();
844 r.mask();
845 let out = json(&r);
846 assert!(!out.contains(SECRET), "leaked: {out}");
847 assert!(!out.contains(&id().to_string()), "leaked an id: {out}");
848 assert!(r.restricted);
849 assert_eq!(r.item_number, RESTRICTED_LABEL);
850 assert!(r.project_code.is_none() && r.revision_id.is_none());
851 }
852
853 #[test]
854 fn masking_twice_changes_nothing() {
855 let mut once = bom_line();
856 once.mask();
857 let mut twice = once.clone();
858 twice.mask();
859 assert_eq!(json(&once), json(&twice));
860 }
861
862 #[test]
863 fn project_ids_never_serialize() {
864 let line = bom_line();
866 let value: serde_json::Value = serde_json::from_str(&json(&line)).unwrap();
867 assert!(value.get("child_project_id").is_none());
868 let r = reference();
869 let value: serde_json::Value = serde_json::from_str(&json(&r)).unwrap();
870 assert!(value.get("to_project_id").is_none());
871 let i = impact(Some(id()));
872 let value: serde_json::Value = serde_json::from_str(&json(&i)).unwrap();
873 assert!(value.get("source_project_id").is_none());
874 }
875
876 #[test]
877 fn impact_of_a_hidden_source_is_redacted() {
878 let hidden = id();
879 let redacted = impact(Some(hidden)).redact(&actor(vec![hidden]));
880 let out = json(&redacted);
881 assert!(!out.contains(SECRET), "leaked: {out}");
882 assert!(redacted.source_restricted);
883 assert_eq!(redacted.source_number, RESTRICTED_LABEL);
884 assert!(redacted.change_id.is_none() && redacted.change_number.is_none());
885 assert_eq!(redacted.item_number, "DEP-000001");
887 assert_eq!(redacted.event, "released");
888 assert_eq!(redacted.relation, "bom");
889 }
890
891 #[test]
892 fn impact_of_a_visible_source_is_unchanged() {
893 let other = Uuid::from_u128(999);
894 for project in [None, Some(id())] {
895 let before = impact(project);
896 let after = before.clone().redact(&actor(vec![other]));
897 assert_eq!(json(&before), json(&after));
898 assert!(!after.source_restricted);
899 }
900 }
901
902 #[test]
903 fn hide_change_only_removes_the_change() {
904 let mut i = impact(None);
905 i.hide_change();
906 assert!(i.change_id.is_none() && i.change_number.is_none());
907 assert!(
908 json(&i).contains(&secret("number")),
909 "the source stays visible"
910 );
911 assert!(!i.source_restricted);
912 }
913
914 #[test]
915 fn project_restriction_follows_the_visibility_string() {
916 let mut p = Project {
917 id: id(),
918 code: "P".into(),
919 name: "P".into(),
920 description: String::new(),
921 active: true,
922 created_at: Utc::now(),
923 oidc_group: None,
924 member_count: 0,
925 created_by: None,
926 number_scheme: None,
927 visibility: ProjectVisibility::Internal.as_str().into(),
928 };
929 assert!(!p.is_restricted());
930 p.visibility = ProjectVisibility::Restricted.as_str().into();
931 assert!(p.is_restricted());
932 p.visibility = "Restricted".into();
933 assert!(!p.is_restricted(), "the comparison is exact");
934 }
935}