Coverage Report

Created: 2026-03-25 23:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/tenet/tenet/tenet-aws/src/services/quality_attribute.rs
Line
Count
Source
1
//! Quality attribute service for business logic operations
2
//!
3
//! Handles quality attribute management with authorization and validation.
4
5
use std::sync::Arc;
6
7
use crate::{
8
    ApiError,
9
    db::{
10
        TenetDatabase,
11
        project::ProjectRepository,
12
        quality_attribute::QualityAttributeRepository,
13
        quality_component_link::QualityComponentLinkRepository,
14
        traits::{
15
            ProjectRepositoryTrait, QualityAttributeRepositoryTrait,
16
            QualityComponentLinkRepositoryTrait,
17
        },
18
    },
19
    models::{
20
        organisation::OrganisationId,
21
        profile::UserId,
22
        project::ProjectId,
23
        quality_attribute::{
24
            CreateQualityAttributeRequest, QualityAttribute, QualityAttributeId,
25
            UpdateQualityAttributeRequest,
26
        },
27
    },
28
    services::unified_component::UnifiedComponentService,
29
};
30
31
const MAX_QUALITY_ATTRIBUTES: usize = 10;
32
33
/// Service for quality attribute business logic
34
#[derive(Clone)]
35
pub struct QualityAttributeService {
36
    #[cfg(not(test))]
37
    unified: UnifiedComponentService,
38
    #[cfg(test)]
39
    unified: Option<UnifiedComponentService>,
40
    attribute_repo: Arc<dyn QualityAttributeRepositoryTrait>,
41
    link_repo: Arc<dyn QualityComponentLinkRepositoryTrait>,
42
    project_repo: Arc<dyn ProjectRepositoryTrait>,
43
}
44
45
impl QualityAttributeService {
46
    /// Create a new QualityAttributeService
47
6
    pub fn new(database: TenetDatabase) -> Self {
48
6
        Self {
49
6
            #[cfg(not(test))]
50
6
            unified: UnifiedComponentService::new(database.clone()),
51
6
            #[cfg(test)]
52
6
            unified: 
Some(0
UnifiedComponentService::new0
(database.clone())),
53
6
            attribute_repo: Arc::new(QualityAttributeRepository::new(database.clone())),
54
6
            link_repo: Arc::new(QualityComponentLinkRepository::new(database.clone())),
55
6
            project_repo: Arc::new(ProjectRepository::new(database.clone())),
56
6
        }
57
6
    }
58
59
    #[cfg(test)]
60
31
    pub fn new_with_repos(
61
31
        attribute_repo: Arc<dyn QualityAttributeRepositoryTrait>,
62
31
        link_repo: Arc<dyn QualityComponentLinkRepositoryTrait>,
63
31
        project_repo: Arc<dyn ProjectRepositoryTrait>,
64
31
    ) -> Self {
65
31
        Self {
66
31
            unified: None,
67
31
            attribute_repo,
68
31
            link_repo,
69
31
            project_repo,
70
31
        }
71
31
    }
72
73
    /// Verify project exists
74
25
    async fn verify_project_exists(
75
25
        &self,
76
25
        org_id: &OrganisationId,
77
25
        project_id: &ProjectId,
78
25
    ) -> Result<(), ApiError> {
79
25
        if !self.project_repo.project_exists(org_id, project_id).await
?0
{
80
0
            return Err(ApiError::NotFound(format!(
81
0
                "Project {} not found",
82
0
                project_id
83
0
            )));
84
25
        }
85
25
        Ok(())
86
25
    }
87
88
23
    fn validate_priority(priority: i32) -> Result<(), ApiError> {
89
23
        if !(1..=10).contains(&priority) {
90
1
            return Err(ApiError::InvalidRequest(
91
1
                "Priority must be between 1 and 10".to_string(),
92
1
            ));
93
22
        }
94
22
        Ok(())
95
23
    }
96
97
69
    fn normalize_name(name: &str) -> String {
98
69
        name.trim().to_lowercase()
99
69
    }
100
101
18
    fn normalize_optional_string(value: Option<String>) -> Option<String> {
102
18
        value.and_then(|value| 
{2
103
2
            let trimmed = value.trim();
104
2
            if trimmed.is_empty() {
105
0
                None
106
            } else {
107
2
                Some(trimmed.to_string())
108
            }
109
2
        })
110
18
    }
111
112
2
    fn normalize_optional_string_for_update(value: Option<String>) -> Option<String> {
113
2
        value.map(|value| 
{1
114
1
            let trimmed = value.trim();
115
1
            if trimmed.is_empty() {
116
1
                String::new()
117
            } else {
118
0
                trimmed.to_string()
119
            }
120
1
        })
121
2
    }
122
123
19
    fn normalize_constraints(value: Option<Vec<String>>) -> Option<Vec<String>> {
124
19
        value
125
19
            .map(|constraints| 
{3
126
3
                constraints
127
3
                    .into_iter()
128
4
                    .
map3
(|item| item.trim().to_string())
129
4
                    .
filter3
(|item| !item.is_empty())
130
3
                    .collect::<Vec<String>>()
131
3
            })
132
19
            .filter(|constraints| !
constraints3
.
is_empty3
())
133
19
    }
134
135
    // =========================================================================
136
    // Quality Attribute Operations
137
    // =========================================================================
138
139
    /// Create a new quality attribute
140
20
    pub async fn create_quality_attribute(
141
20
        &self,
142
20
        org_id: &OrganisationId,
143
20
        project_id: &ProjectId,
144
20
        _user_id: &UserId,
145
20
        request: &CreateQualityAttributeRequest,
146
20
    ) -> Result<QualityAttribute, ApiError> {
147
20
        self.verify_project_exists(org_id, project_id).await
?0
;
148
149
20
        let name = request.name.trim();
150
20
        if name.is_empty() {
151
0
            return Err(ApiError::InvalidRequest(
152
0
                "Quality attribute name cannot be empty".to_string(),
153
0
            ));
154
20
        }
155
20
        Self::validate_priority(request.priority)
?0
;
156
157
20
        let existing = self.attribute_repo.list_by_project(project_id).await
?0
;
158
20
        if existing.len() >= MAX_QUALITY_ATTRIBUTES {
159
1
            return Err(ApiError::InvalidRequest(format!(
160
1
                "Projects can have at most {} quality attributes",
161
1
                MAX_QUALITY_ATTRIBUTES
162
1
            )));
163
19
        }
164
165
19
        let normalized_name = Self::normalize_name(name);
166
19
        if existing
167
19
            .iter()
168
48
            .
any19
(|attr| Self::normalize_name(&attr.name) == normalized_name)
169
        {
170
1
            return Err(ApiError::InvalidRequest(format!(
171
1
                "Quality attribute '{}' already exists",
172
1
                name
173
1
            )));
174
18
        }
175
18
        let attribute = QualityAttribute::new(
176
18
            org_id.clone(),
177
18
            project_id.clone(),
178
18
            name.to_string(),
179
18
            request.priority,
180
18
            Self::normalize_optional_string(request.description.clone()),
181
18
            Self::normalize_constraints(request.constraints.clone()),
182
        );
183
184
18
        let request = CreateQualityAttributeRequest {
185
18
            name: attribute.name.clone(),
186
18
            priority: attribute.priority,
187
18
            description: attribute.description.clone(),
188
18
            constraints: attribute.constraints.clone(),
189
18
        };
190
        #[cfg(not(test))]
191
        {
192
0
            return self
193
0
                .unified
194
0
                .create_quality_attribute(org_id, project_id, _user_id, &request)
195
0
                .await;
196
        }
197
        #[cfg(test)]
198
        {
199
18
            if let Some(
unified0
) = &self.unified {
200
0
                return unified
201
0
                    .create_quality_attribute(org_id, project_id, _user_id, &request)
202
0
                    .await;
203
18
            }
204
205
18
            self.attribute_repo.create(&attribute).await
?0
;
206
207
18
            Ok(attribute)
208
        }
209
20
    }
210
211
    /// Get a quality attribute by ID
212
2
    pub async fn get_quality_attribute(
213
2
        &self,
214
2
        org_id: &OrganisationId,
215
2
        project_id: &ProjectId,
216
2
        quality_attribute_id: &QualityAttributeId,
217
2
        _user_id: &UserId,
218
2
    ) -> Result<QualityAttribute, ApiError> {
219
        #[cfg(not(test))]
220
        {
221
0
            return self
222
0
                .unified
223
0
                .get_quality_attribute(org_id, project_id, quality_attribute_id)
224
0
                .await;
225
        }
226
        #[cfg(test)]
227
        {
228
2
            if let Some(
unified0
) = &self.unified {
229
0
                return unified
230
0
                    .get_quality_attribute(org_id, project_id, quality_attribute_id)
231
0
                    .await;
232
2
            }
233
234
2
            self.attribute_repo
235
2
                .get(org_id, project_id, quality_attribute_id)
236
2
                .await
?0
237
2
                .ok_or_else(|| 
{1
238
1
                    ApiError::NotFound(format!(
239
1
                        "Quality attribute {} not found",
240
1
                        quality_attribute_id
241
1
                    ))
242
1
                })
243
        }
244
2
    }
245
246
    /// List all quality attributes for a project
247
2
    pub async fn list_quality_attributes(
248
2
        &self,
249
2
        org_id: &OrganisationId,
250
2
        project_id: &ProjectId,
251
2
        _user_id: &UserId,
252
2
    ) -> Result<Vec<QualityAttribute>, ApiError> {
253
        #[cfg(not(test))]
254
        {
255
0
            return self
256
0
                .unified
257
0
                .list_quality_attributes(org_id, project_id)
258
0
                .await;
259
        }
260
        #[cfg(test)]
261
        {
262
2
            if let Some(
unified0
) = &self.unified {
263
0
                return unified.list_quality_attributes(org_id, project_id).await;
264
2
            }
265
2
            self.verify_project_exists(org_id, project_id).await
?0
;
266
267
2
            self.attribute_repo.list_by_project(project_id).await
268
        }
269
2
    }
270
271
    /// Update a quality attribute
272
3
    pub async fn update_quality_attribute(
273
3
        &self,
274
3
        org_id: &OrganisationId,
275
3
        project_id: &ProjectId,
276
3
        quality_attribute_id: &QualityAttributeId,
277
3
        _user_id: &UserId,
278
3
        request: UpdateQualityAttributeRequest,
279
3
    ) -> Result<QualityAttribute, ApiError> {
280
3
        self.verify_project_exists(org_id, project_id).await
?0
;
281
282
3
        let existing = self
283
3
            .attribute_repo
284
3
            .get(org_id, project_id, quality_attribute_id)
285
3
            .await
?0
286
3
            .ok_or_else(|| 
{0
287
0
                ApiError::NotFound(format!(
288
0
                    "Quality attribute {} not found",
289
0
                    quality_attribute_id
290
0
                ))
291
0
            })?;
292
293
3
        if let Some(priority) = request.priority {
294
3
            Self::validate_priority(priority)
?1
;
295
0
        }
296
297
2
        let mut other_attributes = self.attribute_repo.list_by_project(project_id).await
?0
;
298
4
        
other_attributes2
.
retain2
(|attr| attr.id != existing.id);
299
300
2
        if let Some(
ref name1
) = request.name {
301
1
            let trimmed = name.trim();
302
1
            if trimmed.is_empty() {
303
0
                return Err(ApiError::InvalidRequest(
304
0
                    "Quality attribute name cannot be empty".to_string(),
305
0
                ));
306
1
            }
307
1
            let normalized = Self::normalize_name(trimmed);
308
1
            if other_attributes
309
1
                .iter()
310
1
                .any(|attr| Self::normalize_name(&attr.name) == normalized)
311
            {
312
0
                return Err(ApiError::InvalidRequest(format!(
313
0
                    "Quality attribute '{}' already exists",
314
0
                    trimmed
315
0
                )));
316
1
            }
317
1
        }
318
319
2
        let normalized_constraints = request
320
2
            .constraints
321
2
            .map(|constraints| 
Self::normalize_constraints1
(
Some(constraints)1
).
unwrap_or_default1
());
322
323
2
        let update_request = UpdateQualityAttributeRequest {
324
2
            name: request.name.map(|name| 
name.trim()1
.
to_string1
()),
325
2
            priority: request.priority,
326
2
            description: Self::normalize_optional_string_for_update(request.description),
327
2
            constraints: normalized_constraints,
328
        };
329
330
        #[cfg(not(test))]
331
        {
332
0
            return self
333
0
                .unified
334
0
                .update_quality_attribute(
335
0
                    org_id,
336
0
                    project_id,
337
0
                    _user_id,
338
0
                    quality_attribute_id,
339
0
                    update_request,
340
0
                )
341
0
                .await;
342
        }
343
        #[cfg(test)]
344
        {
345
2
            if let Some(
unified0
) = &self.unified {
346
0
                return unified
347
0
                    .update_quality_attribute(
348
0
                        org_id,
349
0
                        project_id,
350
0
                        _user_id,
351
0
                        quality_attribute_id,
352
0
                        update_request,
353
0
                    )
354
0
                    .await;
355
2
            }
356
357
2
            self.attribute_repo
358
2
                .update(org_id, project_id, quality_attribute_id, update_request)
359
2
                .await
?0
360
2
                .ok_or_else(|| 
{0
361
0
                    ApiError::NotFound(format!(
362
0
                        "Quality attribute {} not found",
363
0
                        quality_attribute_id
364
0
                    ))
365
0
                })
366
        }
367
3
    }
368
369
    /// Delete a quality attribute (and its links)
370
1
    pub async fn delete_quality_attribute(
371
1
        &self,
372
1
        org_id: &OrganisationId,
373
1
        project_id: &ProjectId,
374
1
        quality_attribute_id: &QualityAttributeId,
375
1
        _user_id: &UserId,
376
1
    ) -> Result<(), ApiError> {
377
1
        self.attribute_repo
378
1
            .get(org_id, project_id, quality_attribute_id)
379
1
            .await
?0
380
1
            .ok_or_else(|| 
{0
381
0
                ApiError::NotFound(format!(
382
0
                    "Quality attribute {} not found",
383
0
                    quality_attribute_id
384
0
                ))
385
0
            })?;
386
387
1
        self.link_repo
388
1
            .delete_all_for_quality(quality_attribute_id)
389
1
            .await
?0
;
390
        #[cfg(not(test))]
391
        {
392
0
            return self
393
0
                .unified
394
0
                .delete_quality_attribute(org_id, project_id, _user_id, quality_attribute_id)
395
0
                .await;
396
        }
397
        #[cfg(test)]
398
        {
399
1
            if let Some(
unified0
) = &self.unified {
400
0
                return unified
401
0
                    .delete_quality_attribute(org_id, project_id, _user_id, quality_attribute_id)
402
0
                    .await;
403
1
            }
404
405
1
            self.attribute_repo
406
1
                .delete(org_id, project_id, quality_attribute_id)
407
1
                .await
?0
;
408
409
1
            Ok(())
410
        }
411
1
    }
412
}
413
414
// =============================================================================
415
// Tests
416
// =============================================================================
417
418
#[cfg(test)]
419
mod tests {
420
    use super::*;
421
    use crate::db::in_memory::{
422
        InMemoryMemberRepository, InMemoryOrganisationRepository, InMemoryProjectRepository,
423
        InMemoryQualityAttributeRepository, InMemoryQualityComponentLinkRepository,
424
    };
425
    use crate::db::traits::QualityComponentLinkRepositoryTrait;
426
    use crate::models::{
427
        component::ComponentId, organisation::Organisation, project::Project,
428
        quality_component_link::QualityComponentLink,
429
        quality_component_link::QualityLinkComponentType, role::Role,
430
    };
431
    use crate::test_support::{
432
        fixtures::{add_member, seed_org_project_with_admin},
433
        must_ok,
434
    };
435
    use std::sync::Arc;
436
437
5
    fn setup_service(
438
5
        _org_repo: InMemoryOrganisationRepository,
439
5
        _member_repo: InMemoryMemberRepository,
440
5
        project_repo: InMemoryProjectRepository,
441
5
        attribute_repo: InMemoryQualityAttributeRepository,
442
5
        link_repo: InMemoryQualityComponentLinkRepository,
443
5
    ) -> QualityAttributeService {
444
5
        QualityAttributeService::new_with_repos(
445
5
            Arc::new(attribute_repo),
446
5
            Arc::new(link_repo),
447
5
            Arc::new(project_repo),
448
        )
449
5
    }
450
451
5
    fn seed_org_and_project(
452
5
        org_repo: &InMemoryOrganisationRepository,
453
5
        member_repo: &InMemoryMemberRepository,
454
5
        project_repo: &InMemoryProjectRepository,
455
5
    ) -> (Organisation, Project, UserId) {
456
5
        let seeded = seed_org_project_with_admin(org_repo, member_repo, project_repo);
457
5
        (seeded.org, seeded.project, seeded.admin_user)
458
5
    }
459
460
    #[allure_rs::allure_parent_suite("tenet-aws")]
461
    #[allure_rs::allure_test]
462
    #[tokio::test]
463
    async fn test_create_and_list_quality_attributes() {
464
        let org_repo = InMemoryOrganisationRepository::default();
465
        let member_repo = InMemoryMemberRepository::default();
466
        let project_repo = InMemoryProjectRepository::default();
467
        let attribute_repo = InMemoryQualityAttributeRepository::default();
468
        let link_repo = InMemoryQualityComponentLinkRepository::default();
469
470
        let service = setup_service(
471
            org_repo.clone(),
472
            member_repo.clone(),
473
            project_repo.clone(),
474
            attribute_repo,
475
            link_repo,
476
        );
477
478
        let (org, project, user_id) = seed_org_and_project(&org_repo, &member_repo, &project_repo);
479
480
        let request = CreateQualityAttributeRequest {
481
            name: "Security".to_string(),
482
            priority: 1,
483
            description: Some("Protect data".to_string()),
484
            constraints: Some(vec!["No secrets in logs".to_string()]),
485
        };
486
487
        let created = must_ok(
488
            service
489
                .create_quality_attribute(&org.id, &project.id, &user_id, &request)
490
                .await,
491
        );
492
        assert_eq!(created.name, "Security");
493
494
        let list = must_ok(
495
            service
496
                .list_quality_attributes(&org.id, &project.id, &user_id)
497
                .await,
498
        );
499
        assert_eq!(list.len(), 1);
500
        assert_eq!(list[0].id, created.id);
501
    }
502
503
    #[allure_rs::allure_parent_suite("tenet-aws")]
504
    #[allure_rs::allure_test]
505
    #[tokio::test]
506
    async fn test_quality_attribute_unique_name_allows_duplicate_priority() {
507
        let org_repo = InMemoryOrganisationRepository::default();
508
        let member_repo = InMemoryMemberRepository::default();
509
        let project_repo = InMemoryProjectRepository::default();
510
        let attribute_repo = InMemoryQualityAttributeRepository::default();
511
        let link_repo = InMemoryQualityComponentLinkRepository::default();
512
513
        let service = setup_service(
514
            org_repo.clone(),
515
            member_repo.clone(),
516
            project_repo.clone(),
517
            attribute_repo,
518
            link_repo,
519
        );
520
521
        let (org, project, user_id) = seed_org_and_project(&org_repo, &member_repo, &project_repo);
522
523
        let base_request = CreateQualityAttributeRequest {
524
            name: "Security".to_string(),
525
            priority: 1,
526
            description: None,
527
            constraints: None,
528
        };
529
530
        must_ok(
531
            service
532
                .create_quality_attribute(&org.id, &project.id, &user_id, &base_request)
533
                .await,
534
        );
535
536
        let duplicate_name = CreateQualityAttributeRequest {
537
            name: "security".to_string(),
538
            priority: 2,
539
            description: None,
540
            constraints: None,
541
        };
542
        let result = service
543
            .create_quality_attribute(&org.id, &project.id, &user_id, &duplicate_name)
544
            .await;
545
        assert!(matches!(result, Err(ApiError::InvalidRequest(_))));
546
547
        let duplicate_priority = CreateQualityAttributeRequest {
548
            name: "Performance".to_string(),
549
            priority: 1,
550
            description: None,
551
            constraints: None,
552
        };
553
        let result = service
554
            .create_quality_attribute(&org.id, &project.id, &user_id, &duplicate_priority)
555
            .await;
556
        assert!(result.is_ok(), "duplicate priorities should be allowed");
557
    }
558
559
    #[allure_rs::allure_parent_suite("tenet-aws")]
560
    #[allure_rs::allure_test]
561
    #[tokio::test]
562
    async fn test_quality_attribute_limit() {
563
        let org_repo = InMemoryOrganisationRepository::default();
564
        let member_repo = InMemoryMemberRepository::default();
565
        let project_repo = InMemoryProjectRepository::default();
566
        let attribute_repo = InMemoryQualityAttributeRepository::default();
567
        let link_repo = InMemoryQualityComponentLinkRepository::default();
568
569
        let service = setup_service(
570
            org_repo.clone(),
571
            member_repo.clone(),
572
            project_repo.clone(),
573
            attribute_repo,
574
            link_repo,
575
        );
576
577
        let (org, project, user_id) = seed_org_and_project(&org_repo, &member_repo, &project_repo);
578
579
        for idx in 1..=10 {
580
            let request = CreateQualityAttributeRequest {
581
                name: format!("Quality {}", idx),
582
                priority: idx,
583
                description: None,
584
                constraints: None,
585
            };
586
            must_ok(
587
                service
588
                    .create_quality_attribute(&org.id, &project.id, &user_id, &request)
589
                    .await,
590
            );
591
        }
592
593
        let extra = CreateQualityAttributeRequest {
594
            name: "Extra".to_string(),
595
            priority: 10,
596
            description: None,
597
            constraints: None,
598
        };
599
        let result = service
600
            .create_quality_attribute(&org.id, &project.id, &user_id, &extra)
601
            .await;
602
        assert!(matches!(result, Err(ApiError::InvalidRequest(_))));
603
    }
604
605
    #[allure_rs::allure_test]
606
    #[tokio::test]
607
1
    async fn test_get_update_and_validation_paths() {
608
1
        let org_repo = InMemoryOrganisationRepository::default();
609
1
        let member_repo = InMemoryMemberRepository::default();
610
1
        let project_repo = InMemoryProjectRepository::default();
611
1
        let attribute_repo = InMemoryQualityAttributeRepository::default();
612
1
        let link_repo = InMemoryQualityComponentLinkRepository::default();
613
614
1
        let service = setup_service(
615
1
            org_repo.clone(),
616
1
            member_repo.clone(),
617
1
            project_repo.clone(),
618
1
            attribute_repo,
619
1
            link_repo,
620
        );
621
622
1
        let (org, project, user_id) = seed_org_and_project(&org_repo, &member_repo, &project_repo);
623
1
        let first = must_ok(
624
1
            service
625
1
                .create_quality_attribute(
626
1
                    &org.id,
627
1
                    &project.id,
628
1
                    &user_id,
629
1
                    &CreateQualityAttributeRequest {
630
1
                        name: "Security".to_string(),
631
1
                        priority: 1,
632
1
                        description: Some("Protect data".to_string()),
633
1
                        constraints: Some(vec!["encrypted".to_string()]),
634
1
                    },
635
1
                )
636
1
                .await,
637
        );
638
1
        let second = must_ok(
639
1
            service
640
1
                .create_quality_attribute(
641
1
                    &org.id,
642
1
                    &project.id,
643
1
                    &user_id,
644
1
                    &CreateQualityAttributeRequest {
645
1
                        name: "Performance".to_string(),
646
1
                        priority: 2,
647
1
                        description: None,
648
1
                        constraints: None,
649
1
                    },
650
1
                )
651
1
                .await,
652
        );
653
654
1
        let fetched = must_ok(
655
1
            service
656
1
                .get_quality_attribute(&org.id, &project.id, &first.id, &user_id)
657
1
                .await,
658
        );
659
1
        assert_eq!(fetched.id, first.id);
660
661
1
        let updated = must_ok(
662
1
            service
663
1
                .update_quality_attribute(
664
1
                    &org.id,
665
1
                    &project.id,
666
1
                    &first.id,
667
1
                    &user_id,
668
1
                    UpdateQualityAttributeRequest {
669
1
                        name: Some("Security Updated".to_string()),
670
1
                        priority: Some(3),
671
1
                        description: Some("".to_string()),
672
1
                        constraints: Some(vec![" ".to_string(), "auditable".to_string()]),
673
1
                    },
674
1
                )
675
1
                .await,
676
        );
677
1
        assert_eq!(updated.name, "Security Updated");
678
1
        assert_eq!(updated.priority, 3);
679
1
        assert_eq!(updated.description, None);
680
1
        assert_eq!(updated.constraints, Some(vec!["auditable".to_string()]));
681
682
1
        let duplicate_priority = service
683
1
            .update_quality_attribute(
684
1
                &org.id,
685
1
                &project.id,
686
1
                &first.id,
687
1
                &user_id,
688
1
                UpdateQualityAttributeRequest {
689
1
                    name: None,
690
1
                    priority: Some(second.priority),
691
1
                    description: None,
692
1
                    constraints: None,
693
1
                },
694
1
            )
695
1
            .await;
696
1
        assert!(
697
1
            duplicate_priority.is_ok(),
698
            "updating to an existing priority should be allowed"
699
        );
700
701
1
        let invalid_priority = service
702
1
            .update_quality_attribute(
703
1
                &org.id,
704
1
                &project.id,
705
1
                &first.id,
706
1
                &user_id,
707
1
                UpdateQualityAttributeRequest {
708
1
                    name: None,
709
1
                    priority: Some(11),
710
1
                    description: None,
711
1
                    constraints: None,
712
1
                },
713
1
            )
714
1
            .await;
715
1
        assert!(
matches!0
(invalid_priority, Err(ApiError::InvalidRequest(_))));
716
    }
717
718
    #[allure_rs::allure_test]
719
    #[tokio::test]
720
1
    async fn test_delete_quality_attribute_auth_is_enforced_at_boundary_and_cleans_links() {
721
1
        let org_repo = InMemoryOrganisationRepository::default();
722
1
        let member_repo = InMemoryMemberRepository::default();
723
1
        let project_repo = InMemoryProjectRepository::default();
724
1
        let attribute_repo = InMemoryQualityAttributeRepository::default();
725
1
        let link_repo = InMemoryQualityComponentLinkRepository::default();
726
727
1
        let service = setup_service(
728
1
            org_repo.clone(),
729
1
            member_repo.clone(),
730
1
            project_repo.clone(),
731
1
            attribute_repo,
732
1
            link_repo.clone(),
733
        );
734
735
1
        let (org, project, admin_user) =
736
1
            seed_org_and_project(&org_repo, &member_repo, &project_repo);
737
1
        let member_user = add_member(
738
1
            &member_repo,
739
1
            &org.id,
740
1
            "550e8400-e29b-41d4-a716-446655440001",
741
1
            Role::User,
742
1
            Some(admin_user.clone()),
743
        );
744
745
1
        let attribute = must_ok(
746
1
            service
747
1
                .create_quality_attribute(
748
1
                    &org.id,
749
1
                    &project.id,
750
1
                    &admin_user,
751
1
                    &CreateQualityAttributeRequest {
752
1
                        name: "Maintainability".to_string(),
753
1
                        priority: 4,
754
1
                        description: None,
755
1
                        constraints: None,
756
1
                    },
757
1
                )
758
1
                .await,
759
        );
760
761
1
        let link = QualityComponentLink::new(
762
1
            org.id.clone(),
763
1
            project.id.clone(),
764
1
            attribute.id.clone(),
765
1
            ComponentId::new(),
766
1
            QualityLinkComponentType::Container,
767
1
            None,
768
1
            Some("linked".to_string()),
769
        );
770
1
        must_ok(link_repo.create(&link).await);
771
1
        assert_eq!(
772
1
            must_ok(link_repo.list_by_quality(&attribute.id).await).len(),
773
            1
774
        );
775
776
1
        let deleted = service
777
1
            .delete_quality_attribute(&org.id, &project.id, &attribute.id, &member_user)
778
1
            .await;
779
1
        assert!(deleted.is_ok());
780
781
1
        assert!(
782
1
            service
783
1
                .get_quality_attribute(&org.id, &project.id, &attribute.id, &member_user)
784
1
                .await
785
1
                .is_err()
786
        );
787
1
        assert!(must_ok(link_repo.list_by_quality(&attribute.id).await).is_empty());
788
    }
789
}