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

testit-tms / project-importer / 25048095500

28 Apr 2026 10:38AM UTC coverage: 76.721%. First build
25048095500

Pull #13

github

web-flow
Merge f39db3d61 into 12fb69798
Pull Request #13: fix: html escaping issue

227 of 329 branches covered (69.0%)

Branch coverage included in aggregate %.

76 of 86 new or added lines in 1 file covered. (88.37%)

1266 of 1617 relevant lines covered (78.29%)

13.32 hits per line

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

93.96
/Importer/Client/Implementations/ClientAdapter.cs
1
using Importer.Models;
2
using Microsoft.Extensions.Logging;
3
using Microsoft.Extensions.Options;
4
using System.Collections;
5
using System.Net;
6
using System.Reflection;
7
using System.Runtime.CompilerServices;
8
using System.Text.RegularExpressions;
9
using Models;
10
using TestIT.ApiClient.Api;
11
using TestIT.ApiClient.Client;
12
using TestIT.ApiClient.Model;
13
using Attribute = Models.Attribute;
14
using LinkType = TestIT.ApiClient.Model.LinkType;
15

16
namespace Importer.Client.Implementations;
17

18
public class ClientAdapter(
49✔
19
    ILogger<ClientAdapter> logger,
49✔
20
    IOptions<AppConfig> appConfig,
49✔
21
    IAdapterHelper adapterHelper,
49✔
22
    IAttachmentsApi attachmentsApi,
49✔
23
    IProjectsApi projectsApi,
49✔
24
    IProjectAttributesApi projectAttributesApi,
49✔
25
    IProjectSectionsApi projectSectionsApi,
49✔
26
    ISectionsApi sectionsApi,
49✔
27
    ICustomAttributesApi customAttributesApi,
49✔
28
    IWorkItemsApi workItemsApi,
49✔
29
    IParametersApi parametersApi
49✔
30
) : IClientAdapter
49✔
31
{
32
    private const int TenMinutes = 60000;
33
    private static readonly HashSet<string> SanitizeExcludedFields = new(StringComparer.OrdinalIgnoreCase)
1✔
34
    {
1✔
35
        "ExternalId",
1✔
36
        "AutoTestExternalId"
1✔
37
    };
1✔
38
    private static readonly Regex HtmlTagRegex = new(
1✔
39
        "<[a-zA-Z!/][^<>\"']*(?:\"[^\"]*\"[^<>\"']*|'[^']*'[^<>\"']*)*>",
1✔
40
        RegexOptions.Compiled);
1✔
41
    private static LinkType GetLinkTypeOrDefault(global::Models.LinkType type) =>
42
        Enum.IsDefined(typeof(global::Models.LinkType), type)
4!
43
            ? Enum.Parse<LinkType>(type.ToString())
4✔
44
            : LinkType.Related;
4✔
45
    private static string SanitizeText(string? value)
46
    {
101✔
47
        if (string.IsNullOrEmpty(value))
101✔
48
            return value ?? string.Empty;
6!
49

50
        return HtmlTagRegex.Replace(value, match => WebUtility.HtmlEncode(match.Value));
118✔
51
    }
101✔
52
    private static void SanitizeModelStrings(object? model)
53
    {
24✔
54
        if (model == null)
24✔
NEW
55
            return;
×
56

57
        SanitizeObject(model, new HashSet<object>(ReferenceEqualityComparer.Instance));
24✔
58
    }
24✔
59
    private static void SanitizeObject(object model, HashSet<object> visited)
60
    {
196✔
61
        var type = model.GetType();
196✔
62
        if (type == typeof(string) || type.IsPrimitive || type.IsEnum || model is decimal || model is Guid || model is DateTime)
196✔
63
            return;
87✔
64

65
        if (!visited.Add(model))
109✔
NEW
66
            return;
×
67

68
        if (model is IDictionary dictionary)
109✔
69
        {
5✔
70
            var keys = dictionary.Keys.Cast<object>().ToList();
5✔
71
            foreach (var key in keys)
23✔
72
            {
4✔
73
                var value = dictionary[key];
4✔
74
                if (value is string s)
4!
75
                    dictionary[key] = SanitizeText(s);
4✔
NEW
76
                else if (value != null)
×
NEW
77
                    SanitizeObject(value, visited);
×
78
            }
4✔
79

80
            return;
5✔
81
        }
82

83
        if (model is IEnumerable enumerable)
104✔
84
        {
51✔
85
            foreach (var item in enumerable)
209✔
86
                if (item != null)
28✔
87
                    SanitizeObject(item, visited);
28✔
88
            return;
51✔
89
        }
90

91
        foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
709✔
92
        {
275✔
93
            if (!property.CanRead || property.GetIndexParameters().Length > 0)
275!
NEW
94
                continue;
×
95

96
            var value = property.GetValue(model);
275✔
97
            if (value == null)
275✔
98
                continue;
51✔
99

100
            if (value is string str)
224✔
101
            {
80✔
102
                if (!property.CanWrite || SanitizeExcludedFields.Contains(property.Name))
80!
103
                    continue;
3✔
104

105
                property.SetValue(model, SanitizeText(str));
77✔
106
                continue;
77✔
107
            }
108

109
            SanitizeObject(value, visited);
144✔
110
        }
144✔
111
    }
196✔
112

113

114
    public async Task<Guid> GetProject(string name)
115
    {
6✔
116
        if (!string.IsNullOrEmpty(appConfig.Value.Tms.ProjectName))
6✔
117
        {
1✔
118
            logger.LogInformation("Import by custom project name {Name}",
1✔
119
                appConfig.Value.Tms.ProjectName);
1✔
120
            name = appConfig.Value.Tms.ProjectName;
1✔
121
        }
1✔
122

123
        logger.LogInformation("Getting project {Name}", name);
6✔
124

125
        try
126
        {
6✔
127
            var projects = await
6✔
128
                projectsApi.ApiV2ProjectsSearchPostAsync(
6✔
129
                    null, null, null!, null!, null!,
6✔
130
                    new ProjectsFilterModel(name));
6✔
131

132
            logger.LogDebug("Got projects {@Project} by name {Name}", projects, name);
5✔
133

134
            if (projects.Count != 0)
5✔
135
                foreach (var project in projects)
17✔
136
                    if (project.Name == name)
4✔
137
                    {
3✔
138
                        logger.LogInformation("Got project {Name} with id {Id}", project.Name, project.Id);
3✔
139

140
                        if (!appConfig.Value.Tms.ImportToExistingProject)
3✔
141
                            throw new Exception("Project with the same name already exists");
1✔
142

143
                        return project.Id;
2✔
144
                    }
145

146
            return Guid.Empty;
2✔
147
        }
148
        catch (Exception e)
2✔
149
        {
2✔
150
            logger.LogError(e.ToString());
2✔
151
            logger.LogError(e.StackTrace);
2✔
152
            logger.LogError("Project {Name}: {Message}", name, e.Message);
2✔
153
            throw;
2✔
154
        }
155
    }
4✔
156

157
    public async Task<Guid> CreateProject(string name)
158
    {
3✔
159
        if (!string.IsNullOrEmpty(appConfig.Value.Tms.ProjectName)) name = appConfig.Value.Tms.ProjectName;
4✔
160

161
        logger.LogInformation("Creating project {Name}", name);
3✔
162

163
        try
164
        {
3✔
165
            var model = new CreateProjectApiModel(name: name);
3✔
166
            SanitizeModelStrings(model);
3✔
167
            var resp = await projectsApi.CreateProjectAsync(model);
3✔
168

169
            logger.LogDebug("Created project {@Project}", resp);
2✔
170
            logger.LogInformation("Created project {Name} with id {Id}", name, resp.Id);
2✔
171

172
            return resp.Id;
2✔
173
        }
174
        catch (Exception e)
1✔
175
        {
1✔
176
            logger.LogError("Could not create project {Name}: {Message}", name, e.Message);
1✔
177
            throw;
1✔
178
        }
179
    }
2✔
180

181
    public async Task<Guid> ImportSection(Guid projectId, Guid parentSectionId, Section section)
182
    {
3✔
183
        logger.LogInformation("Importing section {Name}", section.Name);
3✔
184

185
        try
186
        {
3✔
187
            var model = new SectionPostModel(
3✔
188
                section.Name, parentId: parentSectionId, projectId: projectId, attachments: [])
3✔
189
            {
3✔
190
                PostconditionSteps = section.PostconditionSteps.Select(s => new StepPostModel
1✔
191
                {
1✔
192
                    Action = SanitizeText(s.Action),
1✔
193
                    Expected = SanitizeText(s.Expected)
1✔
194
                }).ToList(),
1✔
195
                PreconditionSteps = section.PreconditionSteps.Select(s => new StepPostModel
1✔
196
                {
1✔
197
                    Action = SanitizeText(s.Action),
1✔
198
                    Expected = SanitizeText(s.Expected)
1✔
199
                }).ToList()
1✔
200
            };
3✔
201
            SanitizeModelStrings(model);
3✔
202

203
            logger.LogDebug("Importing section {@Section}", model);
3✔
204

205
            var resp = await sectionsApi.CreateSectionAsync(model);
3✔
206

207
            logger.LogDebug("Imported section {@Section}", resp);
2✔
208
            logger.LogInformation("Imported section {Name} with id {Id}", section.Name, resp.Id);
2✔
209

210
            return resp.Id;
2✔
211
        }
212
        catch (Exception e)
1✔
213
        {
1✔
214
            logger.LogError("Could not import section {Name}: {Message}", section.Name, e.Message);
1✔
215
            throw;
1✔
216
        }
217
    }
2✔
218

219
    public async Task<TmsAttribute> ImportAttribute(Attribute attribute)
220
    {
4✔
221
        logger.LogInformation("Importing attribute {Name}", attribute.Name);
4✔
222

223
        try
224
        {
4✔
225
            var model = new GlobalCustomAttributePostModel(attribute.Name)
4✔
226
            {
4✔
227
                Type = Enum.Parse<CustomAttributeTypesEnum>(attribute.Type.ToString()),
4✔
228
                IsRequired = attribute.IsRequired,
4✔
229
                IsEnabled = attribute.IsActive,
4✔
230
                Options = attribute.Options.Select(o => new CustomAttributeOptionPostModel(o)).ToList()
3✔
231
            };
4✔
232
            if (model.Options.Count == 0 && (
4✔
233
                    model.Type == CustomAttributeTypesEnum.Options
4✔
234
                    || model.Type == CustomAttributeTypesEnum.MultipleOptions
4✔
235
                ))
4✔
236
                model.Options.Add(new CustomAttributeOptionPostModel("null"));
1✔
237
            SanitizeModelStrings(model);
4✔
238

239
            logger.LogDebug("Importing attribute {@Attribute}", model);
4✔
240

241
            var resp = await customAttributesApi.ApiV2CustomAttributesGlobalPostAsync(model);
4✔
242

243
            logger.LogDebug("Imported attribute {@Attribute}", resp);
3✔
244
            logger.LogInformation("Imported attribute {Name} with id {Id}", attribute.Name, resp.Id);
3✔
245

246
            return new TmsAttribute
3✔
247
            {
3✔
248
                Id = resp.Id,
3✔
249
                Name = resp.Name,
3✔
250
                Type = resp.Type.ToString(),
3✔
251
                IsRequired = resp.IsRequired,
3✔
252
                IsEnabled = resp.IsEnabled,
3✔
253
                Options = resp.Options.Select(o => new TmsAttributeOptions
3✔
254
                {
3✔
255
                    Id = o.Id,
3✔
256
                    Value = o.Value,
3✔
257
                    IsDefault = o.IsDefault
3✔
258
                }).ToList()
3✔
259
            };
3✔
260
        }
261
        catch (Exception e)
1✔
262
        {
1✔
263
            logger.LogError("Could not import attribute {Name}: {Message}", attribute.Name, e.Message);
1✔
264
            throw;
1✔
265
        }
266
    }
3✔
267

268
    public async Task<TmsAttribute> GetAttribute(Guid id)
269
    {
2✔
270
        logger.LogInformation("Getting attribute {Id}", id);
2✔
271

272
        try
273
        {
2✔
274
            var resp = await customAttributesApi.ApiV2CustomAttributesIdGetAsync(id);
2✔
275

276
            logger.LogDebug("Got attribute {@Attribute}", resp);
1✔
277

278
            return new TmsAttribute
1✔
279
            {
1✔
280
                Id = resp.Id,
1✔
281
                Name = resp.Name,
1✔
282
                Type = resp.Type.ToString(),
1✔
283
                IsRequired = resp.IsRequired,
1✔
284
                IsEnabled = resp.IsEnabled,
1✔
285
                Options = resp.Options.Select(o => new TmsAttributeOptions
1✔
286
                {
1✔
287
                    Id = o.Id,
1✔
288
                    Value = o.Value,
1✔
289
                    IsDefault = o.IsDefault
1✔
290
                }).ToList()
1✔
291
            };
1✔
292
        }
293
        catch (Exception e)
1✔
294
        {
1✔
295
            logger.LogError("Could not get attribute {Id}: {Message}", id, e.Message);
1✔
296
            throw;
1✔
297
        }
298
    }
1✔
299

300
    public async Task<Guid> ImportSharedStep(Guid projectId, Guid parentSectionId, SharedStep sharedStep)
301
    {
2✔
302
        try
303
        {
2✔
304
            var model = new CreateWorkItemApiModel(
2✔
305
                steps: new List<CreateStepApiModel>(),
2✔
306
                preconditionSteps: new List<CreateStepApiModel>(),
2✔
307
                postconditionSteps: new List<CreateStepApiModel>(),
2✔
308
                attributes: new Dictionary<string, object>(),
2✔
309
                links: new List<CreateLinkApiModel>(),
2✔
310
                tags: new List<TagModel>(),
2✔
311
                name: sharedStep.Name)
2✔
312
            {
2✔
313
                EntityTypeName = WorkItemEntityTypeApiModel.SharedSteps,
2✔
314
                Description = sharedStep.Description,
2✔
315
                SectionId = parentSectionId,
2✔
316
                State = Enum.Parse<WorkItemStateApiModel>(sharedStep.State.ToString()),
2✔
317
                Priority = Enum.Parse<WorkItemPriorityApiModel>(sharedStep.Priority.ToString()),
2✔
318
                Steps = sharedStep.Steps.Select(s =>
2✔
319
                    new CreateStepApiModel
1✔
320
                    {
1✔
321
                        Action = SanitizeText(s.Action),
1✔
322
                        Expected = SanitizeText(s.Expected)
1✔
323
                    }).ToList(),
1✔
324
                Attributes = sharedStep.Attributes
2✔
325
                    .ToDictionary(a => a.Id.ToString(),
1✔
326
                        a => a.Value),
1✔
327
                Tags = sharedStep.Tags.Select(t => new TagModel(t)).ToList(),
2✔
328
                Links = sharedStep.Links.Select(l =>
2✔
329
                    new CreateLinkApiModel(url: l.GetUrlEncode())
2✔
330
                    {
2✔
331
                        Title = l.Title,
2✔
332
                        Description = l.Description,
2✔
333
                        Type = GetLinkTypeOrDefault(l.Type)
2✔
334
                    }).ToList(),
2✔
335
                Name = sharedStep.Name,
2✔
336
                ProjectId = projectId,
2✔
337
                Attachments = sharedStep.Attachments.Select(a => new AssignAttachmentApiModel(Guid.Parse(a))).ToList()
×
338
            };
2✔
339
            SanitizeModelStrings(model);
2✔
340

341
            logger.LogDebug("Importing shared step {Name} and {@Model}", sharedStep.Name, model);
2✔
342

343
            var resp = await workItemsApi.ApiV2WorkItemsPostAsync(model);
2✔
344

345
            logger.LogDebug("Imported shared step {@SharedStep}", resp);
1✔
346

347
            logger.LogInformation("Imported shared step {Name} with id {Id}", sharedStep.Name, resp.Id);
1✔
348

349
            return resp.Id;
1✔
350
        }
351
        catch (Exception e)
1✔
352
        {
1✔
353
            logger.LogError("Could not import shared step {Name}: {Message}", sharedStep.Name, e.Message);
1✔
354
            throw;
1✔
355
        }
356
    }
1✔
357

358

359

360
    public async Task<bool> ImportTestCase(Guid projectId, Guid parentSectionId, TmsTestCase testCase)
361
    {
3✔
362
        logger.LogInformation("Importing test case {Name}", testCase.Name);
3✔
363

364

365
        try
366
        {
3✔
367
            var attributes = testCase.Attributes
3✔
368
                .GroupBy(attr => attr.Id)
6✔
369
                .Select(group =>
3✔
370
                {
5✔
371
                    // Filter values
3✔
372
                    var validValues = group
5✔
373
                        // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
5✔
374
                        .Where(attr => attr.Value != null &&
6✔
375
                                       !(attr.Value is string str && string.IsNullOrEmpty(str)))
6✔
376
                        .Select(attr => attr.Value)
4✔
377
                        .ToList();
5✔
378

3✔
379

3✔
380
                    // If there are some valid values - take first
3✔
381
                    // else return null for filtering
3✔
382
                    var finalValue = validValues.Count > 0 ? validValues[0] : null;
5✔
383

3✔
384
                    return new
5✔
385
                    {
5✔
386
                        Id = group.Key,
5✔
387
                        Value = finalValue
5✔
388
                    };
5✔
389
                })
5✔
390
                // delete nulls
3✔
391
                .Where(item => item.Value != null)
5✔
392
                .ToDictionary(
3✔
393
                    item => item.Id.ToString(),
3✔
394
                    item => item.Value!
3✔
395
                );
3✔
396

397

398
            var model = new CreateWorkItemApiModel(
3!
399
                steps: new List<CreateStepApiModel>(),
3✔
400
                preconditionSteps: new List<CreateStepApiModel>(),
3✔
401
                postconditionSteps: new List<CreateStepApiModel>(),
3✔
402
                attributes: new Dictionary<string, object>(),
3✔
403
                links: new List<CreateLinkApiModel>(),
3✔
404
                tags: new List<TagModel>(),
3✔
405
                name: testCase.Name)
3✔
406
            {
3✔
407
                EntityTypeName = WorkItemEntityTypeApiModel.TestCases,
3✔
408
                SectionId = parentSectionId,
3✔
409
                State = Enum.Parse<WorkItemStateApiModel>(testCase.State.ToString()),
3✔
410
                Priority = Enum.Parse<WorkItemPriorityApiModel>(testCase.Priority.ToString()),
3✔
411
                PreconditionSteps = testCase.PreconditionSteps.Select(s =>
3✔
412
                    new CreateStepApiModel
2✔
413
                    {
2✔
414
                        Action = SanitizeText(s.Action),
2✔
415
                        Expected = SanitizeText(s.Expected)
2✔
416
                    }).ToList(),
2✔
417
                PostconditionSteps = testCase.PostconditionSteps.Select(s =>
3✔
418
                    new CreateStepApiModel
2✔
419
                    {
2✔
420
                        Action = SanitizeText(s.Action),
2✔
421
                        Expected = SanitizeText(s.Expected)
2✔
422
                    }).ToList(),
2✔
423
                Steps = testCase.Steps.Select(s =>
3✔
424
                    new CreateStepApiModel
2✔
425
                    {
2✔
426
                        Action = SanitizeText(s.Action),
2✔
427
                        Expected = SanitizeText(s.Expected),
2✔
428
                        WorkItemId = s.SharedStepId,
2✔
429
                        TestData = SanitizeText(s.TestData)
2✔
430
                    }).ToList(),
2✔
431
                Attributes = attributes,
3✔
432
                Tags = testCase.Tags.Select(t => new TagModel(t)).ToList(),
3✔
433
                Links = testCase.Links.Select(l =>
3✔
434
                    new CreateLinkApiModel(url: l.GetUrlEncode())
2✔
435
                    {
2✔
436
                        Title = l.Title,
2✔
437
                        Description = l.Description,
2✔
438
                        Type = GetLinkTypeOrDefault(l.Type)
2✔
439
                    }).ToList(),
2✔
440
                Name = testCase.Name,
3✔
441
                ProjectId = projectId,
3✔
442
                Attachments = testCase.Attachments.Select(a => new AssignAttachmentApiModel(Guid.Parse(a))).ToList(),
1✔
443
                Iterations = testCase.TmsIterations.Select(i =>
3✔
444
                {
1✔
445
                    var parameters = i.Parameters.Select(p => new ParameterIterationModel(p)).ToList();
3✔
446
                    return new AssignIterationApiModel(parameters);
1✔
447
                }).ToList(),
1✔
448
                Duration = testCase.Duration == 0 ? TenMinutes : testCase.Duration,
3✔
449
                Description = testCase.Description
3✔
450
            };
3✔
451
            SanitizeModelStrings(model);
3✔
452

453
            logger.LogDebug("Importing test case {Name} and {@Model}", testCase.Name, model);
3✔
454

455
            var response = await adapterHelper.RetryCaller(
3✔
456
                async () => await workItemsApi.ApiV2WorkItemsPostAsync(model));
6✔
457

458
            logger.LogDebug("Imported test case {@TestCase}", response);
2✔
459

460
            logger.LogInformation("Imported test case {Name} with id {Id}", testCase.Name, response.Id);
2✔
461
        }
2✔
462
        catch (Exception e)
1✔
463
        {
1✔
464
            logger.LogError("Could not import test case {Name}: {Message}: {InnerMessage}; {Stack}",
1!
465
                testCase.Name, e.Message, e.InnerException?.Message, e.StackTrace);
1✔
466
            throw;
1✔
467
        }
468

469
        return true;
2✔
470
    }
2✔
471

472

473

474
    public async Task<Guid> GetRootSectionId(Guid projectId)
475
    {
2✔
476
        logger.LogInformation("Getting root section id");
2✔
477

478
        try
479
        {
2✔
480
            var section = await projectSectionsApi.GetSectionsByProjectIdAsync(projectId.ToString());
2✔
481

482
            logger.LogDebug("Got root section {@Section}", section.First());
1✔
483

484
            return section.First().Id;
1✔
485
        }
486
        catch (Exception e)
1✔
487
        {
1✔
488
            logger.LogError("Could not get root section id: {Message}", e.Message);
1✔
489
            throw;
1✔
490
        }
491
    }
1✔
492

493
    public async Task<List<TmsAttribute>> GetProjectAttributes()
494
    {
2✔
495
        logger.LogInformation("Getting project attributes");
2✔
496

497
        try
498
        {
2✔
499
            var attributes = await customAttributesApi.ApiV2CustomAttributesSearchPostAsync(
2✔
500
                customAttributeSearchQueryModel: new CustomAttributeSearchQueryModel(isGlobal: true,
2✔
501
                    isDeleted: false));
2✔
502

503
            logger.LogDebug("Got project attributes {@Attributes}", attributes);
1✔
504

505
            return attributes.Select(a => new TmsAttribute
2✔
506
            {
2✔
507
                Id = a.Id,
2✔
508
                Name = a.Name,
2✔
509
                Type = a.Type.ToString(),
2✔
510
                IsEnabled = a.IsEnabled,
2✔
511
                IsRequired = a.IsRequired,
2✔
512
                IsGlobal = a.IsGlobal,
2✔
513
                Options = a.Options.Select(o => new TmsAttributeOptions
1✔
514
                {
1✔
515
                    Id = o.Id,
1✔
516
                    Value = o.Value,
1✔
517
                    IsDefault = o.IsDefault
1✔
518
                }).ToList()
1✔
519
            }).ToList();
2✔
520
        }
521
        catch (Exception e)
1✔
522
        {
1✔
523
            logger.LogError("Could not get project attributes: {Message}", e.Message);
1✔
524
            throw;
1✔
525
        }
526
    }
1✔
527

528
    public async Task<List<TmsAttribute>> GetRequiredProjectAttributesByProjectId(Guid projectId)
529
    {
2✔
530
        logger.LogInformation("Getting required project attributes by project id {Id}", projectId);
2✔
531

532
        try
533
        {
2✔
534
            var attributes = await projectAttributesApi.SearchAttributesInProjectAsync(
2✔
535
                projectId.ToString(), projectAttributesFilterModel: new ProjectAttributesFilterModel(
2✔
536
                    "",
2✔
537
                    true,
2✔
538
                    types: new List<CustomAttributeTypesEnum>
2✔
539
                    {
2✔
540
                        CustomAttributeTypesEnum.String,
2✔
541
                        CustomAttributeTypesEnum.Options,
2✔
542
                        CustomAttributeTypesEnum.MultipleOptions,
2✔
543
                        CustomAttributeTypesEnum.User,
2✔
544
                        CustomAttributeTypesEnum.Datetime
2✔
545
                    }
2✔
546
                ));
2✔
547

548
            var requiredAttributes = attributes
1✔
549
                .Select(a => new TmsAttribute
2✔
550
                {
2✔
551
                    Id = a.Id,
2✔
552
                    Name = a.Name,
2✔
553
                    Type = a.Type.ToString(),
2✔
554
                    IsEnabled = a.IsEnabled,
2✔
555
                    IsRequired = a.IsRequired,
2✔
556
                    IsGlobal = a.IsGlobal,
2✔
557
                    Options = a.Options.Select(o => new TmsAttributeOptions
1✔
558
                    {
1✔
559
                        Id = o.Id,
1✔
560
                        Value = o.Value,
1✔
561
                        IsDefault = o.IsDefault
1✔
562
                    }).ToList()
1✔
563
                }).ToList();
2✔
564

565
            logger.LogDebug("Got required project attributes by project id {id}: {@Attributes}", projectId,
1✔
566
                requiredAttributes);
1✔
567

568
            return requiredAttributes;
1✔
569
        }
570
        catch (Exception e)
1✔
571
        {
1✔
572
            logger.LogError("Could not get required project attributes by project id {Id}: {Message}", projectId,
1✔
573
                e.Message);
1✔
574
            throw;
1✔
575
        }
576
    }
1✔
577

578
    public async Task<TmsAttribute> GetProjectAttributeById(Guid id)
579
    {
2✔
580
        logger.LogInformation("Getting project attribute by id {Id}", id);
2✔
581

582
        try
583
        {
2✔
584
            var attribute = await customAttributesApi.ApiV2CustomAttributesIdGetAsync(id);
2✔
585

586
            var customAttribute = new TmsAttribute
1✔
587
            {
1✔
588
                Id = attribute.Id,
1✔
589
                Name = attribute.Name,
1✔
590
                Type = attribute.Type.ToString(),
1✔
591
                IsEnabled = attribute.IsEnabled,
1✔
592
                IsRequired = attribute.IsRequired,
1✔
593
                IsGlobal = attribute.IsGlobal,
1✔
594
                Options = attribute.Options.Select(o => new TmsAttributeOptions
1✔
595
                {
1✔
596
                    Id = o.Id,
1✔
597
                    Value = o.Value,
1✔
598
                    IsDefault = o.IsDefault
1✔
599
                }).ToList()
1✔
600
            };
1✔
601

602
            logger.LogDebug("Got project attribute by id {id}: {@Attribute}", id, customAttribute);
1✔
603

604
            return customAttribute;
1✔
605
        }
606
        catch (Exception e)
1✔
607
        {
1✔
608
            logger.LogError("Could not get project attribute by id {Id}: {Message}", id, e.Message);
1✔
609
            throw;
1✔
610
        }
611
    }
1✔
612

613
    public async Task AddAttributesToProject(Guid projectId, IEnumerable<Guid> attributeIds)
614
    {
2✔
615
        logger.LogInformation("Adding attributes to project");
2✔
616

617
        try
618
        {
2✔
619
            await projectsApi.AddGlobalAttributesToProjectAsync(projectId.ToString(),
2✔
620
                attributeIds.ToList());
2✔
621
        }
1✔
622
        catch (Exception e)
1✔
623
        {
1✔
624
            logger.LogError("Could not add attributes to project: {Message}", e.Message);
1✔
625
            throw;
1✔
626
        }
627
    }
1✔
628

629

630
    public async Task<TmsAttribute> UpdateAttribute(TmsAttribute attribute)
631
    {
2✔
632
        logger.LogInformation("Updating attribute {Name}", attribute.Name);
2✔
633

634
        try
635
        {
2✔
636
            var model = new GlobalCustomAttributeUpdateModel(attribute.Name)
2✔
637
            {
2✔
638
                IsEnabled = attribute.IsEnabled,
2✔
639
                IsRequired = attribute.IsRequired,
2✔
640
                Options = attribute.Options.Select(o => new CustomAttributeOptionModel
1✔
641
                {
1✔
642
                    Id = o.Id,
1✔
643
                    Value = o.Value,
1✔
644
                    IsDefault = o.IsDefault
1✔
645
                }).ToList()
1✔
646
            };
2✔
647
            SanitizeModelStrings(model);
2✔
648

649
            logger.LogDebug("Updating attribute {@Model}", model);
2✔
650

651
            var resp = await customAttributesApi
2✔
652
                .ApiV2CustomAttributesGlobalIdPutAsync(attribute.Id,
2✔
653
                    model);
2✔
654

655
            logger.LogDebug("Updated attribute {@Response}", resp);
1✔
656

657
            attribute.Options = resp.Options.Select(o => new TmsAttributeOptions
2✔
658
            {
2✔
659
                Id = o.Id,
2✔
660
                Value = o.Value,
2✔
661
                IsDefault = o.IsDefault
2✔
662
            }).ToList();
2✔
663

664
            return attribute;
1✔
665
        }
666

667
        catch (Exception e)
1✔
668
        {
1✔
669
            logger.LogError(e.StackTrace);
1✔
670
            logger.LogError(e.ToString());
1✔
671
            logger.LogError("Could not update attribute {Name}: {Message}", attribute.Name, e.Message);
1✔
672
            throw;
1✔
673
        }
674
    }
1✔
675

676
    public async Task UpdateProjectAttribute(Guid projectId, TmsAttribute attribute)
677
    {
2✔
678
        logger.LogInformation("Updating project attribute {Name}", attribute.Name);
2✔
679

680
        try
681
        {
2✔
682
            var model = new CustomAttributePutModel(attribute.Id, name: attribute.Name)
2✔
683
            {
2✔
684
                IsEnabled = attribute.IsEnabled,
2✔
685
                IsRequired = attribute.IsRequired,
2✔
686
                Options = attribute.Options.Select(o => new CustomAttributeOptionModel
1✔
687
                {
1✔
688
                    Id = o.Id,
1✔
689
                    Value = o.Value,
1✔
690
                    IsDefault = o.IsDefault
1✔
691
                }).ToList()
1✔
692
            };
2✔
693
            SanitizeModelStrings(model);
2✔
694

695
            logger.LogDebug("Updating attribute {@Model}", model);
2✔
696

697
            await projectAttributesApi.UpdateProjectsAttributeAsync(
2✔
698
                projectId.ToString(), model);
2✔
699
        }
1✔
700

701
        catch (Exception e)
1✔
702
        {
1✔
703
            logger.LogError(e.StackTrace);
1✔
704
            logger.LogError(e.ToString());
1✔
705
            logger.LogError("Could not update attribute {Name}: {Message}", attribute.Name, e.Message);
1✔
706
            throw;
1✔
707
        }
708
    }
1✔
709

710
    public async Task<Guid> UploadAttachment(string fileName, Stream content)
711
    {
5✔
712
        if (string.IsNullOrEmpty(fileName))
5✔
713
            throw new ArgumentNullException(nameof(fileName));
2✔
714

715
        if (content == null)
3✔
716
            throw new ArgumentNullException(nameof(content));
1✔
717

718
        logger.LogDebug("Uploading attachment {Name}", fileName);
2✔
719

720
        try
721
        {
2✔
722
            var response = await adapterHelper.RetryCaller(
2✔
723
                async () => await attachmentsApi.ApiV2AttachmentsPostAsync(
4✔
724
                    new FileParameter(
4✔
725
                        Path.GetFileName(fileName),
4✔
726
                        content: content,
4✔
727
                        contentType: "application/octet-stream")));
4✔
728

729
            logger.LogDebug("Uploaded attachment {@Response}", response);
1✔
730

731
            return response!.Id;
1✔
732
        }
733
        catch (Exception e)
1✔
734
        {
1✔
735
            logger.LogError("Could not upload attachment {Name}: {Message}: {Inner}, {StackTrace}", fileName,
1!
736
                e.Message, e.InnerException?.Message, e.StackTrace);
1✔
737
            throw;
1✔
738
        }
739
    }
1✔
740

741
    public async Task<TmsParameter> CreateParameter(Parameter parameter)
742
    {
4✔
743
        logger.LogInformation("Creating parameter {Name}", parameter.Name);
4✔
744

745
        try
746
        {
4✔
747
            // check parameter and if "" change to N/A
748
            if (parameter.Value == null || parameter.Value.Trim() == string.Empty)
4✔
749
            {
2✔
750
                parameter.Value = "N/A";
2✔
751
            }
2✔
752
            var model = new CreateParameterApiModel(name: parameter.Name,
4✔
753
                value: parameter.Value);
4✔
754
            SanitizeModelStrings(model);
4✔
755

756
            logger.LogDebug("Creating parameter {@Model}", model);
4✔
757

758
            var resp = await parametersApi.CreateParameterAsync(model);
4✔
759

760
            logger.LogDebug("Created parameter {@Response}", resp);
3✔
761

762
            return new TmsParameter
3✔
763
            {
3✔
764
                Id = resp.Id,
3✔
765
                Value = resp.Value,
3✔
766
                Name = resp.Name,
3✔
767
                ParameterKeyId = resp.ParameterKeyId
3✔
768
            };
3✔
769
        }
770
        catch (Exception e)
1✔
771
        {
1✔
772
            logger.LogError("Could not create parameter {Name}: {Message}", parameter.Name, e.Message);
1✔
773
            throw;
1✔
774
        }
775
    }
3✔
776

777
    public async Task<List<TmsParameter>> GetParameter(string name)
778
    {
2✔
779
        logger.LogInformation("Getting parameter {Name}", name);
2✔
780

781
        try
782
        {
2✔
783
            var resp = await parametersApi.ApiV2ParametersSearchPostAsync(
2✔
784
                parametersFilterApiModel:
2✔
785
                new ParametersFilterApiModel(name: name, isDeleted: false));
2✔
786

787

788
            logger.LogDebug("Got parameter {@Response}", resp);
1✔
789

790
            return resp.Select(p =>
1✔
791
                    new TmsParameter
2✔
792
                    {
2✔
793
                        Id = p.Id,
2✔
794
                        Value = p.Value,
2✔
795
                        Name = p.Name,
2✔
796
                        ParameterKeyId = p.ParameterKeyId
2✔
797
                    })
2✔
798
                .ToList();
1✔
799
        }
800
        catch (Exception e)
1✔
801
        {
1✔
802
            logger.LogError("Could not get parameter {Name}: {Message}", name, e.Message);
1✔
803
            throw;
1✔
804
        }
805
    }
1✔
806

807
    public async Task<Guid> GetSection(Guid projectId, Guid parentSectionId, Section section)
808
    {
×
809
        logger.LogInformation("Importing section {Name}", section.Name);
×
810

811
        try
812
        {
×
813
            var model = new SectionPostModel(
×
814
                section.Name, parentId: parentSectionId, projectId: projectId)
×
815
            {
×
816
                PostconditionSteps = section.PostconditionSteps.Select(s => new StepPostModel
×
817
                {
×
NEW
818
                    Action = SanitizeText(s.Action),
×
NEW
819
                    Expected = SanitizeText(s.Expected)
×
820
                }).ToList(),
×
821
                PreconditionSteps = section.PreconditionSteps.Select(s => new StepPostModel
×
822
                {
×
NEW
823
                    Action = SanitizeText(s.Action),
×
NEW
824
                    Expected = SanitizeText(s.Expected)
×
825
                }).ToList()
×
826
            };
×
NEW
827
            SanitizeModelStrings(model);
×
828

829
            logger.LogDebug("Importing section {@Section}", model);
×
830

831
            var resp = await sectionsApi.CreateSectionAsync(model);
×
832

833
            logger.LogDebug("Imported section {@Section}", resp);
×
834
            logger.LogInformation("Imported section {Name} with id {Id}", section.Name, resp.Id);
×
835

836
            return resp.Id;
×
837
        }
838
        catch (Exception e)
×
839
        {
×
840
            logger.LogError("Could not import section {Name}: {Message}", section.Name, e.Message);
×
841
            throw;
×
842
        }
843
    }
×
844
}
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