From 09f4e4fc8cc73c62191d46f2519e65a5a688fc58 Mon Sep 17 00:00:00 2001 From: Dmitry Gridnev Date: Mon, 30 Oct 2023 13:14:47 +0300 Subject: [PATCH] Added XRay exporter (#21) * Added XRay project * Added client * Added section service * Added attachment and testcases services * Some fixes * Added tests * Added config * Fixed project * Fixed project * Added readme --------- Co-authored-by: Dmitry.Gridnev --- .github/workflows/release.yaml | 48 +++ .github/workflows/validate.yaml | 2 + Migrators/Migrators.sln | 12 + Migrators/XRayExporter/App.cs | 25 ++ Migrators/XRayExporter/Client/Client.cs | 161 ++++++++ Migrators/XRayExporter/Client/IClient.cs | 13 + Migrators/XRayExporter/Models/Constants.cs | 9 + Migrators/XRayExporter/Models/JiraItem.cs | 36 ++ Migrators/XRayExporter/Models/JiraLink.cs | 34 ++ Migrators/XRayExporter/Models/JiraProject.cs | 16 + Migrators/XRayExporter/Models/SectionData.cs | 9 + Migrators/XRayExporter/Models/TestCaseData.cs | 11 + Migrators/XRayExporter/Models/XRayFolders.cs | 22 + Migrators/XRayExporter/Models/XRayTests.cs | 97 +++++ Migrators/XRayExporter/Program.cs | 73 ++++ Migrators/XRayExporter/Readme.md | 56 +++ .../Services/AttachmentService.cs | 30 ++ .../XRayExporter/Services/ExportService.cs | 61 +++ .../Services/IAttachmentService.cs | 6 + .../XRayExporter/Services/IExportService.cs | 6 + .../XRayExporter/Services/ISectionService.cs | 8 + .../XRayExporter/Services/ITestCaseService.cs | 8 + .../XRayExporter/Services/SectionService.cs | 71 ++++ .../XRayExporter/Services/TestCaseService.cs | 344 ++++++++++++++++ Migrators/XRayExporter/XRayExporter.csproj | 36 ++ Migrators/XRayExporter/xray.config.json | 8 + .../AttachmentServiceTests.cs | 87 ++++ .../XRayExporterTests/ExportServiceTests.cs | 302 ++++++++++++++ .../XRayExporterTests/SectionServiceTests.cs | 115 ++++++ .../XRayExporterTests/TestCaseServiceTests.cs | 380 ++++++++++++++++++ .../XRayExporterTests.csproj | 32 ++ .../ZephyrScaleExporter/Client/Client.cs | 2 +- .../Services/ExportService.cs | 1 + README.md | 1 + 34 files changed, 2121 insertions(+), 1 deletion(-) create mode 100644 Migrators/XRayExporter/App.cs create mode 100644 Migrators/XRayExporter/Client/Client.cs create mode 100644 Migrators/XRayExporter/Client/IClient.cs create mode 100644 Migrators/XRayExporter/Models/Constants.cs create mode 100644 Migrators/XRayExporter/Models/JiraItem.cs create mode 100644 Migrators/XRayExporter/Models/JiraLink.cs create mode 100644 Migrators/XRayExporter/Models/JiraProject.cs create mode 100644 Migrators/XRayExporter/Models/SectionData.cs create mode 100644 Migrators/XRayExporter/Models/TestCaseData.cs create mode 100644 Migrators/XRayExporter/Models/XRayFolders.cs create mode 100644 Migrators/XRayExporter/Models/XRayTests.cs create mode 100644 Migrators/XRayExporter/Program.cs create mode 100644 Migrators/XRayExporter/Readme.md create mode 100644 Migrators/XRayExporter/Services/AttachmentService.cs create mode 100644 Migrators/XRayExporter/Services/ExportService.cs create mode 100644 Migrators/XRayExporter/Services/IAttachmentService.cs create mode 100644 Migrators/XRayExporter/Services/IExportService.cs create mode 100644 Migrators/XRayExporter/Services/ISectionService.cs create mode 100644 Migrators/XRayExporter/Services/ITestCaseService.cs create mode 100644 Migrators/XRayExporter/Services/SectionService.cs create mode 100644 Migrators/XRayExporter/Services/TestCaseService.cs create mode 100644 Migrators/XRayExporter/XRayExporter.csproj create mode 100644 Migrators/XRayExporter/xray.config.json create mode 100644 Migrators/XRayExporterTests/AttachmentServiceTests.cs create mode 100644 Migrators/XRayExporterTests/ExportServiceTests.cs create mode 100644 Migrators/XRayExporterTests/SectionServiceTests.cs create mode 100644 Migrators/XRayExporterTests/TestCaseServiceTests.cs create mode 100644 Migrators/XRayExporterTests/XRayExporterTests.csproj diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8367bf7..802258c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -11,6 +11,7 @@ env: AZURE_DIR: ./Migrators/AzureExporter ZEPHYR_SCALE_DIR: ./Migrators/ZephyrScaleExporter ZEPHYR_SQUAD_DIR: ./Migrators/ZephyrSquadExporter + XRAY_DIR: ./Migrators/XRayExporter jobs: importer_build: @@ -239,6 +240,53 @@ jobs: --output ./publish \ --no-restore ${{ env.ZEPHYR_SQUAD_DIR }} + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: publish/${{ matrix.artifact_name }} + asset_name: ${{ matrix.asset_name }} + tag: ${{ github.ref }} + overwrite: true + + xray_build: + needs: zephyr_squad_build + runs-on: ubuntu-latest + strategy: + matrix: + include: + - os: win-x64 + artifact_name: XRayExporter.exe + asset_name: XRayExporter-win-x64-${{ github.event.release.tag_name }}.exe + - os: linux-x64 + artifact_name: XRayExporter + asset_name: XRayExporter-linux-x64-${{ github.event.release.tag_name }} + - os: osx-x64 + artifact_name: XRayExporter + asset_name: XRayExporter-osx-x64-${{ github.event.release.tag_name }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7 + + - name: Restore dependencies + run: dotnet restore ${{ env.XRAY_DIR }} + + - name: Publish project + run: | + dotnet publish \ + -r ${{ matrix.os }} \ + --configuration Release \ + --self-contained true \ + --output ./publish \ + --no-restore ${{ env.XRAY_DIR }} + - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index a039d01..1b39cc3 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -13,6 +13,7 @@ jobs: ./Migrators/AzureExporter, ./Migrators/ZephyrScaleExporter, ./Migrators/ZephyrSquadExporter, + ./Migrators/XRayExporter, ./Migrators/Importer ] steps: @@ -39,6 +40,7 @@ jobs: ./Migrators/AzureExporterTests, ./Migrators/ZephyrScaleExporterTests, ./Migrators/ZephyrSquadExporterTests, + ./Migrators/XRayExporterTests, ./Migrators/ImporterTests ] steps: diff --git a/Migrators/Migrators.sln b/Migrators/Migrators.sln index 6efdd4b..dfe07b2 100644 --- a/Migrators/Migrators.sln +++ b/Migrators/Migrators.sln @@ -36,6 +36,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZephyrSquadExporter", "Zeph EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZephyrSquadExporterTests", "ZephyrSquadExporterTests\ZephyrSquadExporterTests.csproj", "{C2B65002-89C3-4FD6-A945-87379D552098}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XRayExporter", "XRayExporter\XRayExporter.csproj", "{1E3A8E07-1023-4A40-90DF-1A2CA6BB707C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XRayExporterTests", "XRayExporterTests\XRayExporterTests.csproj", "{563D5041-6E07-4D52-BBB9-C87245604E6F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -90,6 +94,14 @@ Global {C2B65002-89C3-4FD6-A945-87379D552098}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2B65002-89C3-4FD6-A945-87379D552098}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2B65002-89C3-4FD6-A945-87379D552098}.Release|Any CPU.Build.0 = Release|Any CPU + {1E3A8E07-1023-4A40-90DF-1A2CA6BB707C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E3A8E07-1023-4A40-90DF-1A2CA6BB707C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E3A8E07-1023-4A40-90DF-1A2CA6BB707C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E3A8E07-1023-4A40-90DF-1A2CA6BB707C}.Release|Any CPU.Build.0 = Release|Any CPU + {563D5041-6E07-4D52-BBB9-C87245604E6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {563D5041-6E07-4D52-BBB9-C87245604E6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {563D5041-6E07-4D52-BBB9-C87245604E6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {563D5041-6E07-4D52-BBB9-C87245604E6F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Migrators/XRayExporter/App.cs b/Migrators/XRayExporter/App.cs new file mode 100644 index 0000000..b508d07 --- /dev/null +++ b/Migrators/XRayExporter/App.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; +using XRayExporter.Services; + +namespace XRayExporter; + +public class App +{ + private readonly ILogger _logger; + private readonly IExportService _exportService; + + public App(ILogger logger, IExportService exportService) + { + _logger = logger; + _exportService = exportService; + } + + public void Run(string[] args) + { + _logger.LogInformation("Starting application"); + + _exportService.ExportProject().Wait(); + + _logger.LogInformation("Ending application"); + } +} diff --git a/Migrators/XRayExporter/Client/Client.cs b/Migrators/XRayExporter/Client/Client.cs new file mode 100644 index 0000000..e2397ac --- /dev/null +++ b/Migrators/XRayExporter/Client/Client.cs @@ -0,0 +1,161 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using XRayExporter.Models; + +namespace XRayExporter.Client; + +public class Client : IClient +{ + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + private readonly string _projectKey; + + public Client(ILogger logger, IConfiguration configuration) + { + _logger = logger; + + var section = configuration.GetSection("xray"); + var url = section["url"]; + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentException("Url is not specified"); + } + + var token = section["token"]; + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentException("Token is not specified"); + } + + var projectKey = section["projectKey"]; + if (string.IsNullOrEmpty(projectKey)) + { + throw new ArgumentException("Project key is not specified"); + } + + _projectKey = projectKey; + _httpClient = new HttpClient(); + _httpClient.BaseAddress = new Uri(url); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + + public async Task GetProject() + { + _logger.LogInformation("Getting project {ProjectKey}", _projectKey); + + var response = await _httpClient.GetAsync("rest/api/2/project"); + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Failed to get project. Status code: {StatusCode}. Response: {Response}", + response.StatusCode, await response.Content.ReadAsStringAsync()); + + throw new Exception($"Failed to get project. Status code: {response.StatusCode}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var projects = JsonSerializer.Deserialize>(content); + var project = projects!.FirstOrDefault(p => + string.Equals(p.Key, _projectKey, StringComparison.InvariantCultureIgnoreCase)); + + if (project != null) return project; + + _logger.LogError("Project not found"); + + throw new Exception("Project not found"); + } + + public async Task> GetFolders() + { + _logger.LogInformation("Getting folders for project {ProjectKey}", _projectKey); + + var response = + await _httpClient.GetAsync($"rest/raven/1.0/api/testrepository/{_projectKey.ToUpper()}/folders"); + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Failed to get folders. Status code: {StatusCode}. Response: {Response}", + response.StatusCode, await response.Content.ReadAsStringAsync()); + + throw new Exception($"Failed to get folders. Status code: {response.StatusCode}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var folders = JsonSerializer.Deserialize(content); + + return folders!.Folders; + } + + public async Task> GetTestFromFolder(int folderId) + { + _logger.LogInformation("Getting test from folder {FolderId}", folderId); + + var response = + await _httpClient.GetAsync( + $"rest/raven/1.0/api/testrepository/{_projectKey.ToUpper()}/folders/{folderId}/tests"); + if (!response.IsSuccessStatusCode) + { + _logger.LogError( + "Failed to get test from folder {FolderId}. Status code: {StatusCode}. Response: {Response}", + folderId, response.StatusCode, await response.Content.ReadAsStringAsync()); + + throw new Exception($"Failed to get test from folder {folderId}. Status code: {response.StatusCode}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var tests = JsonSerializer.Deserialize(content); + + return tests!.Tests; + } + + public async Task GetTest(string testKey) + { + _logger.LogInformation("Getting test {TestKey}", testKey); + + var response = + await _httpClient.GetAsync( + $"rest/raven/1.0/api/test?keys={testKey}"); + if (!response.IsSuccessStatusCode) + { + _logger.LogError( + "Failed to get test {TestKey}. Status code: {StatusCode}. Response: {Response}", + testKey, response.StatusCode, await response.Content.ReadAsStringAsync()); + + throw new Exception($"Failed to get test {testKey}. Status code: {response.StatusCode}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var tests = JsonSerializer.Deserialize>(content); + + return tests!.First(); + } + + public async Task GetItem(string link) + { + _logger.LogInformation("Getting item {Link}", link); + + var response = + await _httpClient.GetAsync(link.Split(_httpClient.BaseAddress.ToString())[1]); + if (!response.IsSuccessStatusCode) + { + _logger.LogError( + "Failed to get item {Link}. Status code: {StatusCode}. Response: {Response}", + link, response.StatusCode, await response.Content.ReadAsStringAsync()); + + throw new Exception($"Failed to get item {link}. Status code: {response.StatusCode}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var item = JsonSerializer.Deserialize(content); + + return item; + } + + public async Task DownloadAttachment(string link) + { + _logger.LogInformation("Downloading attachment {Link}", link); + + return + await _httpClient.GetByteArrayAsync(link.Split(_httpClient.BaseAddress.ToString())[1]); + } +} diff --git a/Migrators/XRayExporter/Client/IClient.cs b/Migrators/XRayExporter/Client/IClient.cs new file mode 100644 index 0000000..82a64bf --- /dev/null +++ b/Migrators/XRayExporter/Client/IClient.cs @@ -0,0 +1,13 @@ +using XRayExporter.Models; + +namespace XRayExporter.Client; + +public interface IClient +{ + Task GetProject(); + Task> GetFolders(); + Task> GetTestFromFolder(int folderId); + Task GetTest(string testKey); + Task GetItem(string link); + Task DownloadAttachment(string link); +} diff --git a/Migrators/XRayExporter/Models/Constants.cs b/Migrators/XRayExporter/Models/Constants.cs new file mode 100644 index 0000000..025e49c --- /dev/null +++ b/Migrators/XRayExporter/Models/Constants.cs @@ -0,0 +1,9 @@ +namespace XRayExporter.Models; + +public static class Constants +{ + public const string XrayReporter = "Xray Reporter"; + public const string XrayType = "Xray Type"; + public const string XrayStatus = "Xray Status"; + public const string XrayArchived = "Xray Archived"; +} diff --git a/Migrators/XRayExporter/Models/JiraItem.cs b/Migrators/XRayExporter/Models/JiraItem.cs new file mode 100644 index 0000000..87786e7 --- /dev/null +++ b/Migrators/XRayExporter/Models/JiraItem.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace XRayExporter.Models; + +public class JiraItem +{ + [JsonPropertyName("fields")] + public Fields Fields { get; set; } +} + +public class Fields +{ + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("attachment")] + public List Attachments { get; set; } + + [JsonPropertyName("summary")] + public string Summary { get; set; } + + [JsonPropertyName("labels")] + public List Labels { get; set; } + + [JsonPropertyName("issuelinks")] + public List IssueLinks { get; set; } +} + +public class Attachment +{ + [JsonPropertyName("filename")] + public string FileName { get; set; } + + [JsonPropertyName("content")] + public string Content { get; set; } +} diff --git a/Migrators/XRayExporter/Models/JiraLink.cs b/Migrators/XRayExporter/Models/JiraLink.cs new file mode 100644 index 0000000..0a8ca58 --- /dev/null +++ b/Migrators/XRayExporter/Models/JiraLink.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace XRayExporter.Models; + +public class JiraLink +{ + [JsonPropertyName("type")] + public Type Type { get; set; } + + [JsonPropertyName("inwardIssue")] + public Issue? InwardIssue { get; set; } + + [JsonPropertyName("outwardIssue")] + public Issue? OutwardIssue { get; set; } +} + +public class Type +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("inward")] + public string Inward { get; set; } +} + +public class Issue +{ + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("self")] + public string Self { get; set; } +} + diff --git a/Migrators/XRayExporter/Models/JiraProject.cs b/Migrators/XRayExporter/Models/JiraProject.cs new file mode 100644 index 0000000..f82f0f5 --- /dev/null +++ b/Migrators/XRayExporter/Models/JiraProject.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace XRayExporter.Models; + +public class JiraProject +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } +} + diff --git a/Migrators/XRayExporter/Models/SectionData.cs b/Migrators/XRayExporter/Models/SectionData.cs new file mode 100644 index 0000000..0b68c5b --- /dev/null +++ b/Migrators/XRayExporter/Models/SectionData.cs @@ -0,0 +1,9 @@ +using Models; + +namespace XRayExporter.Models; + +public class SectionData +{ + public List
Sections { get; set; } + public Dictionary SectionMap { get; set; } +} diff --git a/Migrators/XRayExporter/Models/TestCaseData.cs b/Migrators/XRayExporter/Models/TestCaseData.cs new file mode 100644 index 0000000..df141d8 --- /dev/null +++ b/Migrators/XRayExporter/Models/TestCaseData.cs @@ -0,0 +1,11 @@ +using Models; +using Attribute = Models.Attribute; + +namespace XRayExporter.Models; + +public class TestCaseData +{ + public List TestCases { get; set; } + public List SharedSteps { get; set; } + public List Attributes { get; set; } +} diff --git a/Migrators/XRayExporter/Models/XRayFolders.cs b/Migrators/XRayExporter/Models/XRayFolders.cs new file mode 100644 index 0000000..23fb8e6 --- /dev/null +++ b/Migrators/XRayExporter/Models/XRayFolders.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace XRayExporter.Models; + +public class XRayFolders +{ + [JsonPropertyName("folders")] + public List Folders { get; set; } +} + +public class XrayFolder +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("folders")] + public List Folders { get; set; } +} + diff --git a/Migrators/XRayExporter/Models/XRayTests.cs b/Migrators/XRayExporter/Models/XRayTests.cs new file mode 100644 index 0000000..6b26c63 --- /dev/null +++ b/Migrators/XRayExporter/Models/XRayTests.cs @@ -0,0 +1,97 @@ +using System.Text.Json.Serialization; + +namespace XRayExporter.Models; + +public class XRayTests +{ + [JsonPropertyName("tests")] + public List Tests { get; set; } +} + +public class XRayTest +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("key")] + public string Key { get; set; } +} + +public class XRayTestFull : XRayTest +{ + [JsonPropertyName("self")] + public string Self { get; set; } + + [JsonPropertyName("reporter")] + public string Reporter { get; set; } + + [JsonPropertyName("precondition")] + public List Preconditions { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("archived")] + public bool Archived { get; set; } + + [JsonPropertyName("definition")] + public Definition Definition { get; set; } +} + +public class Precondition +{ + [JsonPropertyName("condition")] + public string Condition { get; set; } +} + +public class Definition +{ + [JsonPropertyName("steps")] + public List Steps { get; set; } +} + +public class Steps +{ + [JsonPropertyName("step")] + public Step Step { get; set; } + + [JsonPropertyName("data")] + public Data Data { get; set; } + + [JsonPropertyName("result")] + public Result Result { get; set; } + + [JsonPropertyName("attachments")] + public List Attachments { get; set; } +} + +public class Step +{ + [JsonPropertyName("rendered")] + public string Rendered { get; set; } +} + +public class Data +{ + [JsonPropertyName("rendered")] + public string Rendered { get; set; } +} + +public class Result +{ + [JsonPropertyName("rendered")] + public string Rendered { get; set; } +} + +public class XRayAttachments +{ + [JsonPropertyName("fileName")] + public string FileName { get; set; } + + [JsonPropertyName("fileURL")] + public string FileURL { get; set; } +} + diff --git a/Migrators/XRayExporter/Program.cs b/Migrators/XRayExporter/Program.cs new file mode 100644 index 0000000..74b80f0 --- /dev/null +++ b/Migrators/XRayExporter/Program.cs @@ -0,0 +1,73 @@ +using JsonWriter; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using Serilog.Expressions; +using Serilog.Settings.Configuration; +using XRayExporter.Client; +using XRayExporter.Services; + +namespace XRayExporter +{ + internal class Program + { + static void Main(string[] args) + { + using var host = CreateHostBuilder(args).Build(); + using var scope = host.Services.CreateScope(); + + var services = scope.ServiceProvider; + + try + { + services.GetRequiredService().Run(args); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + + static IHostBuilder CreateHostBuilder(string[] strings) + { + var options = new ConfigurationReaderOptions(typeof(ConsoleLoggerConfigurationExtensions).Assembly, + typeof(SerilogExpression).Assembly); + + return Host.CreateDefaultBuilder() + .UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration, options) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .MinimumLevel.Debug() + .WriteTo.File("logs/log.txt", + restrictedToMinimumLevel: LogEventLevel.Debug, + outputTemplate: + "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}" + ) + .WriteTo.Console(LogEventLevel.Information) + ) + .ConfigureServices((_, services) => + { + services.AddSingleton(); + services.AddSingleton(SetupConfiguration()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + } + + private static IConfiguration SetupConfiguration() + { + return new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("xray.config.json") + .AddEnvironmentVariables() + .Build(); + } + } +} diff --git a/Migrators/XRayExporter/Readme.md b/Migrators/XRayExporter/Readme.md new file mode 100644 index 0000000..78ba935 --- /dev/null +++ b/Migrators/XRayExporter/Readme.md @@ -0,0 +1,56 @@ +# XRay Exporter + +You can use this exporter to export your test cases from XRay. + +## Download + +You can download the latest version of the XRayExporter from the [releases](https://github.com/testit-tms/migrators/releases/latest) page. + +## How to use + +1. Configure connection in the `xray.config.json` file and save it in the XRayExporter location. + +```json +{ + "resultPath" : "/Users/user01/Documents/importer/xray", + "xray" : { + "url" : "https://jira.server.ru/", + "token" : "MTg1Mzk4NDQxMTE3Ojy/3+hs+mkQ8cqhpKh0bMUPwU3e", + "projectKey" : "XRAYT" + } +} +``` + +Where: + +- resultPath - path to the folder where the results will be saved +- xray.url - url to the XRay server with organization name +- xray.token - token for access to the XRay server +- xray.projectKey - key of the project in the XRay server + +1. Run the exporter with the following command: + +```bash +.\XRayExporter +``` + +3. Check the results in the folder specified in the `resultPath` parameter. + +4. Use the results in the [importer](https://github.com/testit-tms/migrators/tree/main/Migrators/Importer/Readme.md). + +## Contributing + +You can help to develop the project. Any contributions are **greatly appreciated**. + +- If you have suggestions for adding or removing projects, feel free + to [open an issue](https://github.com/testit-tms/migrators/issues/new) to discuss it, or create a direct pull + request after you edit the *README.md* file with necessary changes. +- Make sure to check your spelling and grammar. +- Create individual PR for each suggestion. +- Read the [Code Of Conduct](https://github.com/testit-tms/migrators/blob/main/CODE_OF_CONDUCT.md) before posting + your first idea as well. + +## License + +Distributed under the Apache-2.0 License. +See [LICENSE](https://github.com/testit-tms/migrators/blob/main/LICENSE) for more information. diff --git a/Migrators/XRayExporter/Services/AttachmentService.cs b/Migrators/XRayExporter/Services/AttachmentService.cs new file mode 100644 index 0000000..7c29534 --- /dev/null +++ b/Migrators/XRayExporter/Services/AttachmentService.cs @@ -0,0 +1,30 @@ +using JsonWriter; +using Microsoft.Extensions.Logging; +using XRayExporter.Client; + +namespace XRayExporter.Services; + +public class AttachmentService : IAttachmentService +{ + private readonly ILogger _logger; + private readonly IClient _client; + private readonly IWriteService _writeService; + + public AttachmentService(ILogger logger, IClient client, IWriteService writeService) + { + _logger = logger; + _client = client; + _writeService = writeService; + } + + public async Task DownloadAttachment(Guid testCase, string link, string filename) + { + _logger.LogInformation("Downloading attachment {Filename}", filename); + + var bytes = await _client.DownloadAttachment(link); + + var name = await _writeService.WriteAttachment(testCase, bytes, filename); + + return name; + } +} diff --git a/Migrators/XRayExporter/Services/ExportService.cs b/Migrators/XRayExporter/Services/ExportService.cs new file mode 100644 index 0000000..9fa093a --- /dev/null +++ b/Migrators/XRayExporter/Services/ExportService.cs @@ -0,0 +1,61 @@ +using JsonWriter; +using Microsoft.Extensions.Logging; +using Models; +using XRayExporter.Client; + +namespace XRayExporter.Services; + +public class ExportService : IExportService +{ + private readonly ILogger _logger; + private readonly IClient _client; + private readonly ISectionService _sectionService; + private readonly ITestCaseService _testCaseService; + private readonly IWriteService _writeService; + + public ExportService(ILogger logger, IClient client, ISectionService sectionService, + ITestCaseService testCaseService, IWriteService writeService) + { + _logger = logger; + _client = client; + _sectionService = sectionService; + _testCaseService = testCaseService; + _writeService = writeService; + } + + public async Task ExportProject() + { + _logger.LogInformation("Exporting project..."); + + var project = await _client.GetProject(); + var sections = await _sectionService.ConvertSections(); + var testCases = await _testCaseService.ConvertTestCases(sections.SectionMap); + + foreach (var sharedStep in testCases.SharedSteps) + { + await _writeService.WriteSharedStep(sharedStep); + } + + foreach (var testCase in testCases.TestCases) + { + await _writeService.WriteTestCase(testCase); + } + + var root = new Root + { + ProjectName = project.Name, + Attributes = testCases.Attributes, + Sections = sections.Sections, + SharedSteps = testCases.SharedSteps + .Select(s => s.Id) + .ToList(), + TestCases = testCases.TestCases + .Select(t => t.Id) + .ToList() + }; + + await _writeService.WriteMainJson(root); + + _logger.LogInformation("Project exported"); + } +} diff --git a/Migrators/XRayExporter/Services/IAttachmentService.cs b/Migrators/XRayExporter/Services/IAttachmentService.cs new file mode 100644 index 0000000..00f0eaa --- /dev/null +++ b/Migrators/XRayExporter/Services/IAttachmentService.cs @@ -0,0 +1,6 @@ +namespace XRayExporter.Services; + +public interface IAttachmentService +{ + Task DownloadAttachment(Guid testCase, string link, string filename); +} diff --git a/Migrators/XRayExporter/Services/IExportService.cs b/Migrators/XRayExporter/Services/IExportService.cs new file mode 100644 index 0000000..c3d8a90 --- /dev/null +++ b/Migrators/XRayExporter/Services/IExportService.cs @@ -0,0 +1,6 @@ +namespace XRayExporter.Services; + +public interface IExportService +{ + Task ExportProject(); +} diff --git a/Migrators/XRayExporter/Services/ISectionService.cs b/Migrators/XRayExporter/Services/ISectionService.cs new file mode 100644 index 0000000..f7dd0e5 --- /dev/null +++ b/Migrators/XRayExporter/Services/ISectionService.cs @@ -0,0 +1,8 @@ +using XRayExporter.Models; + +namespace XRayExporter.Services; + +public interface ISectionService +{ + Task ConvertSections(); +} diff --git a/Migrators/XRayExporter/Services/ITestCaseService.cs b/Migrators/XRayExporter/Services/ITestCaseService.cs new file mode 100644 index 0000000..16646a0 --- /dev/null +++ b/Migrators/XRayExporter/Services/ITestCaseService.cs @@ -0,0 +1,8 @@ +using XRayExporter.Models; + +namespace XRayExporter.Services; + +public interface ITestCaseService +{ + Task ConvertTestCases(Dictionary sectionMap); +} diff --git a/Migrators/XRayExporter/Services/SectionService.cs b/Migrators/XRayExporter/Services/SectionService.cs new file mode 100644 index 0000000..f10ab15 --- /dev/null +++ b/Migrators/XRayExporter/Services/SectionService.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Logging; +using Models; +using XRayExporter.Client; +using XRayExporter.Models; +using Step = Models.Step; + +namespace XRayExporter.Services; + +public class SectionService : ISectionService +{ + private readonly ILogger _logger; + private readonly IClient _client; + private readonly Dictionary _sectionMap; + + public SectionService(ILogger logger, IClient client) + { + _logger = logger; + _client = client; + _sectionMap = new Dictionary(); + } + + public async Task ConvertSections() + { + _logger.LogInformation("Converting sections"); + + var folders = await _client.GetFolders(); + + var sections = ConvertSections(folders); + + var sectionData = new SectionData + { + Sections = sections, + SectionMap = _sectionMap + }; + + _logger.LogInformation("Sections converted"); + + return sectionData; + } + + private List
ConvertSections(IEnumerable folders) + { + var xrayFolders = folders.ToList(); + + if (!xrayFolders.Any()) + { + return new List
(); + } + + var sections = new List
(); + + foreach (var child in xrayFolders) + { + _logger.LogDebug("Converting folder {@Folder}", child); + + var section = new Section + { + Id = Guid.NewGuid(), + Name = child.Name, + Sections = ConvertSections(child.Folders), + PostconditionSteps = new List(), + PreconditionSteps = new List() + }; + + sections.Add(section); + _sectionMap.Add(child.Id, section.Id); + } + + return sections; + } +} diff --git a/Migrators/XRayExporter/Services/TestCaseService.cs b/Migrators/XRayExporter/Services/TestCaseService.cs new file mode 100644 index 0000000..61749a1 --- /dev/null +++ b/Migrators/XRayExporter/Services/TestCaseService.cs @@ -0,0 +1,344 @@ +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Models; +using XRayExporter.Client; +using XRayExporter.Models; +using Attribute = Models.Attribute; +using Constants = XRayExporter.Models.Constants; +using Step = Models.Step; + +namespace XRayExporter.Services; + +public class TestCaseService : ITestCaseService +{ + private readonly ILogger _logger; + private readonly IClient _client; + private readonly IAttachmentService _attachmentService; + private readonly Dictionary _attributeMap; + private readonly Dictionary _sharedSteps; + private readonly Dictionary _testCases; + + private const int DefaultDuration = 10000; + private const string SharedStepMark = "This step was calling test issue"; + private const string SharedStepRegex = "href=\"([^\"]+)\""; + + public TestCaseService(ILogger logger, IClient client, IAttachmentService attachmentService) + { + _logger = logger; + _client = client; + _attachmentService = attachmentService; + _attributeMap = new Dictionary(); + _sharedSteps = new Dictionary(); + _testCases = new Dictionary(); + } + + public async Task ConvertTestCases(Dictionary sectionMap) + { + _logger.LogInformation("Converting test cases"); + + InitializeAttributes(); + + foreach (var section in sectionMap) + { + var testCasesFromFolder = await _client.GetTestFromFolder(section.Key); + + foreach (var test in testCasesFromFolder) + { + if (_sharedSteps.TryGetValue(test.Key, out var sharedStep)) + { + sharedStep.SectionId = section.Value; + continue; + } + + var testCase = await _client.GetTest(test.Key); + var item = await _client.GetItem(testCase.Self); + var testCaseId = Guid.NewGuid(); + var steps = await ConvertStep(testCaseId, testCase); + var attachments = await ConvertAttachments(testCaseId, item.Fields.Attachments); + + steps.ForEach(s => + attachments.AddRange(s.ActionAttachments) + ); + + var newTestCase = new TestCase + { + Id = testCaseId, + Name = item.Fields.Summary, + Description = item.Fields.Description, + Steps = steps, + Attributes = ConvertAttributes(testCase), + PreconditionSteps = ConvertPreconditionSteps(testCase.Preconditions), + PostconditionSteps = new List(), + Attachments = attachments, + Duration = DefaultDuration, + State = StateType.NotReady, + Priority = PriorityType.Medium, + SectionId = section.Value, + Tags = item.Fields.Labels, + Iterations = new List(), + Links = ConvertLink(item.Fields.IssueLinks) + }; + + _testCases.Add(test.Key, newTestCase); + } + } + + return new TestCaseData + { + TestCases = _testCases.Values.ToList(), + SharedSteps = _sharedSteps.Values.ToList(), + Attributes = _attributeMap.Values.ToList() + }; + } + + private void InitializeAttributes() + { + _attributeMap[Constants.XrayReporter] = new Attribute + { + Id = Guid.NewGuid(), + Name = Constants.XrayReporter, + Type = AttributeType.String, + Options = new List(), + IsRequired = false, + IsActive = true + }; + + _attributeMap[Constants.XrayType] = new Attribute + { + Id = Guid.NewGuid(), + Name = Constants.XrayType, + Type = AttributeType.Options, + Options = new List(), + IsRequired = false, + IsActive = true + }; + + _attributeMap[Constants.XrayStatus] = new Attribute + { + Id = Guid.NewGuid(), + Name = Constants.XrayStatus, + Type = AttributeType.Options, + Options = new List(), + IsRequired = false, + IsActive = true + }; + + _attributeMap[Constants.XrayArchived] = new Attribute + { + Id = Guid.NewGuid(), + Name = Constants.XrayArchived, + Type = AttributeType.Options, + Options = new List(), + IsRequired = false, + IsActive = true + }; + } + + private List ConvertAttributes(XRayTestFull test) + { + var attributes = new List(); + + if (!_attributeMap[Constants.XrayType].Options + .Any(o => o.Equals(test.Type, StringComparison.InvariantCultureIgnoreCase))) + { + _attributeMap[Constants.XrayType].Options.Add(test.Type); + } + + attributes.Add(new CaseAttribute + { + Id = _attributeMap[Constants.XrayType].Id, + Value = test.Type + }); + + if (!_attributeMap[Constants.XrayStatus].Options + .Any(o => o.Equals(test.Status, StringComparison.InvariantCultureIgnoreCase))) + { + _attributeMap[Constants.XrayStatus].Options.Add(test.Status); + } + + attributes.Add(new CaseAttribute + { + Id = _attributeMap[Constants.XrayStatus].Id, + Value = test.Status + }); + + if (!_attributeMap[Constants.XrayArchived].Options + .Any(o => o.Equals(test.Archived.ToString(), StringComparison.InvariantCultureIgnoreCase))) + { + _attributeMap[Constants.XrayArchived].Options.Add(test.Archived.ToString()); + } + + attributes.Add(new CaseAttribute + { + Id = _attributeMap[Constants.XrayArchived].Id, + Value = test.Archived.ToString() + }); + + attributes.Add(new CaseAttribute + { + Id = _attributeMap[Constants.XrayReporter].Id, + Value = test.Reporter + }); + + return attributes; + } + + private async Task> ConvertStep(Guid testCaseId, XRayTestFull test) + { + var steps = new List(); + + foreach (var step in test.Definition.Steps) + { + if (step.Step.Rendered.Contains(SharedStepMark, StringComparison.InvariantCultureIgnoreCase)) + { + var match = Regex.Match(step.Step.Rendered, SharedStepRegex); + + if (match.Success) + { + var sharedStepId = await ConvertSharedStep(match.Groups[1].Value.Split("/").Last()); + + steps.Add(new Step + { + SharedStepId = sharedStepId, + Action = string.Empty, + Expected = string.Empty, + TestData = string.Empty, + ActionAttachments = new List(), + ExpectedAttachments = new List(), + TestDataAttachments = new List() + }); + + continue; + } + } + + steps.Add(new Step + { + Action = step.Step.Rendered, + Expected = step.Result.Rendered, + TestData = step.Data.Rendered, + ActionAttachments = new List(), + ExpectedAttachments = new List(), + TestDataAttachments = new List() + }); + + foreach (var attachment in step.Attachments) + { + var attachmentName = + await _attachmentService.DownloadAttachment(testCaseId, attachment.FileURL, attachment.FileName); + steps.Last().ActionAttachments.Add(attachmentName); + steps.Last().Action += $"

<<<{attachmentName}>>>

"; + } + } + + return steps; + } + + private static List ConvertPreconditionSteps(IEnumerable preconditions) + { + return preconditions + .Select(p => new Step + { + Action = p.Condition, + Expected = string.Empty, + TestData = string.Empty, + ActionAttachments = new List(), + ExpectedAttachments = new List(), + TestDataAttachments = new List() + } + ) + .ToList(); + } + + private async Task> ConvertAttachments(Guid testCaseId, List attachments) + { + var attachmentNames = new List(); + + foreach (var attachment in attachments) + { + var attachmentName = + await _attachmentService.DownloadAttachment(testCaseId, attachment.Content, attachment.FileName); + attachmentNames.Add(attachmentName); + } + + return attachmentNames; + } + + private async Task ConvertSharedStep(string itemKey) + { + if (_sharedSteps.TryGetValue(itemKey, out var step)) + { + return step.Id; + } + + if (_testCases.TryGetValue(itemKey, out var existingTestCase)) + { + var convertedSharedStep = new SharedStep + { + Id = existingTestCase.Id, + Name = existingTestCase.Name, + Description = existingTestCase.Description, + Steps = existingTestCase.Steps, + Attributes = existingTestCase.Attributes, + Attachments = existingTestCase.Attachments, + State = StateType.NotReady, + Priority = PriorityType.Medium, + Tags = existingTestCase.Tags, + Links = existingTestCase.Links, + SectionId = existingTestCase.SectionId + }; + + _testCases.Remove(itemKey); + _sharedSteps.Add(itemKey, convertedSharedStep); + + return convertedSharedStep.Id; + } + + var testCase = await _client.GetTest(itemKey); + var item = await _client.GetItem(testCase.Self); + var testCaseId = Guid.NewGuid(); + var steps = await ConvertStep(testCaseId, testCase); + var attachments = await ConvertAttachments(testCaseId, item.Fields.Attachments); + + var sharedStep = new SharedStep + { + Id = testCaseId, + Name = item.Fields.Summary, + Description = item.Fields.Description, + Steps = steps, + Attributes = ConvertAttributes(testCase), + Attachments = attachments, + State = StateType.NotReady, + Priority = PriorityType.Medium, + Tags = item.Fields.Labels, + Links = new List() + }; + + _sharedSteps.Add(itemKey, sharedStep); + + return testCaseId; + } + + private static List ConvertLink(IEnumerable jiraLinks) + { + var links = new List(); + + foreach (var jiraLink in jiraLinks) + { + var url = jiraLink.InwardIssue != null + ? $"{jiraLink.InwardIssue.Self.Split("/rest").First()}/browse/{jiraLink.InwardIssue.Key}" + : $"{jiraLink.OutwardIssue?.Self.Split("/rest").First()}/browse/{jiraLink.OutwardIssue?.Key}"; + + var newLink = new Link + { + Title = jiraLink.Type.Name, + Description = jiraLink.Type.Inward, + Url = url + }; + + links.Add(newLink); + } + + return links; + } +} diff --git a/Migrators/XRayExporter/XRayExporter.csproj b/Migrators/XRayExporter/XRayExporter.csproj new file mode 100644 index 0000000..57f6ccf --- /dev/null +++ b/Migrators/XRayExporter/XRayExporter.csproj @@ -0,0 +1,36 @@ + + + + Exe + net7.0 + enable + enable + true + true + win-x64;linux-x64;osx-x64 + + + + + + + + + + + + + + + + + Always + + + + + + + + + diff --git a/Migrators/XRayExporter/xray.config.json b/Migrators/XRayExporter/xray.config.json new file mode 100644 index 0000000..65a6322 --- /dev/null +++ b/Migrators/XRayExporter/xray.config.json @@ -0,0 +1,8 @@ +{ + "resultPath" : "/Users/user01/Documents/importer/xray", + "xray" : { + "url" : "https://jira.server.ru/", + "token" : "MTg1Mzk4NDQxMTE3Ojy/3+hs+mkQ8cqhpKh0bMUPwU3e", + "projectKey" : "XRAYT" + } +} diff --git a/Migrators/XRayExporterTests/AttachmentServiceTests.cs b/Migrators/XRayExporterTests/AttachmentServiceTests.cs new file mode 100644 index 0000000..339a402 --- /dev/null +++ b/Migrators/XRayExporterTests/AttachmentServiceTests.cs @@ -0,0 +1,87 @@ +using System.Text; +using JsonWriter; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NUnit.Framework; +using XRayExporter.Client; +using XRayExporter.Services; + +namespace XRayExporterTests; + +public class AttachmentServiceTests +{ + private ILogger _logger; + private IClient _client; + private IWriteService _writeService; + + [SetUp] + public void Setup() + { + _logger = Substitute.For>(); + _client = Substitute.For(); + _writeService = Substitute.For(); + } + + [Test] + public async Task DownloadAttachment_Success() + { + // Arrange + var bytes = Encoding.UTF8.GetBytes("Test"); + var guid = Guid.NewGuid(); + + _client.DownloadAttachment(Arg.Any()) + .Returns(bytes); + + _writeService.WriteAttachment(guid, bytes, Arg.Any()) + .Returns("Test.txt"); + + var service = new AttachmentService(_logger, _client, _writeService); + + // Act + var result = await service.DownloadAttachment(guid, "https://example.com/Test.txt", "Test.txt"); + + // Assert + Assert.That(result, Is.EqualTo("Test.txt")); + } + + [Test] + public async Task DownloadAttachment_FailedDownloadAttachment() + { + // Arrange + var guid = Guid.NewGuid(); + + _client.DownloadAttachment(Arg.Any()) + .Throws(new Exception("Failed to download attachment")); + + var service = new AttachmentService(_logger, _client, _writeService); + + // Act + Assert.ThrowsAsync(async () => + await service.DownloadAttachment(guid, "https://example.com/Test.txt", "Test.txt")); + + // Assert + await _writeService.DidNotReceive() + .WriteAttachment(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Test] + public async Task DownloadAttachment_FailedWriteAttachment() + { + // Arrange + var bytes = Encoding.UTF8.GetBytes("Test"); + var guid = Guid.NewGuid(); + + _client.DownloadAttachment(Arg.Any()) + .Returns(bytes); + + _writeService.WriteAttachment(guid, bytes, Arg.Any()) + .Throws(new Exception("Failed to write attachment")); + + var service = new AttachmentService(_logger, _client, _writeService); + + // Act + Assert.ThrowsAsync(async () => + await service.DownloadAttachment(guid, "https://example.com/Test.txt", "Test.txt")); + } +} diff --git a/Migrators/XRayExporterTests/ExportServiceTests.cs b/Migrators/XRayExporterTests/ExportServiceTests.cs new file mode 100644 index 0000000..8a7a221 --- /dev/null +++ b/Migrators/XRayExporterTests/ExportServiceTests.cs @@ -0,0 +1,302 @@ +using JsonWriter; +using Microsoft.Extensions.Logging; +using Models; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NUnit.Framework; +using XRayExporter.Client; +using XRayExporter.Models; +using XRayExporter.Services; +using Attribute = Models.Attribute; +using Step = Models.Step; +using TestCaseData = XRayExporter.Models.TestCaseData; + +namespace XRayExporterTests; + +public class ExportServiceTests +{ + private ILogger _logger; + private IClient _client; + private ISectionService _sectionService; + private ITestCaseService _testCaseService; + private IWriteService _writeService; + + private JiraProject _jiraProject; + private SectionData _sectionData; + private TestCaseData _testCaseData; + + [SetUp] + public void Setup() + { + _logger = Substitute.For>(); + _client = Substitute.For(); + _sectionService = Substitute.For(); + _testCaseService = Substitute.For(); + _writeService = Substitute.For(); + + _jiraProject = new JiraProject + { + Key = "Xray", + Name = "My project" + }; + + var sectionId = Guid.NewGuid(); + _sectionData = new SectionData + { + Sections = new List
+ { + new() + { + Id = sectionId, + Name = "Section 1", + PreconditionSteps = new List(), + PostconditionSteps = new List(), + Sections = new List
() + } + }, + SectionMap = new Dictionary + { + { 1, sectionId } + } + }; + + _testCaseData = new TestCaseData + { + Attributes = new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Attribute 1", + IsActive = true, + IsRequired = false, + Type = AttributeType.String, + Options = new List() + } + }, + SharedSteps = new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Shared step 1" + } + }, + TestCases = new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Test case 1" + } + } + }; + } + + [Test] + public async Task ExportProject_FailedGetProject() + { + // Arrange + _client.GetProject() + .Throws(new Exception("Failed to get project")); + + var exportService = new ExportService(_logger, _client, _sectionService, _testCaseService, _writeService); + + // Act + Assert.ThrowsAsync(async () => await exportService.ExportProject()); + + // Assert + await _sectionService.DidNotReceive() + .ConvertSections(); + + await _testCaseService.DidNotReceive() + .ConvertTestCases(Arg.Any>()); + + await _writeService.DidNotReceive() + .WriteSharedStep(Arg.Any()); + + await _writeService.DidNotReceive() + .WriteTestCase(Arg.Any()); + + await _writeService.DidNotReceive() + .WriteMainJson(Arg.Any()); + } + + [Test] + public async Task ExportProject_FailedConvertSections() + { + // Arrange + _client.GetProject() + .Returns(_jiraProject); + + _sectionService.ConvertSections() + .Throws(new Exception("Failed to convert sections")); + + var exportService = new ExportService(_logger, _client, _sectionService, _testCaseService, _writeService); + + // Act + Assert.ThrowsAsync(async () => await exportService.ExportProject()); + + // Assert + await _testCaseService.DidNotReceive() + .ConvertTestCases(Arg.Any>()); + + await _writeService.DidNotReceive() + .WriteSharedStep(Arg.Any()); + + await _writeService.DidNotReceive() + .WriteTestCase(Arg.Any()); + + await _writeService.DidNotReceive() + .WriteMainJson(Arg.Any()); + } + + [Test] + public async Task ExportProject_FailedConvertDidNotReceive() + { + // Arrange + _client.GetProject() + .Returns(_jiraProject); + + _sectionService.ConvertSections() + .Returns(_sectionData); + + _testCaseService.ConvertTestCases(_sectionData.SectionMap) + .Throws(new Exception("Failed to convert test cases")); + + var exportService = new ExportService(_logger, _client, _sectionService, _testCaseService, _writeService); + + // Act + Assert.ThrowsAsync(async () => await exportService.ExportProject()); + + // Assert + await _writeService.DidNotReceive() + .WriteSharedStep(Arg.Any()); + + await _writeService.DidNotReceive() + .WriteTestCase(Arg.Any()); + + await _writeService.DidNotReceive() + .WriteMainJson(Arg.Any()); + } + + [Test] + public async Task ExportProject_FailedWriteSharedStep() + { + // Arrange + _client.GetProject() + .Returns(_jiraProject); + + _sectionService.ConvertSections() + .Returns(_sectionData); + + _testCaseService.ConvertTestCases(_sectionData.SectionMap) + .Returns(_testCaseData); + + _writeService.WriteSharedStep(_testCaseData.SharedSteps[0]) + .Throws(new Exception("Failed to write shared step")); + + var exportService = new ExportService(_logger, _client, _sectionService, _testCaseService, _writeService); + + // Act + Assert.ThrowsAsync(async () => await exportService.ExportProject()); + + // Assert + + await _writeService.DidNotReceive() + .WriteTestCase(Arg.Any()); + + await _writeService.DidNotReceive() + .WriteMainJson(Arg.Any()); + } + + [Test] + public async Task ExportProject_FailedWriteTestCase() + { + // Arrange + _client.GetProject() + .Returns(_jiraProject); + + _sectionService.ConvertSections() + .Returns(_sectionData); + + _testCaseService.ConvertTestCases(_sectionData.SectionMap) + .Returns(_testCaseData); + + _writeService.WriteTestCase(_testCaseData.TestCases[0]) + .Throws(new Exception("Failed to write test case")); + + var exportService = new ExportService(_logger, _client, _sectionService, _testCaseService, _writeService); + + // Act + Assert.ThrowsAsync(async () => await exportService.ExportProject()); + + // Assert + await _writeService.Received() + .WriteSharedStep(_testCaseData.SharedSteps[0]); + + await _writeService.DidNotReceive() + .WriteMainJson(Arg.Any()); + } + + [Test] + public async Task ExportProject_FailedWriteMainJson() + { + // Arrange + _client.GetProject() + .Returns(_jiraProject); + + _sectionService.ConvertSections() + .Returns(_sectionData); + + _testCaseService.ConvertTestCases(_sectionData.SectionMap) + .Returns(_testCaseData); + + _writeService.WriteMainJson(Arg.Any()) + .Throws(new Exception("Failed to write test case")); + + var exportService = new ExportService(_logger, _client, _sectionService, _testCaseService, _writeService); + + // Act + Assert.ThrowsAsync(async () => await exportService.ExportProject()); + + // Assert + await _writeService.Received() + .WriteSharedStep(_testCaseData.SharedSteps[0]); + + await _writeService.Received() + .WriteTestCase(_testCaseData.TestCases[0]); + } + + [Test] + public async Task ExportProject_Success() + { + // Arrange + _client.GetProject() + .Returns(_jiraProject); + + _sectionService.ConvertSections() + .Returns(_sectionData); + + _testCaseService.ConvertTestCases(_sectionData.SectionMap) + .Returns(_testCaseData); + + _writeService.WriteMainJson(Arg.Any()) + .Throws(new Exception("Failed to write test case")); + + var exportService = new ExportService(_logger, _client, _sectionService, _testCaseService, _writeService); + + // Act + Assert.ThrowsAsync(async () => await exportService.ExportProject()); + + // Assert + await _writeService.Received() + .WriteSharedStep(_testCaseData.SharedSteps[0]); + + await _writeService.Received() + .WriteTestCase(_testCaseData.TestCases[0]); + + await _writeService.Received() + .WriteMainJson(Arg.Any()); + } +} diff --git a/Migrators/XRayExporterTests/SectionServiceTests.cs b/Migrators/XRayExporterTests/SectionServiceTests.cs new file mode 100644 index 0000000..d6b80f7 --- /dev/null +++ b/Migrators/XRayExporterTests/SectionServiceTests.cs @@ -0,0 +1,115 @@ +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NUnit.Framework; +using XRayExporter.Client; +using XRayExporter.Models; +using XRayExporter.Services; + +namespace XRayExporterTests; + +public class SectionServiceTests +{ + private ILogger _logger; + private IClient _client; + + [SetUp] + public void Setup() + { + _logger = Substitute.For>(); + _client = Substitute.For(); + } + + [Test] + public async Task ConvertSections_WhenNoFolders_ReturnsEmptyList() + { + // Arrange + _client.GetFolders().Returns(new List()); + + var sectionService = new SectionService(_logger, _client); + + // Act + var result = await sectionService.ConvertSections(); + + // Assert + Assert.That(result.Sections, Is.Empty); + } + + [Test] + public async Task ConvertSections_WhenFolders_ReturnsSections() + { + // Arrange + var folders = new List + { + new() + { + Id = 1, + Name = "Folder 1", + Folders = new List + { + new() + { + Id = 2, + Name = "Folder 1.1", + Folders = new List + { + new() + { + Id = 3, + Name = "Folder 1.1.1", + Folders = new List() + } + } + }, + new() + { + Id = 4, + Name = "Folder 1.2", + Folders = new List() + } + } + }, + new() + { + Id = 5, + Name = "Folder 2", + Folders = new List() + } + }; + + _client.GetFolders().Returns(folders); + + var sectionService = new SectionService(_logger, _client); + + // Act + var result = await sectionService.ConvertSections(); + + // Assert + Assert.That(result.Sections, Has.Count.EqualTo(2)); + Assert.That(result.Sections[0].Name, Is.EqualTo("Folder 1")); + Assert.That(result.Sections[0].Sections, Has.Count.EqualTo(2)); + Assert.That(result.Sections[0].Sections[0].Name, Is.EqualTo("Folder 1.1")); + Assert.That(result.Sections[0].Sections[0].Sections, Has.Count.EqualTo(1)); + Assert.That(result.Sections[0].Sections[0].Sections[0].Name, Is.EqualTo("Folder 1.1.1")); + Assert.That(result.Sections[0].Sections[0].Sections[0].Sections, Is.Empty); + Assert.That(result.Sections[0].Sections[1].Name, Is.EqualTo("Folder 1.2")); + Assert.That(result.Sections[0].Sections[1].Sections, Is.Empty); + Assert.That(result.Sections[1].Name, Is.EqualTo("Folder 2")); + Assert.That(result.Sections[1].Sections, Is.Empty); + Assert.That(result.SectionMap, Has.Count.EqualTo(5)); + } + + [Test] + public async Task ConvertSections_FailedGetFolders() + { + // Arrange + _client.GetFolders() + .Throws(new Exception("Failed to get folders")); + + var sectionService = new SectionService(_logger, _client); + + // Act + Assert.ThrowsAsync(async () => await sectionService.ConvertSections()); + } + +} diff --git a/Migrators/XRayExporterTests/TestCaseServiceTests.cs b/Migrators/XRayExporterTests/TestCaseServiceTests.cs new file mode 100644 index 0000000..0e242b9 --- /dev/null +++ b/Migrators/XRayExporterTests/TestCaseServiceTests.cs @@ -0,0 +1,380 @@ +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NUnit.Framework; +using XRayExporter.Client; +using XRayExporter.Models; +using XRayExporter.Services; + +namespace XRayExporterTests; + +public class TestCaseServiceTests +{ + private ILogger _logger; + private IClient _client; + private IAttachmentService _attachmentService; + private Dictionary _sectionMap; + private List _tests; + private XRayTestFull _testCase; + private JiraItem _jiraItem; + private XRayTestFull _shareStep; + private JiraItem _jiraItem2; + + [SetUp] + public void Setup() + { + _logger = Substitute.For>(); + _client = Substitute.For(); + _attachmentService = Substitute.For(); + + _sectionMap = new Dictionary + { + { 1, Guid.NewGuid() } + }; + + _tests = new List + { + new() + { + Key = "TEST-1", + Id = 1 + } + }; + + _testCase = new XRayTestFull + { + Id = _tests[0].Id, + Key = _tests[0].Key, + Self = "https://xray.cloud.xpand-it.com/api/v2/tests/1", + Reporter = "admin", + Preconditions = new List + { + new() + { + Condition = "Precondition 1" + } + }, + Type = "Manual", + Status = "Approved", + Archived = false, + Definition = new Definition + { + Steps = new List + { + new() + { + Step = new Step + { + Rendered = "Step 1", + }, + Data = new Data + { + Rendered = "Data 1" + }, + Result = new Result + { + Rendered = "Result 1" + }, + Attachments = new List + { + new() + { + FileURL = "https://xray.cloud.xpand-it.com/secure/attachment/1/2.png", + FileName = "2.png" + } + } + }, + new() + { + Step = new Step + { + Rendered = + """

This step was calling test issue XRAYT-2 (possibly downgraded)

""", + }, + Data = new Data + { + Rendered = string.Empty + }, + Result = new Result + { + Rendered = string.Empty + }, + Attachments = new List() + } + } + } + }; + + _jiraItem = new JiraItem + { + Fields = new Fields + { + Description = "Description", + Attachments = new List + { + new() + { + Content = "https://xray.cloud.xpand-it.com/secure/attachment/1/1.png", + FileName = "1.png" + } + }, + Summary = "Summary", + Labels = new List + { + "Label 1", + "Label 2" + }, + IssueLinks = new List + { + new() + { + Type = new XRayExporter.Models.Type + { + Name = "Relates", + Inward = "is tested by" + }, + InwardIssue = new Issue + { + Key = "TEST-2", + Self = "https://xray.cloud.xpand-it.com/rest/api/2/issue/43321" + }, + OutwardIssue = null + }, + new() + { + Type = new XRayExporter.Models.Type + { + Name = "Problem/Incident", + Inward = "is caused by" + }, + InwardIssue = null, + OutwardIssue = new Issue + { + Key = "TEST-3", + Self = "https://xray.cloud.xpand-it.com/rest/api/3/issue/43321" + } + } + } + } + }; + + _shareStep = new XRayTestFull + { + Id = 2, + Key = "TEST-2", + Self = "https://xray.cloud.xpand-it.com/api/v2/tests/2", + Reporter = "admin", + Preconditions = new List + { + new() + { + Condition = "Precondition 1" + } + }, + Type = "Automated", + Status = "Draft", + Archived = false, + Definition = new Definition + { + Steps = new List() + } + }; + + _jiraItem2 = new JiraItem + { + Fields = new Fields + { + Description = "Description", + Attachments = new List(), + Summary = "Summary", + Labels = new List + { + "Label 1", + "Label 2" + }, + IssueLinks = new List() + } + }; + } + + [Test] + public async Task ConvertTestCases_FailedGetTestFromFolder() + { + // Arrange + _client.GetTestFromFolder(_sectionMap.First().Key) + .Throws(new Exception("Failed to get test from folder")); + + var testCaseService = new TestCaseService(_logger, _client, _attachmentService); + + // Act + Assert.ThrowsAsync(() => testCaseService.ConvertTestCases(_sectionMap)); + + // Assert + await _client.DidNotReceive() + .GetTest(Arg.Any()); + + await _client.DidNotReceive() + .GetItem(Arg.Any()); + + await _attachmentService.DidNotReceive() + .DownloadAttachment(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ConvertTestCases_FailedGetTest() + { + // Arrange + _client.GetTestFromFolder(_sectionMap.First().Key) + .Returns(_tests); + + _client.GetTest(_tests[0].Key) + .Throws(new Exception("Failed to get test")); + + var testCaseService = new TestCaseService(_logger, _client, _attachmentService); + + // Act + Assert.ThrowsAsync(() => testCaseService.ConvertTestCases(_sectionMap)); + + // Assert + await _client.DidNotReceive() + .GetItem(Arg.Any()); + + await _attachmentService.DidNotReceive() + .DownloadAttachment(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ConvertTestCases_FailedGetItem() + { + // Arrange + _client.GetTestFromFolder(_sectionMap.First().Key) + .Returns(_tests); + + _client.GetTest(_tests[0].Key) + .Returns(_testCase); + + _client.GetItem(_testCase.Self) + .Throws(new Exception("Failed to get item")); + + var testCaseService = new TestCaseService(_logger, _client, _attachmentService); + + // Act + Assert.ThrowsAsync(() => testCaseService.ConvertTestCases(_sectionMap)); + + // Assert + await _attachmentService.DidNotReceive() + .DownloadAttachment(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ConvertTestCases_FailedDownloadAttachment() + { + // Arrange + _client.GetTestFromFolder(_sectionMap.First().Key) + .Returns(_tests); + + _client.GetTest(_tests[0].Key) + .Returns(_testCase); + + _client.GetItem(_testCase.Self) + .Returns(_jiraItem); + + _attachmentService.DownloadAttachment(Arg.Any(), _testCase.Definition.Steps[0].Attachments[0].FileURL, + _testCase.Definition.Steps[0].Attachments[0].FileName) + .Throws(new Exception("Failed to download attachment")); + + var testCaseService = new TestCaseService(_logger, _client, _attachmentService); + + // Act + Assert.ThrowsAsync(() => testCaseService.ConvertTestCases(_sectionMap)); + } + + [Test] + public async Task ConvertTestCases_Success_WithSharedStep() + { + // Arrange + _client.GetTestFromFolder(_sectionMap.First().Key) + .Returns(_tests); + + _client.GetTest(_tests[0].Key) + .Returns(_testCase); + + _client.GetTest(_shareStep.Key) + .Returns(_shareStep); + + _client.GetItem(_testCase.Self) + .Returns(_jiraItem); + + _client.GetItem(_shareStep.Self) + .Returns(_jiraItem2); + + _attachmentService.DownloadAttachment(Arg.Any(), _testCase.Definition.Steps[0].Attachments[0].FileURL, + _testCase.Definition.Steps[0].Attachments[0].FileName) + .Returns(_testCase.Definition.Steps[0].Attachments[0].FileName); + + _attachmentService.DownloadAttachment(Arg.Any(), _jiraItem.Fields.Attachments[0].Content, + _jiraItem.Fields.Attachments[0].FileName) + .Returns(_jiraItem.Fields.Attachments[0].FileName); + + var testCaseService = new TestCaseService(_logger, _client, _attachmentService); + + // Act + var result = await testCaseService.ConvertTestCases(_sectionMap); + + // Assert + Assert.That(result.Attributes, Has.Count.EqualTo(4)); + Assert.That(result.Attributes[0].Name, Is.EqualTo(Constants.XrayReporter)); + Assert.That(result.Attributes[0].Options, Has.Count.EqualTo(0)); + Assert.That(result.Attributes[1].Name, Is.EqualTo(Constants.XrayType)); + Assert.That(result.Attributes[1].Options, Has.Count.EqualTo(2)); + Assert.That(result.Attributes[1].Options[1], Is.EqualTo("Manual")); + Assert.That(result.Attributes[1].Options[0], Is.EqualTo("Automated")); + Assert.That(result.Attributes[2].Name, Is.EqualTo(Constants.XrayStatus)); + Assert.That(result.Attributes[2].Options, Has.Count.EqualTo(2)); + Assert.That(result.Attributes[2].Options[1], Is.EqualTo("Approved")); + Assert.That(result.Attributes[2].Options[0], Is.EqualTo("Draft")); + Assert.That(result.Attributes[3].Name, Is.EqualTo(Constants.XrayArchived)); + Assert.That(result.Attributes[3].Options, Has.Count.EqualTo(1)); + Assert.That(result.Attributes[3].Options[0], Is.EqualTo("False")); + Assert.That(result.TestCases, Has.Count.EqualTo(1)); + Assert.That(result.TestCases[0].Name, Is.EqualTo(_jiraItem.Fields.Summary)); + Assert.That(result.TestCases[0].Description, Is.EqualTo(_jiraItem.Fields.Description)); + Assert.That(result.TestCases[0].PreconditionSteps, Has.Count.EqualTo(1)); + Assert.That(result.TestCases[0].PreconditionSteps[0].Action, Is.EqualTo(_testCase.Preconditions[0].Condition)); + Assert.That(result.TestCases[0].Steps, Has.Count.EqualTo(2)); + Assert.That(result.TestCases[0].Steps[0].Action, Is.EqualTo("Step 1

<<<2.png>>>

")); + Assert.That(result.TestCases[0].Steps[0].Expected, Is.EqualTo(_testCase.Definition.Steps[0].Result.Rendered)); + Assert.That(result.TestCases[0].Steps[0].TestData, Is.EqualTo(_testCase.Definition.Steps[0].Data.Rendered)); + Assert.That(result.TestCases[0].Steps[0].ActionAttachments, Has.Count.EqualTo(1)); + Assert.That(result.TestCases[0].Steps[0].ExpectedAttachments, Has.Count.EqualTo(0)); + Assert.That(result.TestCases[0].Steps[0].TestDataAttachments, Has.Count.EqualTo(0)); + Assert.That(result.TestCases[0].Steps[1].Action, Is.EqualTo(string.Empty)); + Assert.That(result.TestCases[0].Steps[1].Expected, Is.EqualTo(string.Empty)); + Assert.That(result.TestCases[0].Steps[1].TestData, Is.EqualTo(string.Empty)); + Assert.That(result.TestCases[0].Steps[1].ActionAttachments, Has.Count.EqualTo(0)); + Assert.That(result.TestCases[0].Steps[1].ExpectedAttachments, Has.Count.EqualTo(0)); + Assert.That(result.TestCases[0].Steps[1].TestDataAttachments, Has.Count.EqualTo(0)); + Assert.That(result.TestCases[0].Tags, Has.Count.EqualTo(2)); + Assert.That(result.TestCases[0].Tags[0], Is.EqualTo("Label 1")); + Assert.That(result.TestCases[0].Tags[1], Is.EqualTo("Label 2")); + Assert.That(result.TestCases[0].Links, Has.Count.EqualTo(2)); + Assert.That(result.TestCases[0].Links[0].Description, Is.EqualTo("is tested by")); + Assert.That(result.TestCases[0].Links[0].Url, Is.EqualTo("https://xray.cloud.xpand-it.com/browse/TEST-2")); + Assert.That(result.TestCases[0].Links[0].Title, Is.EqualTo("Relates")); + Assert.That(result.TestCases[0].Links[1].Description, Is.EqualTo("is caused by")); + Assert.That(result.TestCases[0].Links[1].Url, Is.EqualTo("https://xray.cloud.xpand-it.com/browse/TEST-3")); + Assert.That(result.TestCases[0].Links[1].Title, Is.EqualTo("Problem/Incident")); + Assert.That(result.TestCases[0].Attachments, Has.Count.EqualTo(2)); + Assert.That(result.TestCases[0].Attachments[0], Is.EqualTo("1.png")); + Assert.That(result.TestCases[0].Attachments[1], Is.EqualTo("2.png")); + Assert.That(result.SharedSteps, Has.Count.EqualTo(1)); + Assert.That(result.SharedSteps[0].Name, Is.EqualTo(_jiraItem2.Fields.Summary)); + Assert.That(result.SharedSteps[0].Description, Is.EqualTo(_jiraItem2.Fields.Description)); + Assert.That(result.SharedSteps[0].Steps, Has.Count.EqualTo(0)); + Assert.That(result.SharedSteps[0].Tags, Has.Count.EqualTo(2)); + Assert.That(result.SharedSteps[0].Tags[0], Is.EqualTo("Label 1")); + Assert.That(result.SharedSteps[0].Tags[1], Is.EqualTo("Label 2")); + Assert.That(result.SharedSteps[0].Links, Has.Count.EqualTo(0)); + Assert.That(result.SharedSteps[0].Attachments, Has.Count.EqualTo(0)); + } +} diff --git a/Migrators/XRayExporterTests/XRayExporterTests.csproj b/Migrators/XRayExporterTests/XRayExporterTests.csproj new file mode 100644 index 0000000..c738aa2 --- /dev/null +++ b/Migrators/XRayExporterTests/XRayExporterTests.csproj @@ -0,0 +1,32 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Migrators/ZephyrScaleExporter/Client/Client.cs b/Migrators/ZephyrScaleExporter/Client/Client.cs index a2a1ef4..153dda3 100644 --- a/Migrators/ZephyrScaleExporter/Client/Client.cs +++ b/Migrators/ZephyrScaleExporter/Client/Client.cs @@ -43,7 +43,7 @@ public Client(ILogger logger, IConfiguration configuration) public async Task GetProject() { - _logger.LogInformation("Getting project {projectName}", _projectName); + _logger.LogInformation("Getting project {ProjectName}", _projectName); var response = await _httpClient.GetAsync("projects"); if (!response.IsSuccessStatusCode) diff --git a/Migrators/ZephyrSquadExporter/Services/ExportService.cs b/Migrators/ZephyrSquadExporter/Services/ExportService.cs index 029cbc9..b8cdd7c 100644 --- a/Migrators/ZephyrSquadExporter/Services/ExportService.cs +++ b/Migrators/ZephyrSquadExporter/Services/ExportService.cs @@ -32,6 +32,7 @@ public ExportService(ILogger logger, IFolderService folderService _projectName = projectName; } + public async Task ExportProject() { _logger.LogInformation("Exporting project"); diff --git a/README.md b/README.md index 1267d23..616cf04 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,6 @@ We support the following systems: - [Azure DevOps](https://github.com/testit-tms/migrators/tree/main/Migrators/AzureExporter/Readme.md) - [Zephyr Scale](https://github.com/testit-tms/migrators/tree/main/Migrators/ZephyrScaleExporter/Readme.md) - [Zephyr Squad](https://github.com/testit-tms/migrators/tree/main/Migrators/ZephyrSquadExporter/Readme.md) +- [XRay](https://github.com/testit-tms/migrators/tree/main/Migrators/XrayExporter/Readme.md) We are constantly working on new migrators. If you need a migrator for a system that is not listed here, please contact us at [support@yoonion.ru](mailto:support@yoonion.ru).