diff --git a/AsbCloud.sln b/AsbCloud.sln index e4d9f235..a33359b7 100644 --- a/AsbCloud.sln +++ b/AsbCloud.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsbCloudWebApi.Tests", "Asb EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SignalRTestClient", "SignalRTestClient\SignalRTestClient.csproj", "{E6B97963-4CEA-47B6-A0C8-625FFA9B7D69}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsbCloudWebApi.IntegrationTests", "AsbCloudWebApi.IntegrationTests\AsbCloudWebApi.IntegrationTests.csproj", "{2A937DFD-8E78-4204-A6B9-F3195EAA5818}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {E6B97963-4CEA-47B6-A0C8-625FFA9B7D69}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6B97963-4CEA-47B6-A0C8-625FFA9B7D69}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6B97963-4CEA-47B6-A0C8-625FFA9B7D69}.Release|Any CPU.Build.0 = Release|Any CPU + {2A937DFD-8E78-4204-A6B9-F3195EAA5818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A937DFD-8E78-4204-A6B9-F3195EAA5818}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A937DFD-8E78-4204-A6B9-F3195EAA5818}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A937DFD-8E78-4204-A6B9-F3195EAA5818}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AsbCloudWebApi.IntegrationTests/ApiTokenHelper.cs b/AsbCloudWebApi.IntegrationTests/ApiTokenHelper.cs new file mode 100644 index 00000000..2e4e5dc0 --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/ApiTokenHelper.cs @@ -0,0 +1,50 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using AsbCloudDb.Model; +using Microsoft.IdentityModel.Tokens; + +namespace AsbCloudWebApi.IntegrationTests; + +public static class ApiTokenHelper +{ + public static string GetAdminUserToken() + { + var user = new User() + { + Id = 1, + IdCompany = 1, + Login = "test_user" + }; + var roles = new[] { "root" }; + + return CreateToken(user, roles); + } + + private static string CreateToken(User user, IEnumerable roles) + { + var claims = new List + { + new("id", user.Id.ToString()), + new(ClaimsIdentity.DefaultNameClaimType, user.Login), + new("idCompany", user.IdCompany.ToString()), + }; + + claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + + const string secret = "супер секретный ключ для шифрования"; + + var key = Encoding.ASCII.GetBytes(secret); + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = "a", + Audience = "a", + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/AsbCloudWebApi.IntegrationTests.csproj b/AsbCloudWebApi.IntegrationTests/AsbCloudWebApi.IntegrationTests.csproj new file mode 100644 index 00000000..1824d4ce --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/AsbCloudWebApi.IntegrationTests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/AsbCloudWebApi.IntegrationTests/BaseIntegrationTest.cs b/AsbCloudWebApi.IntegrationTests/BaseIntegrationTest.cs new file mode 100644 index 00000000..7017e79c --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/BaseIntegrationTest.cs @@ -0,0 +1,40 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using AsbCloudDb.Model; +using AsbCloudWebApi.IntegrationTests.Clients; +using Microsoft.Extensions.DependencyInjection; +using Refit; +using Xunit; + +namespace AsbCloudWebApi.IntegrationTests; + +public abstract class BaseIntegrationTest : IClassFixture +{ + private readonly IServiceScope scope; + private readonly HttpClient httpClient; + + protected readonly IAsbCloudDbContext dbContext; + + protected IAdminDepositClient adminDepositClient; + + protected BaseIntegrationTest(WebAppFactoryFixture factory) + { + scope = factory.Services.CreateScope(); + httpClient = factory.CreateClient(); + + dbContext = scope.ServiceProvider.GetRequiredService(); + + var jwtToken = ApiTokenHelper.GetAdminUserToken(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); + + var jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + var refitSettings = new RefitSettings(new SystemTextJsonContentSerializer(jsonSerializerOptions)); + + adminDepositClient = RestService.For(httpClient, refitSettings); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/Clients/IAdminDepositClient.cs b/AsbCloudWebApi.IntegrationTests/Clients/IAdminDepositClient.cs new file mode 100644 index 00000000..ddc32005 --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/Clients/IAdminDepositClient.cs @@ -0,0 +1,27 @@ +using AsbCloudApp.Data; +using Refit; + +namespace AsbCloudWebApi.IntegrationTests.Clients; + +public interface IAdminDepositClient +{ + private const string BaseRoute = "/api/admin/deposit"; + + [Post(BaseRoute)] + Task> InsertAsync([Body] DepositBaseDto deposit); + + [Post($"{BaseRoute}/range")] + Task> InsertRangeAsync([Body] IEnumerable deposits); + + [Put($"{BaseRoute}")] + Task> UpdateAsync([Body] DepositBaseDto deposit); + + [Get(BaseRoute + "/{id}")] + Task> GetOrDefaultAsync(int id); + + [Get(BaseRoute)] + Task>> GetAllAsync(); + + [Delete(BaseRoute + "/{id}")] + Task> DeleteAsync(int id); +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/Controllers/AdminDepositControllerTests.cs b/AsbCloudWebApi.IntegrationTests/Controllers/AdminDepositControllerTests.cs new file mode 100644 index 00000000..81c76420 --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/Controllers/AdminDepositControllerTests.cs @@ -0,0 +1,186 @@ +using System.Net; +using AsbCloudApp.Data; +using AsbCloudWebApi.IntegrationTests.TestFakers; +using Mapster; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace AsbCloudWebApi.IntegrationTests.Controllers; + +public class AdminDepositControllerTests : BaseIntegrationTest +{ + public AdminDepositControllerTests(WebAppFactoryFixture factory) + : base(factory) + { + } + + [Fact] + public async Task InsertAsync_ReturnsSuccess_WhenNewItemIsValid() + { + //arrange + var expectedDto = DepositTestFaker.GetDeposit().Adapt(); + + //act + var response = await adminDepositClient.InsertAsync(expectedDto); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content > 0); + + var entity = await dbContext.Deposits.FirstOrDefaultAsync(d => d.Id == response.Content); + var actualDto = entity?.Adapt(); + + var excludeProps = new[] { nameof(DepositBaseDto.Id) }; + MatchHelper.Match(expectedDto, actualDto, excludeProps); + } + + [Fact] + public async Task InsertRangeAsync_ReturnsSuccess_WhenAllNewItemsIsValid() + { + //arrange + var dto = DepositTestFaker.GetDeposit().Adapt(); + + //act + var responce = await adminDepositClient.InsertRangeAsync(new[] { dto }); + + //assert + Assert.Equal(HttpStatusCode.OK, responce.StatusCode); + Assert.Equal(1, responce.Content); + + var entity = await dbContext.Deposits.OrderBy(d => d.Id).LastOrDefaultAsync(); + var deposit = entity?.Adapt(); + + var excludeProps = new[] { nameof(DepositBaseDto.Id) }; + MatchHelper.Match(dto, deposit, excludeProps); + } + + [Fact] + public async Task UpdateAsync_ReturnsBadRequest_WhenUpdatedItemHasInvalidId() + { + //arrange + var dto = DepositTestFaker.GetDeposit().Adapt(); + + //act + var response = await adminDepositClient.UpdateAsync(dto); + + //assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task UpdateAsync_ReturnsSuccess_WhenUpdatedItemIsValid() + { + //arrange + var dto = DepositTestFaker.GetDeposit().Adapt(); + var insertResponse = await adminDepositClient.InsertAsync(dto); + + dto.Id = insertResponse.Content; + dto.Caption = "Test"; + dto.Latitude = 50; + dto.Longitude = 50; + dto.Timezone = new SimpleTimezoneDto + { + IsOverride = true, + Hours = 12 + }; + + //act + var response = await adminDepositClient.UpdateAsync(dto); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content > 0); + + var entity = await dbContext.Deposits.FirstOrDefaultAsync(d => d.Id == response.Content); + var deposit = entity?.Adapt(); + + MatchHelper.Match(dto, deposit); + } + + [Fact] + public async Task GetOrDefaultAsync_ReturnsSuccess_WhenIdIsValid() + { + //arrange + var dto = DepositTestFaker.GetDeposit().Adapt(); + var insertResponse = await adminDepositClient.InsertAsync(dto); + var id = insertResponse.Content; + + //act + var response = await adminDepositClient.GetOrDefaultAsync(id); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var entity = await dbContext.Deposits.FirstOrDefaultAsync(d => d.Id == response.Content.Id); + var deposit = entity?.Adapt(); + + var excludeProps = new[] { nameof(DepositBaseDto.Id) }; + MatchHelper.Match(dto, deposit, excludeProps); + } + + [Fact] + public async Task GetOrDefaultAsync_ReturnsNoContent_WhenIdIsInvalid() + { + //arrange + const int id = 0; + + //act + var responce = await adminDepositClient.GetOrDefaultAsync(id); + + //assert + Assert.Equal(HttpStatusCode.NoContent, responce.StatusCode); + } + + [Fact] + public async Task GetAllAsync_ReturnsSuccess() + { + //act + var response = await adminDepositClient.GetAllAsync(); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var expectedCount = await dbContext.Deposits.CountAsync(); + Assert.Equal(expectedCount, response.Content.Count()); + + + var entity = await dbContext.Deposits.FirstOrDefaultAsync(); + var dto = entity?.Adapt(); + + MatchHelper.Match(dto, response.Content.FirstOrDefault()); + } + + [Fact] + public async Task DeleteAsync_ReturnsSuccess_WhenIdIsValid() + { + //arrange + var dto = DepositTestFaker.GetDeposit().Adapt(); + var insertResponse = await adminDepositClient.InsertAsync(dto); + var id = insertResponse.Content; + + //act + var response = await adminDepositClient.DeleteAsync(id); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content > 0); + + var entity = await dbContext.Deposits.FirstOrDefaultAsync(d => d.Id == dto.Id); + Assert.Null(entity); + } + + [Fact] + public async Task DeleteAsync_ReturnsNoContent_WhenIdIsInvalid() + { + //arrange + const int id = 0; + + //act + var response = await adminDepositClient.DeleteAsync(id); + + //assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/MatchHelper.cs b/AsbCloudWebApi.IntegrationTests/MatchHelper.cs new file mode 100644 index 00000000..6a8704f8 --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/MatchHelper.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using Xunit.Sdk; + +namespace AsbCloudWebApi.IntegrationTests; + +public static class MatchHelper +{ + public static void Match(T expected, T actual, IEnumerable? excludeProps = null) + { + if (ReferenceEquals(expected, actual)) + throw new EqualException(expected, actual); + + if (expected is null || actual is null) + throw new EqualException(expected, actual); + + var props = typeof(T).GetProperties( + BindingFlags.Public + | BindingFlags.Instance).Where(prop => prop.CanWrite); + + if (excludeProps is not null && excludeProps.Any()) + props = props.Where(prop => !excludeProps.Contains(prop.Name)); + + foreach (var prop in props) + { + var objValue = prop.GetValue(expected); + var anotherValue = prop.GetValue(actual); + if (objValue != null && !objValue.Equals(anotherValue)) + throw new EqualException(expected, actual); + } + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/TestFakers/DepositTestFaker.cs b/AsbCloudWebApi.IntegrationTests/TestFakers/DepositTestFaker.cs new file mode 100644 index 00000000..c2f6e098 --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/TestFakers/DepositTestFaker.cs @@ -0,0 +1,25 @@ +using AsbCloudDb.Model; +using Bogus; + +namespace AsbCloudWebApi.IntegrationTests.TestFakers; + +public static class DepositTestFaker +{ + public static Deposit GetDeposit() => + GetDepositFaker().Generate(); + + public static IEnumerable GetDeposits(int count) => + GetDepositFaker().Generate(count); + + private static Faker GetDepositFaker() => + new Faker() + .RuleFor(d => d.Id, 0) + .RuleFor(d => d.Caption, f => f.Random.String2(1, 50)) + .RuleFor(d => d.Latitude, f => f.Random.Int(-90, 90)) + .RuleFor(d => d.Longitude, f => f.Random.Int(-180, 180)) + .RuleFor(d => d.Timezone, f => new SimpleTimezone + { + Hours = f.Random.Int(1, 12), + IsOverride = f.Random.Bool() + }); +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs b/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs new file mode 100644 index 00000000..a3f578ac --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs @@ -0,0 +1,55 @@ +using AsbCloudDb; +using AsbCloudDb.Model; +using AsbCloudWebApi.IntegrationTests.TestFakers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace AsbCloudWebApi.IntegrationTests; + +public class WebAppFactoryFixture : WebApplicationFactory, + IAsyncLifetime +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + var connectionString = configuration.GetConnectionString("TestConnection"); + + builder.ConfigureServices(services => + { + var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + + if (descriptor is not null) + services.Remove(descriptor); + + services.AddDbContext(options => + options.UseNpgsql(connectionString)); + }); + } + + public async Task InitializeAsync() + { + using var scope = Services.CreateScope(); + var scopedServices = scope.ServiceProvider; + var dbContext = scopedServices.GetRequiredService(); + + dbContext.Database.EnsureCreatedAndMigrated(); + + dbContext.Deposits.AddRange(DepositTestFaker.GetDeposits(15)); + await dbContext.SaveChangesAsync(); + } + + public new async Task DisposeAsync() + { + using var scope = Services.CreateScope(); + var scopedServices = scope.ServiceProvider; + var dbContext = scopedServices.GetRequiredService(); + + await dbContext.Database.EnsureDeletedAsync(); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.Tests/AsbCloudWebApi.Tests.csproj b/AsbCloudWebApi.Tests/AsbCloudWebApi.Tests.csproj index 03c795e6..06d8d055 100644 --- a/AsbCloudWebApi.Tests/AsbCloudWebApi.Tests.csproj +++ b/AsbCloudWebApi.Tests/AsbCloudWebApi.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/AsbCloudWebApi/appsettings.json b/AsbCloudWebApi/appsettings.json index 6801ff38..96a3f3bf 100644 --- a/AsbCloudWebApi/appsettings.json +++ b/AsbCloudWebApi/appsettings.json @@ -9,7 +9,7 @@ "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True", "DebugConnection": "Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True;Include Error Detail=True", - "ServerConnection": "Host=192.168.1.70;Database=postgres;Username=postgres;Password=q;Persist Security Info=True", + "TestConnection": "Host=localhost;Database=test;Username=postgres;Password=q;Persist Security Info=True;Include Error Detail=True", "LocalConnection": "Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True" }, "AllowedHosts": "*",