• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DataBiosphere / consent / #5745

25 Apr 2025 02:58PM UTC coverage: 79.365% (+0.3%) from 79.087%
#5745

push

web-flow
[DT-1167]Make Progress Reports Submittable (#2496)

Co-authored-by: rjohanek <rjohanek@broadinstitute.org>

93 of 93 new or added lines in 2 files covered. (100.0%)

5 existing lines in 2 files now uncovered.

10346 of 13036 relevant lines covered (79.36%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.83
/src/main/java/org/broadinstitute/consent/http/models/DataAccessRequestData.java
1
package org.broadinstitute.consent.http.models;
2

3
import com.fasterxml.jackson.annotation.JsonInclude;
4
import com.fasterxml.jackson.annotation.JsonInclude.Include;
5
import com.google.gson.Gson;
6
import com.google.gson.annotations.SerializedName;
7
import java.util.Arrays;
8
import java.util.Collections;
9
import java.util.List;
10
import java.util.Objects;
11
import java.util.stream.Collectors;
12
import java.util.stream.Stream;
13

14
@JsonInclude(Include.NON_NULL)
15
public class DataAccessRequestData {
1✔
16

17
  /**
18
   * These properties are deprecated and should no longer be used. In many cases, they represent
19
   * user properties, consent related properties, deprecated properties, or duplicate existing DAR
20
   * fields. See <a href="https://broadworkbench.atlassian.net/browse/DUOS-728">DUOS-728</a> for
21
   * more info.
22
   */
23
  public static final List<String> DEPRECATED_PROPS = Arrays
1✔
24
      .asList("referenceId", "investigator",
1✔
25
          "institution", "department", "address1", "address2", "city", "zipcode", "zipCode",
26
          "state", "country", "researcher", "userId", "isThePi", "havePi",
27
          "profileName", "pubmedId", "scientificUrl", "eraExpiration", "academicEmail",
28
          "eraAuthorized", "nihUsername", "linkedIn", "orcid", "researcherGate", "datasetDetail",
29
          "datasets", "datasetId", "validRestriction", "restriction", "translatedUseRestriction",
30
          "createDate", "sortDate", "additionalEmail", "checkNotifications", "partialDarCode");
31

32
  @Deprecated
33
  private String referenceId;
34
  private String projectTitle;
35
  private Boolean checkNihDataOnly;
36
  private String rus;
37
  @SerializedName(value = "nonTechRus", alternate = "non_tech_rus")
38
  private String nonTechRus;
39
  private Boolean diseases;
40
  private Boolean methods;
41
  private Boolean controls;
42
  private Boolean population;
43
  private Boolean other;
44
  private String otherText;
45
  private List<OntologyEntry> ontologies;
46
  private Boolean forProfit;
47
  @SerializedName(value = "oneGender", alternate = "onegender")
48
  private Boolean oneGender;
49
  private String gender;
50
  private Boolean pediatric;
51
  @SerializedName(value = "illegalBehavior", alternate = "illegalbehave")
52
  private Boolean illegalBehavior;
53
  private Boolean addiction;
54
  @SerializedName(value = "sexualDiseases", alternate = "sexualdiseases")
55
  private Boolean sexualDiseases;
56
  @SerializedName(value = "stigmatizedDiseases", alternate = "stigmatizediseases")
57
  private Boolean stigmatizedDiseases;
58
  @SerializedName(value = "vulnerablePopulation", alternate = "vulnerablepop")
59
  private Boolean vulnerablePopulation;
60
  @SerializedName(value = "populationMigration", alternate = "popmigration")
61
  private Boolean populationMigration;
62
  @SerializedName(value = "psychiatricTraits", alternate = "psychtraits")
63
  private Boolean psychiatricTraits;
64
  @SerializedName(value = "notHealth", alternate = "nothealth")
65
  private Boolean notHealth;
66
  private Boolean hmb;
67
  private String status;
68
  private Boolean poa;
69
  private List<DatasetEntry> datasets;
70
  @SerializedName(value = "darCode", alternate = "dar_code")
71
  private String darCode;
72
  private Object restriction;
73
  @SerializedName(value = "validRestriction", alternate = "valid_restriction")
74
  private Boolean validRestriction;
75
  @Deprecated
76
  private Long createDate;
77
  @Deprecated
78
  private Long sortDate;
79
  @Deprecated
80
  @SerializedName(value = "datasetIds", alternate = {"datasetId", "datasetid"})
81
  private List<Integer> datasetIds;
82

83
  // Progress Report/Closeout Fields
84
  private String progressReportSummary;
85
  private String intellectualPropertySummary;
86
  private List<Publication> publications;
87
  private List<Presentation> presentations;
88
  private DataManagementIncident dmi;
89
  private String researchPlans;
90
  private CloseoutSupplement closeoutSupplement;
91

92
  private Boolean anvilUse;
93
  private Boolean cloudUse;
94
  private Boolean localUse;
95
  private String cloudProvider;
96
  private String cloudProviderType;
97
  private String cloudProviderDescription;
98
  private Boolean geneticStudiesOnly;
99
  private Boolean irb;
100
  private String irbDocumentLocation;
101
  private String irbDocumentName;
102
  private String irbProtocolExpiration;
103
  private String itDirector;
104
  private String itDirectorEmail;
105
  private String signingOfficial;
106
  private String signingOfficialEmail;
107
  private Boolean publication;
108
  private Boolean collaboration;
109
  private String collaborationLetterLocation;
110
  private String collaborationLetterName;
111
  private Boolean forensicActivities;
112
  private Boolean sharingDistribution;
113
  private List<Collaborator> labCollaborators;
114
  private List<Collaborator> internalCollaborators;
115
  private List<Collaborator> externalCollaborators;
116
  private Boolean dsAcknowledgement;
117
  private Boolean gsoAcknowledgement;
118
  private Boolean pubAcknowledgement;
119
  private String piName;
120
  private String piEmail;
121

122
  @Override
123
  public String toString() {
124
    return new Gson().toJson(this);
1✔
125
  }
126

127
  public static DataAccessRequestData fromString(String jsonString) {
128
    DataAccessRequestData data = new Gson().fromJson(jsonString, DataAccessRequestData.class);
1✔
129
    validateOntologyEntries(data);
1✔
130
    return data;
1✔
131
  }
132

133
  public String getReferenceId() {
134
    return referenceId;
1✔
135
  }
136

137
  public void setReferenceId(String referenceId) {
138
    this.referenceId = referenceId;
1✔
139
  }
1✔
140

141
  public String getProjectTitle() {
142
    return projectTitle;
1✔
143
  }
144

145
  public void setProjectTitle(String projectTitle) {
146
    this.projectTitle = projectTitle;
1✔
147
  }
1✔
148

149
  public Boolean getCheckNihDataOnly() {
150
    return checkNihDataOnly;
×
151
  }
152

153
  public void setCheckNihDataOnly(Boolean checkNihDataOnly) {
154
    this.checkNihDataOnly = checkNihDataOnly;
×
155
  }
×
156

157
  public String getRus() {
158
    return rus;
1✔
159
  }
160

161
  public void setRus(String rus) {
162
    this.rus = rus;
1✔
163
  }
1✔
164

165
  public String getNonTechRus() {
166
    return nonTechRus;
1✔
167
  }
168

169
  public void setNonTechRus(String nonTechRus) {
170
    this.nonTechRus = nonTechRus;
1✔
171
  }
1✔
172

173
  public Boolean getDiseases() {
174
    return diseases;
×
175
  }
176

177
  public void setDiseases(Boolean diseases) {
178
    this.diseases = diseases;
1✔
179
  }
1✔
180

181
  public Boolean getMethods() {
182
    return methods;
1✔
183
  }
184

185
  public void setMethods(Boolean methods) {
186
    this.methods = methods;
1✔
187
  }
1✔
188

189
  public Boolean getControls() {
190
    return controls;
1✔
191
  }
192

193
  public void setControls(Boolean controls) {
194
    this.controls = controls;
1✔
195
  }
1✔
196

197
  public Boolean getPopulation() {
198
    return population;
1✔
199
  }
200

201
  public void setPopulation(Boolean population) {
202
    this.population = population;
1✔
203
  }
1✔
204

205
  public Boolean getOther() {
206
    return other;
1✔
207
  }
208

209
  public void setOther(Boolean other) {
210
    this.other = other;
1✔
211
  }
1✔
212

213
  public String getOtherText() {
214
    return otherText;
1✔
215
  }
216

217
  public void setOtherText(String otherText) {
218
    this.otherText = otherText;
1✔
219
  }
1✔
220

221
  public List<OntologyEntry> getOntologies() {
222
    if (Objects.isNull(ontologies)) {
1✔
223
      return Collections.emptyList();
1✔
224
    }
225
    return ontologies;
1✔
226
  }
227

228
  public void setOntologies(List<OntologyEntry> ontologies) {
229
    this.ontologies = ontologies;
1✔
230
  }
1✔
231

232
  public Boolean getForProfit() {
233
    return forProfit;
1✔
234
  }
235

236
  public void setForProfit(Boolean forProfit) {
237
    this.forProfit = forProfit;
1✔
238
  }
1✔
239

240
  public Boolean getOneGender() {
241
    return oneGender;
×
242
  }
243

244
  public void setOneGender(Boolean oneGender) {
245
    this.oneGender = oneGender;
1✔
246
  }
1✔
247

248
  public String getGender() {
249
    return gender;
1✔
250
  }
251

252
  public void setGender(String gender) {
253
    this.gender = gender;
1✔
254
  }
1✔
255

256
  public Boolean getPediatric() {
257
    return pediatric;
1✔
258
  }
259

260
  public void setPediatric(Boolean pediatric) {
261
    this.pediatric = pediatric;
1✔
262
  }
1✔
263

264
  public Boolean getIllegalBehavior() {
265
    return illegalBehavior;
1✔
266
  }
267

268
  public void setIllegalBehavior(Boolean illegalBehavior) {
269
    this.illegalBehavior = illegalBehavior;
1✔
270
  }
1✔
271

272
  public Boolean getAddiction() {
273
    return addiction;
1✔
274
  }
275

276
  public void setAddiction(Boolean addiction) {
277
    this.addiction = addiction;
1✔
278
  }
1✔
279

280
  public Boolean getSexualDiseases() {
281
    return sexualDiseases;
1✔
282
  }
283

284
  public void setSexualDiseases(Boolean sexualDiseases) {
285
    this.sexualDiseases = sexualDiseases;
1✔
286
  }
1✔
287

288
  public Boolean getStigmatizedDiseases() {
289
    return stigmatizedDiseases;
1✔
290
  }
291

292
  public void setStigmatizedDiseases(Boolean stigmatizedDiseases) {
293
    this.stigmatizedDiseases = stigmatizedDiseases;
1✔
294
  }
1✔
295

296
  public Boolean getVulnerablePopulation() {
297
    return vulnerablePopulation;
1✔
298
  }
299

300
  public void setVulnerablePopulation(Boolean vulnerablePopulation) {
301
    this.vulnerablePopulation = vulnerablePopulation;
1✔
302
  }
1✔
303

304
  public Boolean getPopulationMigration() {
305
    return populationMigration;
1✔
306
  }
307

308
  public void setPopulationMigration(Boolean populationMigration) {
309
    this.populationMigration = populationMigration;
1✔
310
  }
1✔
311

312
  public Boolean getPsychiatricTraits() {
313
    return psychiatricTraits;
1✔
314
  }
315

316
  public void setPsychiatricTraits(Boolean psychiatricTraits) {
317
    this.psychiatricTraits = psychiatricTraits;
1✔
318
  }
1✔
319

320
  public Boolean getNotHealth() {
321
    return notHealth;
1✔
322
  }
323

324
  public void setNotHealth(Boolean notHealth) {
325
    this.notHealth = notHealth;
1✔
326
  }
1✔
327

328
  public Boolean getHmb() {
329
    return hmb;
1✔
330
  }
331

332
  public void setHmb(Boolean hmb) {
333
    this.hmb = hmb;
1✔
334
  }
1✔
335

336
  public List<DatasetEntry> getDatasets() {
337
    if (Objects.isNull(datasets)) {
×
338
      return Collections.emptyList();
×
339
    }
340
    return datasets;
×
341
  }
342

343
  public void setDatasets(List<DatasetEntry> datasets) {
344
    this.datasets = datasets;
1✔
345
  }
1✔
346

347
  @Deprecated
348
  public String getDarCode() {
349
    return darCode;
×
350
  }
351

352
  @Deprecated
353
  public void setDarCode(String darCode) {
354
    this.darCode = darCode;
1✔
355
  }
1✔
356

357
  public Object getRestriction() {
358
    return restriction;
1✔
359
  }
360

361
  public void setRestriction(Object restriction) {
362
    this.restriction = restriction;
×
363
  }
×
364

365
  public Boolean getValidRestriction() {
366
    return validRestriction;
1✔
367
  }
368

369
  public void setValidRestriction(Boolean validRestriction) {
370
    this.validRestriction = validRestriction;
1✔
371
  }
1✔
372

373
  public Long getCreateDate() {
UNCOV
374
    return createDate;
×
375
  }
376

377
  public void setCreateDate(Long createDate) {
378
    this.createDate = createDate;
1✔
379
  }
1✔
380

381
  public Long getSortDate() {
UNCOV
382
    return sortDate;
×
383
  }
384

385
  public void setSortDate(Long sortDate) {
UNCOV
386
    this.sortDate = sortDate;
×
UNCOV
387
  }
×
388

389
  /**
390
   * Used for the initial population of datasets a DAR is associated to. Intended to be used solely
391
   * for simpler construction from the UI.
392
   *
393
   * @return List of dataset ids associated to the DAR.
394
   */
395
  public List<Integer> getDatasetIds() {
396
    if (Objects.isNull(datasetIds)) {
1✔
397
      return Collections.emptyList();
1✔
398
    }
399
    return datasetIds;
1✔
400
  }
401

402
  public String getStatus() {
403
    return status;
1✔
404
  }
405

406
  public void setStatus(String status) {
407
    this.status = status;
1✔
408
  }
1✔
409

410
  public Boolean getPoa() {
411
    return poa;
1✔
412
  }
413

414
  public void setPoa(Boolean poa) {
415
    this.poa = poa;
1✔
416
  }
1✔
417

418
  public Boolean getCloudUse() {
419
    return cloudUse;
×
420
  }
421

422
  public void setCloudUse(Boolean cloudUse) {
423
    this.cloudUse = cloudUse;
1✔
424
  }
1✔
425

426
  public Boolean getAnvilUse() {
427
    return anvilUse;
×
428
  }
429

430
  public void setAnvilUse(Boolean anvilUse) {
431
    this.anvilUse = anvilUse;
1✔
432
  }
1✔
433

434
  public String getCloudProvider() {
435
    return cloudProvider;
×
436
  }
437

438
  public void setCloudProvider(String cloudProvider) {
439
    this.cloudProvider = cloudProvider;
1✔
440
  }
1✔
441

442
  public String getCloudProviderType() {
443
    return cloudProviderType;
×
444
  }
445

446
  public void setCloudProviderType(String cloudProviderType) {
447
    this.cloudProviderType = cloudProviderType;
×
448
  }
×
449

450
  public Boolean getGeneticStudiesOnly() {
451
    return geneticStudiesOnly;
×
452
  }
453

454
  public void setGeneticStudiesOnly(Boolean geneticStudiesOnly) {
455
    this.geneticStudiesOnly = geneticStudiesOnly;
×
456
  }
×
457

458
  public Boolean getIrb() {
459
    return irb;
×
460
  }
461

462
  public void setIrb(Boolean irb) {
463
    this.irb = irb;
×
464
  }
×
465

466
  public String getIrbDocumentLocation() {
467
    return irbDocumentLocation;
1✔
468
  }
469

470
  public void setIrbDocumentLocation(String irbDocumentLocation) {
471
    this.irbDocumentLocation = irbDocumentLocation;
1✔
472
  }
1✔
473

474
  public String getIrbDocumentName() {
475
    return irbDocumentName;
1✔
476
  }
477

478
  public void setIrbDocumentName(String irbDocumentName) {
479
    this.irbDocumentName = irbDocumentName;
1✔
480
  }
1✔
481

482
  public String getIrbProtocolExpiration() {
483
    return irbProtocolExpiration;
×
484
  }
485

486
  public void setIrbProtocolExpiration(String irbProtocolExpiration) {
487
    this.irbProtocolExpiration = irbProtocolExpiration;
×
488
  }
×
489

490
  public Boolean getPublication() {
491
    return publication;
×
492
  }
493

494
  public void setPublication(Boolean publication) {
495
    this.publication = publication;
×
496
  }
×
497

498
  public Boolean getCollaboration() {
499
    return collaboration;
×
500
  }
501

502
  public void setCollaboration(Boolean collaboration) {
503
    this.collaboration = collaboration;
×
504
  }
×
505

506
  public String getCollaborationLetterLocation() {
507
    return collaborationLetterLocation;
1✔
508
  }
509

510
  public void setCollaborationLetterLocation(String collaborationLetterLocation) {
511
    this.collaborationLetterLocation = collaborationLetterLocation;
1✔
512
  }
1✔
513

514
  public String getCollaborationLetterName() {
515
    return collaborationLetterName;
1✔
516
  }
517

518
  public void setCollaborationLetterName(String collaborationLetterName) {
519
    this.collaborationLetterName = collaborationLetterName;
1✔
520
  }
1✔
521

522
  public Boolean getForensicActivities() {
523
    return forensicActivities;
×
524
  }
525

526
  public void setForensicActivities(Boolean forensicActivities) {
527
    this.forensicActivities = forensicActivities;
×
528
  }
×
529

530
  public Boolean getSharingDistribution() {
531
    return sharingDistribution;
×
532
  }
533

534
  public void setSharingDistribution(Boolean sharingDistribution) {
535
    this.sharingDistribution = sharingDistribution;
×
536
  }
×
537

538
  public List<Collaborator> getLabCollaborators() {
539
    if (Objects.isNull(labCollaborators)) {
1✔
540
      return Collections.emptyList();
1✔
541
    }
542
    return labCollaborators;
1✔
543
  }
544

545
  public void setLabCollaborators(
546
      List<Collaborator> labCollaborators) {
547
    this.labCollaborators = labCollaborators;
1✔
548
  }
1✔
549

550
  public List<Collaborator> getInternalCollaborators() {
551
    if (Objects.isNull(internalCollaborators)) {
1✔
552
      return Collections.emptyList();
1✔
553
    }
554
    return internalCollaborators;
1✔
555
  }
556

557
  public void setInternalCollaborators(
558
      List<Collaborator> internalCollaborators) {
559
    this.internalCollaborators = internalCollaborators;
1✔
560
  }
1✔
561

562
  public List<Collaborator> getLabAndInternalCollaborators() {
563
    return Stream.of(getInternalCollaborators(), getLabCollaborators())
1✔
564
        .flatMap(List::stream)
1✔
565
        .toList();
1✔
566
  }
567

568
  public List<Collaborator> getExternalCollaborators() {
569
    if (Objects.isNull(externalCollaborators)) {
1✔
570
      return Collections.emptyList();
1✔
571
    }
572
    return externalCollaborators;
1✔
573
  }
574

575
  public void setExternalCollaborators(
576
      List<Collaborator> externalCollaborators) {
577
    this.externalCollaborators = externalCollaborators;
1✔
578
  }
1✔
579

580
  public Boolean getLocalUse() {
581
    return localUse;
×
582
  }
583

584
  public void setLocalUse(Boolean localUse) {
585
    this.localUse = localUse;
×
586
  }
×
587

588
  public String getCloudProviderDescription() {
589
    return cloudProviderDescription;
×
590
  }
591

592
  public void setCloudProviderDescription(String cloudProviderDescription) {
593
    this.cloudProviderDescription = cloudProviderDescription;
1✔
594
  }
1✔
595

596
  public String getItDirector() {
597
    return itDirector;
×
598
  }
599

600
  public void setItDirector(String itDirector) {
601
    this.itDirector = itDirector;
×
602
  }
×
603

604
  public String getItDirectorEmail() {
605
    return itDirectorEmail;
1✔
606
  }
607

608
  public void setItDirectorEmail(String itDirectorEmail) {
609
    this.itDirectorEmail = itDirectorEmail;
1✔
610
  }
1✔
611

612
  public String getSigningOfficial() {
613
    return signingOfficial;
×
614
  }
615

616
  public void setSigningOfficial(String signingOfficial) {
617
    this.signingOfficial = signingOfficial;
×
618
  }
×
619

620
  public String getSigningOfficialEmail() {
621
    return signingOfficialEmail;
1✔
622
  }
623

624
  public void setSigningOfficialEmail(String signingOfficialEmail) {
625
    this.signingOfficialEmail = signingOfficialEmail;
1✔
626
  }
1✔
627

628
  public void setDSAcknowledgement(Boolean dsAcknowledgement) {
629
    this.dsAcknowledgement = dsAcknowledgement;
×
630
  }
×
631

632
  public Boolean getDSAcknowledgement() {
633
    return dsAcknowledgement;
×
634
  }
635

636
  public void setGSOAcknowledgement(Boolean gsoAcknowledgement) {
637
    this.gsoAcknowledgement = gsoAcknowledgement;
×
638
  }
×
639

640
  public Boolean getGSOAcknowledgement() {
641
    return gsoAcknowledgement;
×
642
  }
643

644
  public void setPubAcknowledgement(Boolean pubAcknowledgement) {
645
    this.pubAcknowledgement = pubAcknowledgement;
×
646
  }
×
647

648
  public Boolean getPubAcknowledgement() {
649
    return pubAcknowledgement;
×
650
  }
651

652
  public String getPiName() {
653
    return piName;
×
654
  }
655

656
  public void setPiName(String piName) {
657
    this.piName = piName;
×
658
  }
×
659

660
  public String getPiEmail() {
661
    return piEmail;
1✔
662
  }
663

664
  public void setPiEmail(String piEmail) {
665
    this.piEmail = piEmail;
1✔
666
  }
1✔
667

668
  // Validate all ontology entries
669
  private static void validateOntologyEntries(DataAccessRequestData data) {
670
    if (Objects.nonNull(data)
1✔
671
        && !data.getOntologies().isEmpty()) {
1✔
672
      List<OntologyEntry> filteredEntries =
×
673
          data.getOntologies().stream()
×
674
              .filter(Objects::nonNull)
×
675
              .filter(e -> Objects.nonNull(e.getId()))
×
676
              .filter(e -> Objects.nonNull(e.getLabel()))
×
677
              .collect(Collectors.toList());
×
678
      if (filteredEntries.isEmpty()) {
×
679
        data.setOntologies(Collections.emptyList());
×
680
      } else {
681
        data.setOntologies(filteredEntries);
×
682
      }
683
    }
684
  }
1✔
685

686
  public String getProgressReportSummary() {
687
    return progressReportSummary;
1✔
688
  }
689

690
  public void setProgressReportSummary(String progressReportSummary) {
691
    this.progressReportSummary = progressReportSummary;
1✔
692
  }
1✔
693

694
  public String getIntellectualPropertySummary() {
695
    return intellectualPropertySummary;
1✔
696
  }
697

698
  public void setIntellectualPropertySummary(String intellectualPropertySummary) {
699
    this.intellectualPropertySummary = intellectualPropertySummary;
1✔
700
  }
1✔
701

702
  public List<Publication> getPublications() {
703
    return publications;
1✔
704
  }
705

706
  public void setPublications(
707
      List<Publication> publications) {
708
    this.publications = publications;
1✔
709
  }
1✔
710

711
  public List<Presentation> getPresentations() {
712
    return presentations;
1✔
713
  }
714

715
  public void setPresentations(
716
      List<Presentation> presentations) {
717
    this.presentations = presentations;
1✔
718
  }
1✔
719

720
  public DataManagementIncident getDmi() {
721
    return dmi;
1✔
722
  }
723

724
  public void setDmi(DataManagementIncident dmi) {
725
    this.dmi = dmi;
1✔
726
  }
1✔
727

728
  public String getResearchPlans() {
729
    return researchPlans;
1✔
730
  }
731

732
  public void setResearchPlans(String researchPlans) {
733
    this.researchPlans = researchPlans;
1✔
734
  }
1✔
735

736
  public CloseoutSupplement getCloseoutSupplement() {
737
    return closeoutSupplement;
1✔
738
  }
739

740
  public void setCloseoutSupplement(
741
      CloseoutSupplement closeoutSupplement) {
742
    this.closeoutSupplement = closeoutSupplement;
1✔
743
  }
1✔
744
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc