Compare commits
48 Commits
9c372b31a6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f51f508ce | |||
| 62824549fc | |||
| 7d7f5750e3 | |||
| 789ba35c60 | |||
| 1226c42f19 | |||
| 5e083aeaf6 | |||
| 69bb19e4eb | |||
| db791a1183 | |||
| ad77c2fe2b | |||
| 87a0241f11 | |||
| 5956f27646 | |||
| 69901a295c | |||
| 527759eb7b | |||
| d4fff6741c | |||
| a10070b9c7 | |||
| d10d1a6fdb | |||
| 97a275478d | |||
| 731eab3898 | |||
| f018e62163 | |||
| 10e02b5e9b | |||
| c365af1d42 | |||
| 7ddc346e88 | |||
| 925293d626 | |||
| 9b024967e6 | |||
| 288d470c1b | |||
| 84a72a8557 | |||
| d4223ed38f | |||
| 9f2c5db825 | |||
| 18cbc2225f | |||
| 267c4165dd | |||
| ef1c1d8ba1 | |||
| 8d4ae30224 | |||
| 02e7ed7030 | |||
| 9595bedd8e | |||
| af661632cc | |||
| 5062887010 | |||
| b41d5c5d33 | |||
| 4b377ce9f4 | |||
| 5e084ab0a8 | |||
| 559804765b | |||
| 5da1e2fd75 | |||
| ab32be98a6 | |||
| 8681247e76 | |||
| f6dbf489ad | |||
| eaa06029bb | |||
| 9e16d6004a | |||
| 0df7449a99 | |||
| 7f61e011ed |
10
.drone.yml
10
.drone.yml
@@ -42,9 +42,11 @@ steps:
|
||||
- name: docker build and push
|
||||
image: docker:24.0.7
|
||||
commands:
|
||||
- docker build . -t $docker_registry$docker_repo:$DRONE_BRANCH
|
||||
- dockerImageWithTag="$docker_registry$docker_repo:$DRONE_BRANCH"
|
||||
- docker build . -t $dockerImageWithTag
|
||||
- echo $docker_password | docker login --username $docker_username --password-stdin $docker_registry
|
||||
- docker push $docker_registry$docker_repo:$DRONE_BRANCH
|
||||
- docker push $dockerImageWithTag
|
||||
- echo "Built and pushed $dockerImageWithTag"
|
||||
environment:
|
||||
docker_username:
|
||||
from_secret: docker_username
|
||||
@@ -60,6 +62,10 @@ steps:
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
- production
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
depends_on:
|
||||
- compile (.NET)
|
||||
- test
|
||||
|
||||
30
README.md
30
README.md
@@ -2,18 +2,20 @@
|
||||
|
||||
Vegasco (**VE**hicle **GAS** **CO**nsumption) application.
|
||||
|
||||
Includes the backend (`src/Vegasco.Server.Api`) and the frontend (`src/Vegasco-Web`). Utilizes [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview).
|
||||
Includes the backend (`src/Vegasco.Server.Api`) and the frontend (`src/Vegasco-Web`). Uses [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Configuration
|
||||
|
||||
| Configuration | Description | Default | Required |
|
||||
|--------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|----------|
|
||||
| JWT:MetadataUrl | The oidc meta data url | - | true |
|
||||
| JWT:ValidAudience | The valid audience of the JWT token. | - | true |
|
||||
| JWT:NameClaimType | The claim type of the user's name claim. For keycloak, using `preferred_username` is often the better choice. | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name | false |
|
||||
| JWT:AllowHttpMetadataUrl | Whether to allow the meta data url to have http as protocol. Always true when `ASPNETCORE_ENVIRONMENT=true` | false | false |
|
||||
| Configuration | Description | Default | Required |
|
||||
|------------------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|----------|
|
||||
| JWT:MetadataUrl | The oidc meta data url | - | true |
|
||||
| JWT:ValidAudience | The valid audience of the JWT token. | - | true |
|
||||
| JWT:NameClaimType | The claim type of the user's name claim. For keycloak, using `preferred_username` is often the better choice. | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name | false |
|
||||
| JWT:AllowHttpMetadataUrl | Whether to allow the meta data url to have http as protocol. Always true when `ASPNETCORE_ENVIRONMENT=true` | false | false |
|
||||
| ConnectionStrings:seq | The seq http endpoint to send the logs and traces to. If not set, logs and traces will not be sent to seq. | - | false |
|
||||
| ConnectionStrings:vegasco-database | The connection string to the postgres database. | - | true |
|
||||
|
||||
The application uses the prefix `Vegasco_` for environment variable names. The prefix is removed when the application reads the environment variables and duplicate entries are overwritten by the environment variables.
|
||||
|
||||
@@ -67,3 +69,17 @@ creates a Postgres database as a docker container, and starts the Api with the c
|
||||
Ensure you have an identity provider set up, for example Keycloak, and configured the relevant options described above.
|
||||
|
||||
Then, to run the application, ensure you have Docker running, then run either the `http` or `https` launch profile of the `Vegasco.Server.AppHost` project.
|
||||
|
||||
## Deployment
|
||||
|
||||
Build server by running in project root:
|
||||
|
||||
```shell
|
||||
docker build . -t docker.nuyken.dev/vegasco/api:main
|
||||
```
|
||||
|
||||
Builder web client by running in `src/Vegasco-Web`:
|
||||
|
||||
```shell
|
||||
docker build -t docker.nuyken.dev/vegasco/web:main --build-arg CONFIGURATION=production .
|
||||
```
|
||||
|
||||
10
src/Vegasco-Web/.dockerignore
Normal file
10
src/Vegasco-Web/.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
2
src/Vegasco-Web/.vscode/tasks.json
vendored
2
src/Vegasco-Web/.vscode/tasks.json
vendored
@@ -14,7 +14,7 @@
|
||||
"options": {
|
||||
"env": {
|
||||
"PORT": "44200",
|
||||
"services__Vegasco-Server-Api__https__0": "https://localhost:7098",
|
||||
"services__Api__https__0": "https://localhost:7098",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
|
||||
19
src/Vegasco-Web/Dockerfile
Normal file
19
src/Vegasco-Web/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:latest AS build
|
||||
RUN npm install -g pnpm
|
||||
ARG CONFIGURATION=development
|
||||
WORKDIR /usr/local/app
|
||||
COPY . .
|
||||
RUN pnpm install
|
||||
RUN pnpm "build:$CONFIGURATION"
|
||||
|
||||
FROM nginx:alpine
|
||||
RUN rm /etc/nginx/conf.d/*
|
||||
RUN apk add --update dos2unix
|
||||
ENV DOLLAR=$
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=build /usr/local/app/dist/Vegasco-Web/browser .
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
RUN dos2unix /etc/nginx/nginx.conf
|
||||
COPY webserver.conf.template /etc/nginx/templates/webserver.conf.template
|
||||
RUN dos2unix /etc/nginx/templates/webserver.conf.template
|
||||
EXPOSE 80
|
||||
@@ -16,7 +16,7 @@ Once the server is running, open your browser and navigate to `http://localhost:
|
||||
|
||||
Because the solution utilizes Aspire which injects endpoint references for the API as environment variables, this application uses a proxy to access the API. The proxy is configured in the `proxy.config.js` file which is used in the `serve` section of the `angular.json` file. This makes the dev server provide a proxy when serving the application.
|
||||
|
||||
The environment variables for the API endpoint are named `services__Vegasco-Server-Api__https__0` and `services__Vegasco-Server-Api__http__0` for the https and the http endpoints respectively. If the https endpoint is not configured, the http endpoint is used. At least one of them has to be configured.
|
||||
The environment variables for the API endpoint are named `services__Api__https__0` and `services__Api__http__0` for the https and the http endpoints respectively. If the https endpoint is not configured, the http endpoint is used. At least one of them has to be configured.
|
||||
|
||||
To allow the dev proxy to accept otherwise untrusted server certificates, set `NODE_ENV` to `development`. Otherwise the dev proxy rejects untrusted certificates.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/tmp",
|
||||
"outputPath": "dist/Vegasco-Web",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
|
||||
8
src/Vegasco-Web/nginx.conf
Normal file
8
src/Vegasco-Web/nginx.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
events { }
|
||||
http {
|
||||
include mime.types;
|
||||
|
||||
resolver 127.0.0.11;
|
||||
|
||||
include /etc/nginx/conf.d/webserver.conf;
|
||||
}
|
||||
@@ -3,11 +3,12 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start:withInstall": "pnpm install && pnpm run start",
|
||||
"start": "run-script-os",
|
||||
"start:win32": "ng serve --port %PORT% --configuration development",
|
||||
"start:default": "ng serve --port $PORT --configuration development",
|
||||
"build": "ng build",
|
||||
"build": "pnpm build:development",
|
||||
"build:development": "ng build",
|
||||
"build:production": "ng build --configuration production",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module.exports = {
|
||||
"/api": {
|
||||
target:
|
||||
process.env["services__Vegasco-Server-Api__https__0"] ||
|
||||
process.env["services__Vegasco-Server-Api__http__0"],
|
||||
process.env["services__Api__https__0"] ||
|
||||
process.env["services__Api__http__0"],
|
||||
secure: process.env["NODE_ENV"] !== "development",
|
||||
pathRewrite: {
|
||||
"^/api": "",
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
throwError
|
||||
} from 'rxjs';
|
||||
import { CarCardComponent } from './components/car-card/car-card.component';
|
||||
import { SelectedCarService } from '@vegasco-web/modules/entries/services/selected-car.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-entries',
|
||||
@@ -52,6 +53,7 @@ import { CarCardComponent } from './components/car-card/car-card.component';
|
||||
export class CarsComponent {
|
||||
private readonly carClient = inject(CarClient);
|
||||
private readonly messageService = inject(MessageService);
|
||||
private readonly selectedCarService = inject(SelectedCarService);
|
||||
|
||||
protected readonly nonDeletedCars$: Observable<Car[]>;
|
||||
|
||||
@@ -78,13 +80,21 @@ export class CarsComponent {
|
||||
);
|
||||
}
|
||||
|
||||
onCarDeleted(entry: Car): void {
|
||||
this.deletedCars$.next([...this.deletedCars$.value, entry.id]);
|
||||
onCarDeleted(car: Car): void {
|
||||
this.deletedCars$.next([...this.deletedCars$.value, car.id]);
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Auto gelöscht',
|
||||
detail: 'Das Auto wurde erfolgreich gelöscht.',
|
||||
});
|
||||
this.resetSelectedCarIfDeleted(car);
|
||||
}
|
||||
|
||||
private resetSelectedCarIfDeleted(car: Car) {
|
||||
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||
if (selectedCarId === car.id) {
|
||||
this.selectedCarService.setSelectedCarId(null);
|
||||
}
|
||||
}
|
||||
|
||||
private handleGetCarsError(error: unknown): Observable<never> {
|
||||
|
||||
@@ -201,6 +201,14 @@ export class EditCarComponent implements OnInit {
|
||||
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||
});
|
||||
break;
|
||||
case error.status === 409:
|
||||
this.messageService.add({
|
||||
severity: 'warn',
|
||||
summary: 'Konflikt',
|
||||
detail:
|
||||
'Es existiert bereits ein Auto mit diesem Namen. Bitte wähle einen anderen Namen.',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.error(error);
|
||||
this.messageService.add({
|
||||
|
||||
@@ -73,7 +73,7 @@ export class EntriesComponent implements OnInit {
|
||||
const entries = this.consumptionClient.getAll()
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
map(response => response.consumptions),
|
||||
map(response => response.consumptions.sort((a, b) => b.dateTime.localeCompare(a.dateTime))),
|
||||
catchError((error) => this.handleGetEntriesError(error))
|
||||
);
|
||||
|
||||
|
||||
12
src/Vegasco-Web/webserver.conf.template
Normal file
12
src/Vegasco-Web/webserver.conf.template
Normal file
@@ -0,0 +1,12 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location ~ ^/api/(.*) {
|
||||
proxy_pass ${apiUrl}/${DOLLAR}1;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files ${DOLLAR}uri ${DOLLAR}uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Vegasco.Server.Api.Authentication;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
@@ -10,6 +11,7 @@ namespace Vegasco.Server.Api.Cars;
|
||||
public static class CreateCar
|
||||
{
|
||||
public record Request(string Name);
|
||||
|
||||
public record Response(Guid Id, string Name);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
@@ -19,7 +21,8 @@ public static class CreateCar
|
||||
.WithTags("Cars")
|
||||
.WithDescription("Creates a new car")
|
||||
.Produces<Response>(201)
|
||||
.ProducesValidationProblem();
|
||||
.ProducesValidationProblem()
|
||||
.Produces(409);
|
||||
}
|
||||
|
||||
public class Validator : AbstractValidator<Request>
|
||||
@@ -32,41 +35,62 @@ public static class CreateCar
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IResult> Endpoint(
|
||||
private static async Task<IResult> Endpoint(
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
ApplicationDbContext dbContext,
|
||||
UserAccessor userAccessor,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
||||
ILogger logger = loggerFactory.CreateLogger(typeof(CreateCar));
|
||||
|
||||
List<ValidationResult> failedValidations =
|
||||
await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
string[] errors = failedValidations
|
||||
.Where(x => !x.IsValid)
|
||||
.SelectMany(x => x.Errors)
|
||||
.Select(x => x.ErrorMessage)
|
||||
.ToArray();
|
||||
|
||||
logger.LogDebug(
|
||||
"Validation failed for request {@Request} with errors {@Errors}",
|
||||
request,
|
||||
errors);
|
||||
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
bool isDuplicate = await dbContext.Cars
|
||||
.AnyAsync(x => x.Name.ToUpper() == request.Name.ToUpper(), cancellationToken);
|
||||
|
||||
if (isDuplicate)
|
||||
{
|
||||
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
|
||||
return TypedResults.Conflict();
|
||||
}
|
||||
|
||||
string userId = userAccessor.GetUserId();
|
||||
|
||||
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
|
||||
if (user is null)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Id = userId
|
||||
};
|
||||
logger.LogDebug("User with ID '{UserId}' not found, creating new user", userId);
|
||||
|
||||
user = new User { Id = userId };
|
||||
await dbContext.Users.AddAsync(user, cancellationToken);
|
||||
}
|
||||
|
||||
Car car = new()
|
||||
{
|
||||
Name = request.Name,
|
||||
UserId = userId
|
||||
};
|
||||
Car car = new() { Name = request.Name.Trim(), UserId = userId };
|
||||
|
||||
await dbContext.Cars.AddAsync(car, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogTrace("Created new car: {@Car}", car);
|
||||
|
||||
Response response = new(car.Id.Value, car.Name);
|
||||
return TypedResults.Created($"/v1/cars/{car.Id}", response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Diagnostics;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Cars;
|
||||
@@ -15,13 +16,16 @@ public static class DeleteCar
|
||||
.Produces(404);
|
||||
}
|
||||
|
||||
public static async Task<IResult> Endpoint(
|
||||
private static async Task<IResult> Endpoint(
|
||||
Guid id,
|
||||
ApplicationDbContext dbContext,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await dbContext.Cars
|
||||
Activity? activity = Activity.Current;
|
||||
activity?.SetTag("id", id);
|
||||
|
||||
int rows = await dbContext.Cars
|
||||
.Where(x => x.Id == new CarId(id))
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
@@ -32,7 +36,7 @@ public static class DeleteCar
|
||||
|
||||
if (rows > 1)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(nameof(DeleteCar));
|
||||
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteCar));
|
||||
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{CarId}'", rows, id);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public static class GetCar
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var response = new Response(car.Id.Value, car.Name);
|
||||
Response response = new Response(car.Id.Value, car.Name);
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Diagnostics;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Cars;
|
||||
@@ -34,11 +35,15 @@ public static class GetCars
|
||||
ApplicationDbContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Activity? activity = Activity.Current;
|
||||
|
||||
List<ResponseDto> cars = await dbContext.Cars
|
||||
.Select(x => new ResponseDto(x.Id.Value, x.Name))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var response = new ApiResponse
|
||||
activity?.SetTag("carCount", cars.Count);
|
||||
|
||||
ApiResponse response = new()
|
||||
{
|
||||
Cars = cars
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Vegasco.Server.Api.Authentication;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
@@ -9,6 +10,7 @@ namespace Vegasco.Server.Api.Cars;
|
||||
public static class UpdateCar
|
||||
{
|
||||
public record Request(string Name);
|
||||
|
||||
public record Response(Guid Id, string Name);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
@@ -19,7 +21,8 @@ public static class UpdateCar
|
||||
.WithDescription("Updates a car by ID")
|
||||
.Produces<Response>()
|
||||
.ProducesValidationProblem()
|
||||
.Produces(404);
|
||||
.Produces(404)
|
||||
.Produces(409);
|
||||
}
|
||||
|
||||
public class Validator : AbstractValidator<Request>
|
||||
@@ -32,17 +35,31 @@ public static class UpdateCar
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IResult> Endpoint(
|
||||
private static async Task<IResult> Endpoint(
|
||||
Guid id,
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
ApplicationDbContext dbContext,
|
||||
UserAccessor userAccessor,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ILogger logger = loggerFactory.CreateLogger(typeof(UpdateCar));
|
||||
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
string[] errors = failedValidations
|
||||
.Where(x => !x.IsValid)
|
||||
.SelectMany(x => x.Errors)
|
||||
.Select(x => x.ErrorMessage)
|
||||
.ToArray();
|
||||
|
||||
logger.LogDebug(
|
||||
"Validation failed for request {@Request} with errors {@Errors}",
|
||||
request,
|
||||
errors);
|
||||
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
@@ -53,10 +70,21 @@ public static class UpdateCar
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
car.Name = request.Name;
|
||||
bool isDuplicate = await dbContext.Cars
|
||||
.AnyAsync(x => x.Name.ToUpper() == request.Name.ToUpper(), cancellationToken);
|
||||
|
||||
if (isDuplicate)
|
||||
{
|
||||
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
|
||||
return TypedResults.Conflict();
|
||||
}
|
||||
|
||||
car.Name = request.Name.Trim();
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogTrace("Updated car: {@Car}", car);
|
||||
|
||||
Response response = new(car.Id.Value, car.Name);
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public static class DependencyInjectionExtensions
|
||||
/// <param name="builder"></param>
|
||||
public static void AddApiServices(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.AddBuilderServices();
|
||||
|
||||
builder.Services
|
||||
.AddMiscellaneousServices()
|
||||
.AddCustomOpenApi()
|
||||
@@ -27,6 +29,24 @@ public static class DependencyInjectionExtensions
|
||||
builder.AddDbContext();
|
||||
}
|
||||
|
||||
private static IHostApplicationBuilder AddBuilderServices(this IHostApplicationBuilder builder)
|
||||
{
|
||||
string? seqHost = builder.Configuration.GetConnectionString("seq");
|
||||
if (!string.IsNullOrEmpty(seqHost))
|
||||
{
|
||||
builder.AddSeqEndpoint("seq", o =>
|
||||
{
|
||||
var apiKey = builder.Configuration.GetValue<string>("seq-api-key");
|
||||
if (!string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
o.ApiKey = apiKey;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton(() =>
|
||||
@@ -121,7 +141,7 @@ public static class DependencyInjectionExtensions
|
||||
.ValidateFluently()
|
||||
.ValidateOnStart();
|
||||
|
||||
var jwtOptions = services.BuildServiceProvider().GetRequiredService<IOptions<JwtOptions>>();
|
||||
IOptions<JwtOptions> jwtOptions = services.BuildServiceProvider().GetRequiredService<IOptions<JwtOptions>>();
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using System.Diagnostics;
|
||||
using Vegasco.Server.Api.Cars;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
@@ -25,13 +26,13 @@ public static class CreateConsumption
|
||||
{
|
||||
public Validator(TimeProvider timeProvider)
|
||||
{
|
||||
DateTime todayEndOfDay = timeProvider.GetUtcNow()
|
||||
Func<DateTimeOffset> getTodayEndOfDay = () => timeProvider.GetUtcNow()
|
||||
.Date
|
||||
.AddDays(1)
|
||||
.AddTicks(-1);
|
||||
|
||||
|
||||
RuleFor(x => x.DateTime.ToUniversalTime())
|
||||
.LessThanOrEqualTo(todayEndOfDay)
|
||||
.LessThanOrEqualTo(_ => getTodayEndOfDay())
|
||||
.WithName(nameof(Request.DateTime));
|
||||
|
||||
RuleFor(x => x.Distance)
|
||||
@@ -49,11 +50,25 @@ public static class CreateConsumption
|
||||
ApplicationDbContext dbContext,
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ILogger logger = loggerFactory.CreateLogger(typeof(CreateConsumption));
|
||||
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
string[] errors = failedValidations
|
||||
.Where(x => !x.IsValid)
|
||||
.SelectMany(x => x.Errors)
|
||||
.Select(x => x.ErrorMessage)
|
||||
.ToArray();
|
||||
|
||||
logger.LogDebug(
|
||||
"Validation failed for request {@Request} with errors {@Errors}",
|
||||
request,
|
||||
errors);
|
||||
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
@@ -74,6 +89,8 @@ public static class CreateConsumption
|
||||
dbContext.Consumptions.Add(consumption);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogTrace("Created new consumption: {@Consumption}", consumption);
|
||||
|
||||
return TypedResults.Created($"consumptions/{consumption.Id.Value}",
|
||||
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount,
|
||||
consumption.CarId.Value));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Diagnostics;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
@@ -21,7 +22,10 @@ public static class DeleteConsumption
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await dbContext.Consumptions
|
||||
Activity? activity = Activity.Current;
|
||||
activity?.SetTag("id", id);
|
||||
|
||||
int rows = await dbContext.Consumptions
|
||||
.Where(x => x.Id == new ConsumptionId(id))
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
@@ -32,7 +36,7 @@ public static class DeleteConsumption
|
||||
|
||||
if (rows > 1)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(nameof(DeleteConsumption));
|
||||
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteConsumption));
|
||||
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{ConsumptionId}'", rows, id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Diagnostics;
|
||||
using Vegasco.Server.Api.Cars;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
@@ -49,39 +50,53 @@ public static class GetConsumptions
|
||||
private static async Task<Ok<ApiResponse>> Endpoint(
|
||||
[AsParameters] Request request,
|
||||
ApplicationDbContext dbContext,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<Consumption> consumptions = await dbContext.Consumptions
|
||||
.OrderByDescending(x => x.DateTime)
|
||||
ILogger logger = loggerFactory.CreateLogger(typeof(GetConsumptions));
|
||||
|
||||
logger.LogTrace("Received request to get consumptions with parameters: {@Request}", request);
|
||||
Activity? activity = Activity.Current;
|
||||
|
||||
Dictionary<CarId, List<Consumption>> consumptionsByCar = await dbContext.Consumptions
|
||||
.Include(x => x.Car)
|
||||
.ToListAsync(cancellationToken);
|
||||
.GroupBy(x => x.CarId)
|
||||
.ToDictionaryAsync(x => x.Key, x => x.OrderByDescending(x => x.DateTime).ToList(), cancellationToken);
|
||||
|
||||
List<ResponseDto> responses = [];
|
||||
|
||||
for (int i = 0; i < consumptions.Count; i++)
|
||||
foreach (List<Consumption> consumptions in consumptionsByCar.Select(x => x.Value))
|
||||
{
|
||||
Consumption consumption = consumptions[i];
|
||||
|
||||
double? literPer100Km = null;
|
||||
|
||||
bool isLast = i == consumptions.Count - 1;
|
||||
if (!isLast)
|
||||
for (int i = 0; i < consumptions.Count; i++)
|
||||
{
|
||||
Consumption previousConsumption = consumptions[i + 1];
|
||||
double distanceDiff = consumption.Distance - previousConsumption.Distance;
|
||||
literPer100Km = consumption.Amount / (distanceDiff / 100);
|
||||
Consumption consumption = consumptions[i];
|
||||
|
||||
double? literPer100Km = null;
|
||||
|
||||
bool isLast = i == consumptions.Count - 1;
|
||||
if (!isLast)
|
||||
{
|
||||
Consumption previousConsumption = consumptions[i + 1];
|
||||
double distanceDiff = consumption.Distance - previousConsumption.Distance;
|
||||
literPer100Km = consumption.Amount / (distanceDiff / 100);
|
||||
}
|
||||
|
||||
responses.Add(new ResponseDto(
|
||||
consumption.Id.Value,
|
||||
consumption.DateTime,
|
||||
consumption.Distance,
|
||||
consumption.Amount,
|
||||
CarDto.FromCar(consumption.Car),
|
||||
literPer100Km));
|
||||
}
|
||||
|
||||
responses.Add(new ResponseDto(
|
||||
consumption.Id.Value,
|
||||
consumption.DateTime,
|
||||
consumption.Distance,
|
||||
consumption.Amount,
|
||||
CarDto.FromCar(consumption.Car),
|
||||
literPer100Km));
|
||||
}
|
||||
|
||||
activity?.SetTag("consumptionCount", responses.Count);
|
||||
|
||||
ApiResponse apiResponse = new() { Consumptions = responses };
|
||||
ApiResponse apiResponse = new()
|
||||
{
|
||||
Consumptions = responses
|
||||
};
|
||||
return TypedResults.Ok(apiResponse);
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,13 @@ public static class UpdateConsumption
|
||||
{
|
||||
public Validator(TimeProvider timeProvider)
|
||||
{
|
||||
DateTime todayEndOfDay = timeProvider.GetUtcNow()
|
||||
Func<DateTimeOffset> getTodayEndOfDay = () => timeProvider.GetUtcNow()
|
||||
.Date
|
||||
.AddDays(1)
|
||||
.AddTicks(-1);
|
||||
|
||||
RuleFor(x => x.DateTime.ToUniversalTime())
|
||||
.LessThanOrEqualTo(todayEndOfDay)
|
||||
.LessThanOrEqualTo(_ => getTodayEndOfDay())
|
||||
.WithName(nameof(Request.DateTime));
|
||||
|
||||
RuleFor(x => x.Distance)
|
||||
@@ -48,11 +48,25 @@ public static class UpdateConsumption
|
||||
Guid id,
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ILogger logger = loggerFactory.CreateLogger(typeof(UpdateConsumption));
|
||||
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
string[] errors = failedValidations
|
||||
.Where(x => !x.IsValid)
|
||||
.SelectMany(x => x.Errors)
|
||||
.Select(x => x.ErrorMessage)
|
||||
.ToArray();
|
||||
|
||||
logger.LogDebug(
|
||||
"Validation failed for request {@Request} with errors {@Errors}",
|
||||
request,
|
||||
errors);
|
||||
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
@@ -68,6 +82,9 @@ public static class UpdateConsumption
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TypedResults.Ok(new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.CarId.Value));
|
||||
logger.LogTrace("Updated consumption: {@Consumption}", consumption);
|
||||
|
||||
return TypedResults.Ok(new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance,
|
||||
consumption.Amount, consumption.CarId.Value));
|
||||
}
|
||||
}
|
||||
@@ -41,5 +41,6 @@ public static class EndpointExtensions
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
|
||||
GetServerInfo.MapEndpoint(versionedApis);
|
||||
GetCurrentTime.MapEndpoint(versionedApis);
|
||||
}
|
||||
}
|
||||
|
||||
21
src/Vegasco.Server.Api/Info/GetCurrentTime.cs
Normal file
21
src/Vegasco.Server.Api/Info/GetCurrentTime.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
||||
namespace Vegasco.Server.Api.Info;
|
||||
|
||||
public static class GetCurrentTime
|
||||
{
|
||||
public record Response(DateTimeOffset CurrentTime);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapGet("info/time", Endpoint)
|
||||
.WithTags("Info");
|
||||
}
|
||||
|
||||
private static Ok<Response> Endpoint(
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
return TypedResults.Ok(new Response(timeProvider.GetUtcNow()));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Vegasco.Server.Api.Info;
|
||||
|
||||
public class GetServerInfo
|
||||
public static class GetServerInfo
|
||||
{
|
||||
public record Response(
|
||||
string FullVersion,
|
||||
|
||||
@@ -11,12 +11,12 @@ public class ApplyMigrationsService(
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = activitySource.StartActivity("ApplyMigrations");
|
||||
using Activity? activity = activitySource.StartActivity("ApplyMigrations");
|
||||
|
||||
logger.LogInformation("Starting migrations");
|
||||
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
await using ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,23 +13,24 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.3.0" />
|
||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.1" />
|
||||
<PackageReference Include="Aspire.Seq" Version="9.5.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="OpenTelemetry" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.4.16" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.9.0" />
|
||||
<PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" PrivateAssets="all" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="StronglyTypedId.Templates" Version="1.0.0-beta08" />
|
||||
</ItemGroup>
|
||||
@@ -40,7 +41,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.7.115" />
|
||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.8.118" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,7 +4,7 @@ public static class Constants
|
||||
{
|
||||
public static class Projects
|
||||
{
|
||||
public const string Api = "Vegasco-Server-Api";
|
||||
public const string Api = "Api";
|
||||
}
|
||||
|
||||
public static class Database
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Nerdbank.GitVersioning">
|
||||
<Version>3.7.115</Version>
|
||||
<Version>3.8.118</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Vegasco.Server.AppHost.Shared;
|
||||
|
||||
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
IResourceBuilder<PostgresDatabaseResource> postgres = builder.AddPostgres(Constants.Database.ServiceName)
|
||||
IResourceBuilder<PostgresServerResource> postgresBuilder = builder.AddPostgres(Constants.Database.ServiceName)
|
||||
.WithLifetime(ContainerLifetime.Persistent)
|
||||
.WithDataVolume();
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
postgresBuilder = postgresBuilder
|
||||
.WithPgWeb()
|
||||
.WithPgAdmin();
|
||||
}
|
||||
|
||||
IResourceBuilder<SeqResource> seq = builder.AddSeq("seq")
|
||||
.WithLifetime(ContainerLifetime.Persistent)
|
||||
.WithDataVolume()
|
||||
.WithExternalHttpEndpoints()
|
||||
.WithImageTag("latest");
|
||||
|
||||
IResourceBuilder<PostgresDatabaseResource> postgres = postgresBuilder
|
||||
.AddDatabase(Constants.Database.Name);
|
||||
|
||||
IResourceBuilder<ProjectResource> api = builder
|
||||
.AddProject<Projects.Vegasco_Server_Api>(Constants.Projects.Api)
|
||||
.WithReference(postgres)
|
||||
.WaitFor(postgres);
|
||||
.WaitFor(postgres)
|
||||
.WithReference(seq)
|
||||
.WaitFor(seq);
|
||||
|
||||
builder
|
||||
.AddNpmApp("Vegasco-Web", "../Vegasco-Web", scriptName: "start:withInstall")
|
||||
.AddNpmApp("Vegasco-Web", "../Vegasco-Web")
|
||||
.WithReference(api)
|
||||
.WaitFor(api)
|
||||
.WithHttpEndpoint(port: 44200, env: "PORT", isProxied: false)
|
||||
.WithHttpEndpoint(port: 44200, env: "PORT")
|
||||
.WithExternalHttpEndpoints()
|
||||
.WithHttpHealthCheck("/", 200);
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.3.0" />
|
||||
<PackageReference Include="Aspire.Hosting.NodeJs" Version="9.3.1" />
|
||||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.3.0" />
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" />
|
||||
<PackageReference Include="Aspire.Hosting.NodeJs" Version="9.5.1" />
|
||||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.5.1" />
|
||||
<PackageReference Update="Nerdbank.GitVersioning">
|
||||
<Version>3.7.115</Version>
|
||||
<Version>3.8.118</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Aspire.Hosting.Seq" Version="9.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -25,4 +26,14 @@
|
||||
<ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RestoreNpm" BeforeTargets="Build" Condition=" '$(DesignTimeBuild)' != 'true' ">
|
||||
<ItemGroup>
|
||||
<PackageJsons Include="..\*\package.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Install npm packages if node_modules is missing -->
|
||||
<Message Importance="Normal" Text="Installing npm packages for %(PackageJsons.RelativeDir)" Condition="!Exists('%(PackageJsons.RootDir)%(PackageJsons.Directory)/node_modules')" />
|
||||
<Exec Command="pnpm install" WorkingDirectory="%(PackageJsons.RootDir)%(PackageJsons.Directory)" Condition="!Exists('%(PackageJsons.RootDir)%(PackageJsons.Directory)/node_modules')" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.3.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.5.1" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageReference Update="Nerdbank.GitVersioning">
|
||||
<Version>3.7.115</Version>
|
||||
<Version>3.8.118</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ 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());
|
||||
Faker faker = new();
|
||||
return new CreateCar.Request(faker.Person.FirstName);
|
||||
}
|
||||
|
||||
internal UpdateCar.Request UpdateCarRequest()
|
||||
{
|
||||
return new UpdateCar.Request(_faker.Vehicle.Model());
|
||||
Faker faker = new();
|
||||
return new UpdateCar.Request(faker.Person.FirstName);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public class CreateCarTests : IAsyncLifetime
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var createdCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
CreateCar.Response? 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))
|
||||
@@ -46,14 +46,14 @@ public class CreateCarTests : IAsyncLifetime
|
||||
public async Task CreateCar_ShouldReturnValidationProblems_WhenRequestIsNotValid()
|
||||
{
|
||||
// Arrange
|
||||
var createCarRequest = new CreateCar.Request("");
|
||||
CreateCar.Request 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? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class DeleteCarTests : IAsyncLifetime
|
||||
public async Task DeleteCar_ShouldReturnNotFound_WhenCarDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var randomCarId = Guid.NewGuid();
|
||||
Guid randomCarId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{randomCarId}");
|
||||
@@ -43,7 +43,7 @@ public class DeleteCarTests : IAsyncLifetime
|
||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||
createCarResponse.EnsureSuccessStatusCode();
|
||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
|
||||
// Act
|
||||
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{createdCar!.Id}");
|
||||
|
||||
@@ -21,7 +21,7 @@ public class GetCarTests : IAsyncLifetime
|
||||
public async Task GetCar_ShouldReturnNotFound_WhenCarDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var randomCarId = Guid.NewGuid();
|
||||
Guid randomCarId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/cars/{randomCarId}");
|
||||
@@ -37,14 +37,14 @@ public class GetCarTests : IAsyncLifetime
|
||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||
createCarResponse.EnsureSuccessStatusCode();
|
||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
CreateCar.Response? 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>();
|
||||
GetCar.Response? car = await response.Content.ReadFromJsonAsync<GetCar.Response>();
|
||||
car.Should().BeEquivalentTo(createdCar);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class GetCarsTests : IAsyncLifetime
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
||||
GetCars.ApiResponse? apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
||||
apiResponse!.Cars.Should().BeEmpty();
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ public class GetCarsTests : IAsyncLifetime
|
||||
List<CreateCar.Response> createdCars = [];
|
||||
|
||||
const int numberOfCars = 5;
|
||||
for (var i = 0; i < numberOfCars; i++)
|
||||
for (int 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>();
|
||||
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
createdCars.Add(createdCar!);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class GetCarsTests : IAsyncLifetime
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
||||
GetCars.ApiResponse? apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
||||
apiResponse!.Cars.Should().BeEquivalentTo(createdCars);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public class UpdateCarTests : IAsyncLifetime
|
||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||
createCarResponse.EnsureSuccessStatusCode();
|
||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
|
||||
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
|
||||
|
||||
@@ -40,7 +40,7 @@ public class UpdateCarTests : IAsyncLifetime
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var updatedCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
CreateCar.Response? updatedCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
updatedCar!.Id.Should().Be(createdCar.Id);
|
||||
updatedCar.Should().BeEquivalentTo(updateCarRequest, o => o.ExcludingMissingMembers());
|
||||
|
||||
@@ -57,16 +57,16 @@ public class UpdateCarTests : IAsyncLifetime
|
||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||
createCarResponse.EnsureSuccessStatusCode();
|
||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
|
||||
var updateCarRequest = new UpdateCar.Request("");
|
||||
UpdateCar.Request 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? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -80,7 +80,7 @@ public class UpdateCarTests : IAsyncLifetime
|
||||
{
|
||||
// Arrange
|
||||
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
|
||||
var randomCarId = Guid.NewGuid();
|
||||
Guid randomCarId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{randomCarId}", updateCarRequest);
|
||||
|
||||
@@ -5,20 +5,19 @@ namespace Vegasco.Server.Api.Tests.Integration;
|
||||
|
||||
internal class ConsumptionFaker
|
||||
{
|
||||
private readonly Faker _faker = new();
|
||||
|
||||
internal CreateConsumption.Request CreateConsumptionRequest(Guid carId)
|
||||
{
|
||||
Faker faker = new();
|
||||
return new CreateConsumption.Request(
|
||||
_faker.Date.RecentOffset(),
|
||||
_faker.Random.Int(1, 1_000),
|
||||
_faker.Random.Int(20, 70),
|
||||
faker.Date.RecentOffset(),
|
||||
faker.Random.Int(1, 1_000),
|
||||
faker.Random.Int(20, 70),
|
||||
carId);
|
||||
}
|
||||
|
||||
internal UpdateConsumption.Request UpdateConsumptionRequest()
|
||||
{
|
||||
CreateConsumption.Request createRequest = CreateConsumptionRequest(default);
|
||||
CreateConsumption.Request createRequest = CreateConsumptionRequest(Guid.Empty);
|
||||
return new UpdateConsumption.Request(
|
||||
createRequest.DateTime,
|
||||
createRequest.Distance,
|
||||
|
||||
@@ -39,7 +39,7 @@ public class CreateConsumptionTests : IAsyncLifetime
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||
createdConsumption.Should().BeEquivalentTo(createConsumptionRequest, o => o.ExcludingMissingMembers());
|
||||
|
||||
_dbContext.Consumptions.Should().HaveCount(1)
|
||||
@@ -64,7 +64,7 @@ public class CreateConsumptionTests : IAsyncLifetime
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||
ValidationProblemDetails? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||
x.Equals(nameof(createConsumptionRequest.CarId), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -76,7 +76,7 @@ public class CreateConsumptionTests : IAsyncLifetime
|
||||
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>();
|
||||
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
return createdCarResponse!;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ public class DeleteConsumptionTests : IAsyncLifetime
|
||||
public async Task DeleteConsumption_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var consumptionId = Guid.NewGuid();
|
||||
Guid consumptionId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
using HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/consumptions/{consumptionId}");
|
||||
@@ -58,7 +58,7 @@ public class DeleteConsumptionTests : IAsyncLifetime
|
||||
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>();
|
||||
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||
return createdConsumption!;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public class DeleteConsumptionTests : IAsyncLifetime
|
||||
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>();
|
||||
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
return createdCarResponse!;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
||||
// Assert
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var consumption = await response.Content.ReadFromJsonAsync<GetConsumption.Response>();
|
||||
GetConsumption.Response? consumption = await response.Content.ReadFromJsonAsync<GetConsumption.Response>();
|
||||
consumption.Should().BeEquivalentTo(createdConsumption);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
||||
public async Task GetConsumptions_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var consumptionId = Guid.NewGuid();
|
||||
Guid consumptionId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
using HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/consumptions{consumptionId}");
|
||||
@@ -60,7 +60,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
||||
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>();
|
||||
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||
return createdConsumption!;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
||||
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>();
|
||||
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
return createdCarResponse!;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
||||
// Assert
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var updatedConsumption = await response.Content.ReadFromJsonAsync<UpdateConsumption.Response>();
|
||||
UpdateConsumption.Response? updatedConsumption = await response.Content.ReadFromJsonAsync<UpdateConsumption.Response>();
|
||||
updatedConsumption.Should().BeEquivalentTo(updateConsumptionRequest, o => o.ExcludingMissingMembers());
|
||||
|
||||
_dbContext.Consumptions.Should().HaveCount(1)
|
||||
@@ -59,7 +59,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
||||
// Arrange
|
||||
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
||||
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest() with { Distance = -42 };
|
||||
var randomGuid = Guid.NewGuid();
|
||||
Guid randomGuid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
|
||||
@@ -67,7 +67,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
||||
// Assert
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||
ValidationProblemDetails? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||
x.Equals(nameof(updateConsumptionRequest.Distance), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -80,7 +80,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
||||
// Arrange
|
||||
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
||||
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest();
|
||||
var randomGuid = Guid.NewGuid();
|
||||
Guid randomGuid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
|
||||
@@ -98,7 +98,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
||||
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>();
|
||||
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||
return createdConsumption!;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
||||
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>();
|
||||
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||
return createdCarResponse!;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Extensions;
|
||||
using System.Net.Http.Json;
|
||||
using Vegasco.Server.Api.Info;
|
||||
|
||||
namespace Vegasco.Server.Api.Tests.Integration.Info;
|
||||
|
||||
[Collection(SharedTestCollection.Name)]
|
||||
public sealed class GetCurrentTimeTests
|
||||
{
|
||||
private readonly WebAppFactory _factory;
|
||||
|
||||
public GetCurrentTimeTests(WebAppFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetServerInfo_ShouldReturnServerInfo_WhenCalled()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
using HttpResponseMessage response = await _factory.HttpClient.GetAsync("/v1/info/time");
|
||||
|
||||
// Assert
|
||||
response.IsSuccessStatusCode.Should().BeTrue();
|
||||
GetCurrentTime.Response? timeInfo = await response.Content.ReadFromJsonAsync<GetCurrentTime.Response>();
|
||||
timeInfo.Should().NotBeNull();
|
||||
timeInfo.CurrentTime.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(10));
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public class GetServerInfoTests
|
||||
|
||||
// Assert
|
||||
response.IsSuccessStatusCode.Should().BeTrue();
|
||||
var serverInfo = await response.Content.ReadFromJsonAsync<GetServerInfo.Response>();
|
||||
GetServerInfo.Response? serverInfo = await response.Content.ReadFromJsonAsync<GetServerInfo.Response>();
|
||||
serverInfo!.Environment.Should().NotBeEmpty();
|
||||
serverInfo.CommitDate.Should().BeAfter(23.August(2024))
|
||||
.And.NotBeAfter(DateTime.Now);
|
||||
|
||||
@@ -19,7 +19,7 @@ internal sealed class PostgresRespawner : IDisposable
|
||||
DbConnection connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
var respawner = await Respawner.CreateAsync(connection,
|
||||
Respawner respawner = await Respawner.CreateAsync(connection,
|
||||
new RespawnerOptions
|
||||
{
|
||||
SchemasToInclude = ["public"],
|
||||
|
||||
@@ -10,21 +10,21 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" Version="1.14.0" />
|
||||
<PackageReference Include="Bogus" Version="35.6.3" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageReference Include="Bogus" Version="35.6.4" />
|
||||
<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="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<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="System.Formats.Asn1" Version="9.0.10" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.7.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -40,7 +40,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.7.115" />
|
||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.8.118" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="8.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.7.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -34,7 +34,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.7.115" />
|
||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.8.118" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<File Path="version.json" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Vegasco.Server.Api/Vegasco.Server.Api.csproj" />
|
||||
<Project Path="src/Vegasco.Server.AppHost.Shared/Vegasco.Server.AppHost.Shared.csproj" />
|
||||
<Project Path="src/Vegasco.Server.AppHost/Vegasco.Server.AppHost.csproj" />
|
||||
<Project Path="src/Vegasco.Server.ServiceDefaults/Vegasco.Server.ServiceDefaults.csproj" />
|
||||
<Project Path="src/Vegasco.Server.Api/Vegasco.Server.Api.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/Vegasco.Server.Api.Tests.Integration/Vegasco.Server.Api.Tests.Integration.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user