Compare commits
69 Commits
ddd
...
5727707cce
| Author | SHA1 | Date | |
|---|---|---|---|
| 5727707cce | |||
| f00e3cdb6a | |||
| 20ba638b64 | |||
| d80a53761d | |||
| e29c5b2458 | |||
| 7aa8599535 | |||
| 16bc250789 | |||
| 9847b6e6f7 | |||
| ada0e2f665 | |||
| b28bd2826b | |||
| a1999bfe41 | |||
| 9d71c86474 | |||
| d91b837e44 | |||
| b3ca1ba703 | |||
| 108960d074 | |||
| 05686c4cdd | |||
| cb440e7c6d | |||
| 4ea0978cf6 | |||
| ff2707a0e8 | |||
| 6d23494fd3 | |||
| bbac953660 | |||
| 854be19fd5 | |||
| cf1a086e31 | |||
| 918477fb3a | |||
| 857863a4d8 | |||
| 7a2c50cb9a | |||
| 5d0a49632a | |||
| 0e065b58b7 | |||
| 22f47f4461 | |||
| d6c75654b0 | |||
| 136dd2311d | |||
| 351a1a4635 | |||
| 4db35dbdb5 | |||
| d0704aea12 | |||
| 92e91de9c2 | |||
| de7e9a7131 | |||
| 6b422545d9 | |||
| 4a1f1a5a67 | |||
| d3d3675e3d | |||
| 88090878ee | |||
| f410f69e9d | |||
| 036f4d1dfc | |||
| ea689bb7a1 | |||
| 4855336c33 | |||
| ad9391093d | |||
| 89afc435fc | |||
| 2d79b5a0bf | |||
| dcb82414b9 | |||
| d19d68f5a2 | |||
| 1c88d2b2c6 | |||
| 155ed22fb0 | |||
| 4a46c46222 | |||
| f4846bc66a | |||
| 70f47b0dd1 | |||
| e20f713fdb | |||
| 2463c11be3 | |||
| d47e4c1971 | |||
| 4bfc57ef9f | |||
| 5c532a6bb5 | |||
| 4f287d85dd | |||
| 7f734aa2a2 | |||
| 81b5c89a25 | |||
| 1d6ecfee6e | |||
| 4be9fd2043 | |||
| 19b105b0e8 | |||
| ff2da66a22 | |||
| 877e7989cd | |||
| a708ed25e7 | |||
| e579d76560 |
@@ -1,7 +1,7 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
#**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
|
||||
87
.drone.yml
Normal file
87
.drone.yml
Normal file
@@ -0,0 +1,87 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: Build and test
|
||||
|
||||
trigger:
|
||||
event:
|
||||
include:
|
||||
- push
|
||||
- pull_request
|
||||
- custom
|
||||
|
||||
steps:
|
||||
- name: compile
|
||||
image: mcr.microsoft.com/dotnet/sdk:9.0-alpine
|
||||
environment:
|
||||
CI_WORKSPACE: "/drone/src"
|
||||
commands:
|
||||
- dotnet build
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run
|
||||
|
||||
- name: test
|
||||
image: quay.io/testcontainers/dind-drone-plugin
|
||||
environment:
|
||||
CI_WORKSPACE: "/drone/src"
|
||||
settings:
|
||||
build_image: mcr.microsoft.com/dotnet/sdk:9.0-alpine
|
||||
cmd:
|
||||
- dotnet test --no-build
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run
|
||||
depends_on:
|
||||
- compile
|
||||
|
||||
- name: docker build and push
|
||||
image: docker:24.0.7
|
||||
commands:
|
||||
- docker build . -t $docker_registry$docker_repo:$DRONE_BRANCH
|
||||
- echo $docker_password | docker login --username $docker_username --password-stdin $docker_registry
|
||||
- docker push $docker_registry$docker_repo:$DRONE_BRANCH
|
||||
environment:
|
||||
docker_username:
|
||||
from_secret: docker_username
|
||||
docker_password:
|
||||
from_secret: docker_password
|
||||
docker_repo:
|
||||
from_secret: docker_repo
|
||||
docker_registry:
|
||||
from_secret: docker_registry
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
depends_on:
|
||||
- compile
|
||||
- test
|
||||
|
||||
- name: Telegram notification
|
||||
image: appleboy/drone-telegram
|
||||
settings:
|
||||
token:
|
||||
from_secret: telegram_token
|
||||
to:
|
||||
from_secret: telegram_user_id
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
depends_on:
|
||||
- compile
|
||||
- test
|
||||
- docker build and push
|
||||
|
||||
services:
|
||||
- name: docker
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run
|
||||
|
||||
volumes:
|
||||
- name: dockersock
|
||||
temp: { }
|
||||
2
Create-Migration.ps1
Normal file
2
Create-Migration.ps1
Normal file
@@ -0,0 +1,2 @@
|
||||
dotnet ef migrations add $args[0] --project .\src\WebApi\WebApi.csproj --output-dir Persistence/Migrations
|
||||
dotnet ef migrations script --idempotent --project .\src\WebApi\WebApi.csproj --output migrations/migration.sql
|
||||
9
Directory.Build.props
Normal file
9
Directory.Build.props
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<Version>3.6.141</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
USER app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["src/Vegasco.Server.Api/Vegasco.Server.Api.csproj", "src/Vegasco.Server.Api/"]
|
||||
RUN dotnet restore "./src/Vegasco.Server.Api/Vegasco.Server.Api.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/Vegasco.Server.Api"
|
||||
RUN dotnet build "./Vegasco.Server.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./Vegasco.Server.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
HEALTHCHECK --interval=20s --timeout=1s --start-period=10s --retries=3 CMD curl --fail http://localhost:8080/health || exit 1
|
||||
ENTRYPOINT ["dotnet", "Vegasco.Server.Api.dll"]
|
||||
68
README.md
68
README.md
@@ -1 +1,67 @@
|
||||
# vegasco-server
|
||||
# Vegasco Server
|
||||
|
||||
Backend for the vegasco (**VE**hicle **GAS** **CO**nsumption) application.
|
||||
|
||||
## 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 |
|
||||
|
||||
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.
|
||||
|
||||
Example:
|
||||
|
||||
- `foo=bar1`
|
||||
- `Vegasco_foo=bar2`
|
||||
|
||||
Results in:
|
||||
|
||||
- `foo=bar2`
|
||||
- `Vegasco_foo=bar2`
|
||||
|
||||
Configuration hierarchy in environment variables is usually denoted using a colon (`:`). But because on some systems the colon character is a reserved character, you can use a double underscore (`__`) as an alternative. The application will replace the double underscore with a colon when reading the environment variables.
|
||||
|
||||
Example:
|
||||
|
||||
The environment variable `foo__bar=value` (as well as `Vegasco_foo__bar=value`) will be converted to `foo:bar=value` in the application.
|
||||
|
||||
### Configuration examples
|
||||
|
||||
As environment variables:
|
||||
|
||||
```env
|
||||
Vegasco_JWT__Authority=https://example.authority.com
|
||||
Vegasco_JWT__Audience=example-audience
|
||||
Vegasco_JWT__Issuer=https://example.authority.com/realms/example-realm/
|
||||
Vegasco_JWT__NameClaimType=preferred_username
|
||||
```
|
||||
|
||||
As appsettings.json (or a environment specific appsettings.*.json):
|
||||
|
||||
**Note: the `Vegasco_` prefix is only for environment variables**
|
||||
|
||||
```json
|
||||
{
|
||||
"JWT": {
|
||||
"Authority": "https://example.authority.com/realms/example-realm",
|
||||
"Audience": "example-audience",
|
||||
"Issuer": "https://example.authority.com/realms/example-realm/",
|
||||
"NameClaimType": "preferred_username"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running the application
|
||||
|
||||
The solution uses Aspire to orchestrate the application. Specifically, it introduces sensible service defaults, including but not limited to OpenTelemetry,
|
||||
creates a Postgres database as a docker container, and starts the Api with the correct configuration to communicate with the database.
|
||||
|
||||
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 the `Vegasco.Server.AppHost` launch profile.
|
||||
|
||||
1
Run-PostgresDb.ps1
Normal file
1
Run-PostgresDb.ps1
Normal file
@@ -0,0 +1 @@
|
||||
docker run -d -p 5432:5432 --restart always --name vegasco-test-db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres postgres:16.3-alpine
|
||||
@@ -1,6 +0,0 @@
|
||||
<Solution>
|
||||
<Project Path="src\Vegasco.WebApi\Vegasco.WebApi.csproj" Type="C#" />
|
||||
<Properties Name="Visual Studio">
|
||||
<Property Name="OpenWith" Value="Visual Studio Version 17" />
|
||||
</Properties>
|
||||
</Solution>
|
||||
17
src/Vegasco-Web/.editorconfig
Normal file
17
src/Vegasco-Web/.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
42
src/Vegasco-Web/.gitignore
vendored
Normal file
42
src/Vegasco-Web/.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
4
src/Vegasco-Web/.vscode/extensions.json
vendored
Normal file
4
src/Vegasco-Web/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
20
src/Vegasco-Web/.vscode/launch.json
vendored
Normal file
20
src/Vegasco-Web/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
src/Vegasco-Web/.vscode/tasks.json
vendored
Normal file
42
src/Vegasco-Web/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
59
src/Vegasco-Web/README.md
Normal file
59
src/Vegasco-Web/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# VegascoWeb
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.1.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
101
src/Vegasco-Web/angular.json
Normal file
101
src/Vegasco-Web/angular.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"Vegasco-Web": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"options": {
|
||||
"proxyConfig": "proxy.config.js"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "Vegasco-Web:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "Vegasco-Web:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Vegasco-Web/package.json
Normal file
36
src/Vegasco-Web/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "vegasco-web",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^20.0.3",
|
||||
"@angular/compiler": "^20.0.3",
|
||||
"@angular/core": "^20.0.3",
|
||||
"@angular/forms": "^20.0.3",
|
||||
"@angular/platform-browser": "^20.0.3",
|
||||
"@angular/router": "^20.0.3",
|
||||
"rxjs": "~7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "~0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.0.2",
|
||||
"@angular/cli": "^20.0.2",
|
||||
"@angular/compiler-cli": "^20.0.3",
|
||||
"@types/jasmine": "~5.1.8",
|
||||
"jasmine-core": "~5.7.1",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.8.3"
|
||||
}
|
||||
}
|
||||
5108
src/Vegasco-Web/pnpm-lock.yaml
generated
Normal file
5108
src/Vegasco-Web/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
src/Vegasco-Web/proxy.config.js
Normal file
11
src/Vegasco-Web/proxy.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
"/api": {
|
||||
target:
|
||||
process.env["services__Vegasco-Server-Api__https__0"] ||
|
||||
process.env["services__Vegasco-Server-Api__http__0"],
|
||||
secure: process.env["NODE_ENV"] !== "development",
|
||||
pathRewrite: {
|
||||
"^/api": "",
|
||||
},
|
||||
},
|
||||
};
|
||||
BIN
src/Vegasco-Web/public/favicon.ico
Normal file
BIN
src/Vegasco-Web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
14
src/Vegasco-Web/src/app/app.config.ts
Normal file
14
src/Vegasco-Web/src/app/app.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import {provideHttpClient} from '@angular/common/http';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
]
|
||||
};
|
||||
366
src/Vegasco-Web/src/app/app.html
Normal file
366
src/Vegasco-Web/src/app/app.html
Normal file
@@ -0,0 +1,366 @@
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
||||
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
<style>
|
||||
:host {
|
||||
--bright-blue: oklch(51.01% 0.274 263.83);
|
||||
--electric-violet: oklch(53.18% 0.28 296.97);
|
||||
--french-violet: oklch(47.66% 0.246 305.88);
|
||||
--vivid-pink: oklch(69.02% 0.277 332.77);
|
||||
--hot-red: oklch(61.42% 0.238 15.34);
|
||||
--orange-red: oklch(63.32% 0.24 31.68);
|
||||
|
||||
--gray-900: oklch(19.37% 0.006 300.98);
|
||||
--gray-700: oklch(36.98% 0.014 302.71);
|
||||
--gray-400: oklch(70.9% 0.015 304.04);
|
||||
|
||||
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
|
||||
180deg,
|
||||
var(--orange-red) 0%,
|
||||
var(--vivid-pink) 50%,
|
||||
var(--electric-violet) 100%
|
||||
);
|
||||
|
||||
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
|
||||
90deg,
|
||||
var(--orange-red) 0%,
|
||||
var(--vivid-pink) 50%,
|
||||
var(--electric-violet) 100%
|
||||
);
|
||||
|
||||
--pill-accent: var(--bright-blue);
|
||||
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.125rem;
|
||||
color: var(--gray-900);
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.125rem;
|
||||
margin: 0;
|
||||
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
box-sizing: inherit;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.angular-logo {
|
||||
max-width: 9.2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
background: var(--red-to-pink-to-purple-vertical-gradient);
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.pill-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--pill-accent: var(--bright-blue);
|
||||
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
|
||||
color: var(--pill-accent);
|
||||
padding-inline: 0.75rem;
|
||||
padding-block: 0.375rem;
|
||||
border-radius: 2.75rem;
|
||||
border: 0;
|
||||
transition: background 0.3s ease;
|
||||
font-family: var(--inter-font);
|
||||
font-size: 0.875rem;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 1.4rem;
|
||||
letter-spacing: -0.00875rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pill:hover {
|
||||
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.pill-group .pill:nth-child(6n + 1) {
|
||||
--pill-accent: var(--bright-blue);
|
||||
}
|
||||
|
||||
.pill-group .pill:nth-child(6n + 2) {
|
||||
--pill-accent: var(--french-violet);
|
||||
}
|
||||
|
||||
.pill-group .pill:nth-child(6n + 3),
|
||||
.pill-group .pill:nth-child(6n + 4),
|
||||
.pill-group .pill:nth-child(6n + 5) {
|
||||
--pill-accent: var(--hot-red);
|
||||
}
|
||||
|
||||
.pill-group svg {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.73rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.social-links path {
|
||||
transition: fill 0.3s ease;
|
||||
fill: var(--gray-400);
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.social-links a:hover svg path {
|
||||
fill: var(--gray-900);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.content {
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||
margin-block: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
<div class="left-side">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 982 239"
|
||||
fill="none"
|
||||
class="angular-logo"
|
||||
>
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#c)"
|
||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="c"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF41F8"/>
|
||||
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5"/>
|
||||
<stop offset="1" stop-color="#FF41F8" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="0"
|
||||
x2="982"
|
||||
y1="192"
|
||||
y2="192"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F0060B"/>
|
||||
<stop offset="0" stop-color="#F0070C"/>
|
||||
<stop offset=".526" stop-color="#CC26D5"/>
|
||||
<stop offset="1" stop-color="#7702FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h982v239H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<h1>Hello, {{ title }}</h1>
|
||||
<p>Congratulations! Your app is running. 🎉</p>
|
||||
<div>
|
||||
@if (serverInfo$ | async; as serverInfo) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Commit ID</th>
|
||||
<th>Commit Date</th>
|
||||
<th>Environment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ serverInfo.fullVersion }}</td>
|
||||
<td>{{ serverInfo.commitId }}</td>
|
||||
<td>{{ serverInfo.commitDate | date:"dd.MM.yyyy HH:mm:ss" }}</td>
|
||||
<td>{{ serverInfo.environment }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider" role="separator" aria-label="Divider"></div>
|
||||
<div class="right-side">
|
||||
<div class="pill-group">
|
||||
@for (item of [
|
||||
{title: 'Explore the Docs', link: 'https://angular.dev'},
|
||||
{title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials'},
|
||||
{title: 'CLI Docs', link: 'https://angular.dev/tools/cli'},
|
||||
{title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service'},
|
||||
{title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools'},
|
||||
]; track item.title) {
|
||||
<a
|
||||
class="pill"
|
||||
[href]="item.link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="14"
|
||||
viewBox="0 -960 960 960"
|
||||
width="14"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a
|
||||
href="https://github.com/angular/angular"
|
||||
aria-label="Github"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Github"
|
||||
>
|
||||
<path
|
||||
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/angular"
|
||||
aria-label="Twitter"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Twitter"
|
||||
>
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
|
||||
aria-label="Youtube"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="29"
|
||||
height="20"
|
||||
viewBox="0 0 29 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Youtube"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
|
||||
<router-outlet/>
|
||||
3
src/Vegasco-Web/src/app/app.routes.ts
Normal file
3
src/Vegasco-Web/src/app/app.routes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
||||
0
src/Vegasco-Web/src/app/app.scss
Normal file
0
src/Vegasco-Web/src/app/app.scss
Normal file
23
src/Vegasco-Web/src/app/app.spec.ts
Normal file
23
src/Vegasco-Web/src/app/app.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Vegasco-Web');
|
||||
});
|
||||
});
|
||||
33
src/Vegasco-Web/src/app/app.ts
Normal file
33
src/Vegasco-Web/src/app/app.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {AsyncPipe, DatePipe} from '@angular/common';
|
||||
import {tap} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [AsyncPipe, DatePipe, RouterOutlet],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss'
|
||||
})
|
||||
export class App {
|
||||
protected title = 'Vegasco-Web';
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
protected readonly serverInfo$;
|
||||
|
||||
constructor() {
|
||||
this.serverInfo$ = this.http.get<ServerInfo>('/api/v1/info/server')
|
||||
.pipe(takeUntilDestroyed());
|
||||
}
|
||||
}
|
||||
|
||||
interface ServerInfo {
|
||||
fullVersion: string;
|
||||
commitId: string;
|
||||
commitDate: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
13
src/Vegasco-Web/src/index.html
Normal file
13
src/Vegasco-Web/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>VegascoWeb</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
6
src/Vegasco-Web/src/main.ts
Normal file
6
src/Vegasco-Web/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
1
src/Vegasco-Web/src/styles.scss
Normal file
1
src/Vegasco-Web/src/styles.scss
Normal file
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
15
src/Vegasco-Web/tsconfig.app.json
Normal file
15
src/Vegasco-Web/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
34
src/Vegasco-Web/tsconfig.json
Normal file
34
src/Vegasco-Web/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
src/Vegasco-Web/tsconfig.spec.json
Normal file
14
src/Vegasco-Web/tsconfig.spec.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
3
src/Vegasco.Server.Api/Assembly.cs
Normal file
3
src/Vegasco.Server.Api/Assembly.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using StronglyTypedIds;
|
||||
|
||||
[assembly: StronglyTypedIdDefaults(Template.Guid, "guid-efcore")]
|
||||
28
src/Vegasco.Server.Api/Authentication/JwtOptions.cs
Normal file
28
src/Vegasco.Server.Api/Authentication/JwtOptions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace Vegasco.Server.Api.Authentication;
|
||||
|
||||
public class JwtOptions
|
||||
{
|
||||
public const string SectionName = "JWT";
|
||||
|
||||
public string ValidAudience { get; set; } = "";
|
||||
|
||||
public string MetadataUrl { get; set; } = "";
|
||||
|
||||
public string? NameClaimType { get; set; }
|
||||
|
||||
public bool AllowHttpMetadataUrl { get; set; }
|
||||
}
|
||||
|
||||
public class JwtOptionsValidator : AbstractValidator<JwtOptions>
|
||||
{
|
||||
public JwtOptionsValidator()
|
||||
{
|
||||
RuleFor(x => x.ValidAudience)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(x => x.MetadataUrl)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
78
src/Vegasco.Server.Api/Authentication/UserAccessor.cs
Normal file
78
src/Vegasco.Server.Api/Authentication/UserAccessor.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Vegasco.Server.Api.Authentication;
|
||||
|
||||
public sealed class UserAccessor
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IOptions<JwtOptions> _jwtOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the username upon first retrieval
|
||||
/// </summary>
|
||||
private string? _cachedUsername;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the id upon first retrieval
|
||||
/// </summary>
|
||||
private string? _cachedId;
|
||||
|
||||
public UserAccessor(IHttpContextAccessor httpContextAccessor, IOptions<JwtOptions> jwtOptions)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_jwtOptions = jwtOptions;
|
||||
}
|
||||
|
||||
public string GetUsername()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedUsername))
|
||||
{
|
||||
_cachedUsername = GetClaimValue(_jwtOptions.Value.NameClaimType ?? ClaimTypes.Name);
|
||||
}
|
||||
|
||||
return _cachedUsername;
|
||||
}
|
||||
|
||||
public string GetUserId()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedId))
|
||||
{
|
||||
_cachedId = GetClaimValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
|
||||
return _cachedId;
|
||||
}
|
||||
|
||||
private string GetClaimValue(string claimType)
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
|
||||
if (httpContext is null)
|
||||
{
|
||||
ThrowForMissingHttpContext();
|
||||
}
|
||||
|
||||
string? claimValue = httpContext.User.FindFirstValue(claimType);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(claimValue))
|
||||
{
|
||||
ThrowForMissingClaim(claimType);
|
||||
}
|
||||
|
||||
return claimValue;
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForMissingHttpContext()
|
||||
{
|
||||
throw new InvalidOperationException("No HttpContext available.");
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForMissingClaim(string claimType)
|
||||
{
|
||||
throw new InvalidOperationException($"No claim of type '{claimType}' found on the current user.");
|
||||
}
|
||||
}
|
||||
42
src/Vegasco.Server.Api/Cars/Car.cs
Normal file
42
src/Vegasco.Server.Api/Cars/Car.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Vegasco.Server.Api.Consumptions;
|
||||
using Vegasco.Server.Api.Users;
|
||||
|
||||
namespace Vegasco.Server.Api.Cars;
|
||||
|
||||
public class Car
|
||||
{
|
||||
public CarId Id { get; set; } = CarId.New();
|
||||
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
public string UserId { get; set; } = "";
|
||||
|
||||
public virtual User User { get; set; } = null!;
|
||||
|
||||
public virtual ICollection<Consumption> Consumptions { get; set; } = [];
|
||||
}
|
||||
|
||||
public class CarTableConfiguration : IEntityTypeConfiguration<Car>
|
||||
{
|
||||
public const int NameMaxLength = 50;
|
||||
|
||||
public void Configure(EntityTypeBuilder<Car> builder)
|
||||
{
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Id)
|
||||
.HasConversion<CarId.EfCoreValueConverter>();
|
||||
|
||||
builder.Property(x => x.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(NameMaxLength);
|
||||
|
||||
builder.Property(x => x.UserId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(x => x.User)
|
||||
.WithMany(x => x.Cars);
|
||||
}
|
||||
}
|
||||
6
src/Vegasco.Server.Api/Cars/CarId.cs
Normal file
6
src/Vegasco.Server.Api/Cars/CarId.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using StronglyTypedIds;
|
||||
|
||||
namespace Vegasco.Server.Api.Cars;
|
||||
|
||||
[StronglyTypedId]
|
||||
public partial struct CarId;
|
||||
69
src/Vegasco.Server.Api/Cars/CreateCar.cs
Normal file
69
src/Vegasco.Server.Api/Cars/CreateCar.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Vegasco.Server.Api.Authentication;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
using Vegasco.Server.Api.Users;
|
||||
|
||||
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)
|
||||
{
|
||||
return builder
|
||||
.MapPost("cars", Endpoint)
|
||||
.WithTags("Cars");
|
||||
}
|
||||
|
||||
public class Validator : AbstractValidator<Request>
|
||||
{
|
||||
public Validator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.MaximumLength(CarTableConfiguration.NameMaxLength);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IResult> Endpoint(
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
ApplicationDbContext dbContext,
|
||||
UserAccessor userAccessor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
string userId = userAccessor.GetUserId();
|
||||
|
||||
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
|
||||
if (user is null)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Id = userId
|
||||
};
|
||||
await dbContext.Users.AddAsync(user, cancellationToken);
|
||||
}
|
||||
|
||||
Car car = new()
|
||||
{
|
||||
Name = request.Name,
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
await dbContext.Cars.AddAsync(car, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
Response response = new(car.Id.Value, car.Name);
|
||||
return TypedResults.Created($"/v1/cars/{car.Id}", response);
|
||||
}
|
||||
}
|
||||
31
src/Vegasco.Server.Api/Cars/DeleteCar.cs
Normal file
31
src/Vegasco.Server.Api/Cars/DeleteCar.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Cars;
|
||||
|
||||
public static class DeleteCar
|
||||
{
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapDelete("cars/{id:guid}", Endpoint)
|
||||
.WithTags("Cars");
|
||||
}
|
||||
|
||||
public static async Task<IResult> Endpoint(
|
||||
Guid id,
|
||||
ApplicationDbContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Car? car = await dbContext.Cars.FindAsync([new CarId(id)], cancellationToken: cancellationToken);
|
||||
|
||||
if (car is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
dbContext.Cars.Remove(car);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
}
|
||||
31
src/Vegasco.Server.Api/Cars/GetCar.cs
Normal file
31
src/Vegasco.Server.Api/Cars/GetCar.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Cars;
|
||||
|
||||
public static class GetCar
|
||||
{
|
||||
public record Response(Guid Id, string Name);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapGet("cars/{id:guid}", Endpoint)
|
||||
.WithTags("Cars");
|
||||
}
|
||||
|
||||
private static async Task<IResult> Endpoint(
|
||||
Guid id,
|
||||
ApplicationDbContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Car? car = await dbContext.Cars.FindAsync([new CarId(id)], cancellationToken: cancellationToken);
|
||||
|
||||
if (car is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var response = new Response(car.Id.Value, car.Name);
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
}
|
||||
46
src/Vegasco.Server.Api/Cars/GetCars.cs
Normal file
46
src/Vegasco.Server.Api/Cars/GetCars.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Cars;
|
||||
|
||||
public static class GetCars
|
||||
{
|
||||
public class ApiResponse
|
||||
{
|
||||
public IEnumerable<ResponseDto> Cars { get; set; } = [];
|
||||
}
|
||||
|
||||
public record ResponseDto(Guid Id, string Name);
|
||||
|
||||
public class Request
|
||||
{
|
||||
[FromQuery(Name = "page")] public int? Page { get; set; }
|
||||
[FromQuery(Name = "pageSize")] public int? PageSize { get; set; }
|
||||
}
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapGet("cars", Endpoint)
|
||||
.WithDescription("Returns all cars")
|
||||
.WithTags("Cars");
|
||||
}
|
||||
|
||||
private static async Task<Ok<ApiResponse>> Endpoint(
|
||||
[AsParameters] Request request,
|
||||
ApplicationDbContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<ResponseDto> cars = await dbContext.Cars
|
||||
.Select(x => new ResponseDto(x.Id.Value, x.Name))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var response = new ApiResponse
|
||||
{
|
||||
Cars = cars
|
||||
};
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
}
|
||||
58
src/Vegasco.Server.Api/Cars/UpdateCar.cs
Normal file
58
src/Vegasco.Server.Api/Cars/UpdateCar.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Vegasco.Server.Api.Authentication;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
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)
|
||||
{
|
||||
return builder
|
||||
.MapPut("cars/{id:guid}", Endpoint)
|
||||
.WithTags("Cars");
|
||||
}
|
||||
|
||||
public class Validator : AbstractValidator<Request>
|
||||
{
|
||||
public Validator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.MaximumLength(CarTableConfiguration.NameMaxLength);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IResult> Endpoint(
|
||||
Guid id,
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
ApplicationDbContext dbContext,
|
||||
UserAccessor userAccessor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
Car? car = await dbContext.Cars.FindAsync([new CarId(id)], cancellationToken: cancellationToken);
|
||||
|
||||
if (car is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
car.Name = request.Name;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
Response response = new(car.Id.Value, car.Name);
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
}
|
||||
9
src/Vegasco.Server.Api/Common/Constants.cs
Normal file
9
src/Vegasco.Server.Api/Common/Constants.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Vegasco.Server.Api.Common;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public static class Authorization
|
||||
{
|
||||
public const string RequireAuthenticatedUserPolicy = "RequireAuthenticatedUser";
|
||||
}
|
||||
}
|
||||
159
src/Vegasco.Server.Api/Common/DependencyInjectionExtensions.cs
Normal file
159
src/Vegasco.Server.Api/Common/DependencyInjectionExtensions.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Vegasco.Server.Api.Authentication;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Common;
|
||||
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all the Api related services to the Dependency Injection container.
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
public static void AddApiServices(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.Services
|
||||
.AddMiscellaneousServices()
|
||||
.AddCustomOpenApi()
|
||||
.AddApiVersioning()
|
||||
.AddAuthenticationAndAuthorization(builder.Environment);
|
||||
|
||||
builder.AddDbContext();
|
||||
}
|
||||
|
||||
private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton(() =>
|
||||
{
|
||||
string assemblyName = Assembly.GetExecutingAssembly()
|
||||
.GetName()
|
||||
.Name ?? "Vegasco.Server.Api";
|
||||
return new ActivitySource(assemblyName);
|
||||
});
|
||||
|
||||
services.AddResponseCompression();
|
||||
|
||||
services.AddValidatorsFromAssemblies(
|
||||
[
|
||||
typeof(IApiMarker).Assembly
|
||||
], ServiceLifetime.Singleton);
|
||||
|
||||
services.AddHealthChecks();
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddHostedService<ApplyMigrationsService>();
|
||||
|
||||
services.AddRequestLocalization(o =>
|
||||
{
|
||||
string[] cultures =
|
||||
[
|
||||
"en-US",
|
||||
"en",
|
||||
"de-DE",
|
||||
"de"
|
||||
];
|
||||
|
||||
o.SetDefaultCulture(cultures[0])
|
||||
.AddSupportedCultures(cultures)
|
||||
.AddSupportedUICultures(cultures);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddCustomOpenApi(this IServiceCollection services)
|
||||
{
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddOpenApi(o =>
|
||||
{
|
||||
o.CreateSchemaReferenceId = jsonTypeInfo =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(jsonTypeInfo.Type.FullName))
|
||||
{
|
||||
return jsonTypeInfo.Type.Name;
|
||||
}
|
||||
|
||||
string? fullClassName = jsonTypeInfo.Type.FullName;
|
||||
|
||||
if (!string.IsNullOrEmpty(jsonTypeInfo.Type.Namespace))
|
||||
{
|
||||
fullClassName = fullClassName
|
||||
.Replace(jsonTypeInfo.Type.Namespace, "")
|
||||
.TrimStart('.');
|
||||
}
|
||||
|
||||
fullClassName = fullClassName.Replace('+', '_');
|
||||
return fullClassName;
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddApiVersioning(this IServiceCollection services)
|
||||
{
|
||||
services.AddApiVersioning(o =>
|
||||
{
|
||||
o.DefaultApiVersion = new ApiVersion(1);
|
||||
o.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
o.ReportApiVersions = true;
|
||||
})
|
||||
.AddApiExplorer(o =>
|
||||
{
|
||||
o.GroupNameFormat = "'v'V";
|
||||
o.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddAuthenticationAndAuthorization(this IServiceCollection services, IHostEnvironment environment)
|
||||
{
|
||||
services.AddOptions<JwtOptions>()
|
||||
.BindConfiguration(JwtOptions.SectionName)
|
||||
.ValidateFluently()
|
||||
.ValidateOnStart();
|
||||
|
||||
var jwtOptions = services.BuildServiceProvider().GetRequiredService<IOptions<JwtOptions>>();
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
|
||||
{
|
||||
o.MetadataAddress = jwtOptions.Value.MetadataUrl;
|
||||
|
||||
o.TokenValidationParameters.ValidAudience = jwtOptions.Value.ValidAudience;
|
||||
o.TokenValidationParameters.ValidateAudience = true;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(jwtOptions.Value.NameClaimType))
|
||||
{
|
||||
o.TokenValidationParameters.NameClaimType = jwtOptions.Value.NameClaimType;
|
||||
}
|
||||
|
||||
o.RequireHttpsMetadata = !jwtOptions.Value.AllowHttpMetadataUrl && !environment.IsDevelopment();
|
||||
});
|
||||
|
||||
services.AddAuthorizationBuilder()
|
||||
.AddPolicy(Constants.Authorization.RequireAuthenticatedUserPolicy, p => p
|
||||
.RequireAuthenticatedUser()
|
||||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme));
|
||||
|
||||
services.AddScoped<UserAccessor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IHostApplicationBuilder AddDbContext(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.AddNpgsqlDbContext<ApplicationDbContext>(AppHost.Shared.Constants.Database.Name);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
37
src/Vegasco.Server.Api/Common/FluentValidationOptions.cs
Normal file
37
src/Vegasco.Server.Api/Common/FluentValidationOptions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Vegasco.Server.Api.Common;
|
||||
|
||||
public class FluentValidationOptions<TOptions> : IValidateOptions<TOptions>
|
||||
where TOptions : class
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TOptions>> _validators;
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public FluentValidationOptions(string? name, IEnumerable<IValidator<TOptions>> validators)
|
||||
{
|
||||
Name = name;
|
||||
_validators = validators;
|
||||
}
|
||||
|
||||
public ValidateOptionsResult Validate(string? name, TOptions options)
|
||||
{
|
||||
if (name is not null && name != Name)
|
||||
{
|
||||
return ValidateOptionsResult.Skip;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
List<ValidationResult> failedValidations = _validators.ValidateAllAsync(options).Result;
|
||||
if (failedValidations.Count == 0)
|
||||
{
|
||||
return ValidateOptionsResult.Success;
|
||||
}
|
||||
|
||||
return ValidateOptionsResult.Fail(failedValidations.SelectMany(x => x.Errors.Select(x => x.ErrorMessage)));
|
||||
}
|
||||
}
|
||||
3
src/Vegasco.Server.Api/Common/IApiMarker.cs
Normal file
3
src/Vegasco.Server.Api/Common/IApiMarker.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Vegasco.Server.Api.Common;
|
||||
|
||||
public interface IApiMarker;
|
||||
45
src/Vegasco.Server.Api/Common/StartupExtensions.cs
Normal file
45
src/Vegasco.Server.Api/Common/StartupExtensions.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using System.Globalization;
|
||||
using Vegasco.Server.Api.Endpoints;
|
||||
using Vegasco.Server.ServiceDefaults;
|
||||
|
||||
namespace Vegasco.Server.Api.Common;
|
||||
|
||||
internal static class StartupExtensions
|
||||
{
|
||||
internal static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
builder.Configuration.AddEnvironmentVariables("Vegasco_");
|
||||
|
||||
builder.AddApiServices();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
return app;
|
||||
}
|
||||
|
||||
internal static WebApplication ConfigureRequestPipeline(this WebApplication app)
|
||||
{
|
||||
app.UseRequestLocalization(o =>
|
||||
{
|
||||
o.ApplyCurrentCultureToResponseHeaders = true;
|
||||
});
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapEndpoints();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi("/swagger/{documentName}/swagger.json");
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
62
src/Vegasco.Server.Api/Common/ValidatorExtensions.cs
Normal file
62
src/Vegasco.Server.Api/Common/ValidatorExtensions.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Vegasco.Server.Api.Common;
|
||||
|
||||
public static class ValidatorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously validates an instance of <typeparamref name="T"/> against all <see cref="IValidator{T}"/> instances in <paramref name="validators"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="validators"></param>
|
||||
/// <param name="instance"></param>
|
||||
/// <returns>The failed validation results.</returns>
|
||||
public static async Task<List<ValidationResult>> ValidateAllAsync<T>(this IEnumerable<IValidator<T>> validators, T instance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<Task<ValidationResult>> validationTasks = validators
|
||||
.Select(validator => validator.ValidateAsync(instance, cancellationToken))
|
||||
.ToList();
|
||||
|
||||
await Task.WhenAll(validationTasks);
|
||||
|
||||
List<ValidationResult> failedValidations = validationTasks
|
||||
.Select(x => x.Result)
|
||||
.Where(x => !x.IsValid)
|
||||
.ToList();
|
||||
|
||||
return failedValidations;
|
||||
}
|
||||
|
||||
public static Dictionary<string, string[]> ToCombinedDictionary(this IEnumerable<ValidationResult> validationResults)
|
||||
{
|
||||
// Use a hash set to avoid duplicate error messages.
|
||||
Dictionary<string, HashSet<string>> combinedErrors = [];
|
||||
|
||||
foreach (ValidationFailure? error in validationResults.SelectMany(x => x.Errors))
|
||||
{
|
||||
if (!combinedErrors.TryGetValue(error.PropertyName, out HashSet<string>? value))
|
||||
{
|
||||
value = [error.ErrorMessage];
|
||||
combinedErrors[error.PropertyName] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
value.Add(error.ErrorMessage);
|
||||
}
|
||||
|
||||
return combinedErrors.ToDictionary(x => x.Key, x => x.Value.ToArray());
|
||||
}
|
||||
|
||||
public static OptionsBuilder<T> ValidateFluently<T>(this OptionsBuilder<T> builder)
|
||||
where T : class
|
||||
{
|
||||
builder.Services.AddTransient<IValidateOptions<T>>(serviceProvider =>
|
||||
{
|
||||
IEnumerable<IValidator<T>> validators = serviceProvider.GetServices<IValidator<T>>() ?? [];
|
||||
return new FluentValidationOptions<T>(builder.Name, validators);
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
52
src/Vegasco.Server.Api/Consumptions/Consumption.cs
Normal file
52
src/Vegasco.Server.Api/Consumptions/Consumption.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Vegasco.Server.Api.Cars;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
|
||||
public class Consumption
|
||||
{
|
||||
public ConsumptionId Id { get; set; } = ConsumptionId.New();
|
||||
|
||||
public DateTimeOffset DateTime { get; set; }
|
||||
|
||||
public double Distance { get; set; }
|
||||
|
||||
public double Amount { get; set; }
|
||||
|
||||
public bool IgnoreInCalculation { get; set; }
|
||||
|
||||
public CarId CarId { get; set; }
|
||||
|
||||
public virtual Car Car { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class ConsumptionTableConfiguration : IEntityTypeConfiguration<Consumption>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Consumption> builder)
|
||||
{
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Id)
|
||||
.HasConversion<ConsumptionId.EfCoreValueConverter>();
|
||||
|
||||
builder.Property(x => x.DateTime)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.Distance)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.Amount)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.IgnoreInCalculation)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.CarId)
|
||||
.IsRequired()
|
||||
.HasConversion<CarId.EfCoreValueConverter>();
|
||||
|
||||
builder.HasOne(x => x.Car)
|
||||
.WithMany(x => x.Consumptions);
|
||||
}
|
||||
}
|
||||
7
src/Vegasco.Server.Api/Consumptions/ConsumptionId.cs
Normal file
7
src/Vegasco.Server.Api/Consumptions/ConsumptionId.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using StronglyTypedIds;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
|
||||
|
||||
[StronglyTypedId]
|
||||
public partial struct ConsumptionId;
|
||||
74
src/Vegasco.Server.Api/Consumptions/CreateConsumption.cs
Normal file
74
src/Vegasco.Server.Api/Consumptions/CreateConsumption.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Vegasco.Server.Api.Cars;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
|
||||
public static class CreateConsumption
|
||||
{
|
||||
public record Request(DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
||||
|
||||
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapPost("consumptions", Endpoint)
|
||||
.WithTags("Consumptions");
|
||||
}
|
||||
|
||||
public class Validator : AbstractValidator<Request>
|
||||
{
|
||||
public Validator(TimeProvider timeProvider)
|
||||
{
|
||||
RuleFor(x => x.DateTime.ToUniversalTime())
|
||||
.LessThanOrEqualTo(timeProvider.GetUtcNow())
|
||||
.WithName(nameof(Request.DateTime));
|
||||
|
||||
RuleFor(x => x.Distance)
|
||||
.GreaterThan(0);
|
||||
|
||||
RuleFor(x => x.Amount)
|
||||
.GreaterThan(0);
|
||||
|
||||
RuleFor(x => x.CarId)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> Endpoint(
|
||||
ApplicationDbContext dbContext,
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
Car? car = await dbContext.Cars.FindAsync([new CarId(request.CarId)], cancellationToken);
|
||||
if (car is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var consumption = new Consumption
|
||||
{
|
||||
DateTime = request.DateTime.ToUniversalTime(),
|
||||
Distance = request.Distance,
|
||||
Amount = request.Amount,
|
||||
IgnoreInCalculation = request.IgnoreInCalculation,
|
||||
CarId = new CarId(request.CarId)
|
||||
};
|
||||
|
||||
dbContext.Consumptions.Add(consumption);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TypedResults.Created($"consumptions/{consumption.Id.Value}",
|
||||
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.IgnoreInCalculation, consumption.CarId.Value));
|
||||
}
|
||||
}
|
||||
30
src/Vegasco.Server.Api/Consumptions/DeleteConsumptions.cs
Normal file
30
src/Vegasco.Server.Api/Consumptions/DeleteConsumptions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
|
||||
public static class DeleteConsumption
|
||||
{
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapDelete("consumptions/{id:guid}", Endpoint)
|
||||
.WithTags("Consumptions");
|
||||
}
|
||||
|
||||
private static async Task<IResult> Endpoint(
|
||||
ApplicationDbContext dbContext,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Consumption? consumption = await dbContext.Consumptions.FindAsync([new ConsumptionId(id)], cancellationToken);
|
||||
if (consumption is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
dbContext.Consumptions.Remove(consumption);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
}
|
||||
32
src/Vegasco.Server.Api/Consumptions/GetConsumption.cs
Normal file
32
src/Vegasco.Server.Api/Consumptions/GetConsumption.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
|
||||
public static class GetConsumption
|
||||
{
|
||||
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapGet("consumptions/{id:guid}", Endpoint)
|
||||
.WithTags("Consumptions");
|
||||
}
|
||||
|
||||
private static async Task<IResult> Endpoint(
|
||||
ApplicationDbContext dbContext,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Consumption? consumption = await dbContext.Consumptions.FindAsync([new ConsumptionId(id)], cancellationToken);
|
||||
|
||||
if (consumption is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var response = new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance,
|
||||
consumption.Amount, consumption.IgnoreInCalculation, consumption.CarId.Value);
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
}
|
||||
53
src/Vegasco.Server.Api/Consumptions/GetConsumptions.cs
Normal file
53
src/Vegasco.Server.Api/Consumptions/GetConsumptions.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
|
||||
public static class GetConsumptions
|
||||
{
|
||||
public class ApiResponse
|
||||
{
|
||||
public IEnumerable<ResponseDto> Consumptions { get; set; } = [];
|
||||
}
|
||||
|
||||
public record ResponseDto(
|
||||
Guid Id,
|
||||
DateTimeOffset DateTime,
|
||||
double Distance,
|
||||
double Amount,
|
||||
bool IgnoreInCalculation,
|
||||
Guid CarId);
|
||||
|
||||
public class Request
|
||||
{
|
||||
[FromQuery(Name = "page")] public int? Page { get; set; }
|
||||
[FromQuery(Name = "pageSize")] public int? PageSize { get; set; }
|
||||
}
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapGet("consumptions", Endpoint)
|
||||
.WithDescription("Returns all consumption entries")
|
||||
.WithTags("Consumptions");
|
||||
}
|
||||
|
||||
private static async Task<Ok<ApiResponse>> Endpoint(
|
||||
[AsParameters] Request request,
|
||||
ApplicationDbContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<ResponseDto> consumptions = await dbContext.Consumptions
|
||||
.Select(x =>
|
||||
new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var apiResponse = new ApiResponse
|
||||
{
|
||||
Consumptions = consumptions
|
||||
};
|
||||
return TypedResults.Ok(apiResponse);
|
||||
}
|
||||
}
|
||||
65
src/Vegasco.Server.Api/Consumptions/UpdateConsumption.cs
Normal file
65
src/Vegasco.Server.Api/Consumptions/UpdateConsumption.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
namespace Vegasco.Server.Api.Consumptions;
|
||||
|
||||
public static class UpdateConsumption
|
||||
{
|
||||
public record Request(DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation);
|
||||
|
||||
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapPut("consumptions/{id:guid}", Endpoint)
|
||||
.WithTags("Consumptions");
|
||||
}
|
||||
|
||||
public class Validator : AbstractValidator<Request>
|
||||
{
|
||||
public Validator(TimeProvider timeProvider)
|
||||
{
|
||||
RuleFor(x => x.DateTime.ToUniversalTime())
|
||||
.LessThanOrEqualTo(timeProvider.GetUtcNow())
|
||||
.WithName(nameof(Request.DateTime));
|
||||
|
||||
RuleFor(x => x.Distance)
|
||||
.GreaterThan(0);
|
||||
|
||||
RuleFor(x => x.Amount)
|
||||
.GreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> Endpoint(
|
||||
ApplicationDbContext dbContext,
|
||||
Guid id,
|
||||
Request request,
|
||||
IEnumerable<IValidator<Request>> validators,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||
if (failedValidations.Count > 0)
|
||||
{
|
||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||
}
|
||||
|
||||
Consumption? consumption = await dbContext.Consumptions.FindAsync([new ConsumptionId(id)], cancellationToken);
|
||||
if (consumption is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
consumption.DateTime = request.DateTime.ToUniversalTime();
|
||||
consumption.Distance = request.Distance;
|
||||
consumption.Amount = request.Amount;
|
||||
consumption.IgnoreInCalculation = request.IgnoreInCalculation;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TypedResults.Ok(new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.IgnoreInCalculation, consumption.CarId.Value));
|
||||
}
|
||||
}
|
||||
45
src/Vegasco.Server.Api/Endpoints/EndpointExtensions.cs
Normal file
45
src/Vegasco.Server.Api/Endpoints/EndpointExtensions.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Asp.Versioning.Builder;
|
||||
using Asp.Versioning.Conventions;
|
||||
using Vegasco.Server.Api.Cars;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Consumptions;
|
||||
using Vegasco.Server.Api.Info;
|
||||
|
||||
namespace Vegasco.Server.Api.Endpoints;
|
||||
|
||||
public static class EndpointExtensions
|
||||
{
|
||||
public static void MapEndpoints(this IEndpointRouteBuilder builder)
|
||||
{
|
||||
ApiVersionSet apiVersionSet = builder.NewApiVersionSet()
|
||||
.HasApiVersion(1.0)
|
||||
.Build();
|
||||
|
||||
RouteGroupBuilder versionedApis = builder.MapGroup("/v{apiVersion:apiVersion}")
|
||||
.WithApiVersionSet(apiVersionSet);
|
||||
|
||||
GetCar.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
GetCars.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
CreateCar.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
UpdateCar.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
DeleteCar.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
|
||||
GetConsumptions.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
GetConsumption.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
CreateConsumption.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
UpdateConsumption.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
DeleteConsumption.MapEndpoint(versionedApis)
|
||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||
|
||||
GetServerInfo.MapEndpoint(versionedApis);
|
||||
}
|
||||
}
|
||||
29
src/Vegasco.Server.Api/Info/GetServerInfo.cs
Normal file
29
src/Vegasco.Server.Api/Info/GetServerInfo.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
||||
namespace Vegasco.Server.Api.Info;
|
||||
|
||||
public class GetServerInfo
|
||||
{
|
||||
public record Response(
|
||||
string FullVersion,
|
||||
string CommitId,
|
||||
DateTimeOffset CommitDate,
|
||||
string Environment);
|
||||
|
||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.MapGet("info/server", Endpoint)
|
||||
.WithTags("Info");
|
||||
}
|
||||
|
||||
private static Ok<Response> Endpoint(
|
||||
IHostEnvironment environment)
|
||||
{
|
||||
return TypedResults.Ok(new Response(
|
||||
ThisAssembly.AssemblyInformationalVersion,
|
||||
ThisAssembly.GitCommitId,
|
||||
new DateTimeOffset(ThisAssembly.GitCommitDate, TimeSpan.Zero),
|
||||
environment.EnvironmentName));
|
||||
}
|
||||
}
|
||||
22
src/Vegasco.Server.Api/Persistence/ApplicationDbContext.cs
Normal file
22
src/Vegasco.Server.Api/Persistence/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Vegasco.Server.Api.Cars;
|
||||
using Vegasco.Server.Api.Common;
|
||||
using Vegasco.Server.Api.Consumptions;
|
||||
using Vegasco.Server.Api.Users;
|
||||
|
||||
namespace Vegasco.Server.Api.Persistence;
|
||||
|
||||
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Car> Cars { get; set; }
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
|
||||
public DbSet<Consumption> Consumptions { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(IApiMarker).Assembly);
|
||||
}
|
||||
}
|
||||
24
src/Vegasco.Server.Api/Persistence/ApplyMigrationsService.cs
Normal file
24
src/Vegasco.Server.Api/Persistence/ApplyMigrationsService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Vegasco.Server.Api.Persistence;
|
||||
|
||||
public class ApplyMigrationsService(
|
||||
ILogger<ApplyMigrationsService> logger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ActivitySource activitySource)
|
||||
: IHostedService
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = activitySource.StartActivity("ApplyMigrations");
|
||||
|
||||
logger.LogInformation("Starting migrations");
|
||||
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
121
src/Vegasco.Server.Api/Persistence/Migrations/20240818105918_Initial.Designer.cs
generated
Normal file
121
src/Vegasco.Server.Api/Persistence/Migrations/20240818105918_Initial.Designer.cs
generated
Normal file
@@ -0,0 +1,121 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Vegasco.Server.Api.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240818105918_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Cars");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Consumptions.Consumption", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<double>("Amount")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<Guid>("CarId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("DateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<double>("Distance")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IgnoreInCalculation")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CarId");
|
||||
|
||||
b.ToTable("Consumptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Users.User", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||
{
|
||||
b.HasOne("Vegasco.Server.Api.Users.User", "User")
|
||||
.WithMany("Cars")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Consumptions.Consumption", b =>
|
||||
{
|
||||
b.HasOne("Vegasco.Server.Api.Cars.Car", "Car")
|
||||
.WithMany("Consumptions")
|
||||
.HasForeignKey("CarId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Car");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||
{
|
||||
b.Navigation("Consumptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Users.User", b =>
|
||||
{
|
||||
b.Navigation("Cars");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Vegasco.Server.Api.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Cars",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
UserId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Cars", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Cars_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Consumptions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
Distance = table.Column<double>(type: "double precision", nullable: false),
|
||||
Amount = table.Column<double>(type: "double precision", nullable: false),
|
||||
IgnoreInCalculation = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CarId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Consumptions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Consumptions_Cars_CarId",
|
||||
column: x => x.CarId,
|
||||
principalTable: "Cars",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Cars_UserId",
|
||||
table: "Cars",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Consumptions_CarId",
|
||||
table: "Consumptions",
|
||||
column: "CarId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Consumptions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Cars");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Vegasco.Server.Api.Persistence;
|
||||
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Vegasco.Server.Api.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Cars");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Consumptions.Consumption", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<double>("Amount")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<Guid>("CarId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("DateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<double>("Distance")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IgnoreInCalculation")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CarId");
|
||||
|
||||
b.ToTable("Consumptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Users.User", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||
{
|
||||
b.HasOne("Vegasco.Server.Api.Users.User", "User")
|
||||
.WithMany("Cars")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Consumptions.Consumption", b =>
|
||||
{
|
||||
b.HasOne("Vegasco.Server.Api.Cars.Car", "Car")
|
||||
.WithMany("Consumptions")
|
||||
.HasForeignKey("CarId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Car");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||
{
|
||||
b.Navigation("Consumptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Vegasco.Server.Api.Users.User", b =>
|
||||
{
|
||||
b.Navigation("Cars");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/Vegasco.Server.Api/Program.cs
Normal file
6
src/Vegasco.Server.Api/Program.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Vegasco.Server.Api.Common;
|
||||
|
||||
WebApplication.CreateBuilder(args)
|
||||
.ConfigureServices()
|
||||
.ConfigureRequestPipeline()
|
||||
.Run();
|
||||
15
src/Vegasco.Server.Api/Properties/launchSettings.json
Normal file
15
src/Vegasco.Server.Api/Properties/launchSettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7098;http://localhost:5076"
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json"
|
||||
}
|
||||
10
src/Vegasco.Server.Api/Users/User.cs
Normal file
10
src/Vegasco.Server.Api/Users/User.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Vegasco.Server.Api.Cars;
|
||||
|
||||
namespace Vegasco.Server.Api.Users;
|
||||
|
||||
public class User
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
public virtual IList<Car> Cars { get; set; } = [];
|
||||
}
|
||||
12
src/Vegasco.Server.Api/Users/UserTableConfiguration.cs
Normal file
12
src/Vegasco.Server.Api/Users/UserTableConfiguration.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Vegasco.Server.Api.Users;
|
||||
|
||||
public class UserTableConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.HasKey(user => user.Id);
|
||||
}
|
||||
}
|
||||
45
src/Vegasco.Server.Api/Vegasco.Server.Api.csproj
Normal file
45
src/Vegasco.Server.Api/Vegasco.Server.Api.csproj
Normal file
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>4bf893d3-0c16-41ec-8b46-2768d841215d</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<RootNamespace>Vegasco.Server.Api</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<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="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">
|
||||
<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="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.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" PrivateAssets="all" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="StronglyTypedId.Templates" Version="1.0.0-beta08" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Vegasco.Server.AppHost.Shared\Vegasco.Server.AppHost.Shared.csproj" />
|
||||
<ProjectReference Include="..\Vegasco.Server.ServiceDefaults\Vegasco.Server.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.7.115" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
10
src/Vegasco.Server.Api/appsettings.json
Normal file
10
src/Vegasco.Server.Api/appsettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Vegasco": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
16
src/Vegasco.Server.AppHost.Shared/Constants.cs
Normal file
16
src/Vegasco.Server.AppHost.Shared/Constants.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Vegasco.Server.AppHost.Shared;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public static class Projects
|
||||
{
|
||||
public const string Api = "Vegasco-Server-Api";
|
||||
}
|
||||
|
||||
public static class Database
|
||||
{
|
||||
public const string ServiceName = "postgres";
|
||||
|
||||
public const string Name = "vegasco-database";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Nerdbank.GitVersioning">
|
||||
<Version>3.7.115</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
21
src/Vegasco.Server.AppHost/Program.cs
Normal file
21
src/Vegasco.Server.AppHost/Program.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Vegasco.Server.AppHost.Shared;
|
||||
|
||||
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
IResourceBuilder<PostgresDatabaseResource> postgres = builder.AddPostgres(Constants.Database.ServiceName)
|
||||
.WithLifetime(ContainerLifetime.Persistent)
|
||||
.WithDataVolume()
|
||||
.AddDatabase(Constants.Database.Name);
|
||||
|
||||
IResourceBuilder<ProjectResource> api = builder
|
||||
.AddProject<Projects.Vegasco_Server_Api>(Constants.Projects.Api)
|
||||
.WithReference(postgres)
|
||||
.WaitFor(postgres);
|
||||
|
||||
builder
|
||||
.AddNpmApp("Vegasco-Web", "../Vegasco-Web")
|
||||
.WithReference(api)
|
||||
.WaitFor(api)
|
||||
.WithHttpEndpoint(targetPort: 4200);
|
||||
|
||||
builder.Build().Run();
|
||||
29
src/Vegasco.Server.AppHost/Properties/launchSettings.json
Normal file
29
src/Vegasco.Server.AppHost/Properties/launchSettings.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:17055;http://localhost:15102",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21122",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22235"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:15102",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19222",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20257"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Vegasco.Server.AppHost/Vegasco.Server.AppHost.csproj
Normal file
28
src/Vegasco.Server.AppHost/Vegasco.Server.AppHost.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireHost>true</IsAspireHost>
|
||||
<UserSecretsId>bb714834-9872-4af6-b154-0b98b14fcca2</UserSecretsId>
|
||||
</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 Update="Nerdbank.GitVersioning">
|
||||
<Version>3.7.115</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Vegasco.Server.AppHost.Shared\Vegasco.Server.AppHost.Shared.csproj" IsAspireProjectResource="false" />
|
||||
<ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -4,6 +4,5 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
}
|
||||
9
src/Vegasco.Server.AppHost/appsettings.json
Normal file
9
src/Vegasco.Server.AppHost/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Aspire.Hosting.Dcp": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/Vegasco.Server.ServiceDefaults/Extensions.cs
Normal file
119
src/Vegasco.Server.ServiceDefaults/Extensions.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace Vegasco.Server.ServiceDefaults;
|
||||
|
||||
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
|
||||
// This project should be referenced by each service project in your solution.
|
||||
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
|
||||
public static class Extensions
|
||||
{
|
||||
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.ConfigureOpenTelemetry();
|
||||
|
||||
builder.AddDefaultHealthChecks();
|
||||
|
||||
builder.Services.AddServiceDiscovery();
|
||||
|
||||
builder.Services.ConfigureHttpClientDefaults(http =>
|
||||
{
|
||||
// Turn on resilience by default
|
||||
http.AddStandardResilienceHandler();
|
||||
|
||||
// Turn on service discovery by default
|
||||
http.AddServiceDiscovery();
|
||||
});
|
||||
|
||||
// Uncomment the following to restrict the allowed schemes for service discovery.
|
||||
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
|
||||
// {
|
||||
// options.AllowedSchemes = ["https"];
|
||||
// });
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation();
|
||||
})
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing.AddSource(builder.Environment.ApplicationName)
|
||||
.AddAspNetCoreInstrumentation()
|
||||
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
|
||||
//.AddGrpcClientInstrumentation()
|
||||
.AddHttpClientInstrumentation();
|
||||
});
|
||||
|
||||
builder.AddOpenTelemetryExporters();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
bool useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
|
||||
|
||||
if (useOtlpExporter)
|
||||
{
|
||||
builder.Services.AddOpenTelemetry().UseOtlpExporter();
|
||||
}
|
||||
|
||||
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
|
||||
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
|
||||
//{
|
||||
// builder.Services.AddOpenTelemetry()
|
||||
// .UseAzureMonitor();
|
||||
//}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Services.AddHealthChecks()
|
||||
// Add a default liveness check to ensure app is responsive
|
||||
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
||||
{
|
||||
// Adding health checks endpoints to applications in non-development environments has security implications.
|
||||
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
// All health checks must pass for app to be considered ready to accept traffic after starting
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
// Only health checks tagged with the "live" tag must pass for app to be considered alive
|
||||
app.MapHealthChecks("/alive", new HealthCheckOptions
|
||||
{
|
||||
Predicate = r => r.Tags.Contains("live")
|
||||
});
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireSharedProject>true</IsAspireSharedProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<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="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>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,30 +0,0 @@
|
||||
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
# This stage is used when running from VS in fast mode (Default for Debug configuration)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
USER app
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
|
||||
# This stage is used to build the service project
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Vegasco.Api/Vegasco.Api.csproj", "Vegasco.Api/"]
|
||||
RUN dotnet restore "./Vegasco.Api/Vegasco.Api.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Vegasco.Api"
|
||||
RUN dotnet build "./Vegasco.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# This stage is used to publish the service project to be copied to the final stage
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./Vegasco.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Vegasco.Api.dll"]
|
||||
@@ -1,44 +0,0 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
var summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
{
|
||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||
new WeatherForecast
|
||||
(
|
||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
Random.Shared.Next(-20, 55),
|
||||
summaries[Random.Shared.Next(summaries.Length)]
|
||||
))
|
||||
.ToArray();
|
||||
return forecast;
|
||||
})
|
||||
.WithName("GetWeatherForecast")
|
||||
.WithOpenApi();
|
||||
|
||||
app.Run();
|
||||
|
||||
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
{
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5236"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7226;http://localhost:5236"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:40988",
|
||||
"sslPort": 44347
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>2855ad97-67f4-455a-81af-69c212566ff2</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
@Vegasco.Api_HostAddress = http://localhost:5236
|
||||
|
||||
GET {{Vegasco.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
19
tests/Vegasco.Server.Api.Tests.Integration/CarFaker.cs
Normal file
19
tests/Vegasco.Server.Api.Tests.Integration/CarFaker.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
@@ -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})?");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Vegasco.Server.Api.Tests.Integration;
|
||||
|
||||
[CollectionDefinition(Name)]
|
||||
public class SharedTestCollection : ICollectionFixture<WebAppFactory>
|
||||
{
|
||||
public const string Name = nameof(SharedTestCollection);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user