Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
622 changes: 622 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

229 changes: 229 additions & 0 deletions PowerPlant.Api.IntegrationTests/ApiIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
using System.Net;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using PowerPlant.Api.Enums;
using PowerPlant.Api.IntegrationTests.Models;
using PowerPlant.Api.Models;

namespace PowerPlant.Api.IntegrationTests;

public class ProductionPlanControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;

public ProductionPlanControllerTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}

[Fact]
public async Task Post_ValidInput_ReturnsExpectedResult()
{
//Arrange
var input = new ProductionPlanRequest
{
Load = 910,
Fuels = new FuelsDTO()
{
GasPricePerMWh = 13.4M,
KerosinePricePerMWh = 50.8M,
Co2PricePerTon = 20M,
WindPercentage = 60
},
PowerPlants = new List<PowerPlantDTO>
{
new()
{
Name = "gasfiredbig1",
Type = PowerPlantTypeDTO.gasfired,
Efficiency = 0.53M,
Pmin = 100M,
Pmax = 460M
},
new()
{
Name = "gasfiredbig2",
Type = PowerPlantTypeDTO.gasfired,
Efficiency = 0.53M,
Pmin = 100M,
Pmax = 460M
},
new()
{
Name = "gasfiredsomewhatsmaller",
Type = PowerPlantTypeDTO.gasfired,
Efficiency = 0.37M,
Pmin = 40M,
Pmax = 210M
},
new()
{
Name = "tj1",
Type = PowerPlantTypeDTO.turbojet,
Efficiency = 0.3M,
Pmin = 0M,
Pmax = 16M
},
new()
{
Name = "windpark1",
Type = PowerPlantTypeDTO.windturbine,
Efficiency = 1M,
Pmin = 0M,
Pmax = 150M
},
new()
{
Name = "windpark2",
Type = PowerPlantTypeDTO.windturbine,
Efficiency = 1M,
Pmin = 0M,
Pmax = 36M
}
}
};

var expectedOutput = new List<ProductionPlanResponseDTO>
{
new()
{
Name = "windpark1",
P = 90.0M
},
new()
{
Name = "windpark2",
P = 21.6M
},
new()
{
Name = "gasfiredbig1",
P = 460.0M
},
new()
{
Name = "gasfiredbig2",
P = 338.4M
},
new()
{
Name = "gasfiredsomewhatsmaller",
P = 0.0M
},
new()
{
Name = "tj1",
P = 0.0M
},
};

var jsonString = JsonSerializer.Serialize(input);
var content = new StringContent(jsonString, Encoding.UTF8, "application/json");

//Act
var response = await _client.PostAsync("/productionplan", content);

//Assert
response.EnsureSuccessStatusCode();

var resultJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ProductionPlanResponseDTO[]>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

result.Should().NotBeNull();
result.Length.Should().Be(6);
result.Should().BeEquivalentTo(expectedOutput);
}

[Fact]
public async Task Post_InValidInput_ReturnsExpected_ValidationException()
{
//Arrange
var input = new ProductionPlanRequest
{
Load = 910,
Fuels = new FuelsDTO()
{
GasPricePerMWh = 13.4M,
KerosinePricePerMWh = 50.8M,
Co2PricePerTon = 20M,
WindPercentage = 60
},
PowerPlants = new List<PowerPlantDTO>
{
new()
{
Name = "",
Type = PowerPlantTypeDTO.gasfired,
Efficiency = 0.53M,
Pmin = 100M,
Pmax = 460M
}
}
};

var jsonString = JsonSerializer.Serialize(input);
var content = new StringContent(jsonString, Encoding.UTF8, "application/json");

//Act
var response = await _client.PostAsync("/productionplan", content);

//Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);

var resultJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ValidationErrorResponse>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

result.Title.Should().Be("Validation failures occurred.");
result.ErrorDetails.Count.Should().Be(1);
foreach (var error in result.ErrorDetails)
{
error.Key.Should().Be("PowerPlants[0].Name");
error.Value.Should().BeEquivalentTo(new List<string> { "Power plant name is required." });
}
}

[Fact]
public async Task Post_InValidLoadInput_ReturnsExpected_BusinessException()
{
//Arrange
var input = new ProductionPlanRequest
{
Load = 2000,
Fuels = new FuelsDTO()
{
GasPricePerMWh = 13.4M,
KerosinePricePerMWh = 50.8M,
Co2PricePerTon = 20M,
WindPercentage = 60
},
PowerPlants = new List<PowerPlantDTO>
{
new()
{
Name = "test",
Type = PowerPlantTypeDTO.gasfired,
Efficiency = 0.53M,
Pmin = 100M,
Pmax = 460M
}
}
};

var jsonString = JsonSerializer.Serialize(input);
var content = new StringContent(jsonString, Encoding.UTF8, "application/json");

//Act
var response = await _client.PostAsync("/productionplan", content);

//Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);

var resultJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<BusinessErrorResponse>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

result.Title.Should().Be("Business failure occured.");
result.ErrorDetails.Should().Be("Powerplants cannot generated enough power for the load requested.");
}
}
10 changes: 10 additions & 0 deletions PowerPlant.Api.IntegrationTests/Models/BusinessErrorResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PowerPlant.Api.IntegrationTests.Models;

public class BusinessErrorResponse
{
public string Type { get; set; } = null!;
public string Title { get; set; } = null!;
public int Status { get; set; }
public string TraceId { get; set; } = null!;
public string ErrorDetails { get; set; } = null!;
}
10 changes: 10 additions & 0 deletions PowerPlant.Api.IntegrationTests/Models/ValidationErrorResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PowerPlant.Api.IntegrationTests.Models;

public class ValidationErrorResponse
{
public string Type { get; set; } = null!;
public string Title { get; set; } = null!;
public int Status { get; set; }
public string TraceId { get; set; } = null!;
public Dictionary<string, List<string>> ErrorDetails { get; set; } = null!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PowerPlant.Api\PowerPlant.Api.csproj" />
</ItemGroup>

</Project>
30 changes: 30 additions & 0 deletions PowerPlant.Api/Common/DecimalJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace PowerPlant.Api.Common;

public class DecimalJsonConverter : JsonConverter<decimal>
{
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
if (decimal.TryParse(reader.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value))
{
return value;
}
}
else if (reader.TokenType == JsonTokenType.Number)
{
return reader.GetDecimal();
}

throw new JsonException();
}

public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("F1", CultureInfo.InvariantCulture));
}
}
26 changes: 26 additions & 0 deletions PowerPlant.Api/Controllers/ErrorsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using PowerPlant.Application.Exceptions;

namespace PowerPlant.Api.Controllers;

[ApiController]
[Route("/error")]
public class ErrorsController : ControllerBase
{
[ApiExplorerSettings(IgnoreApi = true)]
public IActionResult Error()
{
var exception = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error;

return Problem(
title: exception?.Message,
statusCode: exception switch
{
ValidationException => 400,
BusinessException => 400,
_ => 500
}
);
}
}
32 changes: 32 additions & 0 deletions PowerPlant.Api/Controllers/ProductionPlanController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using MapsterMapper;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using PowerPlant.Api.Models;
using PowerPlant.Application.Queries.ProductionPlan;

namespace PowerPlant.Api.Controllers;

[ApiController]
[Route("[controller]")]
public class ProductionPlanController : ControllerBase
{
private readonly IMapper _mapper;
private readonly ISender _mediator;

public ProductionPlanController(
IMapper mapper,
ISender mediator)
{
_mapper = mapper;
_mediator = mediator;
}

[HttpPost(Name = "productionplan")]
[Produces("application/json")]
public async Task<IActionResult> GenerateProductionPlan([FromBody] ProductionPlanRequest request)
{
var query = _mapper.Map<ProductionPlanQuery>(request);
var response = await _mediator.Send(query);
return Ok(_mapper.Map<List<ProductionPlanResponseDTO>>(response));
}
}
8 changes: 8 additions & 0 deletions PowerPlant.Api/Enums/PowerPlantTypeDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PowerPlant.Api.Enums;

public enum PowerPlantTypeDTO
{
gasfired,
turbojet,
windturbine
}
Loading