├── .github └── workflows │ ├── angular.yaml │ ├── codacyCoverage.yml │ └── dotnet.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── doc └── img │ ├── cleanArchitecture.jpg │ ├── contentType.gif │ ├── fileApi_icon.png │ └── upload.gif └── src ├── File.API ├── Configuration │ └── ContainerConfigurationExtension.cs ├── EndpointBuilders │ └── FileEndpointsBuilder.cs ├── Extensions │ ├── FileExtensionContentTypeExtensions.cs │ └── IHandlerExtension.cs ├── File.API.csproj ├── Files │ └── FormFileProxy.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json ├── File.Core ├── Abstractions │ ├── IAddFilesCommandHandler.cs │ ├── IAddFilesCommandValidator.cs │ ├── IConvertToQueryHandler.cs │ ├── IConvertToQueryValidator.cs │ ├── IDownloadFileQueryHandler.cs │ ├── IExportFileQueryHandler.cs │ ├── IExportFileQueryValidator.cs │ ├── IFileByOptionsValidator.cs │ ├── IFileCommandsRepository.cs │ ├── IFileConvertService.cs │ ├── IFileQueriesRepository.cs │ └── IGetFilesInfoQueryHandler.cs ├── Commands │ └── AddFilesCommandHandler.cs ├── Configuration │ └── ContainerConfigurationExtension.cs ├── Extensions │ ├── FileExtensions.cs │ └── ValidotDependencyInjectionExtensions.cs ├── File.Core.csproj ├── Queries │ ├── ConvertToQueryHandler.cs │ ├── DownloadFileQueryHandler.cs │ ├── ExportFileQueryHandler.cs │ └── GetFilesInfoQueryHandler.cs ├── Resources │ ├── ErrorMessages.Designer.cs │ ├── ErrorMessages.resx │ ├── ValidationErrorMessages.Designer.cs │ └── ValidationErrorMessages.resx └── Validation │ ├── AddFileCommandSpecificationHolder.cs │ ├── AddFilesCommandValidator.cs │ ├── ConvertToQuerySpecificationHolder.cs │ ├── ConvertToQueryValidator.cs │ ├── DownloadFileQuerySpecificationHolder.cs │ ├── ExportFileQuerySpecificationHolder.cs │ ├── ExportFileQueryValidator.cs │ ├── FileByOptionsValidator.cs │ └── GeneralPredicates.cs ├── File.Domain ├── Abstractions │ └── IFileProxy.cs ├── Commands │ └── AddFilesCommand.cs ├── Dtos │ ├── BaseFileDto.cs │ ├── FileDto.cs │ ├── FileInfoDto.cs │ └── StringContentFileDto.cs ├── Extensions │ ├── EnumerableExtensions.cs │ ├── FileNameExtensions.cs │ └── FluentResultExtensions.cs ├── File.Domain.csproj ├── Logging │ └── LogEvents.cs ├── Options │ ├── AllowedFile.cs │ └── FilesOptions.cs └── Queries │ ├── ConvertToQuery.cs │ ├── DownloadFileQuery.cs │ └── ExportFileQuery.cs ├── File.Frontend ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── File.Frontend.esproj ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.model.ts │ │ ├── app.module.ts │ │ ├── components │ │ │ ├── select-extension-modal.component.html │ │ │ └── select-extension-modal.component.ts │ │ └── services │ │ │ ├── file-api.http.service.ts │ │ │ ├── file-loading.service.ts │ │ │ └── notification-adapter.service.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── File.Infrastructure ├── Abstractions │ ├── IEncodingFactory.cs │ ├── IFileConverter.cs │ └── IFileConverterFactory.cs ├── Configuration │ └── ContainerConfigurationExtension.cs ├── Database │ ├── EFContext │ │ ├── Entities │ │ │ └── FileEntity.cs │ │ └── FileContext.cs │ └── Repositories │ │ ├── FileCommandsRepository.cs │ │ └── FileQueriesRepository.cs ├── Extensions │ └── ResultExtensions.cs ├── File.Infrastructure.csproj ├── FileConversions │ ├── ConvertedFile.cs │ ├── Converters │ │ ├── JsonToXmlFileConverter.cs │ │ ├── JsonToYamlFileConverter.cs │ │ ├── XmlToJsonFileConverter.cs │ │ ├── XmlToYamlFileConverter.cs │ │ └── YamlToJsonFileConverter.cs │ ├── EncodingFactory.cs │ ├── FileConversionService.cs │ └── FileConverterFactory.cs └── Resources │ ├── ErrorMessages.Designer.cs │ └── ErrorMessages.resx ├── FileSol.sln ├── FileSol.slnLaunch ├── Tests ├── IntegrationTests │ └── File.Infrastructure.IntegrationTests │ │ ├── Assets │ │ ├── new.json │ │ ├── new.xml │ │ ├── new.yaml │ │ └── root.json │ │ ├── Extensions │ │ └── AssertExtensions.cs │ │ ├── File.Infrastructure.IntegrationTests.csproj │ │ ├── FileConversions │ │ └── Converters │ │ │ ├── JsonToXmlFileConverterTests.cs │ │ │ ├── JsonToYamlFileConverterTests.cs │ │ │ ├── XmlToJsonFileConverterTests.cs │ │ │ ├── XmlToYamlFileConverterTests.cs │ │ │ └── YamlToJsonFileConverterTests.cs │ │ └── Usings.cs ├── SystemTests │ └── File.API.SystemTests │ │ ├── Assets │ │ └── new.json │ │ ├── Extensions │ │ ├── HttpClientExtensions.cs │ │ ├── HttpResponseMessageExtensions.cs │ │ └── MultipartFormDataContentExtensions.cs │ │ ├── File.API.SystemTests.csproj │ │ ├── Tests │ │ ├── ConvertTests.cs │ │ ├── DownloadTests.cs │ │ ├── ExportTests.cs │ │ ├── GetTests.cs │ │ ├── SystemTestsBase.cs │ │ └── UploadTests.cs │ │ └── Usings.cs └── UnitTests │ ├── File.API.UnitTests │ ├── File.API.UnitTests.csproj │ └── Usings.cs │ ├── File.Core.UnitTests │ ├── Assets │ │ └── FileMockFactory.cs │ ├── Commands │ │ └── AddFilesCommandHandlerTests.cs │ ├── File.Core.UnitTests.csproj │ ├── Queries │ │ ├── ConvertToQueryHandlerTests.cs │ │ ├── DownloadFileQueryHandlerTests.cs │ │ └── ExportFileQueryHandlerTests.cs │ ├── Usings.cs │ └── Validation │ │ ├── AddFilesCommandValidatorTests.cs │ │ ├── ConvertToQueryValidatorTests.cs │ │ ├── ExportFileQueryValidatorTests.cs │ │ └── FileByOptionsValidatorTests.cs │ ├── File.Domain.UnitTests │ ├── File.Domain.UnitTests.csproj │ └── Usings.cs │ ├── File.Infrastructure.UnitTests │ ├── Assets │ │ ├── InvalidByteData.cs │ │ ├── InvalidConverters.cs │ │ ├── InvalidJsonData.cs │ │ ├── InvalidXmlData.cs │ │ ├── InvalidYamlData.cs │ │ ├── UknownByteData.cs │ │ └── ValidConverters.cs │ ├── Extensions │ │ └── AssertExtensions.cs │ ├── File.Infrastructure.UnitTests.csproj │ ├── FileConversions │ │ ├── Converters │ │ │ ├── JsonToXmlFileConverterTests.cs │ │ │ ├── JsonToYamlFileConverterTests.cs │ │ │ ├── XmlToJsonFileConverterTests.cs │ │ │ ├── XmlToYamlFileConverterTests.cs │ │ │ └── YamlToJsonFileConverterTests.cs │ │ ├── EncodingFactoryTests.cs │ │ ├── FileConversionServiceTests.cs │ │ └── FileConverterFactoryTests.cs │ └── Usings.cs │ └── File.UnitTests.Common │ ├── Extensions │ └── MoqLoggerExtensions.cs │ └── File.UnitTests.Common.csproj └── dotCover.Output.xml /.github/workflows/angular.yaml: -------------------------------------------------------------------------------- 1 | name: Angular Build 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: src/File.Frontend 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | - name: Setup NodeJS 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 20.11.x 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Build Example 26 | run: npm run build 27 | -------------------------------------------------------------------------------- /.github/workflows/codacyCoverage.yml: -------------------------------------------------------------------------------- 1 | name: Codacy Coverage 2 | 3 | on: ["push"] 4 | 5 | jobs: 6 | codacy-coverage-reporter: 7 | runs-on: ubuntu-latest 8 | name: codacy-coverage-reporter 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Run codacy-coverage-reporter 12 | uses: codacy/codacy-coverage-reporter-action@v1 13 | with: 14 | project-token: ${{ secrets.CODACY_API_TOKEN }} 15 | coverage-reports: ./src/dotCover.Output.xml 16 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET Build and Test 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | name: Build & Unit Test 16 | defaults: 17 | run: 18 | working-directory: src 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: 9.0.x 26 | - name: Restore dependencies 27 | run: dotnet restore 28 | - name: Build 29 | run: dotnet build --no-restore 30 | - name: Test 31 | run: dotnet test --filter FullyQualifiedName!~File.API.SystemTests --no-build --verbosity normal 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2022 Gramli 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /doc/img/cleanArchitecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gramli/FileApi/c70c3d71d7bee9efd4f1c033b448e34673318c36/doc/img/cleanArchitecture.jpg -------------------------------------------------------------------------------- /doc/img/contentType.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gramli/FileApi/c70c3d71d7bee9efd4f1c033b448e34673318c36/doc/img/contentType.gif -------------------------------------------------------------------------------- /doc/img/fileApi_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gramli/FileApi/c70c3d71d7bee9efd4f1c033b448e34673318c36/doc/img/fileApi_icon.png -------------------------------------------------------------------------------- /doc/img/upload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gramli/FileApi/c70c3d71d7bee9efd4f1c033b448e34673318c36/doc/img/upload.gif -------------------------------------------------------------------------------- /src/File.API/Configuration/ContainerConfigurationExtension.cs: -------------------------------------------------------------------------------- 1 | namespace File.API.Configuration 2 | { 3 | public static class ContainerConfigurationExtension 4 | { 5 | public static WebApplicationBuilder AddLogging(this WebApplicationBuilder builder) 6 | { 7 | builder.Logging 8 | .ClearProviders() 9 | .AddConsole(); 10 | 11 | return builder; 12 | } 13 | 14 | public static IServiceCollection AddCustomCors(this IServiceCollection services, string myAllowSpecificOrigins) 15 | => services.AddCors(options => 16 | { 17 | options.AddPolicy(name: myAllowSpecificOrigins, policy => 18 | { 19 | policy.WithOrigins("http://127.0.0.1:4200", "http://localhost:4200") 20 | .AllowAnyHeader() 21 | .AllowAnyMethod(); 22 | }); 23 | }); 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/File.API/EndpointBuilders/FileEndpointsBuilder.cs: -------------------------------------------------------------------------------- 1 | using File.API.Extensions; 2 | using File.API.Files; 3 | using File.Core.Abstractions; 4 | using File.Core.Queries; 5 | using File.Domain.Commands; 6 | using File.Domain.Dtos; 7 | using File.Domain.Queries; 8 | using Microsoft.AspNetCore.Http.HttpResults; 9 | using Microsoft.AspNetCore.Mvc; 10 | using SmallApiToolkit.Core.Extensions; 11 | using SmallApiToolkit.Core.Response; 12 | using SmallApiToolkit.Extensions; 13 | 14 | namespace File.API.EndpointBuilders 15 | { 16 | public static class FileEndpointsBuilder 17 | { 18 | public static IEndpointRouteBuilder BuildFileEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) 19 | { 20 | return endpointRouteBuilder 21 | .MapGroup("file") 22 | .MapVersionGroup(1) 23 | .BuildUploadEndpoints() 24 | .BuildDownloadEndpoints() 25 | .BuildGetEndpoints() 26 | .BuildParseEndpoints() 27 | .BuildExportEndpoints(); 28 | } 29 | 30 | private static IEndpointRouteBuilder BuildUploadEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) 31 | { 32 | endpointRouteBuilder.MapPost("upload", 33 | async (IFormFile file, [FromServices] IAddFilesCommandHandler handler, CancellationToken cancellationToken) => 34 | await handler.SendAsync(new AddFilesCommand([new FormFileProxy(file)]), cancellationToken)) 35 | .DisableAntiforgery() 36 | .ProducesDataResponse() 37 | .WithName("AddFiles") 38 | .WithTags("Post"); 39 | return endpointRouteBuilder; 40 | } 41 | 42 | private static IEndpointRouteBuilder BuildDownloadEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) 43 | { 44 | endpointRouteBuilder.MapGet("download", 45 | async ([FromQuery] int id, [FromServices] IDownloadFileQueryHandler handler, CancellationToken cancellationToken) => 46 | await handler.GetFileAsync(new DownloadFileQuery(id), cancellationToken)) 47 | .DisableAntiforgery() 48 | .Produces() 49 | .WithName("DownloadFile") 50 | .WithTags("Get"); 51 | 52 | endpointRouteBuilder.MapGet("downloadAsJson", 53 | async ([FromQuery] int id, [FromServices] IDownloadFileQueryHandler handler, CancellationToken cancellationToken) => 54 | await handler.GetJsonFileAsync(new DownloadFileQuery(id), cancellationToken)) 55 | .DisableAntiforgery() 56 | .ProducesDataResponse() 57 | .WithName("DownloadFileAsJson") 58 | .WithTags("Get"); 59 | 60 | return endpointRouteBuilder; 61 | } 62 | 63 | private static IEndpointRouteBuilder BuildGetEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) 64 | { 65 | endpointRouteBuilder.MapGet("files-info", 66 | async ([FromServices] IGetFilesInfoQueryHandler handler, CancellationToken cancellationToken) => 67 | await handler.SendAsync(EmptyRequest.Instance, cancellationToken)) 68 | .ProducesDataResponse>() 69 | .WithName("GetFilesInfo") 70 | .WithTags("Get"); 71 | return endpointRouteBuilder; 72 | } 73 | 74 | private static IEndpointRouteBuilder BuildParseEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) 75 | { 76 | endpointRouteBuilder.MapPost("export", 77 | async ([FromBody]ExportFileQuery parseFileQuery,[FromServices] IExportFileQueryHandler handler, CancellationToken cancellationToken) => 78 | await handler.GetFileAsync(parseFileQuery, cancellationToken)) 79 | .DisableAntiforgery() 80 | .ProducesDataResponse() 81 | .WithName("Export") 82 | .WithTags("Post"); 83 | return endpointRouteBuilder; 84 | } 85 | 86 | private static IEndpointRouteBuilder BuildExportEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) 87 | { 88 | endpointRouteBuilder.MapPost("convert", 89 | async (IFormFile file, [FromForm]string formatToConvert, [FromServices] IConvertToQueryHandler handler, CancellationToken cancellationToken) => 90 | await handler.GetFileAsync(new ConvertToQuery(new FormFileProxy(file), formatToConvert), cancellationToken)) 91 | .DisableAntiforgery() 92 | .ProducesDataResponse() 93 | .WithName("ConvertTo") 94 | .WithTags("Post"); 95 | return endpointRouteBuilder; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/File.API/Extensions/FileExtensionContentTypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.StaticFiles; 2 | 3 | namespace File.API.Extensions 4 | { 5 | internal static class FileExtensionContentTypeExtensions 6 | { 7 | internal static void AddCustomContentTypes(this WebApplication webApplication) 8 | { 9 | var provider = new FileExtensionContentTypeProvider(); 10 | provider.Mappings[".yaml"] = "text/yaml"; 11 | 12 | webApplication.UseStaticFiles(new StaticFileOptions 13 | { 14 | ContentTypeProvider = provider 15 | }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/File.API/Extensions/IHandlerExtension.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Dtos; 2 | using SmallApiToolkit.Core.RequestHandlers; 3 | using SmallApiToolkit.Core.Response; 4 | 5 | namespace File.API.Extensions 6 | { 7 | internal static class IHandlerExtension 8 | { 9 | internal static async Task GetJsonFileAsync(this IHttpRequestHandler requestHandler, TRequest request, CancellationToken cancellationToken) where TResponse : FileDto 10 | { 11 | var response = await requestHandler.HandleAsync(request, cancellationToken); 12 | if (response.Data is not null) 13 | { 14 | return Results.Json(new DataResponse 15 | { 16 | Errors = response.Errors, 17 | Data = new StringContentFileDto 18 | { 19 | Data = Convert.ToBase64String(response.Data.Data), 20 | ContentType = response.Data.ContentType, 21 | FileName = response.Data.FileName, 22 | Length = response.Data.Length 23 | }, 24 | }, statusCode: (int)response.StatusCode); 25 | } 26 | return Results.NotFound(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/File.API/File.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <_Parameter1>File.API.SystemTests 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/File.API/Files/FormFileProxy.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Domain.Abstractions; 3 | 4 | namespace File.API.Files 5 | { 6 | internal sealed class FormFileProxy : IFileProxy 7 | { 8 | private readonly IFormFile _formFile; 9 | public string ContentType => _formFile.ContentType; 10 | 11 | public long Length => _formFile.Length; 12 | 13 | public string FileName => _formFile.FileName; 14 | 15 | public FormFileProxy(IFormFile formFile) 16 | { 17 | _formFile = Guard.Against.Null(formFile); 18 | } 19 | 20 | public Task CopyToAsync(Stream target, CancellationToken cancellationToken = default) 21 | { 22 | return _formFile.CopyToAsync(target, cancellationToken); 23 | } 24 | 25 | public async Task GetData(CancellationToken cancellationToken = default) 26 | { 27 | using var stream = new MemoryStream(); 28 | await CopyToAsync(stream, cancellationToken); 29 | return stream.ToArray(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/File.API/Program.cs: -------------------------------------------------------------------------------- 1 | using File.API.Configuration; 2 | using File.API.EndpointBuilders; 3 | using File.API.Extensions; 4 | using File.Core.Configuration; 5 | using File.Infrastructure.Configuration; 6 | using SmallApiToolkit.Middleware; 7 | 8 | var myAllowSpecificOrigins = "_myAllowSpecificOrigins"; 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | builder.AddLogging(); 13 | builder.Services.AddInfrastructure(builder.Configuration); 14 | builder.Services.AddCore(builder.Configuration); 15 | 16 | builder.Services.AddEndpointsApiExplorer(); 17 | builder.Services.AddSwaggerGen(); 18 | 19 | builder.Services.AddCustomCors(myAllowSpecificOrigins); 20 | 21 | var app = builder.Build(); 22 | 23 | app.AddCustomContentTypes(); 24 | 25 | if (app.Environment.IsDevelopment()) 26 | { 27 | app.UseSwagger(); 28 | app.UseSwaggerUI(); 29 | } 30 | 31 | app.UseCors(myAllowSpecificOrigins); 32 | 33 | app.UseHttpsRedirection(); 34 | 35 | app.UseMiddleware(); 36 | app.BuildFileEndpoints(); 37 | 38 | app.Run(); 39 | -------------------------------------------------------------------------------- /src/File.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:16841", 8 | "sslPort": 44390 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5249", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7270;http://localhost:5249", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/File.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/File.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "Files": { 10 | "MaxFileLength": 1024, 11 | "AllowedFiles": [ 12 | { 13 | "format": "xml", 14 | "contentType": "text/xml", 15 | "canBeExportedTo": [ "json", "yaml" ] 16 | }, 17 | { 18 | "format": "yaml", 19 | "contentType": "text/yaml", 20 | "canBeExportedTo": [ "json" ] 21 | }, 22 | { 23 | "format": "json", 24 | "contentType": "application/json", 25 | "canBeExportedTo": [ "xml", "yaml" ] 26 | }, 27 | { 28 | "format": "txt", 29 | "contentType": "text/plain" 30 | }, 31 | { 32 | "format": "csv", 33 | "contentType": "text/csv" 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IAddFilesCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Commands; 2 | using SmallApiToolkit.Core.RequestHandlers; 3 | 4 | namespace File.Core.Abstractions 5 | { 6 | public interface IAddFilesCommandHandler : IHttpRequestHandler 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IAddFilesCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | using File.Domain.Commands; 3 | using FluentResults; 4 | using Validot.Results; 5 | 6 | namespace File.Core.Abstractions 7 | { 8 | public interface IAddFilesCommandValidator 9 | { 10 | Result Validate(AddFilesCommand addFilesCommand); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IConvertToQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Queries; 2 | using File.Domain.Dtos; 3 | using SmallApiToolkit.Core.RequestHandlers; 4 | 5 | namespace File.Core.Abstractions 6 | { 7 | public interface IConvertToQueryHandler : IHttpRequestHandler 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IConvertToQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Queries; 2 | using FluentResults; 3 | 4 | namespace File.Core.Abstractions 5 | { 6 | public interface IConvertToQueryValidator 7 | { 8 | Result Validate(ConvertToQuery convertToQuery); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IDownloadFileQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Dtos; 2 | using File.Domain.Queries; 3 | using SmallApiToolkit.Core.RequestHandlers; 4 | 5 | namespace File.Core.Abstractions 6 | { 7 | public interface IDownloadFileQueryHandler : IHttpRequestHandler 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IExportFileQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Dtos; 2 | using File.Domain.Queries; 3 | using SmallApiToolkit.Core.RequestHandlers; 4 | 5 | namespace File.Core.Abstractions 6 | { 7 | public interface IExportFileQueryHandler : IHttpRequestHandler 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IExportFileQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Queries; 2 | using File.Domain.Queries; 3 | using FluentResults; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace File.Core.Abstractions 11 | { 12 | public interface IExportFileQueryValidator 13 | { 14 | Result Validate(ExportFileQuery exportFileQuery); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IFileByOptionsValidator.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | using FluentResults; 3 | 4 | namespace File.Core.Abstractions 5 | { 6 | //TODO CONVERSION VALIDATION 7 | internal interface IFileByOptionsValidator 8 | { 9 | Result Validate(IFileProxy file); 10 | Result Validate(string extension); 11 | Result ValidateConversion(string sourceExtension, string destinationExtension); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IFileCommandsRepository.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Dtos; 2 | 3 | namespace File.Core.Abstractions 4 | { 5 | public interface IFileCommandsRepository 6 | { 7 | Task AddFileAsync(FileDto fileDto, CancellationToken cancellationToken); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IFileConvertService.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | using File.Domain.Dtos; 3 | using FluentResults; 4 | 5 | namespace File.Core.Abstractions 6 | { 7 | public interface IFileConvertService 8 | { 9 | Task> ConvertTo(IFileProxy sourceFile, string destinationFormat, CancellationToken cancellationToken); 10 | Task> ExportTo(FileDto sourceFile, string destinationFormat, CancellationToken cancellationToken); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IFileQueriesRepository.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Dtos; 2 | using File.Domain.Queries; 3 | using FluentResults; 4 | 5 | namespace File.Core.Abstractions 6 | { 7 | public interface IFileQueriesRepository 8 | { 9 | Task> GetFile(DownloadFileQuery downloadFileQuery, CancellationToken cancellationToken); 10 | Task> GetFilesInfo(CancellationToken cancellationToken); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/File.Core/Abstractions/IGetFilesInfoQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Dtos; 2 | using SmallApiToolkit.Core.RequestHandlers; 3 | using SmallApiToolkit.Core.Response; 4 | 5 | namespace File.Core.Abstractions 6 | { 7 | public interface IGetFilesInfoQueryHandler : IHttpRequestHandler, EmptyRequest> 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Core/Commands/AddFilesCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Core.Extensions; 4 | using File.Core.Resources; 5 | using File.Domain.Commands; 6 | using File.Domain.Extensions; 7 | using File.Domain.Logging; 8 | using Microsoft.Extensions.Logging; 9 | using SmallApiToolkit.Core.Extensions; 10 | using SmallApiToolkit.Core.Response; 11 | 12 | namespace File.Core.Commands 13 | { 14 | internal sealed class AddFilesCommandHandler : IAddFilesCommandHandler 15 | { 16 | private readonly IAddFilesCommandValidator _addFilesCommandValidator; 17 | private readonly IFileCommandsRepository _fileCommandsRepository; 18 | private readonly ILogger _logger; 19 | 20 | public AddFilesCommandHandler( 21 | IAddFilesCommandValidator addFilesCommandValidator, 22 | IFileCommandsRepository fileCommandsRepository, 23 | ILogger logger) 24 | { 25 | _addFilesCommandValidator = Guard.Against.Null(addFilesCommandValidator); 26 | _fileCommandsRepository = Guard.Against.Null(fileCommandsRepository); 27 | _logger = Guard.Against.Null(logger); 28 | } 29 | 30 | public async Task> HandleAsync(AddFilesCommand request, CancellationToken cancellationToken) 31 | { 32 | var validationResult = _addFilesCommandValidator.Validate(request); 33 | if(validationResult.IsFailed) 34 | { 35 | return HttpDataResponses.AsBadRequest(validationResult.Errors.ToErrorMessages()); 36 | } 37 | 38 | var errorMessages = new List(); 39 | 40 | await request.Files.ForEachAsync(async file => 41 | { 42 | try 43 | { 44 | var fileDto = await file.CreateFileDto(cancellationToken); 45 | var addResult = await _fileCommandsRepository.AddFileAsync(fileDto, cancellationToken); 46 | if(addResult < 0) 47 | { 48 | var message = string.Format(ErrorMessages.SaveFileFailed, file.FileName); 49 | _logger.LogError(LogEvents.AddFileStreamError, message); 50 | errorMessages.Add(message); 51 | } 52 | } 53 | catch(IOException ioException) 54 | { 55 | var message = string.Format(ErrorMessages.ReadFileStreamFailed, file.FileName); 56 | _logger.LogError(LogEvents.AddFileStreamError, ioException, message); 57 | errorMessages.Add(message); 58 | } 59 | }); 60 | 61 | return HttpDataResponses.AsOK(true, errorMessages); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/File.Core/Configuration/ContainerConfigurationExtension.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Core.Commands; 3 | using File.Core.Extensions; 4 | using File.Core.Queries; 5 | using File.Core.Validation; 6 | using File.Domain.Commands; 7 | using File.Domain.Options; 8 | using File.Domain.Queries; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Validot; 12 | 13 | namespace File.Core.Configuration 14 | { 15 | public static class ContainerConfigurationExtension 16 | { 17 | public static IServiceCollection AddCore(this IServiceCollection serviceCollection, IConfiguration configuration) 18 | { 19 | serviceCollection.Configure(configuration.GetSection(FilesOptions.Files)); 20 | 21 | return serviceCollection 22 | .AddCommandHandlers() 23 | .AddValidation(); 24 | } 25 | 26 | private static IServiceCollection AddCommandHandlers(this IServiceCollection serviceCollection) 27 | { 28 | return serviceCollection 29 | .AddScoped() 30 | .AddScoped() 31 | .AddScoped() 32 | .AddScoped() 33 | .AddScoped(); 34 | } 35 | 36 | private static IServiceCollection AddValidation(this IServiceCollection serviceCollection) 37 | { 38 | return serviceCollection 39 | .AddScoped() 40 | .AddScoped() 41 | .AddScoped() 42 | .AddScoped() 43 | .AddValidotSingleton, AddFileCommandSpecificationHolder, AddFilesCommand>() 44 | .AddValidotSingleton, DownloadFileQuerySpecificationHolder, DownloadFileQuery>() 45 | .AddValidotSingleton, ConvertToQuerySpecificationHolder, ConvertToQuery>() 46 | .AddValidotSingleton, ExportFileQuerySpecificationHolder, ExportFileQuery>(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/File.Core/Extensions/FileExtensions.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | using File.Domain.Dtos; 3 | 4 | namespace File.Core.Extensions 5 | { 6 | internal static class FileExtensions 7 | { 8 | public async static Task CreateFileDto(this IFileProxy file, CancellationToken cancellationToken) 9 | { 10 | return new FileDto 11 | { 12 | ContentType = file.ContentType, 13 | Data = await file.GetData(cancellationToken), 14 | FileName = Path.GetFileName(file.FileName), 15 | Length = file.Length 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/File.Core/Extensions/ValidotDependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Validot; 3 | 4 | namespace File.Core.Extensions 5 | { 6 | public static class ValidotDependencyInjectionExtensions 7 | { 8 | public static IServiceCollection AddValidotSingleton(this IServiceCollection serviceCollection) 9 | where TValidator : IValidator 10 | where THolder : ISpecificationHolder, new() 11 | { 12 | return serviceCollection.AddSingleton(typeof(TValidator), Validator.Factory.Create(new THolder())); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/File.Core/File.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | True 27 | True 28 | ErrorMessages.resx 29 | 30 | 31 | True 32 | True 33 | ValidationErrorMessages.resx 34 | 35 | 36 | 37 | 38 | 39 | ResXFileCodeGenerator 40 | ErrorMessages.Designer.cs 41 | 42 | 43 | ResXFileCodeGenerator 44 | ValidationErrorMessages.Designer.cs 45 | 46 | 47 | 48 | 49 | 50 | <_Parameter1>File.Core.UnitTests 51 | 52 | 53 | 54 | 55 | 56 | 57 | <_Parameter1>DynamicProxyGenAssembly2 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/File.Core/Queries/ConvertToQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Core.Extensions; 4 | using File.Core.Resources; 5 | using File.Domain.Dtos; 6 | using File.Domain.Extensions; 7 | using File.Domain.Logging; 8 | using Microsoft.Extensions.Logging; 9 | using SmallApiToolkit.Core.Extensions; 10 | using SmallApiToolkit.Core.Response; 11 | 12 | namespace File.Core.Queries 13 | { 14 | internal sealed class ConvertToQueryHandler : IConvertToQueryHandler 15 | { 16 | private readonly ILogger _logger; 17 | private readonly IConvertToQueryValidator _convertToQueryValidator; 18 | private readonly IFileConvertService _fileConvertService; 19 | 20 | public ConvertToQueryHandler(ILogger logger, IFileConvertService fileConvertService, IConvertToQueryValidator convertToQueryValidator) 21 | { 22 | _logger = Guard.Against.Null(logger); 23 | _fileConvertService = Guard.Against.Null(fileConvertService); 24 | _convertToQueryValidator = Guard.Against.Null(convertToQueryValidator); 25 | } 26 | 27 | public async Task> HandleAsync(ConvertToQuery request, CancellationToken cancellationToken) 28 | { 29 | var validationResult = _convertToQueryValidator.Validate(request); 30 | if (validationResult.IsFailed) 31 | { 32 | return HttpDataResponses.AsBadRequest(validationResult.Errors.ToErrorMessages()); 33 | } 34 | 35 | var convertResult = await _fileConvertService.ConvertTo(request.File, request.ExtensionToConvert, cancellationToken); 36 | 37 | if(convertResult.IsFailed) 38 | { 39 | _logger.LogError(LogEvents.ConvertFileGeneralError, convertResult.Errors.JoinToMessage()); 40 | return HttpDataResponses.AsBadRequest(string.Format(ErrorMessages.ConvertFileFailed, request.File.FileName, request.ExtensionToConvert)); 41 | } 42 | 43 | return HttpDataResponses.AsOK(await convertResult.Value.CreateFileDto(cancellationToken)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/File.Core/Queries/DownloadFileQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Core.Resources; 4 | using File.Domain.Dtos; 5 | using File.Domain.Extensions; 6 | using File.Domain.Logging; 7 | using File.Domain.Queries; 8 | using Microsoft.Extensions.Logging; 9 | using SmallApiToolkit.Core.Extensions; 10 | using SmallApiToolkit.Core.Response; 11 | using Validot; 12 | 13 | namespace File.Core.Queries 14 | { 15 | internal sealed class DownloadFileQueryHandler : IDownloadFileQueryHandler 16 | { 17 | private readonly IValidator _downloadFileQueryValidator; 18 | private readonly ILogger _logger; 19 | private readonly IFileQueriesRepository _fileQueriesRepository; 20 | public DownloadFileQueryHandler( 21 | IValidator downloadFileQueryValidator, 22 | ILogger logger, 23 | IFileQueriesRepository fileQueriesRepository) 24 | { 25 | _downloadFileQueryValidator = Guard.Against.Null(downloadFileQueryValidator); 26 | _logger = Guard.Against.Null(logger); 27 | _fileQueriesRepository = Guard.Against.Null(fileQueriesRepository); 28 | } 29 | 30 | public async Task> HandleAsync(DownloadFileQuery request, CancellationToken cancellationToken) 31 | { 32 | var validationResult = _downloadFileQueryValidator.Validate(request); 33 | if(validationResult.AnyErrors) 34 | { 35 | _logger.LogError(LogEvents.GetFileValidationError, validationResult.ToString()); 36 | return HttpDataResponses.AsBadRequest(ValidationErrorMessages.InvalidRequest); 37 | } 38 | 39 | var fileResult = await _fileQueriesRepository.GetFile(request, cancellationToken); 40 | 41 | if(fileResult.IsFailed) 42 | { 43 | _logger.LogError(LogEvents.GetFileDatabaseError, fileResult.Errors.JoinToMessage()); 44 | return HttpDataResponses.AsBadRequest(ErrorMessages.FileNotExist); 45 | } 46 | 47 | return HttpDataResponses.AsOK(fileResult.Value); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/File.Core/Queries/ExportFileQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Core.Extensions; 4 | using File.Core.Resources; 5 | using File.Domain.Dtos; 6 | using File.Domain.Extensions; 7 | using File.Domain.Logging; 8 | using File.Domain.Queries; 9 | using Mapster; 10 | using Microsoft.Extensions.Logging; 11 | using SmallApiToolkit.Core.Extensions; 12 | using SmallApiToolkit.Core.Response; 13 | 14 | namespace File.Core.Queries 15 | { 16 | internal sealed class ExportFileQueryHandler : IExportFileQueryHandler 17 | { 18 | private readonly IExportFileQueryValidator _exportFileQueryValidator; 19 | private readonly IFileQueriesRepository _fileQueriesRepository; 20 | private readonly ILogger _logger; 21 | private readonly IFileConvertService _fileConvertService; 22 | private readonly IFileByOptionsValidator _fileByOptionsValidator; 23 | 24 | public ExportFileQueryHandler( 25 | IExportFileQueryValidator exportFileQueryValidator, 26 | IFileQueriesRepository fileQueriesRepository, 27 | ILogger logger, 28 | IFileConvertService fileConvertService, 29 | IFileByOptionsValidator fileByOptionsValidator) 30 | { 31 | _exportFileQueryValidator = Guard.Against.Null(exportFileQueryValidator); 32 | _fileQueriesRepository = Guard.Against.Null(fileQueriesRepository); 33 | _logger = Guard.Against.Null(logger); 34 | _fileConvertService = Guard.Against.Null(fileConvertService); 35 | _fileByOptionsValidator = Guard.Against.Null(fileByOptionsValidator); 36 | } 37 | 38 | public async Task> HandleAsync(ExportFileQuery request, CancellationToken cancellationToken) 39 | { 40 | var validationResult = _exportFileQueryValidator.Validate(request); 41 | if (validationResult.IsFailed) 42 | { 43 | _logger.LogError(LogEvents.ExportFileValidationError, validationResult.ToString()); 44 | return HttpDataResponses.AsBadRequest(validationResult.ToString()); 45 | } 46 | 47 | var fileResult = await _fileQueriesRepository.GetFile(request.Adapt(), cancellationToken); 48 | if(fileResult.IsFailed) 49 | { 50 | _logger.LogError(LogEvents.GetFileDatabaseError, fileResult.Errors.JoinToMessage()); 51 | return HttpDataResponses.AsBadRequest(string.Format(ErrorMessages.ExportFileFailed, request.Id, request.Extension)); 52 | } 53 | 54 | var conversionValidationResult = _fileByOptionsValidator.ValidateConversion(fileResult.Value.FileName.GetFileExtension(), request.Extension); 55 | if (conversionValidationResult.IsFailed) 56 | { 57 | _logger.LogError(LogEvents.ExportFileGeneralError, conversionValidationResult.Errors.JoinToMessage()); 58 | return HttpDataResponses.AsBadRequest(string.Format(ErrorMessages.ExportFileFailed, request.Id, request.Extension)); 59 | } 60 | 61 | var exportResult = await _fileConvertService.ExportTo(fileResult.Value, request.Extension, cancellationToken); 62 | 63 | if(exportResult.IsFailed) 64 | { 65 | _logger.LogError(LogEvents.ExportFileGeneralError, exportResult.Errors.JoinToMessage()); 66 | return HttpDataResponses.AsBadRequest(string.Format(ErrorMessages.ExportFileFailed, request.Id, request.Extension)); 67 | } 68 | 69 | return HttpDataResponses.AsOK(await exportResult.Value.CreateFileDto(cancellationToken)); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/File.Core/Queries/GetFilesInfoQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Domain.Dtos; 4 | using SmallApiToolkit.Core.Extensions; 5 | using SmallApiToolkit.Core.Response; 6 | 7 | namespace File.Core.Queries 8 | { 9 | internal sealed class GetFilesInfoQueryHandler : IGetFilesInfoQueryHandler 10 | { 11 | private readonly IFileQueriesRepository _fileQueriesRepository; 12 | public GetFilesInfoQueryHandler(IFileQueriesRepository fileQueriesRepository) 13 | { 14 | _fileQueriesRepository = Guard.Against.Null(fileQueriesRepository); 15 | } 16 | 17 | public async Task>> HandleAsync(EmptyRequest request, CancellationToken cancellationToken) 18 | { 19 | var filesInfo = await _fileQueriesRepository.GetFilesInfo(cancellationToken); 20 | return HttpDataResponses.AsOK(filesInfo); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/File.Core/Resources/ErrorMessages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace File.Core.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class ErrorMessages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal ErrorMessages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("File.Core.Resources.ErrorMessages", typeof(ErrorMessages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Failed to convert file: {0} to format {1}. 65 | /// 66 | internal static string ConvertFileFailed { 67 | get { 68 | return ResourceManager.GetString("ConvertFileFailed", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Failed to export file id: {0} to format {1}. 74 | /// 75 | internal static string ExportFileFailed { 76 | get { 77 | return ResourceManager.GetString("ExportFileFailed", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to File not exist.. 83 | /// 84 | internal static string FileNotExist { 85 | get { 86 | return ResourceManager.GetString("FileNotExist", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Failed to read file: {0}. 92 | /// 93 | internal static string ReadFileStreamFailed { 94 | get { 95 | return ResourceManager.GetString("ReadFileStreamFailed", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Failed to save file: {0}. 101 | /// 102 | internal static string SaveFileFailed { 103 | get { 104 | return ResourceManager.GetString("SaveFileFailed", resourceCulture); 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/File.Core/Resources/ValidationErrorMessages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace File.Core.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class ValidationErrorMessages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal ValidationErrorMessages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("File.Core.Resources.ValidationErrorMessages", typeof(ValidationErrorMessages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to File is empty {0}.. 65 | /// 66 | internal static string FileIsEmpty { 67 | get { 68 | return ResourceManager.GetString("FileIsEmpty", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Invalid request.. 74 | /// 75 | internal static string InvalidRequest { 76 | get { 77 | return ResourceManager.GetString("InvalidRequest", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Maximum file size exceeded. File: {0}.. 83 | /// 84 | internal static string MaximalFileSize { 85 | get { 86 | return ResourceManager.GetString("MaximalFileSize", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Unsuported conversion. source{0}, destination{1}.. 92 | /// 93 | internal static string UnsuportedConversion { 94 | get { 95 | return ResourceManager.GetString("UnsuportedConversion", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Unsuported format. Extension{0}.. 101 | /// 102 | internal static string UnsupportedExtension { 103 | get { 104 | return ResourceManager.GetString("UnsupportedExtension", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to Unsuported format. File:{0}, contentType: {1}.. 110 | /// 111 | internal static string UnsupportedFormat { 112 | get { 113 | return ResourceManager.GetString("UnsupportedFormat", resourceCulture); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/File.Core/Validation/AddFileCommandSpecificationHolder.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Commands; 2 | using Validot; 3 | 4 | namespace File.Core.Validation 5 | { 6 | internal sealed class AddFileCommandSpecificationHolder : ISpecificationHolder 7 | { 8 | public Specification Specification { get; } 9 | 10 | public AddFileCommandSpecificationHolder() 11 | { 12 | Specification addFileCommandSpecification = s => s 13 | .Member(m => m.Files, m => m.AsCollection(GeneralPredicates.fileSpecification)); 14 | 15 | Specification = addFileCommandSpecification; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/File.Core/Validation/AddFilesCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Domain.Commands; 4 | using FluentResults; 5 | using Validot; 6 | 7 | namespace File.Core.Validation 8 | { 9 | internal sealed class AddFilesCommandValidator : IAddFilesCommandValidator 10 | { 11 | private readonly IValidator _addFilesCommandValidator; 12 | private readonly IFileByOptionsValidator _fileByOptionsValidator; 13 | public AddFilesCommandValidator(IValidator addFilesCommandValidator, IFileByOptionsValidator fileByOptionsValidator) 14 | { 15 | _addFilesCommandValidator = Guard.Against.Null(addFilesCommandValidator); 16 | _fileByOptionsValidator = Guard.Against.Null(fileByOptionsValidator); 17 | } 18 | 19 | public Result Validate(AddFilesCommand addFilesCommand) 20 | { 21 | var validationResult = _addFilesCommandValidator.Validate(addFilesCommand); 22 | if (validationResult.AnyErrors) 23 | { 24 | return Result.Fail(validationResult.ToString()); 25 | } 26 | 27 | foreach(var file in addFilesCommand.Files) 28 | { 29 | var fileValidationResult = _fileByOptionsValidator.Validate(file); 30 | 31 | if(fileValidationResult.IsFailed) 32 | { 33 | return fileValidationResult; 34 | } 35 | } 36 | 37 | return Result.Ok(true); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/File.Core/Validation/ConvertToQuerySpecificationHolder.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Queries; 2 | using Validot; 3 | 4 | namespace File.Core.Validation 5 | { 6 | internal sealed class ConvertToQuerySpecificationHolder : ISpecificationHolder 7 | { 8 | public Specification Specification { get; } 9 | 10 | public ConvertToQuerySpecificationHolder() 11 | { 12 | Specification convertToQuerySpecification = s => s 13 | .Member(m => m.File, GeneralPredicates.fileSpecification); 14 | 15 | Specification = convertToQuerySpecification; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/File.Core/Validation/ConvertToQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Core.Queries; 4 | using File.Domain.Extensions; 5 | using FluentResults; 6 | using Validot; 7 | 8 | namespace File.Core.Validation 9 | { 10 | internal sealed class ConvertToQueryValidator : IConvertToQueryValidator 11 | { 12 | private readonly IValidator _convertToQueryValidator; 13 | private readonly IFileByOptionsValidator _fileByOptionsValidator; 14 | public ConvertToQueryValidator(IValidator convertToQueryValidator, IFileByOptionsValidator fileByOptionsValidator) 15 | { 16 | _convertToQueryValidator = Guard.Against.Null(convertToQueryValidator); 17 | _fileByOptionsValidator = Guard.Against.Null(fileByOptionsValidator); 18 | } 19 | 20 | public Result Validate(ConvertToQuery convertToQuery) 21 | { 22 | var validationResult = _convertToQueryValidator.Validate(convertToQuery); 23 | if (validationResult.AnyErrors) 24 | { 25 | return Result.Fail(validationResult.ToString()); 26 | } 27 | 28 | var fileValidationResult = _fileByOptionsValidator.Validate(convertToQuery.File); 29 | 30 | if (fileValidationResult.IsFailed) 31 | { 32 | return fileValidationResult; 33 | } 34 | 35 | var conversionValidationResult = _fileByOptionsValidator.ValidateConversion(convertToQuery.File.FileName.GetFileExtension(), convertToQuery.ExtensionToConvert); 36 | 37 | if(conversionValidationResult.IsFailed) 38 | { 39 | return conversionValidationResult; 40 | } 41 | 42 | return Result.Ok(true); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/File.Core/Validation/DownloadFileQuerySpecificationHolder.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Queries; 2 | using Validot; 3 | 4 | namespace File.Core.Validation 5 | { 6 | internal sealed class DownloadFileQuerySpecificationHolder : ISpecificationHolder 7 | { 8 | public Specification Specification { get; } 9 | 10 | public DownloadFileQuerySpecificationHolder() 11 | { 12 | Specification downloadFileQuerySpecification = s => s 13 | .Member(m => m.Id, m => m.Rule(GeneralPredicates.isValidId)); 14 | 15 | Specification = downloadFileQuerySpecification; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/File.Core/Validation/ExportFileQuerySpecificationHolder.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Queries; 2 | using Validot; 3 | 4 | namespace File.Core.Validation 5 | { 6 | internal sealed class ExportFileQuerySpecificationHolder : ISpecificationHolder 7 | { 8 | public Specification Specification { get; } 9 | 10 | public ExportFileQuerySpecificationHolder() 11 | { 12 | Specification exportFileQuerySpecification = s => s 13 | .Member(m => m.Id, m => m.Rule(GeneralPredicates.isValidId)); 14 | 15 | Specification = exportFileQuerySpecification; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/File.Core/Validation/ExportFileQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Domain.Queries; 4 | using FluentResults; 5 | using Validot; 6 | 7 | namespace File.Core.Validation 8 | { 9 | internal sealed class ExportFileQueryValidator : IExportFileQueryValidator 10 | { 11 | private readonly IValidator _exportFileQueryValidator; 12 | private readonly IFileByOptionsValidator _fileByOptionsValidator; 13 | 14 | public ExportFileQueryValidator(IValidator exportFileQueryValidator, IFileByOptionsValidator fileByOptionsValidator) 15 | { 16 | _exportFileQueryValidator = Guard.Against.Null(exportFileQueryValidator); 17 | _fileByOptionsValidator = Guard.Against.Null(fileByOptionsValidator); 18 | } 19 | 20 | public Result Validate(ExportFileQuery exportFileQuery) 21 | { 22 | var validationResult = _exportFileQueryValidator.Validate(exportFileQuery); 23 | if (validationResult.AnyErrors) 24 | { 25 | return Result.Fail(validationResult.ToString()); 26 | } 27 | 28 | var fileValidationResult = _fileByOptionsValidator.Validate(exportFileQuery.Extension); 29 | 30 | if (fileValidationResult.IsFailed) 31 | { 32 | return fileValidationResult; 33 | } 34 | 35 | return Result.Ok(true); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/File.Core/Validation/FileByOptionsValidator.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Core.Resources; 4 | using File.Domain.Abstractions; 5 | using File.Domain.Options; 6 | using FluentResults; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace File.Core.Validation 10 | { 11 | internal sealed class FileByOptionsValidator : IFileByOptionsValidator 12 | { 13 | private readonly IOptions _fileOptions; 14 | public FileByOptionsValidator(IOptions fileOptions) 15 | { 16 | _fileOptions = Guard.Against.Null(fileOptions); 17 | } 18 | 19 | public Result Validate(IFileProxy file) 20 | { 21 | var options = _fileOptions.Value.AllowedFiles.SingleOrDefault(x => x.ContentType.Equals(file.ContentType)); 22 | if (options is null) 23 | { 24 | return Result.Fail(string.Format(ValidationErrorMessages.UnsupportedFormat, file.FileName, file.ContentType)); 25 | } 26 | 27 | if (file.Length > _fileOptions.Value.MaxFileLength) 28 | { 29 | return Result.Fail(string.Format(ValidationErrorMessages.MaximalFileSize, file.FileName)); 30 | } 31 | 32 | if(file.Length == 0) 33 | { 34 | return Result.Fail(string.Format(ValidationErrorMessages.FileIsEmpty, file.FileName)); 35 | } 36 | 37 | return Result.Ok(true); 38 | } 39 | 40 | public Result Validate(string extension) 41 | { 42 | var options = _fileOptions.Value.AllowedFiles.SingleOrDefault(x => x.Format.Equals(extension)); 43 | if (options is null) 44 | { 45 | return Result.Fail(string.Format(ValidationErrorMessages.UnsupportedExtension, extension)); 46 | } 47 | 48 | return Result.Ok(true); 49 | } 50 | 51 | public Result ValidateConversion(string sourceExtension, string destinationExtension) 52 | { 53 | var options = _fileOptions.Value.AllowedFiles.SingleOrDefault(x => x.Format.Equals(sourceExtension)); 54 | if (options is null) 55 | { 56 | return Result.Fail(string.Format(ValidationErrorMessages.UnsupportedExtension, sourceExtension)); 57 | } 58 | 59 | if(!options.CanBeExportedTo.Any(x=>x.Equals(destinationExtension, StringComparison.OrdinalIgnoreCase))) 60 | { 61 | return Result.Fail(string.Format(ValidationErrorMessages.UnsuportedConversion, sourceExtension, destinationExtension)); 62 | } 63 | 64 | return Result.Ok(true); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/File.Core/Validation/GeneralPredicates.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | using Validot; 3 | 4 | namespace File.Core.Validation 5 | { 6 | internal static class GeneralPredicates 7 | { 8 | internal static readonly Predicate isValidId = m => m > 0 && m < int.MaxValue; 9 | internal static readonly Predicate isValidFileName = m => m.IndexOfAny(Path.GetInvalidFileNameChars()) < 0; 10 | internal static readonly Specification fileSpecification = f => 11 | f.Member(m => m.FileName, m => m 12 | .NotEmpty() 13 | .And() 14 | .NotWhiteSpace() 15 | .And() 16 | .Rule(isValidFileName)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/File.Domain/Abstractions/IFileProxy.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Abstractions 2 | { 3 | public interface IFileProxy 4 | { 5 | string FileName { get; } 6 | string ContentType { get; } 7 | long Length { get; } 8 | Task CopyToAsync(Stream target, CancellationToken cancellationToken); 9 | Task GetData(CancellationToken cancellationToken); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/File.Domain/Commands/AddFilesCommand.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | 3 | namespace File.Domain.Commands 4 | { 5 | public sealed class AddFilesCommand 6 | { 7 | public IEnumerable Files { get; init; } = []; 8 | 9 | public AddFilesCommand(IEnumerable files) 10 | { 11 | Files = files; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/File.Domain/Dtos/BaseFileDto.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Dtos 2 | { 3 | public abstract class BaseFileDto 4 | { 5 | public string FileName { get; init; } = string.Empty; 6 | 7 | public string ContentType { get; init; } = string.Empty; 8 | 9 | public long Length { get; init; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/File.Domain/Dtos/FileDto.cs: -------------------------------------------------------------------------------- 1 | using SmallApiToolkit.Core.Abstractions; 2 | 3 | namespace File.Domain.Dtos 4 | { 5 | public class FileDto : BaseFileDto, IFile 6 | { 7 | public byte[] Data { get; init; } = []; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/File.Domain/Dtos/FileInfoDto.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Dtos 2 | { 3 | public sealed class FileInfoDto 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; init; } = string.Empty; 7 | public string FileName { get; init; } = string.Empty; 8 | public string ContentType { get; init; } = string.Empty; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Domain/Dtos/StringContentFileDto.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Dtos 2 | { 3 | public sealed class StringContentFileDto : BaseFileDto 4 | { 5 | public string Data { get; init; } = string.Empty; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/File.Domain/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Extensions 2 | { 3 | public static class EnumerableExtensions 4 | { 5 | public static void ForEach(this IEnumerable values, Action action) 6 | { 7 | foreach(var item in values) 8 | { 9 | action(item); 10 | } 11 | } 12 | 13 | public static async Task ForEachAsync(this IEnumerable values, Func action) 14 | { 15 | foreach (var item in values) 16 | { 17 | await action(item); 18 | } 19 | } 20 | 21 | public static bool HasAny(this IEnumerable values) 22 | { 23 | return values?.Any() ?? false; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/File.Domain/Extensions/FileNameExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Extensions 2 | { 3 | public static class FileNameExtensions 4 | { 5 | public static string GetFileExtension(this string fileName) 6 | { 7 | return Path.GetExtension(fileName).Substring(1); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Domain/Extensions/FluentResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace File.Domain.Extensions 4 | { 5 | public static class FluentResultExtensions 6 | { 7 | public static IEnumerable ToErrorMessages(this IList errors) 8 | { 9 | if(!errors.HasAny()) 10 | { 11 | return []; 12 | } 13 | 14 | return errors.Select(error => error.Message); 15 | } 16 | 17 | public static string JoinToMessage(this IList errors) 18 | { 19 | if (!errors.HasAny()) 20 | { 21 | return string.Empty; 22 | } 23 | 24 | return string.Join(',', errors.ToErrorMessages()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/File.Domain/File.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/File.Domain/Logging/LogEvents.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Logging 2 | { 3 | public static class LogEvents 4 | { 5 | public static readonly int GeneralError = 1000; 6 | 7 | //Add File 8 | public static readonly int AddFileGeneralError = 2000; 9 | 10 | public static readonly int AddFileStreamError = 2100; 11 | 12 | public static readonly int AddFileDatabaseError = 2200; 13 | 14 | //Get File 15 | public static readonly int GetFileGeneralError = 3000; 16 | 17 | public static readonly int GetFileStreamError = 3100; 18 | 19 | public static readonly int GetFileDatabaseError = 3200; 20 | 21 | public static readonly int GetFileValidationError = 3300; 22 | 23 | //Export File 24 | public static readonly int ExportFileGeneralError = 4000; 25 | 26 | public static readonly int ExportFileValidationError = 4100; 27 | 28 | //Convert File 29 | public static readonly int ConvertFileGeneralError = 4000; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/File.Domain/Options/AllowedFile.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Options 2 | { 3 | public sealed class AllowedFile 4 | { 5 | public string Format { get; set; } = string.Empty; 6 | 7 | public string ContentType { get; set; } = string.Empty; 8 | 9 | public string[] CanBeExportedTo { get; set; } = []; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/File.Domain/Options/FilesOptions.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Options 2 | { 3 | public sealed class FilesOptions 4 | { 5 | public static readonly string Files= "Files"; 6 | 7 | public uint MaxFileLength { get; set; } 8 | 9 | public AllowedFile[] AllowedFiles { get; set; } = new AllowedFile[0]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/File.Domain/Queries/ConvertToQuery.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | 3 | namespace File.Core.Queries 4 | { 5 | public sealed class ConvertToQuery 6 | { 7 | public IFileProxy File { get; init; } 8 | 9 | public string ExtensionToConvert{ get; init; } = string.Empty; 10 | 11 | public ConvertToQuery(IFileProxy file, string extensionToConvert) 12 | { 13 | File = file; 14 | ExtensionToConvert = extensionToConvert; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/File.Domain/Queries/DownloadFileQuery.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Queries 2 | { 3 | public sealed class DownloadFileQuery 4 | { 5 | public int Id { get; init; } 6 | 7 | public DownloadFileQuery(int id) 8 | { 9 | Id = id; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/File.Domain/Queries/ExportFileQuery.cs: -------------------------------------------------------------------------------- 1 | namespace File.Domain.Queries 2 | { 3 | public sealed class ExportFileQuery 4 | { 5 | public int Id { get; init; } 6 | public string Extension { get; init; } = string.Empty; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/File.Frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/File.Frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /src/File.Frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /src/File.Frontend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "edge", 6 | "request": "launch", 7 | "name": "localhost (Edge)", 8 | "url": "http://localhost:4200", 9 | "webRoot": "${workspaceFolder}" 10 | }, 11 | { 12 | "type": "chrome", 13 | "request": "launch", 14 | "name": "localhost (Chrome)", 15 | "url": "http://localhost:4200", 16 | "webRoot": "${workspaceFolder}" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/File.Frontend/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/File.Frontend/File.Frontend.esproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | npm start 4 | Jasmine 5 | 6 | false 7 | 8 | $(MSBuildProjectDirectory)\dist\File.Frontend\browser\ 9 | 10 | -------------------------------------------------------------------------------- /src/File.Frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "File.Frontend": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "standalone": false 11 | }, 12 | "@schematics/angular:directive": { 13 | "standalone": false 14 | }, 15 | "@schematics/angular:pipe": { 16 | "standalone": false 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:application", 25 | "options": { 26 | "outputPath": "dist/file.frontend", 27 | "index": "src/index.html", 28 | "browser": "src/main.ts", 29 | "polyfills": [ 30 | "zone.js", 31 | "@angular/localize/init" 32 | ], 33 | "tsConfig": "tsconfig.app.json", 34 | "assets": [ 35 | { 36 | "glob": "**/*", 37 | "input": "public" 38 | } 39 | ], 40 | "styles": [ 41 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 42 | "src/styles.css" 43 | ], 44 | "scripts": [ 45 | "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" 46 | ] 47 | }, 48 | "configurations": { 49 | "production": { 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "500kB", 54 | "maximumError": "1MB" 55 | }, 56 | { 57 | "type": "anyComponentStyle", 58 | "maximumWarning": "2kB", 59 | "maximumError": "4kB" 60 | } 61 | ], 62 | "outputHashing": "all" 63 | }, 64 | "development": { 65 | "optimization": false, 66 | "extractLicenses": false, 67 | "sourceMap": true 68 | } 69 | }, 70 | "defaultConfiguration": "production" 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "configurations": { 75 | "production": { 76 | "buildTarget": "File.Frontend:build:production" 77 | }, 78 | "development": { 79 | "buildTarget": "File.Frontend:build:development" 80 | } 81 | }, 82 | "defaultConfiguration": "development" 83 | }, 84 | "extract-i18n": { 85 | "builder": "@angular-devkit/build-angular:extract-i18n" 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "polyfills": [ 91 | "zone.js", 92 | "zone.js/testing", 93 | "@angular/localize/init" 94 | ], 95 | "tsConfig": "tsconfig.spec.json", 96 | "assets": [ 97 | { 98 | "glob": "**/*", 99 | "input": "public" 100 | } 101 | ], 102 | "styles": [ 103 | "src/styles.css" 104 | ], 105 | "scripts": [], 106 | "karmaConfig": "karma.conf.js" 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/File.Frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 5 | plugins: [ 6 | require('karma-jasmine'), 7 | require('karma-chrome-launcher'), 8 | require('karma-jasmine-html-reporter'), 9 | require('karma-coverage'), 10 | require('@angular-devkit/build-angular/plugins/karma') 11 | ], 12 | client: { 13 | jasmine: { 14 | // you can add configuration options for Jasmine here 15 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 16 | // for example, you can disable the random execution with `random: false` 17 | // or set a specific seed with `seed: 4321` 18 | }, 19 | clearContext: false // leave Jasmine Spec Runner output visible in browser 20 | }, 21 | jasmineHtmlReporter: { 22 | suppressAll: true // removes the duplicated traces 23 | }, 24 | coverageReporter: { 25 | dir: require('path').join(__dirname, './coverage/'), 26 | subdir: '.', 27 | reporters: [ 28 | { type: 'html' }, 29 | { type: 'text-summary' } 30 | ] 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | listenAddress: 'localhost', 41 | hostname: 'localhost' 42 | }); 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /src/File.Frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file.frontend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --host=127.0.0.1", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^19.1.4", 14 | "@angular/common": "^19.1.4", 15 | "@angular/compiler": "^19.1.4", 16 | "@angular/core": "^19.1.4", 17 | "@angular/forms": "^19.1.4", 18 | "@angular/platform-browser": "^19.1.4", 19 | "@angular/platform-browser-dynamic": "^19.1.4", 20 | "@angular/router": "^19.1.4", 21 | "@fortawesome/angular-fontawesome": "^1.0.0", 22 | "@fortawesome/fontawesome-svg-core": "^6.6.0", 23 | "@fortawesome/free-solid-svg-icons": "^6.6.0", 24 | "@fortawesome/react-fontawesome": "^0.2.2", 25 | "@ng-bootstrap/ng-bootstrap": "^18.0.0", 26 | "@popperjs/core": "^2.11.8", 27 | "@types/node": "^22.7.2", 28 | "bootstrap": "^5.3.3", 29 | "buffer": "^6.0.3", 30 | "file-saver": "^2.0.5", 31 | "gramli-angular-notifier": "^17.0.0", 32 | "jest-editor-support": "*", 33 | "rxjs": "~7.8.0", 34 | "tslib": "^2.3.0", 35 | "zone.js": "~0.15.0" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "^19.1.6", 39 | "@angular/cli": "^19.1.6", 40 | "@angular/compiler-cli": "^19.1.4", 41 | "@angular/localize": "^19.1.4", 42 | "@types/file-saver": "^2.0.7", 43 | "@types/jasmine": "~5.1.0", 44 | "eslint": "^9.10.0", 45 | "jasmine-core": "~5.2.0", 46 | "karma": "~6.4.0", 47 | "karma-chrome-launcher": "~3.2.0", 48 | "karma-coverage": "~2.2.0", 49 | "karma-jasmine": "~5.1.0", 50 | "karma-jasmine-html-reporter": "~2.1.0", 51 | "typescript": "~5.5.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/File.Frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gramli/FileApi/c70c3d71d7bee9efd4f1c033b448e34673318c36/src/File.Frontend/public/favicon.ico -------------------------------------------------------------------------------- /src/File.Frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .file-input { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

File.Frontend

3 |
4 |

Processing...

5 |
6 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 49 | 50 | 51 |
IdNameFileNameTypeAction
{{ item.id }}{{ item.name }}{{ item.fileName }}{{ item.contentType }} 33 | 39 | 45 | 48 |
52 | 53 | 59 | 60 | 66 | 67 |
68 | 72 | 75 |
76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; 2 | import { IFile } from './app.model'; 3 | import { 4 | faUpload, 5 | faFileImport, 6 | IconDefinition, 7 | } from '@fortawesome/free-solid-svg-icons'; 8 | import { saveAs } from 'file-saver'; 9 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 10 | import { FileLoadingService } from './services/file-loading.service'; 11 | import { NotificationAdapterService } from './services/notification-adapter.service'; 12 | 13 | @Component({ 14 | selector: 'app-root', 15 | templateUrl: './app.component.html', 16 | styleUrl: './app.component.css', 17 | providers: [FileLoadingService], 18 | standalone: false 19 | }) 20 | export class AppComponent implements OnInit { 21 | protected faUpload: IconDefinition = faUpload; 22 | protected faFileImport: IconDefinition = faFileImport; 23 | 24 | @ViewChild('dialog') protected dialogRef!: TemplateRef; 25 | data: IFile[] | undefined; 26 | constructor( 27 | protected fileService: FileLoadingService, 28 | private ngbModal: NgbModal, 29 | private notifierService: NotificationAdapterService 30 | ) {} 31 | 32 | public ngOnInit(): void { 33 | this.fileService.filesInfo.subscribe({ 34 | next: (response) => { 35 | this.data = response; 36 | }, 37 | error: (error) => { 38 | this.notifierService.showError(`Error: ${error}`); 39 | }, 40 | }); 41 | 42 | this.fileService.loadFileData(); 43 | } 44 | 45 | protected convert(file: File, fileName: string): void { 46 | this.ngbModal 47 | .open(this.dialogRef, { 48 | windowClass: 'modal-job-scrollable', 49 | }) 50 | .closed.subscribe((selectedExtension: string) => { 51 | this.fileService.convertFile( 52 | file, 53 | selectedExtension, 54 | (fileContent) => { 55 | saveAs( 56 | fileContent, 57 | this.replaceExtension(fileName, selectedExtension) 58 | ); 59 | this.notifierService.showSuccess( 60 | `Successfuly converted file: ${fileName}` 61 | ); 62 | }, 63 | () => { 64 | this.notifierService.showError( 65 | `Error to convert file: ${fileName}` 66 | ); 67 | } 68 | ); 69 | }); 70 | } 71 | 72 | protected export(id: number): void { 73 | this.ngbModal 74 | .open(this.dialogRef, { 75 | windowClass: 'modal-job-scrollable', 76 | }) 77 | .closed.subscribe((selectedExtension: string) => { 78 | this.fileService.exportFile( 79 | id, 80 | selectedExtension, 81 | (fileContent) => { 82 | this.saveFile(id, fileContent, selectedExtension); 83 | this.notifierService.showSuccess( 84 | `Successfuly exported file id: ${id}` 85 | ); 86 | }, 87 | () => { 88 | this.notifierService.showError(`Error to export file id: ${id}`); 89 | } 90 | ); 91 | }); 92 | } 93 | 94 | protected onDownloadFile(id: number): void { 95 | this.fileService.downloadFile( 96 | id, 97 | (fileContent) => { 98 | this.saveFile(id, fileContent); 99 | this.notifierService.showSuccess( 100 | `Successfuly downloaded file id: ${id}` 101 | ); 102 | }, 103 | () => { 104 | this.notifierService.showError(`Error to download file id: ${id}`); 105 | } 106 | ); 107 | } 108 | 109 | protected onDownloadFileAsJson(id: number): void { 110 | this.fileService.downloadFileAsJson( 111 | id, 112 | (fileContent) => { 113 | this.saveFile(id, fileContent); 114 | this.notifierService.showSuccess( 115 | `Successfuly downloaded as json file id: ${id}` 116 | ); 117 | }, 118 | () => { 119 | this.notifierService.showError(`Error to download file id: ${id}`); 120 | } 121 | ); 122 | } 123 | 124 | protected onUploadFileSelected(event: any): void { 125 | const file = this.getTargetFile(event); 126 | this.fileService.uploadFile( 127 | file, 128 | () => { 129 | this.notifierService.showSuccess( 130 | `Successfuly uploaded file id: ${file.name}` 131 | ); 132 | }, 133 | (error) => { 134 | this.notifierService.showError(`Error: ${error}`); 135 | } 136 | ); 137 | } 138 | 139 | protected onConvertFileSelected(event: any): void { 140 | const file = this.getTargetFile(event); 141 | this.convert(file, file.name); 142 | } 143 | 144 | private saveFile( 145 | id: number, 146 | fileContent: Blob | string, 147 | extension?: string 148 | ): void { 149 | const file = this.data?.filter((file) => file.id === id)[0]; 150 | saveAs( 151 | fileContent, 152 | extension === undefined 153 | ? file?.fileName 154 | : this.replaceExtension(file!.fileName, extension) 155 | ); 156 | } 157 | 158 | private getTargetFile(event: any): File { 159 | return event.target.files[0]; 160 | } 161 | 162 | private replaceExtension(fileName: string, extension: string): string { 163 | return fileName.split('.')[0] + `.${extension}`; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/app.model.ts: -------------------------------------------------------------------------------- 1 | export interface IDataResponse { 2 | data: T; 3 | errors: string[]; 4 | } 5 | 6 | export interface IFile { 7 | id: number; 8 | name: string; 9 | fileName: string; 10 | contentType: string; 11 | } 12 | 13 | export interface IBase64File { 14 | fileName: string; 15 | contentType: string; 16 | length: string; 17 | data: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppRoutingModule } from './app-routing.module'; 4 | import { AppComponent } from './app.component'; 5 | import { provideHttpClient } from '@angular/common/http'; 6 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 7 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { SelectExtensionModalComponent } from './components/select-extension-modal.component'; 10 | import { NotifierModule } from 'gramli-angular-notifier'; 11 | 12 | @NgModule({ 13 | declarations: [AppComponent, SelectExtensionModalComponent], 14 | imports: [ 15 | BrowserModule, 16 | AppRoutingModule, 17 | FontAwesomeModule, 18 | NgbModule, 19 | FormsModule, 20 | NotifierModule.withConfig({ 21 | position: { 22 | horizontal: { 23 | position: 'right', 24 | }, 25 | }, 26 | }), 27 | ], 28 | providers: [provideHttpClient()], 29 | bootstrap: [AppComponent], 30 | }) 31 | export class AppModule {} 32 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/components/select-extension-modal.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 14 |
15 |
16 |
17 |
18 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/components/select-extension-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; 3 | 4 | @Component({ 5 | selector: 'app-select-extension-modal', 6 | templateUrl: './select-extension-modal.component.html', 7 | standalone: false 8 | }) 9 | export class SelectExtensionModalComponent { 10 | @Input({ required: true }) modalRef!: NgbModalRef; 11 | 12 | extensions: string[] = ['json', 'xml', 'yaml']; 13 | 14 | private _selectedExtension: string | undefined; 15 | set selectedExtension(value: string) { 16 | this._selectedExtension = value; 17 | } 18 | 19 | get selectedExtension() { 20 | if (!this._selectedExtension) { 21 | this._selectedExtension = this.extensions[0]; 22 | } 23 | 24 | return this._selectedExtension; 25 | } 26 | 27 | protected submit() { 28 | this.modalRef.close(this.selectedExtension); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/services/file-api.http.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { IBase64File, IDataResponse, IFile } from '../app.model'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class FileApiHttpService { 10 | private apiBaseUrl: string = 'https://localhost:7270/file/v1'; 11 | constructor(private httpClient: HttpClient) {} 12 | 13 | public getFiles(): Observable> { 14 | return this.httpClient.get>( 15 | `${this.apiBaseUrl}/files-info` 16 | ); 17 | } 18 | 19 | public uploadFile(file: File): Observable> { 20 | const formData = new FormData(); 21 | formData.append('file', file); 22 | return this.httpClient.post>( 23 | `${this.apiBaseUrl}/upload`, 24 | formData 25 | ); 26 | } 27 | 28 | public downloadFile(id: number): Observable { 29 | return this.httpClient.get(`${this.apiBaseUrl}/download?id=${id}`, { 30 | responseType: 'blob', 31 | }); 32 | } 33 | 34 | public downloadFileAsString( 35 | id: number 36 | ): Observable> { 37 | return this.httpClient.get>( 38 | `${this.apiBaseUrl}/downloadAsJson?id=${id}` 39 | ); 40 | } 41 | 42 | public exportFile(id: number, extension: string): Observable { 43 | return this.httpClient.post( 44 | `${this.apiBaseUrl}/export`, 45 | { 46 | id, 47 | extension, 48 | }, 49 | { responseType: 'blob' } 50 | ); 51 | } 52 | 53 | public convertFile(file: File, extension: string): Observable { 54 | const formData = new FormData(); 55 | formData.append('file', file); 56 | formData.append('formatToConvert', extension); 57 | return this.httpClient.post(`${this.apiBaseUrl}/convert`, formData, { 58 | responseType: 'blob', 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/services/file-loading.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, Subscription } from 'rxjs'; 3 | import { IFile } from '../app.model'; 4 | import { FileApiHttpService } from './file-api.http.service'; 5 | import { Buffer } from 'buffer'; 6 | 7 | @Injectable() 8 | export class FileLoadingService { 9 | public loading = false; 10 | 11 | private filesInfo$: Subject = new Subject(); 12 | 13 | constructor(private fileService: FileApiHttpService) {} 14 | 15 | public get filesInfo() { 16 | return this.filesInfo$.asObservable(); 17 | } 18 | 19 | public downloadFile( 20 | id: number, 21 | onSuccess: (fileContent: Blob) => void, 22 | onError?: (error: any) => void 23 | ): void { 24 | this.runSubScriptionWithProgress(() => 25 | this.fileService.downloadFile(id).subscribe({ 26 | next: (response) => onSuccess(response), 27 | error: (error: any) => this.processError(error, onError), 28 | }) 29 | ); 30 | } 31 | 32 | public downloadFileAsJson( 33 | id: number, 34 | onSuccess: (fileContent: string) => void, 35 | onError?: (error: any) => void 36 | ): void { 37 | this.runSubScriptionWithProgress(() => 38 | this.fileService.downloadFileAsString(id).subscribe({ 39 | next: (response) => { 40 | const fileContent = Buffer.from( 41 | response.data.data, 42 | 'base64' 43 | ).toString('utf-8'); 44 | onSuccess(fileContent); 45 | }, 46 | error: (error: any) => this.processError(error, onError), 47 | }) 48 | ); 49 | } 50 | 51 | public uploadFile( 52 | file: File, 53 | onSuccess?: () => void, 54 | onError?: (error: any) => void 55 | ): void { 56 | if (file) { 57 | const upload$ = this.fileService.uploadFile(file); 58 | 59 | this.runSubScriptionWithProgress(() => 60 | upload$.subscribe({ 61 | next: () => { 62 | this.loadFileData(); 63 | if (onSuccess) { 64 | onSuccess(); 65 | } 66 | }, 67 | error: (error: any) => this.processError(error, onError), 68 | }) 69 | ); 70 | } 71 | } 72 | 73 | public loadFileData(): void { 74 | this.runSubScriptionWithProgress(() => 75 | this.fileService.getFiles().subscribe({ 76 | next: (response) => { 77 | this.filesInfo$.next(response.data); 78 | }, 79 | error: (error) => this.processError(error), 80 | }) 81 | ); 82 | } 83 | 84 | public exportFile( 85 | id: number, 86 | extension: string, 87 | onSuccess: (fileContent: Blob) => void, 88 | onError?: (error: any) => void 89 | ): void { 90 | this.runSubScriptionWithProgress(() => 91 | this.fileService.exportFile(id, extension).subscribe({ 92 | next: (response) => onSuccess(response), 93 | error: (error: any) => this.processError(error, onError), 94 | }) 95 | ); 96 | } 97 | 98 | public convertFile( 99 | file: File, 100 | extension: string, 101 | onSuccess: (fileContent: Blob) => void, 102 | onError?: (error: any) => void 103 | ): void { 104 | if (file) { 105 | const upload$ = this.fileService.convertFile(file, extension); 106 | 107 | this.runSubScriptionWithProgress(() => 108 | upload$.subscribe({ 109 | next: (response) => { 110 | onSuccess(response); 111 | }, 112 | error: (error: any) => this.processError(error, onError), 113 | }) 114 | ); 115 | } 116 | } 117 | 118 | private processError(error: any, onError?: (error: any) => void): void { 119 | if (onError) { 120 | onError(error); 121 | } else { 122 | console.error(error); 123 | } 124 | } 125 | 126 | private runSubScriptionWithProgress(action: () => Subscription): void { 127 | this.loading = true; 128 | action().add(() => { 129 | this.loading = false; 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/File.Frontend/src/app/services/notification-adapter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NotifierService } from 'gramli-angular-notifier'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class NotificationAdapterService { 8 | constructor(private notifierService: NotifierService) {} 9 | 10 | public showSuccess(message: string) { 11 | this.notifierService.notify('success', message); 12 | } 13 | 14 | public showError(message: string) { 15 | this.notifierService.notify('error', message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/File.Frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FileFrontend 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/File.Frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | 7 | platformBrowserDynamic().bootstrapModule(AppModule, { 8 | ngZoneEventCoalescing: true 9 | }) 10 | .catch(err => console.error(err)); 11 | -------------------------------------------------------------------------------- /src/File.Frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "../node_modules/bootstrap/dist/css/bootstrap.min.css"; 3 | @import "../node_modules/gramli-angular-notifier/styles.scss"; -------------------------------------------------------------------------------- /src/File.Frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [ 8 | "node", 9 | "@angular/localize" 10 | ] 11 | }, 12 | "files": [ 13 | "src/main.ts" 14 | ], 15 | "include": [ 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/File.Frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "bundler", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/File.Frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine", 9 | "@angular/localize" 10 | ] 11 | }, 12 | "include": [ 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Abstractions/IEncodingFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace File.Infrastructure.Abstractions 4 | { 5 | internal interface IEncodingFactory 6 | { 7 | Encoding CreateEncoding(byte[] data); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Abstractions/IFileConverter.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace File.Infrastructure.Abstractions 4 | { 5 | internal interface IFileConverter 6 | { 7 | Task> Convert(string fileContent, CancellationToken cancellationToken); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Abstractions/IFileConverterFactory.cs: -------------------------------------------------------------------------------- 1 | namespace File.Infrastructure.Abstractions 2 | { 3 | internal interface IFileConverterFactory 4 | { 5 | IFileConverter Create(string sourceFormat, string destinationExtension); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Configuration/ContainerConfigurationExtension.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Infrastructure.Abstractions; 3 | using File.Infrastructure.Database.EFContext; 4 | using File.Infrastructure.Database.Repositories; 5 | using File.Infrastructure.FileConversions; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace File.Infrastructure.Configuration 11 | { 12 | public static class ContainerConfigurationExtension 13 | { 14 | public static IServiceCollection AddInfrastructure(this IServiceCollection serviceCollection, IConfiguration configuration) 15 | { 16 | return serviceCollection 17 | .AddDatabase() 18 | .AddConversion(); 19 | } 20 | 21 | private static IServiceCollection AddConversion(this IServiceCollection serviceCollection) 22 | { 23 | return serviceCollection 24 | .AddSingleton() 25 | .AddSingleton() 26 | .AddScoped(); 27 | } 28 | 29 | private static IServiceCollection AddDatabase(this IServiceCollection serviceCollection) 30 | { 31 | return serviceCollection 32 | .AddDbContext(opt => opt.UseInMemoryDatabase("Files")) 33 | .AddScoped() 34 | .AddScoped(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Database/EFContext/Entities/FileEntity.cs: -------------------------------------------------------------------------------- 1 | namespace File.Infrastructure.Database.EFContext.Entities 2 | { 3 | internal sealed class FileEntity 4 | { 5 | public int Id { get; set; } 6 | public string FileName { get; init; } = string.Empty; 7 | public string ContentType { get; init; } = string.Empty; 8 | public byte[] Data { get; set; } = []; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Database/EFContext/FileContext.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Database.EFContext.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace File.Infrastructure.Database.EFContext 5 | { 6 | internal class FileContext : DbContext 7 | { 8 | public FileContext(DbContextOptions options) 9 | : base(options) { } 10 | 11 | public virtual DbSet Files => Set(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Database/Repositories/FileCommandsRepository.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Domain.Dtos; 4 | using File.Infrastructure.Database.EFContext; 5 | using File.Infrastructure.Database.EFContext.Entities; 6 | using Mapster; 7 | 8 | namespace File.Infrastructure.Database.Repositories 9 | { 10 | internal sealed class FileCommandsRepository : IFileCommandsRepository 11 | { 12 | private readonly FileContext _context; 13 | public FileCommandsRepository(FileContext fileContext) 14 | { 15 | _context = Guard.Against.Null(fileContext); 16 | } 17 | public async Task AddFileAsync(FileDto fileDto, CancellationToken cancellationToken) 18 | { 19 | var fileEntity = fileDto.Adapt(); 20 | await _context.Files.AddAsync(fileEntity); 21 | await _context.SaveChangesAsync(cancellationToken); 22 | return fileEntity.Id; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Database/Repositories/FileQueriesRepository.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Domain.Dtos; 4 | using File.Domain.Queries; 5 | using File.Infrastructure.Database.EFContext; 6 | using File.Infrastructure.Resources; 7 | using FluentResults; 8 | using Mapster; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace File.Infrastructure.Database.Repositories 12 | { 13 | internal sealed class FileQueriesRepository : IFileQueriesRepository 14 | { 15 | private readonly FileContext _context; 16 | public FileQueriesRepository(FileContext fileContext) 17 | { 18 | _context = Guard.Against.Null(fileContext); 19 | } 20 | 21 | public async Task> GetFile(DownloadFileQuery downloadFileQuery, CancellationToken cancellationToken) 22 | { 23 | var file = await _context.Files.FirstOrDefaultAsync(x => x.Id.Equals(downloadFileQuery.Id), cancellationToken); 24 | if(file is null) 25 | { 26 | return Result.Fail(string.Format(ErrorMessages.FileNotExists, downloadFileQuery.Id)); 27 | } 28 | 29 | return Result.Ok(new FileDto 30 | { 31 | ContentType = file.ContentType, 32 | Data = file.Data, 33 | FileName = file.FileName, 34 | Length = file.Data.Length, 35 | }); 36 | } 37 | 38 | public async Task> GetFilesInfo(CancellationToken cancellationToken) 39 | { 40 | return await _context.Files.ProjectToType().ToListAsync(cancellationToken); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Extensions/ResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Resources; 2 | using FluentResults; 3 | 4 | namespace File.Infrastructure.Extensions 5 | { 6 | internal static class ResultExtensions 7 | { 8 | //TODO BETTER SOL? 9 | public static Result OkIfNotNull(this string value) 10 | { 11 | return string.IsNullOrEmpty(value) ? 12 | Result.Fail(ErrorMessages.ConversionFailed) : 13 | Result.Ok(value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/File.Infrastructure/File.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <_Parameter1>File.Infrastructure.UnitTests 27 | 28 | 29 | 30 | 31 | 32 | 33 | <_Parameter1>DynamicProxyGenAssembly2 34 | 35 | 36 | 37 | 38 | 39 | <_Parameter1>File.Infrastructure.IntegrationTests 40 | 41 | 42 | 43 | 44 | 45 | True 46 | True 47 | ErrorMessages.resx 48 | 49 | 50 | 51 | 52 | 53 | ResXFileCodeGenerator 54 | ErrorMessages.Designer.cs 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/ConvertedFile.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Domain.Abstractions; 3 | 4 | namespace File.Infrastructure.FileConversions 5 | { 6 | internal sealed class ConvertedFile : IFileProxy 7 | { 8 | public string FileName { get; } 9 | 10 | public string ContentType { get; } 11 | 12 | public long Length => _data.Length; 13 | 14 | private readonly byte[] _data; 15 | 16 | public ConvertedFile(string fileName, string contentType, byte[] data) 17 | { 18 | _data = Guard.Against.Null(data); 19 | FileName = Guard.Against.NullOrEmpty(fileName); 20 | ContentType = Guard.Against.NullOrEmpty(contentType); 21 | } 22 | 23 | public async Task CopyToAsync(Stream target, CancellationToken cancellationToken = default) 24 | { 25 | await target.WriteAsync(_data, cancellationToken); 26 | } 27 | 28 | public Task GetData(CancellationToken cancellationToken = default) 29 | { 30 | return Task.FromResult(_data); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/Converters/JsonToXmlFileConverter.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.Resources; 3 | using FluentResults; 4 | using Newtonsoft.Json; 5 | 6 | namespace File.Infrastructure.FileConversions.Converters 7 | { 8 | internal sealed class JsonToXmlFileConverter : IFileConverter 9 | { 10 | private static readonly string _rootElement = "rootElement"; 11 | public Task> Convert(string jsonString, CancellationToken cancellationToken) 12 | { 13 | var doc = JsonConvert.DeserializeXmlNode(jsonString, _rootElement); 14 | if(doc is null) 15 | { 16 | return Task.FromResult(Result.Fail(ErrorMessages.ConversionFailed)); 17 | } 18 | return Task.FromResult(Result.Ok(doc.OuterXml)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/Converters/JsonToYamlFileConverter.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.Extensions; 3 | using FluentResults; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Converters; 6 | using System.Dynamic; 7 | 8 | namespace File.Infrastructure.FileConversions.Converters 9 | { 10 | internal sealed class JsonToYamlFileConverter : IFileConverter 11 | { 12 | public Task> Convert(string fileContent, CancellationToken cancellationToken) 13 | { 14 | var expConverter = new ExpandoObjectConverter(); 15 | var deserializedObject = JsonConvert.DeserializeObject(fileContent, expConverter); 16 | 17 | var serializer = new YamlDotNet.Serialization.Serializer(); 18 | var yamlContent = serializer.Serialize(deserializedObject); 19 | return Task.FromResult(yamlContent.OkIfNotNull()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/Converters/XmlToJsonFileConverter.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.Extensions; 3 | using FluentResults; 4 | using Newtonsoft.Json; 5 | using System.Xml; 6 | 7 | namespace File.Infrastructure.FileConversions.Converters 8 | { 9 | internal sealed class XmlToJsonFileConverter : IFileConverter 10 | { 11 | public Task> Convert(string fileContent, CancellationToken cancellationToken) 12 | { 13 | var doc = new XmlDocument(); 14 | doc.LoadXml(fileContent); 15 | var jsonText = JsonConvert.SerializeXmlNode(doc); 16 | return Task.FromResult(jsonText.OkIfNotNull()); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/Converters/XmlToYamlFileConverter.cs: -------------------------------------------------------------------------------- 1 | using ChoETL; 2 | using File.Infrastructure.Abstractions; 3 | using File.Infrastructure.Extensions; 4 | using FluentResults; 5 | using System.Text; 6 | 7 | namespace File.Infrastructure.FileConversions.Converters 8 | { 9 | internal sealed class XmlToYamlFileConverter : IFileConverter 10 | { 11 | public Task> Convert(string fileContent, CancellationToken cancellationToken) 12 | { 13 | using var xmlCoReader = ChoXmlReader.LoadText(fileContent); 14 | var stringBuilder = new StringBuilder(); 15 | using var yamlWriter = new ChoYamlWriter(stringBuilder); 16 | yamlWriter.Write(xmlCoReader); 17 | var yamlContent = stringBuilder.ToString(); 18 | return Task.FromResult(yamlContent.OkIfNotNull()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/Converters/YamlToJsonFileConverter.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.Extensions; 3 | using FluentResults; 4 | using Newtonsoft.Json; 5 | using YamlDotNet.Serialization; 6 | 7 | namespace File.Infrastructure.FileConversions.Converters 8 | { 9 | internal sealed class YamlToJsonFileConverter : IFileConverter 10 | { 11 | public Task> Convert(string fileContent, CancellationToken cancellationToken) 12 | { 13 | var deserializer = new Deserializer(); 14 | var yamlObject = deserializer.Deserialize(fileContent); 15 | 16 | var jsonSerializer = new JsonSerializer(); 17 | using var writer = new StringWriter(); 18 | jsonSerializer.Serialize(writer, yamlObject); 19 | 20 | return Task.FromResult(writer.ToString().OkIfNotNull()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/EncodingFactory.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Extensions; 2 | using File.Infrastructure.Abstractions; 3 | using System.Text; 4 | 5 | namespace File.Infrastructure.FileConversions 6 | { 7 | internal sealed class EncodingFactory : IEncodingFactory 8 | { 9 | private static readonly Dictionary _encodingMap = new Dictionary 10 | { 11 | {new byte[]{ 0xef, 0xbb, 0xbf }, Encoding.UTF8 }, 12 | {new byte[]{ 0xff, 0xfe, 0, 0 }, Encoding.UTF32 }, 13 | {new byte[]{ 0xff, 0xfe }, Encoding.Unicode }, 14 | {new byte[]{ 0xfe, 0xff }, Encoding.BigEndianUnicode }, 15 | {new byte[]{ 0, 0, 0xfe, 0xff}, new UTF32Encoding(true, true) } 16 | }; 17 | 18 | public Encoding CreateEncoding(byte[] data) 19 | { 20 | if(!data.HasAny() || data.Length < 4) 21 | { 22 | throw new ArgumentException("EncodingData"); 23 | } 24 | 25 | var firstBytes = new ReadOnlySpan(data, 0, 4); 26 | foreach(var encoder in _encodingMap) 27 | { 28 | if(firstBytes.SequenceEqual(encoder.Key)) 29 | { 30 | return encoder.Value; 31 | } 32 | } 33 | 34 | return Encoding.ASCII; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/FileConversionService.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using File.Core.Abstractions; 3 | using File.Domain.Abstractions; 4 | using File.Domain.Dtos; 5 | using File.Domain.Extensions; 6 | using File.Infrastructure.Abstractions; 7 | using FluentResults; 8 | 9 | namespace File.Infrastructure.FileConversions 10 | { 11 | internal sealed class FileConversionService : IFileConvertService 12 | { 13 | private readonly IFileConverterFactory _fileConverterFactory; 14 | private readonly IEncodingFactory _encodingFactory; 15 | 16 | public FileConversionService(IFileConverterFactory fileConverterFactory, IEncodingFactory encodingFactory) 17 | { 18 | _fileConverterFactory = Guard.Against.Null(fileConverterFactory); 19 | _encodingFactory = Guard.Against.Null(encodingFactory); 20 | } 21 | 22 | public async Task> ConvertTo(IFileProxy sourceFile, string destinationExtension, CancellationToken cancellationToken) 23 | { 24 | var data = await sourceFile.GetData(cancellationToken); 25 | return await ConvertFile(data, sourceFile.FileName, sourceFile.ContentType, destinationExtension, cancellationToken); 26 | } 27 | 28 | public async Task> ExportTo(FileDto sourceFile, string destinationExtension, CancellationToken cancellationToken) 29 | { 30 | return await ConvertFile(sourceFile.Data, sourceFile.FileName, sourceFile.ContentType, destinationExtension, cancellationToken); 31 | } 32 | 33 | private async Task> ConvertFile(byte[] data, string fileName, string contentType, string destinationExtension, CancellationToken cancellationToken) 34 | { 35 | var encoding = _encodingFactory.CreateEncoding(data); 36 | 37 | var converter = _fileConverterFactory.Create(fileName.GetFileExtension(), destinationExtension); 38 | var convertedContentResult = await converter.Convert(encoding.GetString(data), cancellationToken); 39 | 40 | if(convertedContentResult.IsFailed) 41 | { 42 | return Result.Fail(convertedContentResult.Errors); 43 | } 44 | 45 | var convertedData = encoding.GetBytes(convertedContentResult.Value); 46 | var convertedFileName = $"{Path.GetFileNameWithoutExtension(fileName)}.{destinationExtension}"; 47 | 48 | return Result.Ok(new ConvertedFile(convertedFileName, contentType, convertedData)); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/File.Infrastructure/FileConversions/FileConverterFactory.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | 4 | namespace File.Infrastructure.FileConversions 5 | { 6 | internal sealed class FileConverterFactory : IFileConverterFactory 7 | { 8 | private readonly Dictionary<(string,string), IFileConverter> _converters = new Dictionary<(string, string), IFileConverter> 9 | { 10 | {("json","xml") ,new JsonToXmlFileConverter() }, 11 | {("json","yaml") ,new JsonToYamlFileConverter() }, 12 | {("yaml","json") ,new YamlToJsonFileConverter() }, 13 | {("xml","json") ,new XmlToJsonFileConverter() }, 14 | {("xml","yaml") ,new XmlToYamlFileConverter() } 15 | }; 16 | 17 | public IFileConverter Create(string sourceFormat, string destinationFormat) 18 | { 19 | if(!_converters.TryGetValue((sourceFormat, destinationFormat), out var fileConverter)) 20 | { 21 | throw new NotSupportedException($"Not supported conversion: source: {sourceFormat}, destination: {destinationFormat}"); 22 | } 23 | 24 | return fileConverter; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Resources/ErrorMessages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace File.Infrastructure.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class ErrorMessages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal ErrorMessages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("File.Infrastructure.Resources.ErrorMessages", typeof(ErrorMessages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Conversion failed. Unable to convert.. 65 | /// 66 | internal static string ConversionFailed { 67 | get { 68 | return ResourceManager.GetString("ConversionFailed", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Data for conversion are empty.. 74 | /// 75 | internal static string EmptyConversionData { 76 | get { 77 | return ResourceManager.GetString("EmptyConversionData", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to File id:{0} does not exists.. 83 | /// 84 | internal static string FileNotExists { 85 | get { 86 | return ResourceManager.GetString("FileNotExists", resourceCulture); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/File.Infrastructure/Resources/ErrorMessages.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Conversion failed. Unable to convert. 122 | 123 | 124 | Data for conversion are empty. 125 | 126 | 127 | File id:{0} does not exists. 128 | 129 | -------------------------------------------------------------------------------- /src/FileSol.slnLaunch: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "Run API and FE", 4 | "Projects": [ 5 | { 6 | "Path": "File.API\\File.API.csproj", 7 | "Action": "Start", 8 | "DebugTarget": "https" 9 | }, 10 | { 11 | "Path": "File.Frontend\\File.Frontend.esproj", 12 | "Action": "Start", 13 | "DebugTarget": "localhost (Chrome)" 14 | } 15 | ] 16 | } 17 | ] -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/Assets/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "FileAPI", 5 | "description": "File api description", 6 | "version": "1.0.0" 7 | }, 8 | "host": "fileAPI.com", 9 | "schemes": [ 10 | "https" 11 | ], 12 | "basePath": "/v1", 13 | "produces": [ 14 | "application/json" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/Assets/new.xml: -------------------------------------------------------------------------------- 1 | 2 | 2.0 3 | 4 | FileAPI 5 | File api description 6 | 1.0.0 7 | 8 | fileAPI.com 9 | https 10 | /v1 11 | application/json 12 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/Assets/new.yaml: -------------------------------------------------------------------------------- 1 | swagger: 2.0 2 | info: 3 | title: FileAPI 4 | description: File api description 5 | version: 1.0.0 6 | host: fileAPI.com 7 | schemes: 8 | - https 9 | basePath: /v1 10 | produces: 11 | - application/json -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/Assets/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootElement": { 3 | "swagger": "2.0", 4 | "info": { 5 | "title": "FileAPI", 6 | "description": "File api description", 7 | "version": "1.0.0" 8 | }, 9 | "host": "fileAPI.com", 10 | "schemes": "https", 11 | "basePath": "/v1", 12 | "produces": "application/json" 13 | } 14 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/Extensions/AssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace File.Infrastructure.IntegrationTests.Extensions 4 | { 5 | internal static class AssertExtensions 6 | { 7 | public static void EqualFileContent(string expected, string actual) 8 | { 9 | Assert.Equal(0, string.Compare(actual, 10 | System.IO.File.ReadAllText(expected), 11 | CultureInfo.CurrentCulture, CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/File.Infrastructure.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Always 31 | 32 | 33 | Always 34 | 35 | 36 | Always 37 | 38 | 39 | Always 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/FileConversions/Converters/JsonToXmlFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.IntegrationTests.Extensions; 4 | 5 | namespace File.Infrastructure.IntegrationTests.FileConversions.Converters 6 | { 7 | public class JsonToXmlFileConverterTests 8 | { 9 | private readonly IFileConverter _uut; 10 | 11 | public JsonToXmlFileConverterTests() 12 | { 13 | _uut = new JsonToXmlFileConverter(); 14 | } 15 | 16 | [Fact] 17 | public async Task Convert_Success() 18 | { 19 | //Arrange 20 | using var fileStream = new FileStream("Assets/new.json", FileMode.Open, FileAccess.Read, FileShare.Read); 21 | using var reader = new StreamReader(fileStream); 22 | //Act 23 | var result = await _uut.Convert(reader.ReadToEnd(), CancellationToken.None); 24 | //Assert 25 | Assert.True(result.IsSuccess); 26 | Assert.NotEmpty(result.Value); 27 | AssertExtensions.EqualFileContent("Assets/new.xml", result.Value); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/FileConversions/Converters/JsonToYamlFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.IntegrationTests.Extensions; 4 | 5 | namespace File.Infrastructure.IntegrationTests.FileConversions.Converters 6 | { 7 | public class JsonToYamlFileConverterTests 8 | { 9 | private readonly IFileConverter _uut; 10 | 11 | public JsonToYamlFileConverterTests() 12 | { 13 | _uut = new JsonToYamlFileConverter(); 14 | } 15 | 16 | [Fact] 17 | public async Task Convert_Success() 18 | { 19 | //Arrange 20 | using var fileStream = new FileStream("Assets/new.json", FileMode.Open, FileAccess.Read, FileShare.Read); 21 | using var reader = new StreamReader(fileStream); 22 | //Act 23 | var result = await _uut.Convert(reader.ReadToEnd(), CancellationToken.None); 24 | //Assert 25 | Assert.True(result.IsSuccess); 26 | Assert.NotEmpty(result.Value); 27 | AssertExtensions.EqualFileContent("Assets/new.yaml", result.Value); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/FileConversions/Converters/XmlToJsonFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.IntegrationTests.Extensions; 4 | 5 | namespace File.Infrastructure.IntegrationTests.FileConversions.Converters 6 | { 7 | public class XmlToJsonFileConverterTests 8 | { 9 | private readonly IFileConverter _uut; 10 | 11 | public XmlToJsonFileConverterTests() 12 | { 13 | _uut = new XmlToJsonFileConverter(); 14 | } 15 | 16 | [Fact] 17 | public async Task Convert_Success() 18 | { 19 | //Arrange 20 | using var fileStream = new FileStream("Assets/new.xml", FileMode.Open, FileAccess.Read, FileShare.Read); 21 | using var reader = new StreamReader(fileStream); 22 | //Act 23 | var result = await _uut.Convert(reader.ReadToEnd(), CancellationToken.None); 24 | //Assert 25 | Assert.True(result.IsSuccess); 26 | Assert.NotEmpty(result.Value); 27 | AssertExtensions.EqualFileContent("Assets/root.json", result.Value); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/FileConversions/Converters/XmlToYamlFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | 4 | namespace File.Infrastructure.IntegrationTests.FileConversions.Converters 5 | { 6 | public class XmlToYamlFileConverterTests 7 | { 8 | private readonly IFileConverter _uut; 9 | 10 | public XmlToYamlFileConverterTests() 11 | { 12 | _uut = new XmlToYamlFileConverter(); 13 | } 14 | 15 | [Fact] 16 | public async Task Convert_Success() 17 | { 18 | //Arrange 19 | using var fileStream = new FileStream("Assets/new.xml", FileMode.Open, FileAccess.Read, FileShare.Read); 20 | using var reader = new StreamReader(fileStream); 21 | //Act 22 | var result = await _uut.Convert(reader.ReadToEnd(), CancellationToken.None); 23 | //Assert 24 | Assert.True(result.IsSuccess); 25 | Assert.NotEmpty(result.Value); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/FileConversions/Converters/YamlToJsonFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | 4 | namespace File.Infrastructure.IntegrationTests.FileConversions.Converters 5 | { 6 | public class YamlToJsonFileConverterTests 7 | { 8 | private readonly IFileConverter _uut; 9 | 10 | public YamlToJsonFileConverterTests() 11 | { 12 | _uut = new YamlToJsonFileConverter(); 13 | } 14 | 15 | [Fact] 16 | public async Task Convert_Success() 17 | { 18 | //Arrange 19 | using var fileStream = new FileStream("Assets/new.yaml", FileMode.Open, FileAccess.Read, FileShare.Read); 20 | using var reader = new StreamReader(fileStream); 21 | //Act 22 | var result = await _uut.Convert(reader.ReadToEnd(), CancellationToken.None); 23 | //Assert 24 | Assert.True(result.IsSuccess); 25 | Assert.NotEmpty(result.Value); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tests/IntegrationTests/File.Infrastructure.IntegrationTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Assets/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "FileAPI", 5 | "description": "File api description", 6 | "version": "1.0.0" 7 | }, 8 | "host": "fileAPI.com", 9 | "schemes": [ 10 | "https" 11 | ], 12 | "basePath": "/v1", 13 | "produces": [ 14 | "application/json" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Extensions/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace File.API.SystemTests.Extensions 2 | { 3 | internal static class HttpClientExtensions 4 | { 5 | public static async Task UploadAssetsFile(this HttpClient httpClient, string fileName) 6 | { 7 | using var content = new MultipartFormDataContent().AddFileFromAssets(fileName); 8 | return await httpClient.PostAsync("file/v1/upload", content); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Extensions/HttpResponseMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace File.API.SystemTests.Extensions 4 | { 5 | internal static class HttpResponseMessageExtensions 6 | { 7 | public static async Task GetResponseData(this HttpResponseMessage httpResponseMessage) 8 | { 9 | httpResponseMessage.EnsureSuccessStatusCode(); 10 | var resultString = await httpResponseMessage.Content.ReadAsStringAsync(); 11 | return JsonConvert.DeserializeObject(resultString); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Extensions/MultipartFormDataContentExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace File.API.SystemTests.Extensions 2 | { 3 | public static class MultipartFormDataContentExtensions 4 | { 5 | public static MultipartFormDataContent AddFileFromAssets(this MultipartFormDataContent content, string fileName) 6 | { 7 | var stream = new FileStream($"Assets/{fileName}", FileMode.Open, FileAccess.Read, FileShare.Read); 8 | var streamContent = new StreamContent(stream); 9 | streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); 10 | content.Add(streamContent, "file", fileName); 11 | return content; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/File.API.SystemTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Always 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Tests/ConvertTests.cs: -------------------------------------------------------------------------------- 1 | using File.API.SystemTests.Extensions; 2 | 3 | namespace File.API.SystemTests.Tests 4 | { 5 | public class ConvertTests : SystemTestsBase 6 | { 7 | [Fact] 8 | public async Task ConvertFileAsync_ToXml() 9 | { 10 | //Arrange 11 | using var content = new MultipartFormDataContent().AddFileFromAssets("new.json"); 12 | 13 | //Act 14 | using var response = await _httpClient.PostAsync("file/v1/convert?formatToExport=xml", content); 15 | using var stream = await response.Content.ReadAsStreamAsync(); 16 | 17 | //Assert 18 | Assert.True(stream.Length > 0); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Tests/DownloadTests.cs: -------------------------------------------------------------------------------- 1 | using ChoETL; 2 | using File.API.SystemTests.Extensions; 3 | using File.Domain.Dtos; 4 | using SmallApiToolkit.Core.Response; 5 | using System.Net.Http.Json; 6 | 7 | namespace File.API.SystemTests.Tests 8 | { 9 | public class DownloadTests : SystemTestsBase 10 | { 11 | [Fact] 12 | public async Task DownloadFileAsync() 13 | { 14 | //Arrange 15 | using var uploadResponse = (await _httpClient.UploadAssetsFile("new.json")) 16 | .EnsureSuccessStatusCode(); 17 | 18 | using var fileInfo = await _httpClient.GetAsync("file/v1/files-info"); 19 | var fileToDownload = (await fileInfo.GetResponseData>>())?.Data?.First(); 20 | 21 | if(fileToDownload is null) 22 | { 23 | Assert.Fail("Downloaded file is empty."); 24 | } 25 | 26 | //Act 27 | using var response = await _httpClient.GetAsync($"file/v1/download/?id={fileToDownload.Id}"); 28 | using var stream = await response.Content.ReadAsStreamAsync(); 29 | 30 | //Assert 31 | Assert.True(stream.Length > 0); 32 | } 33 | 34 | [Fact] 35 | public async Task DownloadJsonFileAsync() 36 | { 37 | //Arrange 38 | using var uploadResponse = (await _httpClient.UploadAssetsFile("new.json")) 39 | .EnsureSuccessStatusCode(); 40 | 41 | using var fileInfo = await _httpClient.GetAsync("file/v1/files-info"); 42 | var fileToDownload = (await fileInfo.GetResponseData>>())?.Data?.First(); 43 | 44 | if (fileToDownload is null) 45 | { 46 | Assert.Fail("Downloaded file is empty."); 47 | } 48 | 49 | //Act 50 | using var response = await _httpClient.GetAsync($"file/v1/downloadAsJson/?id={fileToDownload.Id}"); 51 | var responseFile = await response.Content.ReadFromJsonAsync>(); 52 | 53 | //Assert 54 | Assert.True(responseFile!.Data!.Data.Length > 0); 55 | Assert.NotEmpty(responseFile!.Data!.FileName); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Tests/ExportTests.cs: -------------------------------------------------------------------------------- 1 | using File.API.SystemTests.Extensions; 2 | using File.Domain.Dtos; 3 | using File.Domain.Queries; 4 | using Newtonsoft.Json; 5 | using SmallApiToolkit.Core.Response; 6 | using System.Text; 7 | 8 | namespace File.API.SystemTests.Tests 9 | { 10 | public class ExportTests : SystemTestsBase 11 | { 12 | [Fact] 13 | public async Task ExportFileAsync_ToXml() 14 | { 15 | //Arrange 16 | using var uploadResponse = (await _httpClient.UploadAssetsFile("new.json")) 17 | .EnsureSuccessStatusCode(); 18 | 19 | using var fileInfo = await _httpClient.GetAsync("file/v1/files-info"); 20 | var fileToDownload = (await fileInfo.GetResponseData>>())?.Data?.First(); 21 | 22 | if (fileToDownload is null) 23 | { 24 | Assert.Fail("Downloaded file is empty."); 25 | } 26 | 27 | var body = JsonConvert.SerializeObject(new ExportFileQuery 28 | { 29 | Id = fileToDownload.Id, 30 | Extension = "xml" 31 | }); 32 | var content = new StringContent(body, Encoding.UTF8, "application/json"); 33 | 34 | //Act 35 | using var response = await _httpClient.PostAsync($"file/v1/export", content); 36 | using var stream = await response.Content.ReadAsStreamAsync(); 37 | 38 | //Assert 39 | Assert.True(stream.Length > 0); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Tests/GetTests.cs: -------------------------------------------------------------------------------- 1 | using File.API.SystemTests.Extensions; 2 | using File.Domain.Dtos; 3 | using SmallApiToolkit.Core.Response; 4 | 5 | namespace File.API.SystemTests.Tests 6 | { 7 | public class GetTests : SystemTestsBase 8 | { 9 | [Fact] 10 | public async Task GetFileInfoAsync() 11 | { 12 | //Arrange 13 | (await _httpClient.UploadAssetsFile("new.json")) 14 | .EnsureSuccessStatusCode(); 15 | 16 | //Act 17 | var result = await _httpClient.GetAsync("file/v1/files-info"); 18 | 19 | //Assert 20 | var resultData = await result.GetResponseData>>(); 21 | Assert.NotNull(resultData); 22 | Assert.NotEmpty(resultData.Data); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Tests/SystemTestsBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | 3 | namespace File.API.SystemTests.Tests 4 | { 5 | public abstract class SystemTestsBase 6 | { 7 | protected readonly HttpClient _httpClient; 8 | 9 | protected SystemTestsBase() 10 | { 11 | var application = new WebApplicationFactory(); 12 | _httpClient = application.CreateClient(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Tests/UploadTests.cs: -------------------------------------------------------------------------------- 1 | using File.API.SystemTests.Extensions; 2 | using SmallApiToolkit.Core.Response; 3 | 4 | namespace File.API.SystemTests.Tests 5 | { 6 | public class UploadTests : SystemTestsBase 7 | { 8 | [Fact] 9 | public async Task UploadFileAsync() 10 | { 11 | //Arrange 12 | //Act 13 | var result = await _httpClient.UploadAssetsFile("new.json"); 14 | 15 | //Assert 16 | var resultData = await result.GetResponseData>(); 17 | Assert.True(resultData.Data); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Tests/SystemTests/File.API.SystemTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.API.UnitTests/File.API.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.API.UnitTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/Assets/FileMockFactory.cs: -------------------------------------------------------------------------------- 1 | using File.Domain.Abstractions; 2 | 3 | namespace File.Core.UnitTests.Assets 4 | { 5 | internal static class FileMockFactory 6 | { 7 | public static Mock CreateMock(byte[] resultFileData, string contentType, string fileName) 8 | { 9 | var resultFileMock = new Mock(); 10 | resultFileMock.SetupGet(x => x.ContentType).Returns(contentType); 11 | resultFileMock.SetupGet(x => x.FileName).Returns(fileName); 12 | resultFileMock.SetupGet(x => x.Length).Returns(resultFileData.Length); 13 | resultFileMock.Setup(x => x.GetData(It.IsAny())).ReturnsAsync(resultFileData); 14 | return resultFileMock; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/File.Core.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/Queries/ConvertToQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Core.Queries; 3 | using File.Core.Resources; 4 | using File.Core.UnitTests.Assets; 5 | using File.Domain.Abstractions; 6 | using File.Domain.Logging; 7 | using File.UnitTests.Common.Extensions; 8 | using System.Net; 9 | 10 | namespace File.Core.UnitTests.Queries 11 | { 12 | public class ConvertToQueryHandlerTests 13 | { 14 | private readonly Mock> _loggerMock; 15 | private readonly Mock _convertToQueryValidatorMock; 16 | private readonly Mock _fileConvertServiceMock; 17 | 18 | private readonly Mock _fileMock; 19 | 20 | private readonly IConvertToQueryHandler _uut; 21 | 22 | public ConvertToQueryHandlerTests() 23 | { 24 | _loggerMock = new Mock>(); 25 | _convertToQueryValidatorMock = new Mock(); 26 | _fileConvertServiceMock = new Mock(); 27 | 28 | _fileMock = new Mock(); 29 | 30 | _uut = new ConvertToQueryHandler(_loggerMock.Object, _fileConvertServiceMock.Object, _convertToQueryValidatorMock.Object); 31 | } 32 | 33 | [Fact] 34 | public async Task RequestValidation_Failed() 35 | { 36 | //Arrange 37 | var failedMessage = nameof(RequestValidation_Failed); 38 | _convertToQueryValidatorMock.Setup(x=>x.Validate(It.IsAny())).Returns(Result.Fail(failedMessage)); 39 | 40 | var request = new ConvertToQuery(_fileMock.Object, string.Empty); 41 | 42 | //Act 43 | var result = await _uut.HandleAsync(request, CancellationToken.None); 44 | 45 | //Assert 46 | Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); 47 | Assert.Single(result.Errors); 48 | Assert.Equal(result.Errors.First(), failedMessage); 49 | _convertToQueryValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(request))), Times.Once); 50 | } 51 | 52 | [Fact] 53 | public async Task FileConvert_Failed() 54 | { 55 | //Arrange 56 | var fileName = nameof(FileConvert_Failed); 57 | var format = "json"; 58 | var request = new ConvertToQuery(_fileMock.Object, format); 59 | var failedMessage = nameof(FileConvert_Failed); 60 | _convertToQueryValidatorMock.Setup(x => x.Validate(It.IsAny())).Returns(Result.Ok(true)); 61 | _fileConvertServiceMock.Setup(x => x.ConvertTo(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(Result.Fail(failedMessage)); 62 | _fileMock.SetupGet(x => x.FileName).Returns(fileName); 63 | 64 | //Act 65 | var result = await _uut.HandleAsync(request, CancellationToken.None); 66 | 67 | //Assert 68 | Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); 69 | Assert.Single(result.Errors); 70 | Assert.Equal(result.Errors.First(), string.Format(ErrorMessages.ConvertFileFailed, fileName, format)); 71 | _convertToQueryValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(request))), Times.Once); 72 | _fileConvertServiceMock.Verify(x => x.ConvertTo(It.IsAny(), It.Is(y=>y.Equals(format)), It.IsAny()), Times.Once); 73 | _loggerMock.VerifyLog(LogLevel.Error, LogEvents.ConvertFileGeneralError, Times.Once()); 74 | } 75 | 76 | [Fact] 77 | public async Task Success() 78 | { 79 | //Arrange 80 | var resultFileData = new byte[10]; 81 | var resultFileMock = FileMockFactory.CreateMock(resultFileData, "application/json", "resultFileName"); 82 | 83 | var format = "xml"; 84 | var request = new ConvertToQuery(_fileMock.Object, format); 85 | _convertToQueryValidatorMock.Setup(x => x.Validate(It.IsAny())).Returns(Result.Ok(true)); 86 | _fileConvertServiceMock.Setup(x => x.ConvertTo(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(Result.Ok(resultFileMock.Object)); 87 | _fileMock.SetupGet(x => x.FileName).Returns(nameof(FileConvert_Failed)); 88 | 89 | //Act 90 | var result = await _uut.HandleAsync(request, CancellationToken.None); 91 | 92 | //Assert 93 | Assert.Equal(HttpStatusCode.OK, result.StatusCode); 94 | _convertToQueryValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(request))), Times.Once); 95 | _fileConvertServiceMock.Verify(x => x.ConvertTo(It.IsAny(), It.Is(y => y.Equals(format)), It.IsAny()), Times.Once); 96 | resultFileMock.VerifyGet(x => x.ContentType, Times.Once); 97 | resultFileMock.VerifyGet(x => x.FileName, Times.Once); 98 | resultFileMock.VerifyGet(x => x.Length, Times.Once); 99 | resultFileMock.Verify(x => x.GetData(It.IsAny()), Times.Once); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/Queries/DownloadFileQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Core.Queries; 3 | using File.Core.Resources; 4 | using File.Domain.Dtos; 5 | using File.Domain.Logging; 6 | using File.Domain.Queries; 7 | using File.UnitTests.Common.Extensions; 8 | using System.Net; 9 | using Validot; 10 | using Validot.Results; 11 | 12 | namespace File.Core.UnitTests.Queries 13 | { 14 | public class DownloadFileQueryHandlerTests 15 | { 16 | private readonly Mock> _downloadFileQueryValidatorMock; 17 | private readonly Mock> _loggerMock; 18 | private readonly Mock _fileQueriesRepositoryMock; 19 | private readonly Mock _validationResult; 20 | 21 | private readonly IDownloadFileQueryHandler _uut; 22 | 23 | public DownloadFileQueryHandlerTests() 24 | { 25 | _downloadFileQueryValidatorMock = new Mock>(); 26 | _loggerMock = new Mock>(); 27 | _fileQueriesRepositoryMock = new Mock(); 28 | 29 | _validationResult = new Mock(); 30 | 31 | _uut = new DownloadFileQueryHandler(_downloadFileQueryValidatorMock.Object, _loggerMock.Object, _fileQueriesRepositoryMock.Object); 32 | } 33 | 34 | [Fact] 35 | public async Task ValidationFailed() 36 | { 37 | //Arrange 38 | var query = new DownloadFileQuery(1); 39 | 40 | _downloadFileQueryValidatorMock.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResult.Object); 41 | _validationResult.SetupGet(x=>x.AnyErrors).Returns(true); 42 | _validationResult.Setup(x => x.ToString()).Returns(string.Empty); 43 | 44 | //Act 45 | var result = await _uut.HandleAsync(query, CancellationToken.None); 46 | 47 | //Assert 48 | Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); 49 | Assert.Null(result.Data); 50 | Assert.Single(result.Errors); 51 | Assert.Equal(ValidationErrorMessages.InvalidRequest, result.Errors.First()); 52 | _downloadFileQueryValidatorMock.Verify(x => x.Validate(It.Is(y=>y.Equals(query)), It.IsAny()), Times.Once); 53 | _validationResult.VerifyGet(x => x.AnyErrors, Times.Once); 54 | _validationResult.Verify(x => x.ToString(), Times.Once); 55 | _loggerMock.VerifyLog(LogLevel.Error, LogEvents.GetFileValidationError, Times.Once()); 56 | } 57 | 58 | [Fact] 59 | public async Task GetFileFailed() 60 | { 61 | //Arrange 62 | var query = new DownloadFileQuery(1); 63 | 64 | _downloadFileQueryValidatorMock.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResult.Object); 65 | _validationResult.SetupGet(x => x.AnyErrors).Returns(false); 66 | 67 | _fileQueriesRepositoryMock.Setup(x => x.GetFile(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Fail(string.Empty)); 68 | 69 | //Act 70 | var result = await _uut.HandleAsync(query, CancellationToken.None); 71 | 72 | //Assert 73 | Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); 74 | Assert.Null(result.Data); 75 | Assert.Single(result.Errors); 76 | Assert.Equal(ErrorMessages.FileNotExist, result.Errors.First()); 77 | _downloadFileQueryValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(query)), It.IsAny()), Times.Once); 78 | _validationResult.VerifyGet(x => x.AnyErrors, Times.Once); 79 | _loggerMock.VerifyLog(LogLevel.Error, LogEvents.GetFileDatabaseError, Times.Once()); 80 | _fileQueriesRepositoryMock.Verify(x => x.GetFile(It.Is(y => y.Equals(query)), It.IsAny()), Times.Once); 81 | } 82 | 83 | [Fact] 84 | public async Task Success() 85 | { 86 | //Arrange 87 | var query = new DownloadFileQuery(1); 88 | var resultDto = new FileDto(); 89 | 90 | _downloadFileQueryValidatorMock.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResult.Object); 91 | _validationResult.SetupGet(x => x.AnyErrors).Returns(false); 92 | 93 | _fileQueriesRepositoryMock.Setup(x => x.GetFile(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Ok(resultDto)); 94 | 95 | //Act 96 | var result = await _uut.HandleAsync(query, CancellationToken.None); 97 | 98 | //Assert 99 | Assert.Equal(HttpStatusCode.OK, result.StatusCode); 100 | Assert.Equal(resultDto, result.Data); 101 | Assert.Empty(result.Errors); 102 | _downloadFileQueryValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(query)), It.IsAny()), Times.Once); 103 | _validationResult.VerifyGet(x => x.AnyErrors, Times.Once); 104 | _fileQueriesRepositoryMock.Verify(x => x.GetFile(It.Is(y => y.Equals(query)), It.IsAny()), Times.Once); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Moq; 3 | global using Microsoft.Extensions.Logging; 4 | global using FluentResults; -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/Validation/AddFilesCommandValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Core.UnitTests.Assets; 3 | using File.Core.Validation; 4 | using File.Domain.Abstractions; 5 | using File.Domain.Commands; 6 | using Validot; 7 | using Validot.Results; 8 | 9 | namespace File.Core.UnitTests.Validation 10 | { 11 | public class AddFilesCommandValidatorTests 12 | { 13 | private readonly Mock> _addFilesCommandValidatorMock; 14 | private readonly Mock _fileByOptionsValidatorMock; 15 | private readonly Mock _validationResultMock; 16 | 17 | private readonly IAddFilesCommandValidator _uut; 18 | 19 | public AddFilesCommandValidatorTests() 20 | { 21 | _addFilesCommandValidatorMock = new Mock>(); 22 | _fileByOptionsValidatorMock = new Mock(); 23 | _validationResultMock = new Mock(); 24 | 25 | _uut = new AddFilesCommandValidator(_addFilesCommandValidatorMock.Object, _fileByOptionsValidatorMock.Object); 26 | } 27 | 28 | [Fact] 29 | public void CommandValidation_Failed() 30 | { 31 | //Arrange 32 | var command = new AddFilesCommand([]); 33 | 34 | _validationResultMock.SetupGet(x=>x.AnyErrors).Returns(true); 35 | _addFilesCommandValidatorMock.Setup(x=>x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResultMock.Object); 36 | 37 | //Act 38 | var result = _uut.Validate(command); 39 | 40 | //Assert 41 | Assert.True(result.IsFailed); 42 | _validationResultMock.VerifyGet(x => x.AnyErrors, Times.Once); 43 | _addFilesCommandValidatorMock.Verify(x => x.Validate(It.Is(y=>y.Equals(command)), It.IsAny()), Times.Once); 44 | } 45 | 46 | [Fact] 47 | public void AllFilesValidation_Failed() 48 | { 49 | //Arrange 50 | var fileMockOne = FileMockFactory.CreateMock(new byte[10], "application/json", "resultFileOneName"); 51 | var fileMockTwo = FileMockFactory.CreateMock(new byte[10], "application/json", "resultFileTwoName"); 52 | 53 | var command = new AddFilesCommand( 54 | [ 55 | fileMockOne.Object, 56 | fileMockTwo.Object 57 | ]); 58 | var failedMessage = "failedMessage"; 59 | 60 | _validationResultMock.SetupGet(x => x.AnyErrors).Returns(false); 61 | _addFilesCommandValidatorMock.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResultMock.Object); 62 | _fileByOptionsValidatorMock.Setup(x => x.Validate(It.IsAny())).Returns(Result.Fail(failedMessage)); 63 | 64 | //Act 65 | var result = _uut.Validate(command); 66 | 67 | //Assert 68 | Assert.True(result.IsFailed); 69 | Assert.True(result.IsFailed); 70 | _validationResultMock.VerifyGet(x => x.AnyErrors, Times.Once); 71 | _addFilesCommandValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(command)), It.IsAny()), Times.Once); 72 | _fileByOptionsValidatorMock.Verify(x => x.Validate(It.IsAny()), Times.Once); 73 | } 74 | 75 | [Fact] 76 | public void OneFilesValidation_Failed() 77 | { 78 | //Arrange 79 | var fileNameOne = "fileNameOne"; 80 | var fileNameTwo = "fileNameTwo"; 81 | 82 | var fileMockOne = FileMockFactory.CreateMock(new byte[10], "application/json", fileNameOne); 83 | var fileMockTwo = FileMockFactory.CreateMock(new byte[10], "application/json", fileNameTwo); 84 | 85 | var command = new AddFilesCommand(new List 86 | { 87 | fileMockOne.Object, 88 | fileMockTwo.Object 89 | }); 90 | var failedMessage = "failedMessage"; 91 | 92 | _validationResultMock.SetupGet(x => x.AnyErrors).Returns(false); 93 | _addFilesCommandValidatorMock.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResultMock.Object); 94 | _fileByOptionsValidatorMock.Setup(x => x.Validate(It.Is(y=>y.FileName.Equals(fileNameTwo)))).Returns(Result.Fail(failedMessage)); 95 | _fileByOptionsValidatorMock.Setup(x => x.Validate(It.Is(y =>y.FileName.Equals(fileNameOne)))).Returns(Result.Ok(true)); 96 | 97 | //Act 98 | var result = _uut.Validate(command); 99 | 100 | //Assert 101 | Assert.True(result.IsFailed); 102 | _validationResultMock.VerifyGet(x => x.AnyErrors, Times.Once); 103 | _addFilesCommandValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(command)), It.IsAny()), Times.Once); 104 | _fileByOptionsValidatorMock.Verify(x => x.Validate(It.IsAny()), Times.Exactly(2)); 105 | } 106 | 107 | [Fact] 108 | public void Success() 109 | { 110 | //Arrange 111 | var fileMockOne = FileMockFactory.CreateMock(new byte[10], "application/json", "fileNameOne"); 112 | var fileMockTwo = FileMockFactory.CreateMock(new byte[10], "application/json", "fileNameTwo"); 113 | 114 | var command = new AddFilesCommand(new List 115 | { 116 | fileMockOne.Object, 117 | fileMockTwo.Object 118 | }); 119 | 120 | _validationResultMock.SetupGet(x => x.AnyErrors).Returns(false); 121 | _addFilesCommandValidatorMock.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResultMock.Object); 122 | _fileByOptionsValidatorMock.Setup(x => x.Validate(It.IsAny())).Returns(Result.Ok(true)); 123 | 124 | //Act 125 | var result = _uut.Validate(command); 126 | 127 | //Assert 128 | Assert.True(result.IsSuccess); 129 | _validationResultMock.VerifyGet(x => x.AnyErrors, Times.Once); 130 | _addFilesCommandValidatorMock.Verify(x => x.Validate(It.Is(y => y.Equals(command)), It.IsAny()), Times.Once); 131 | _fileByOptionsValidatorMock.Verify(x => x.Validate(It.IsAny()), Times.Exactly(2)); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/Validation/ExportFileQueryValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Core.Validation; 3 | using File.Domain.Queries; 4 | using Validot; 5 | using Validot.Results; 6 | 7 | namespace File.Core.UnitTests.Validation 8 | { 9 | public class ExportFileQueryValidatorTests 10 | { 11 | private readonly Mock> _exportFileQueryValidator; 12 | private readonly Mock _fileByOptionsValidator; 13 | 14 | private readonly Mock _validationResultMock; 15 | 16 | private readonly IExportFileQueryValidator _uut; 17 | 18 | public ExportFileQueryValidatorTests() 19 | { 20 | _exportFileQueryValidator = new Mock>(); 21 | _fileByOptionsValidator = new Mock(); 22 | 23 | _validationResultMock = new Mock(); 24 | 25 | _uut = new ExportFileQueryValidator(_exportFileQueryValidator.Object, _fileByOptionsValidator.Object); 26 | } 27 | 28 | [Fact] 29 | public void QueryValidation_Failed() 30 | { 31 | //Arrange 32 | var request = new ExportFileQuery(); 33 | 34 | _exportFileQueryValidator.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResultMock.Object); 35 | _validationResultMock.SetupGet(x => x.AnyErrors).Returns(true); 36 | 37 | //Act 38 | var result = _uut.Validate(request); 39 | 40 | //Assert 41 | Assert.True(result.IsFailed); 42 | _exportFileQueryValidator.Verify(x => x.Validate(It.Is(y => y.Equals(request)), It.IsAny()), Times.Once); 43 | _validationResultMock.VerifyGet(x => x.AnyErrors, Times.Once); 44 | } 45 | 46 | [Fact] 47 | public void ExtensionValidation_Failed() 48 | { 49 | //Arrange 50 | var request = new ExportFileQuery 51 | { 52 | Extension = "ex" 53 | }; 54 | 55 | var failedMessage = "failedMessage"; 56 | 57 | _exportFileQueryValidator.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResultMock.Object); 58 | _validationResultMock.SetupGet(x => x.AnyErrors).Returns(false); 59 | _fileByOptionsValidator.Setup(x => x.Validate(It.IsAny())).Returns(Result.Fail(failedMessage)); 60 | 61 | //Act 62 | var result = _uut.Validate(request); 63 | 64 | //Assert 65 | Assert.True(result.IsFailed); 66 | Assert.Single(result.Errors); 67 | Assert.Equal(failedMessage, result.Errors.First().Message); 68 | _exportFileQueryValidator.Verify(x => x.Validate(It.Is(y => y.Equals(request)), It.IsAny()), Times.Once); 69 | _validationResultMock.VerifyGet(x => x.AnyErrors, Times.Once); 70 | _fileByOptionsValidator.Verify(x => x.Validate(It.Is(y => y.Equals(request.Extension))), Times.Once); 71 | } 72 | 73 | [Fact] 74 | public void Success() 75 | { 76 | //Arrange 77 | var request = new ExportFileQuery 78 | { 79 | Extension = "ex" 80 | }; 81 | 82 | _exportFileQueryValidator.Setup(x => x.Validate(It.IsAny(), It.IsAny())).Returns(_validationResultMock.Object); 83 | _validationResultMock.SetupGet(x => x.AnyErrors).Returns(false); 84 | _fileByOptionsValidator.Setup(x => x.Validate(It.IsAny())).Returns(Result.Ok(true)); 85 | 86 | //Act 87 | var result = _uut.Validate(request); 88 | 89 | //Assert 90 | Assert.True(result.IsSuccess); 91 | Assert.Empty(result.Errors); 92 | _exportFileQueryValidator.Verify(x => x.Validate(It.Is(y => y.Equals(request)), It.IsAny()), Times.Once); 93 | _validationResultMock.VerifyGet(x => x.AnyErrors, Times.Once); 94 | _fileByOptionsValidator.Verify(x => x.Validate(It.Is(y => y.Equals(request.Extension))), Times.Once); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Core.UnitTests/Validation/FileByOptionsValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Core.Resources; 3 | using File.Core.UnitTests.Assets; 4 | using File.Core.Validation; 5 | using File.Domain.Options; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace File.Core.UnitTests.Validation 9 | { 10 | public class FileByOptionsValidatorTests 11 | { 12 | private readonly IFileByOptionsValidator _uut; 13 | 14 | public FileByOptionsValidatorTests() 15 | { 16 | var fileOptions = Options.Create(new FilesOptions 17 | { 18 | MaxFileLength = 10, 19 | AllowedFiles = new [] 20 | { 21 | new AllowedFile 22 | { 23 | CanBeExportedTo = new []{"xml", "csv"}, 24 | ContentType = "application/json", 25 | Format = "json", 26 | } 27 | } 28 | }); 29 | 30 | _uut = new FileByOptionsValidator(fileOptions); 31 | } 32 | 33 | [Fact] 34 | public void ValidateFile_NotAllowedContentType() 35 | { 36 | //Arrange 37 | var fileName = "fileName"; 38 | var contentType = "contentType"; 39 | var fileMock = FileMockFactory.CreateMock(new byte[0], contentType, fileName); 40 | 41 | //Act 42 | var result = _uut.Validate(fileMock.Object); 43 | 44 | //Assert 45 | Assert.True(result.IsFailed); 46 | Assert.Single(result.Errors); 47 | Assert.Equal(string.Format(ValidationErrorMessages.UnsupportedFormat, fileName, contentType), result.Errors.First().Message); 48 | } 49 | 50 | [Fact] 51 | public void ValidateFile_MaxLengthExceed() 52 | { 53 | //Arrange 54 | var fileName = "fileName"; 55 | var contentType = "application/json"; 56 | var fileMock = FileMockFactory.CreateMock(new byte[12], contentType, fileName); 57 | 58 | //Act 59 | var result = _uut.Validate(fileMock.Object); 60 | 61 | //Assert 62 | Assert.True(result.IsFailed); 63 | Assert.Single(result.Errors); 64 | Assert.Equal(string.Format(ValidationErrorMessages.MaximalFileSize, fileName), result.Errors.First().Message); 65 | } 66 | 67 | [Fact] 68 | public void ValidateFile_EmptyFile() 69 | { 70 | //Arrange 71 | var fileName = "fileName"; 72 | var contentType = "application/json"; 73 | var fileMock = FileMockFactory.CreateMock(new byte[0], contentType, fileName); 74 | 75 | //Act 76 | var result = _uut.Validate(fileMock.Object); 77 | 78 | //Assert 79 | Assert.True(result.IsFailed); 80 | Assert.Single(result.Errors); 81 | Assert.Equal(string.Format(ValidationErrorMessages.FileIsEmpty, fileName), result.Errors.First().Message); 82 | } 83 | 84 | [Fact] 85 | public void ValidateExtension_NotAllowedExtension() 86 | { 87 | //Arrange 88 | var extension = "ss"; 89 | 90 | //Act 91 | var result = _uut.Validate(extension); 92 | 93 | //Assert 94 | Assert.True(result.IsFailed); 95 | Assert.Single(result.Errors); 96 | Assert.Equal(string.Format(ValidationErrorMessages.UnsupportedExtension, extension), result.Errors.First().Message); 97 | } 98 | 99 | [Fact] 100 | public void ValidateConversion_NotAllowedSourceExtension() 101 | { 102 | //Arrange 103 | var sourceExtension = "sourceExtension"; 104 | 105 | //Act 106 | var result = _uut.ValidateConversion(sourceExtension, string.Empty); 107 | 108 | //Assert 109 | Assert.True(result.IsFailed); 110 | Assert.Single(result.Errors); 111 | Assert.Equal(string.Format(ValidationErrorMessages.UnsupportedExtension, sourceExtension), result.Errors.First().Message); 112 | } 113 | 114 | [Fact] 115 | public void ValidateConversion_NotAllowedConversion() 116 | { 117 | //Arrange 118 | var sourceExtension = "json"; 119 | var destExtension = "yaml"; 120 | 121 | //Act 122 | var result = _uut.ValidateConversion(sourceExtension, destExtension); 123 | 124 | //Assert 125 | Assert.True(result.IsFailed); 126 | Assert.Single(result.Errors); 127 | Assert.Equal(string.Format(ValidationErrorMessages.UnsuportedConversion, sourceExtension, destExtension), result.Errors.First().Message); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Domain.UnitTests/File.Domain.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Domain.UnitTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Assets/InvalidByteData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace File.Infrastructure.UnitTests.Assets 4 | { 5 | internal class InvalidByteData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { new byte[] { 0xff, 0xbb, } }; 10 | yield return new object[] { new byte[] { 0xef, 0xbb, } }; 11 | yield return new object[] { new byte[] { 0, 0, 0 } }; 12 | } 13 | 14 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Assets/InvalidConverters.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace File.Infrastructure.UnitTests.Assets 4 | { 5 | internal class InvalidConverters : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { "json", "csv" }; 10 | yield return new object[] { "json", "txt" }; 11 | yield return new object[] { "xml", "csv" }; 12 | yield return new object[] { "xml", "txt" }; 13 | yield return new object[] { "yaml", "txt" }; 14 | yield return new object[] { "yaml", "csv" }; 15 | yield return new object[] { "yaml", "xml" }; 16 | yield return new object[] { "csv", "txt" }; 17 | yield return new object[] { "csv", "yaml" }; 18 | yield return new object[] { "csv", "json" }; 19 | yield return new object[] { "csv", "xml" }; 20 | yield return new object[] { "txt", "csv" }; 21 | yield return new object[] { "txt", "yaml" }; 22 | yield return new object[] { "txt", "json" }; 23 | yield return new object[] { "txt", "xml" }; 24 | } 25 | 26 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Assets/InvalidJsonData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace File.Infrastructure.UnitTests.Assets 4 | { 5 | internal class InvalidJsonData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { "root: {}" }; 10 | yield return new object[] { @"\{root: {}}" }; 11 | } 12 | 13 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Assets/InvalidXmlData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace File.Infrastructure.UnitTests.Assets 4 | { 5 | internal class InvalidXmlData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { "" }; 10 | yield return new object[] { "" }; 11 | } 12 | 13 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Assets/InvalidYamlData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace File.Infrastructure.UnitTests.Assets 4 | { 5 | internal class InvalidYamlData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { "addresses: [172.16.60.10/8]\r\ngateway4:172.16.11.1}" }; 10 | } 11 | 12 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Assets/UknownByteData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace File.Infrastructure.UnitTests.Assets 4 | { 5 | internal class UknownByteData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { new byte[] { 0xff, 0xbb, 0,0 } }; 10 | yield return new object[] { new byte[] { 0xef, 0xbb, 0, 0 } }; 11 | yield return new object[] { new byte[] { 0, 0, 0, 0 } }; 12 | } 13 | 14 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Assets/ValidConverters.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.FileConversions.Converters; 2 | using System.Collections; 3 | 4 | namespace File.Infrastructure.UnitTests.Assets 5 | { 6 | internal class ValidConverters : IEnumerable 7 | { 8 | public IEnumerator GetEnumerator() 9 | { 10 | yield return new object[] { "json", "xml", typeof(JsonToXmlFileConverter) }; 11 | yield return new object[] { "json", "yaml", typeof(JsonToYamlFileConverter) }; 12 | yield return new object[] { "yaml", "json", typeof(YamlToJsonFileConverter) }; 13 | yield return new object[] { "xml", "json", typeof(XmlToJsonFileConverter) }; 14 | yield return new object[] { "xml", "yaml", typeof(XmlToYamlFileConverter) }; 15 | } 16 | 17 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Extensions/AssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace File.Infrastructure.UnitTests.Extensions 4 | { 5 | internal static class AssertExtensions 6 | { 7 | public static void EqualFileContent(string expected, string actual) 8 | { 9 | Assert.Equal(0, string.Compare(expected, 10 | System.IO.File.ReadAllText(actual), 11 | CultureInfo.CurrentCulture, CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/File.Infrastructure.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/Converters/JsonToXmlFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.UnitTests.Assets; 4 | using Newtonsoft.Json; 5 | 6 | namespace File.Infrastructure.UnitTests.FileConversions.Converters 7 | { 8 | public class JsonToXmlFileConverterTests 9 | { 10 | private readonly IFileConverter _uut; 11 | 12 | public JsonToXmlFileConverterTests() 13 | { 14 | _uut = new JsonToXmlFileConverter(); 15 | } 16 | 17 | [Fact] 18 | public async Task Empty_Json() 19 | { 20 | //Arrange 21 | var emptyJson = "{}"; 22 | //Act 23 | var result = await _uut.Convert(emptyJson, CancellationToken.None); 24 | //Assert 25 | Assert.True(result.IsSuccess); 26 | Assert.Equal("", result.Value); 27 | } 28 | 29 | 30 | [Theory] 31 | [ClassData(typeof(InvalidJsonData))] 32 | public async Task Invalid_Json(string invalidJson) 33 | { 34 | //Act & Assert 35 | await Assert.ThrowsAsync(() => _uut.Convert(invalidJson, CancellationToken.None)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/Converters/JsonToYamlFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.UnitTests.Assets; 4 | using Newtonsoft.Json; 5 | 6 | namespace File.Infrastructure.Tests.FileConversions.Converters 7 | { 8 | public class JsonToYamlFileConverterTests 9 | { 10 | private readonly IFileConverter _uut; 11 | 12 | public JsonToYamlFileConverterTests() 13 | { 14 | _uut = new JsonToYamlFileConverter(); 15 | } 16 | 17 | [Fact] 18 | public async Task Empty_Json() 19 | { 20 | //Arrange 21 | var emptyJson = "{}"; 22 | //Act 23 | var result = await _uut.Convert(emptyJson, CancellationToken.None); 24 | //Assert 25 | Assert.True(result.IsSuccess); 26 | } 27 | 28 | [Theory] 29 | [ClassData(typeof(InvalidJsonData))] 30 | public async Task Invalid_Json(string invalidJson) 31 | { 32 | //Act & Assert 33 | await Assert.ThrowsAsync(() => _uut.Convert(invalidJson, CancellationToken.None)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/Converters/XmlToJsonFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.UnitTests.Assets; 4 | using Newtonsoft.Json; 5 | using System.Xml; 6 | 7 | namespace File.Infrastructure.UnitTests.FileConversions.Converters 8 | { 9 | public class XmlToJsonFileConverterTests 10 | { 11 | private readonly IFileConverter _uut; 12 | 13 | public XmlToJsonFileConverterTests() 14 | { 15 | _uut = new XmlToJsonFileConverter(); 16 | } 17 | 18 | [Fact] 19 | public async Task Empty_Xml() 20 | { 21 | //Arrange 22 | //Act 23 | var result = await _uut.Convert("", CancellationToken.None); 24 | //Assert 25 | Assert.True(result.IsSuccess); 26 | Assert.NotEmpty(result.Value); 27 | } 28 | 29 | [Theory] 30 | [ClassData(typeof(InvalidXmlData))] 31 | public async Task Invalid_Xml(string invalidXml) 32 | { 33 | //Act & Assert 34 | await Assert.ThrowsAsync(() => _uut.Convert(invalidXml, CancellationToken.None)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/Converters/XmlToYamlFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.UnitTests.Assets; 4 | using System.Xml; 5 | 6 | namespace File.Infrastructure.UnitTests.FileConversions.Converters 7 | { 8 | public class XmlToYamlFileConverterTests 9 | { 10 | private readonly IFileConverter _uut; 11 | 12 | public XmlToYamlFileConverterTests() 13 | { 14 | _uut = new XmlToYamlFileConverter(); 15 | } 16 | 17 | [Fact] 18 | public async Task Empty_Xml() 19 | { 20 | //Arrange 21 | //Act 22 | var result = await _uut.Convert("", CancellationToken.None); 23 | //Assert 24 | Assert.True(result.IsSuccess); 25 | Assert.NotEmpty(result.Value); 26 | } 27 | 28 | [Theory] 29 | [ClassData(typeof(InvalidXmlData))] 30 | public async Task Invalid_Xml(string invalidXml) 31 | { 32 | //Act & Assert 33 | await Assert.ThrowsAsync(() => _uut.Convert(invalidXml, CancellationToken.None)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/Converters/YamlToJsonFileConverterTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions.Converters; 3 | using File.Infrastructure.UnitTests.Assets; 4 | using YamlDotNet.Core; 5 | 6 | namespace File.Infrastructure.UnitTests.FileConversions.Converters 7 | { 8 | public class YamlToJsonFileConverterTests 9 | { 10 | private readonly IFileConverter _uut; 11 | 12 | public YamlToJsonFileConverterTests() 13 | { 14 | _uut = new YamlToJsonFileConverter(); 15 | } 16 | 17 | [Fact] 18 | public async Task Empty_Yaml() 19 | { 20 | //Arrange 21 | //Act 22 | var result = await _uut.Convert("", CancellationToken.None); 23 | //Assert 24 | Assert.True(result.IsSuccess); 25 | Assert.NotEmpty(result.Value); 26 | } 27 | 28 | [Theory] 29 | [ClassData(typeof(InvalidYamlData))] 30 | public async Task Invalid_Yaml(string invalidYaml) 31 | { 32 | //Act & Assert 33 | await Assert.ThrowsAsync(() => _uut.Convert(invalidYaml, CancellationToken.None)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/EncodingFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions; 3 | using File.Infrastructure.UnitTests.Assets; 4 | using System.Text; 5 | 6 | namespace File.Infrastructure.UnitTests.FileConversions 7 | { 8 | public class EncodingFactoryTests 9 | { 10 | private readonly IEncodingFactory _uut; 11 | 12 | public EncodingFactoryTests() 13 | { 14 | _uut = new EncodingFactory(); 15 | } 16 | 17 | [Theory] 18 | [ClassData(typeof(InvalidByteData))] 19 | public void Invalid_Data(byte[] data) 20 | { 21 | //Arrange 22 | //Act&Assert 23 | Assert.Throws(()=> _uut.CreateEncoding(data)); 24 | } 25 | 26 | [Theory] 27 | [ClassData(typeof(UknownByteData))] 28 | public void DefaultEncoding(byte[] data) 29 | { 30 | //Arrange 31 | //Act 32 | var encoding = _uut.CreateEncoding(data); 33 | //Assert 34 | Assert.Equal(Encoding.ASCII, encoding); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/FileConversionServiceTests.cs: -------------------------------------------------------------------------------- 1 | using File.Core.Abstractions; 2 | using File.Domain.Abstractions; 3 | using File.Domain.Dtos; 4 | using File.Infrastructure.Abstractions; 5 | using File.Infrastructure.FileConversions; 6 | using FluentResults; 7 | using Moq; 8 | using System.Text; 9 | 10 | namespace File.Infrastructure.UnitTests.FileConversions 11 | { 12 | public class FileConversionServiceTests 13 | { 14 | private readonly Mock _fileConverterFactoryMock; 15 | private readonly Mock _encodingFactoryMock; 16 | 17 | private readonly IFileConvertService _uut; 18 | public FileConversionServiceTests() 19 | { 20 | _fileConverterFactoryMock = new Mock(); 21 | _encodingFactoryMock = new Mock(); 22 | 23 | _uut = new FileConversionService(_fileConverterFactoryMock.Object, _encodingFactoryMock.Object); 24 | } 25 | 26 | [Fact] 27 | public async Task ConvertTo_Failed() 28 | { 29 | //Arrange 30 | var extension = "tst"; 31 | var file = new Mock(); 32 | file.Setup(x => x.GetData(It.IsAny())).ReturnsAsync(Array.Empty()); 33 | file.SetupGet(x => x.FileName).Returns($"sone.{extension}"); 34 | 35 | _encodingFactoryMock.Setup(x => x.CreateEncoding(It.IsAny())).Returns(Encoding.Default); 36 | 37 | var converter = new Mock(); 38 | var conversionFailedMessage = "failed message"; 39 | converter.Setup(x => x.Convert(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Fail(conversionFailedMessage)); 40 | _fileConverterFactoryMock.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(converter.Object); 41 | 42 | //Act 43 | var result = await _uut.ConvertTo(file.Object, string.Empty, CancellationToken.None); 44 | 45 | //Assert 46 | Assert.True(result.IsFailed); 47 | Assert.Equal(conversionFailedMessage, result.Errors.First().Message); 48 | _encodingFactoryMock.Verify(x => x.CreateEncoding(It.Is(y=>y.Length == 0)), Times.Once); 49 | _fileConverterFactoryMock.Verify(x => x.Create(It.Is(y=>y.Equals(extension)), It.Is(y=>string.IsNullOrEmpty(y))), Times.Once); 50 | converter.Verify(x => x.Convert(It.IsAny(), It.IsAny()), Times.Once); 51 | } 52 | 53 | [Fact] 54 | public async Task ExportTo_Failed() 55 | { 56 | //Arrange 57 | var extension = "ss"; 58 | var file = new FileDto 59 | { 60 | FileName = $"sss.{extension}" 61 | }; 62 | 63 | _encodingFactoryMock.Setup(x => x.CreateEncoding(It.IsAny())).Returns(Encoding.Default); 64 | 65 | var converter = new Mock(); 66 | var conversionFailedMessage = "failed message"; 67 | converter.Setup(x => x.Convert(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Fail(conversionFailedMessage)); 68 | _fileConverterFactoryMock.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(converter.Object); 69 | 70 | //Act 71 | var result = await _uut.ExportTo(file, string.Empty, CancellationToken.None); 72 | 73 | //Assert 74 | Assert.True(result.IsFailed); 75 | Assert.Equal(conversionFailedMessage, result.Errors.First().Message); 76 | _encodingFactoryMock.Verify(x => x.CreateEncoding(It.Is(y => y.Length == 0)), Times.Once); 77 | _fileConverterFactoryMock.Verify(x => x.Create(It.Is(y => y.Equals(extension)), It.Is(y => string.IsNullOrEmpty(y))), Times.Once); 78 | converter.Verify(x => x.Convert(It.IsAny(), It.IsAny()), Times.Once); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/FileConversions/FileConverterFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using File.Infrastructure.Abstractions; 2 | using File.Infrastructure.FileConversions; 3 | using File.Infrastructure.UnitTests.Assets; 4 | 5 | namespace File.Infrastructure.UnitTests.FileConversions 6 | { 7 | public class FileConverterFactoryTests 8 | { 9 | private readonly IFileConverterFactory _uut; 10 | 11 | public FileConverterFactoryTests() 12 | { 13 | _uut = new FileConverterFactory(); 14 | } 15 | 16 | 17 | [Theory] 18 | [ClassData(typeof(ValidConverters))] 19 | public void Create_Converter(string from, string to, Type desiredConverter) 20 | { 21 | //Arrange 22 | 23 | //Act 24 | var result = _uut.Create(from, to); 25 | 26 | //Assert 27 | Assert.NotNull(result); 28 | Assert.IsType(desiredConverter, result); 29 | } 30 | 31 | [Theory] 32 | [ClassData(typeof(InvalidConverters))] 33 | public void Create_Converter_Failed(string from, string to) 34 | { 35 | //Act&Assert 36 | Assert.Throws(() => _uut.Create(from, to)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.Infrastructure.UnitTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.UnitTests.Common/Extensions/MoqLoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Moq; 3 | 4 | namespace File.UnitTests.Common.Extensions 5 | { 6 | public static class MoqLoggerExtensions 7 | { 8 | public static void VerifyLog(this Mock> loggerMock, LogLevel logLevel, EventId eventId, string message, Times times) 9 | { 10 | loggerMock.Verify( 11 | x => x.Log( 12 | It.Is(y => y.Equals(logLevel)), 13 | It.Is(y => y.Equals(eventId)), 14 | It.Is((o, _) => string.Equals(message, o.ToString(), StringComparison.InvariantCultureIgnoreCase)), 15 | It.IsAny(), 16 | It.IsAny>()), 17 | times); 18 | } 19 | 20 | public static void VerifyLog(this Mock> loggerMock, LogLevel logLevel, EventId eventId, Times times) 21 | { 22 | loggerMock.Verify( 23 | x => x.Log( 24 | It.Is(y => y.Equals(logLevel)), 25 | It.Is(y => y.Equals(eventId)), 26 | It.IsAny(), 27 | It.IsAny(), 28 | It.IsAny>()), 29 | times); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/File.UnitTests.Common/File.UnitTests.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------