Rename WebApi project to Vegasco.Server.Api
All checks were successful
continuous-integration/drone/push Build is passing

And update all references including comments etc.
This commit is contained in:
2025-06-12 18:22:37 +02:00
parent 9d71c86474
commit a1999bfe41
71 changed files with 177 additions and 224 deletions

View File

@@ -0,0 +1,19 @@
using Bogus;
using Vegasco.Server.Api.Cars;
namespace Vegasco.Server.Api.Tests.Integration;
internal class CarFaker
{
private readonly Faker _faker = new();
internal CreateCar.Request CreateCarRequest()
{
return new CreateCar.Request(_faker.Vehicle.Model());
}
internal UpdateCar.Request UpdateCarRequest()
{
return new UpdateCar.Request(_faker.Vehicle.Model());
}
}

View File

@@ -0,0 +1,71 @@
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Cars;
[Collection(SharedTestCollection.Name)]
public class CreateCarTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
public CreateCarTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task CreateCar_ShouldCreateCar_WhenRequestIsValid()
{
// Arrange
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
// Act
HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var createdCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
createdCar.Should().BeEquivalentTo(createCarRequest, o => o.ExcludingMissingMembers());
_dbContext.Cars.Should().ContainEquivalentOf(createdCar, o => o.Excluding(x => x!.Id))
.Which.Id.Value.Should().Be(createdCar!.Id);
}
[Fact]
public async Task CreateCar_ShouldReturnValidationProblems_WhenRequestIsNotValid()
{
// Arrange
var createCarRequest = new CreateCar.Request("");
// Act
HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
_dbContext.Cars.Should().NotContainEquivalentOf(createCarRequest, o => o.ExcludingMissingMembers());
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,64 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Cars;
[Collection(SharedTestCollection.Name)]
public class DeleteCarTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
public DeleteCarTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task DeleteCar_ShouldReturnNotFound_WhenCarDoesNotExist()
{
// Arrange
var randomCarId = Guid.NewGuid();
// Act
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{randomCarId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task DeleteCar_ShouldDeleteCar_WhenCarExists()
{
// Arrange
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
// Act
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{createdCar!.Id}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
_dbContext.Cars.Should().BeEmpty();
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,57 @@
using FluentAssertions;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
namespace Vegasco.Server.Api.Tests.Integration.Cars;
[Collection(SharedTestCollection.Name)]
public class GetCarTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly CarFaker _carFaker = new();
public GetCarTests(WebAppFactory factory)
{
_factory = factory;
}
[Fact]
public async Task GetCar_ShouldReturnNotFound_WhenCarDoesNotExist()
{
// Arrange
var randomCarId = Guid.NewGuid();
// Act
HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/cars/{randomCarId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task GetCar_ShouldReturnCar_WhenCarExists()
{
// Arrange
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
// Act
HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/cars/{createdCar!.Id}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var car = await response.Content.ReadFromJsonAsync<GetCar.Response>();
car.Should().BeEquivalentTo(createdCar);
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,66 @@
using FluentAssertions;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
namespace Vegasco.Server.Api.Tests.Integration.Cars;
[Collection(SharedTestCollection.Name)]
public class GetCarsTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly CarFaker _carFaker = new();
public GetCarsTests(WebAppFactory factory)
{
_factory = factory;
}
[Fact]
public async Task GetCars_ShouldReturnEmptyList_WhenNoEntriesExist()
{
// Arrange
// Act
using HttpResponseMessage response = await _factory.HttpClient.GetAsync("v1/cars");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
apiResponse!.Cars.Should().BeEmpty();
}
[Fact]
public async Task GetCars_ShouldReturnEntries_WhenEntriesExist()
{
// Arrange
List<CreateCar.Response> createdCars = [];
const int numberOfCars = 5;
for (var i = 0; i < numberOfCars; i++)
{
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
createdCars.Add(createdCar!);
}
// Act
using HttpResponseMessage response = await _factory.HttpClient.GetAsync("v1/cars");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
apiResponse!.Cars.Should().BeEquivalentTo(createdCars);
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,102 @@
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Cars;
[Collection(SharedTestCollection.Name)]
public class UpdateCarTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
public UpdateCarTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task UpdateCar_ShouldUpdateCar_WhenCarExistsAndRequestIsValid()
{
// Arrange
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
// Act
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{createdCar!.Id}", updateCarRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var updatedCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
updatedCar!.Id.Should().Be(createdCar.Id);
updatedCar.Should().BeEquivalentTo(updateCarRequest, o => o.ExcludingMissingMembers());
_dbContext.Cars.Should().ContainEquivalentOf(updatedCar, o =>
o.ExcludingMissingMembers()
.Excluding(x => x.Id))
.Which.Id.Value.Should().Be(updatedCar.Id);
}
[Fact]
public async Task UpdateCar_ShouldReturnValidationProblems_WhenRequestIsNotValid()
{
// Arrange
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
var updateCarRequest = new UpdateCar.Request("");
// Act
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{createdCar!.Id}", updateCarRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
_dbContext.Cars.Should().ContainSingle(x => x.Id.Value == createdCar.Id)
.Which
.Should().NotBeEquivalentTo(updateCarRequest, o => o.ExcludingMissingMembers());
}
[Fact]
public async Task UpdateCar_ShouldReturnNotFound_WhenNoCarWithIdExists()
{
// Arrange
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
var randomCarId = Guid.NewGuid();
// Act
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{randomCarId}", updateCarRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
_dbContext.Cars.Should().BeEmpty();
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,29 @@
using Bogus;
using Vegasco.Server.Api.Consumptions;
namespace Vegasco.Server.Api.Tests.Integration;
internal class ConsumptionFaker
{
private readonly Faker _faker = new();
internal CreateConsumption.Request CreateConsumptionRequest(Guid carId)
{
return new CreateConsumption.Request(
_faker.Date.RecentOffset(),
_faker.Random.Int(1, 1_000),
_faker.Random.Int(20, 70),
_faker.Random.Bool(),
carId);
}
internal UpdateConsumption.Request UpdateConsumptionRequest()
{
CreateConsumption.Request createRequest = CreateConsumptionRequest(default);
return new UpdateConsumption.Request(
createRequest.DateTime,
createRequest.Distance,
createRequest.Amount,
createRequest.IgnoreInCalculation);
}
}

View File

@@ -0,0 +1,95 @@
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Consumptions;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Consumptions;
[Collection(SharedTestCollection.Name)]
public class CreateConsumptionTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
private readonly ConsumptionFaker _consumptionFaker = new();
public CreateConsumptionTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task CreateConsumption_ShouldCreateConsumption_WhenRequestIsValid()
{
// Arrange
CreateCar.Response createdCarResponse = await CreateCarAsync();
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
// Act
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
createdConsumption.Should().BeEquivalentTo(createConsumptionRequest, o => o.ExcludingMissingMembers());
_dbContext.Consumptions.Should().HaveCount(1)
.And.ContainEquivalentOf(createdConsumption, o =>
o.ExcludingMissingMembers()
.Excluding(x => x!.Id)
.Excluding(x => x!.CarId));
Consumption singleConsumption = _dbContext.Consumptions.Single();
singleConsumption.Id.Value.Should().Be(createdConsumption!.Id);
singleConsumption.CarId.Value.Should().Be(createdConsumption.CarId);
}
[Fact]
public async Task CreateConsumption_ShouldReturnValidationProblems_WhenRequestIsInvalid()
{
// Arrange
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(Guid.Empty);
// Act
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
x.Equals(nameof(createConsumptionRequest.CarId), StringComparison.OrdinalIgnoreCase));
_dbContext.Consumptions.Should().NotContainEquivalentOf(createConsumptionRequest, o => o.ExcludingMissingMembers());
}
private async Task<CreateCar.Response> CreateCarAsync()
{
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
return createdCarResponse!;
}
public Task InitializeAsync()
{
FluentAssertionConfiguration.SetupGlobalConfig();
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,82 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Consumptions;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Consumptions;
[Collection(SharedTestCollection.Name)]
public class DeleteConsumptionTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
private readonly ConsumptionFaker _consumptionFaker = new();
public DeleteConsumptionTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task DeleteConsumption_ShouldDeleteConsumption_WhenConsumptionExists()
{
// Arrange
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
// Act
using HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/consumptions/{createdConsumption.Id}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
_dbContext.Consumptions.Should().NotContain(x => x.Id.Value == createdConsumption.Id);
}
[Fact]
public async Task DeleteConsumption_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
{
// Arrange
var consumptionId = Guid.NewGuid();
// Act
using HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/consumptions/{consumptionId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
private async Task<CreateConsumption.Response> CreateConsumptionAsync()
{
CreateCar.Response createdCarResponse = await CreateCarAsync();
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
response.EnsureSuccessStatusCode();
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
return createdConsumption!;
}
private async Task<CreateCar.Response> CreateCarAsync()
{
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
return createdCarResponse!;
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,88 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Consumptions;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Consumptions;
[Collection(SharedTestCollection.Name)]
public class GetConsumptionTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
private readonly ConsumptionFaker _consumptionFaker = new();
public GetConsumptionTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task GetConsumption_ShouldReturnConsumption_WhenConsumptionExist()
{
// Arrange
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
// Act
using HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/consumptions/{createdConsumption.Id}");
// Assert
string content = await response.Content.ReadAsStringAsync();
response.StatusCode.Should().Be(HttpStatusCode.OK);
var consumption = await response.Content.ReadFromJsonAsync<GetConsumption.Response>();
consumption.Should().BeEquivalentTo(createdConsumption);
}
[Fact]
public async Task GetConsumptions_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
{
// Arrange
var consumptionId = Guid.NewGuid();
// Act
using HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/consumptions{consumptionId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
private async Task<CreateConsumption.Response> CreateConsumptionAsync()
{
CreateCar.Response createdCarResponse = await CreateCarAsync();
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
response.EnsureSuccessStatusCode();
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
return createdConsumption!;
}
private async Task<CreateCar.Response> CreateCarAsync()
{
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
return createdCarResponse!;
}
public Task InitializeAsync()
{
FluentAssertionConfiguration.SetupGlobalConfig();
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,94 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Consumptions;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Consumptions;
[Collection(SharedTestCollection.Name)]
public class GetConsumptionsTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
private readonly ConsumptionFaker _consumptionFaker = new();
public GetConsumptionsTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task GetConsumptions_ShouldReturnConsumptions_WhenConsumptionsExist()
{
// Arrange
List<CreateConsumption.Response> createdConsumptions = [];
const int numberOfConsumptions = 3;
for (var i = 0; i < numberOfConsumptions; i++)
{
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
createdConsumptions.Add(createdConsumption);
}
// Act
using HttpResponseMessage response = await _factory.HttpClient.GetAsync("v1/consumptions");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var apiResponse = await response.Content.ReadFromJsonAsync<GetConsumptions.ApiResponse>();
apiResponse!.Consumptions.Should().BeEquivalentTo(createdConsumptions);
}
[Fact]
public async Task GetConsumptions_ShouldReturnEmptyList_WhenNoConsumptionsExist()
{
// Arrange
// Act
using HttpResponseMessage response = await _factory.HttpClient.GetAsync("v1/consumptions");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var apiResponse = await response.Content.ReadFromJsonAsync<GetConsumptions.ApiResponse>();
apiResponse!.Consumptions.Should().BeEmpty();
}
private async Task<CreateConsumption.Response> CreateConsumptionAsync()
{
CreateCar.Response createdCarResponse = await CreateCarAsync();
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
response.EnsureSuccessStatusCode();
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
return createdConsumption!;
}
private async Task<CreateCar.Response> CreateCarAsync()
{
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
return createdCarResponse!;
}
public Task InitializeAsync()
{
FluentAssertionConfiguration.SetupGlobalConfig();
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,126 @@
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Consumptions;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Tests.Integration.Consumptions;
[Collection(SharedTestCollection.Name)]
public class UpdateConsumptionTests : IAsyncLifetime
{
private readonly WebAppFactory _factory;
private readonly IServiceScope _scope;
private readonly ApplicationDbContext _dbContext;
private readonly CarFaker _carFaker = new();
private readonly ConsumptionFaker _consumptionFaker = new();
public UpdateConsumptionTests(WebAppFactory factory)
{
_factory = factory;
_scope = _factory.Services.CreateScope();
_dbContext = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
[Fact]
public async Task UpdateConsumption_ShouldCreateConsumption_WhenRequestIsValid()
{
// Arrange
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest();
// Act
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{createdConsumption.Id}", updateConsumptionRequest);
// Assert
string content = await response.Content.ReadAsStringAsync();
response.StatusCode.Should().Be(HttpStatusCode.OK);
var updatedConsumption = await response.Content.ReadFromJsonAsync<UpdateConsumption.Response>();
updatedConsumption.Should().BeEquivalentTo(updateConsumptionRequest, o => o.ExcludingMissingMembers());
_dbContext.Consumptions.Should().HaveCount(1)
.And.ContainEquivalentOf(updatedConsumption, o =>
o.ExcludingMissingMembers()
.Excluding(x => x!.Id)
.Excluding(x => x!.CarId));
Consumption singleConsumption = _dbContext.Consumptions.Single();
singleConsumption.Id.Value.Should().Be(updatedConsumption!.Id);
singleConsumption.CarId.Value.Should().Be(updatedConsumption.CarId);
}
[Fact]
public async Task UpdateConsumption_ShouldReturnValidationProblems_WhenRequestIsInvalid()
{
// Arrange
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest() with { Distance = -42 };
var randomGuid = Guid.NewGuid();
// Act
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
// Assert
string content = await response.Content.ReadAsStringAsync();
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
x.Equals(nameof(updateConsumptionRequest.Distance), StringComparison.OrdinalIgnoreCase));
_dbContext.Consumptions.Should().NotContainEquivalentOf(updateConsumptionRequest);
}
[Fact]
public async Task UpdateConsumption_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
{
// Arrange
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest();
var randomGuid = Guid.NewGuid();
// Act
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
// Assert
string content = await response.Content.ReadAsStringAsync();
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
_dbContext.Consumptions.Should().NotContainEquivalentOf(updateConsumptionRequest);
}
private async Task<CreateConsumption.Response> CreateConsumptionAsync()
{
CreateCar.Response createdCarResponse = await CreateCarAsync();
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
response.EnsureSuccessStatusCode();
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
return createdConsumption!;
}
private async Task<CreateCar.Response> CreateCarAsync()
{
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
createCarResponse.EnsureSuccessStatusCode();
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
return createdCarResponse!;
}
public Task InitializeAsync()
{
FluentAssertionConfiguration.SetupGlobalConfig();
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
_scope.Dispose();
await _dbContext.DisposeAsync();
await _factory.ResetDatabaseAsync();
}
}

View File

@@ -0,0 +1,19 @@
using FluentAssertions;
namespace Vegasco.Server.Api.Tests.Integration;
internal static class FluentAssertionConfiguration
{
private const int DateTimeComparisonPrecision = 100;
internal static void SetupGlobalConfig()
{
AssertionOptions.AssertEquivalencyUsing(options => options
.Using<DateTime>(ctx => ctx.Subject.ToUniversalTime().Should().BeCloseTo(ctx.Expectation.ToUniversalTime(), TimeSpan.FromMilliseconds(DateTimeComparisonPrecision)))
.WhenTypeIs<DateTime>());
AssertionOptions.AssertEquivalencyUsing(options => options
.Using<DateTimeOffset>(ctx => ctx.Subject.ToUniversalTime().Should().BeCloseTo(ctx.Expectation.ToUniversalTime(), TimeSpan.FromMilliseconds(DateTimeComparisonPrecision)))
.WhenTypeIs<DateTimeOffset>());
}
}

View File

@@ -0,0 +1,35 @@
using System.Net.Http.Json;
using FluentAssertions;
using FluentAssertions.Extensions;
using Vegasco.Server.Api.Info;
namespace Vegasco.Server.Api.Tests.Integration.Info;
[Collection(SharedTestCollection.Name)]
public class GetServerInfoTests
{
private readonly WebAppFactory _factory;
public GetServerInfoTests(WebAppFactory factory)
{
_factory = factory;
}
[Fact]
public async Task GetServerInfo_ShouldReturnServerInfo_WhenCalled()
{
// Arrange
// Act
using HttpResponseMessage response = await _factory.HttpClient.GetAsync("/v1/info/server");
// Assert
response.IsSuccessStatusCode.Should().BeTrue();
var serverInfo = await response.Content.ReadFromJsonAsync<GetServerInfo.Response>();
serverInfo!.Environment.Should().NotBeEmpty();
serverInfo.CommitDate.Should().BeAfter(23.August(2024))
.And.NotBeAfter(DateTime.Now);
serverInfo.CommitId.Should().MatchRegex(@"[0-9a-f]{40}");
serverInfo.FullVersion.Should().MatchRegex(@"\d\.\d\.\d(-[0-9a-zA-Z]+)?(\+g?[0-9a-f]{10})?");
}
}

View File

@@ -0,0 +1,40 @@
using Npgsql;
using Respawn;
using System.Data.Common;
namespace Vegasco.Server.Api.Tests.Integration;
internal sealed class PostgresRespawner : IDisposable
{
private readonly DbConnection _connection;
private readonly Respawner _respawner;
private PostgresRespawner(Respawner respawner, DbConnection connection)
{
_respawner = respawner;
_connection = connection;
}
public static async Task<PostgresRespawner> CreateAsync(string connectionString)
{
DbConnection connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
var respawner = await Respawner.CreateAsync(connection,
new RespawnerOptions
{
SchemasToInclude = ["public"],
DbAdapter = DbAdapter.Postgres
});
return new PostgresRespawner(respawner, connection);
}
public async Task ResetDatabaseAsync()
{
await _respawner.ResetAsync(_connection);
}
public void Dispose()
{
_connection.Dispose();
}
}

View File

@@ -0,0 +1,7 @@
namespace Vegasco.Server.Api.Tests.Integration;
[CollectionDefinition(Name)]
public class SharedTestCollection : ICollectionFixture<WebAppFactory>
{
public const string Name = nameof(SharedTestCollection);
}

View File

@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
namespace Vegasco.Server.Api.Tests.Integration;
public sealed class TestUserAlwaysAuthorizedPolicyEvaluator : IPolicyEvaluator
{
public const string Username = "Test user";
public static readonly string UserId = Guid.NewGuid().ToString();
public Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
Claim[] claims =
[
new Claim(ClaimTypes.Name, Username),
new Claim("name", Username),
new Claim(ClaimTypes.NameIdentifier, UserId),
new Claim("aud", "https://localhost")
];
ClaimsIdentity identity = new(claims, JwtBearerDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new(identity);
AuthenticationTicket ticket = new(principal, JwtBearerDefaults.AuthenticationScheme);
AuthenticateResult result = AuthenticateResult.Success(ticket);
return Task.FromResult(result); ;
}
public Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context,
object? resource)
{
return Task.FromResult(PolicyAuthorizationResult.Success());
}
}

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.14.0" />
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="[7.2.0,8.0.0)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Respawn" Version="6.2.1" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.5" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Vegasco.Server.AppHost.Shared\Vegasco.Server.AppHost.Shared.csproj" />
<ProjectReference Include="..\..\src\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Nerdbank.GitVersioning" Version="3.7.115" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,72 @@
using DotNet.Testcontainers.Images;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Testcontainers.PostgreSql;
using Vegasco.Server.Api.Common;
namespace Vegasco.Server.Api.Tests.Integration;
public sealed class WebAppFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private readonly PostgreSqlContainer _database = new PostgreSqlBuilder()
.WithImage(DockerImage)
.WithImagePullPolicy(PullPolicy.Always)
.Build();
private const string DockerImage = "postgres:alpine";
public HttpClient HttpClient => CreateClient();
private PostgresRespawner? _postgresRespawner;
public async Task InitializeAsync()
{
await _database.StartAsync();
// Force application startup (i.e. initialization and validation)
_ = CreateClient();
_postgresRespawner = await PostgresRespawner.CreateAsync(_database.GetConnectionString());
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
IEnumerable<KeyValuePair<string, string?>> customConfig =
[
new KeyValuePair<string, string?>($"ConnectionStrings:{AppHost.Shared.Constants.Database.Name}", _database.GetConnectionString()),
new KeyValuePair<string, string?>("JWT:ValidAudience", "https://localhost"),
new KeyValuePair<string, string?>("JWT:MetadataUrl", "https://localhost"),
new KeyValuePair<string, string?>("JWT:NameClaimType", null),
];
builder.UseConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(customConfig)
.Build());
builder.ConfigureServices(services =>
{
});
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IPolicyEvaluator>();
services.AddSingleton<IPolicyEvaluator, TestUserAlwaysAuthorizedPolicyEvaluator>();
});
}
public async Task ResetDatabaseAsync()
{
await _postgresRespawner!.ResetDatabaseAsync();
}
async Task IAsyncLifetime.DisposeAsync()
{
_postgresRespawner!.Dispose();
await _database.DisposeAsync();
}
}