Compare commits

...

9 Commits

Author SHA1 Message Date
06d88a02ae
change geographical names in seeding data
All checks were successful
/ build (push) Successful in 7m12s
/ tests (push) Successful in 2m55s
/ build-docker (push) Successful in 5m24s
2025-06-15 13:56:48 +03:00
8348dd5e84
allow unauthorized access to GetAddressesPageQuery
All checks were successful
/ build (push) Successful in 6m15s
/ tests (push) Successful in 1m27s
/ build-docker (push) Successful in 6m0s
2025-06-11 18:47:14 +03:00
feeaf06330
add infrastructure configuration to tests
All checks were successful
/ build-docker (push) Successful in 6m19s
/ tests (push) Successful in 56s
/ build (push) Successful in 5m10s
2025-06-09 18:43:16 +03:00
fbe26898fa
add data seeding
Some checks failed
/ build-docker (push) Has been skipped
/ tests (push) Failing after 33s
/ build (push) Successful in 5m24s
2025-06-09 18:36:51 +03:00
44dceee7b8
fix null reference exception when no departure or arrival addresses are found 2025-06-09 16:22:03 +03:00
8bed1d39f3
improve user claim retrieval 2025-06-09 14:17:10 +03:00
29f0614e56
add example configuraion file 2025-06-09 13:40:35 +03:00
4f28a73af6
add configuration files to .gitignore 2025-06-09 13:05:51 +03:00
457e72bc95
fix: file configuration for different environments
now configuration from file appsettings.${env}.json overwrites default configuration file

${env} is retrieved from TravelGuide_Environment environment variable
2025-06-09 12:44:52 +03:00
11 changed files with 896 additions and 156 deletions

3
.gitignore vendored
View File

@ -3,6 +3,9 @@
##
## Get latest from `dotnet new gitignore`
appsettings*.json
!appsettings.Example.json
# dotenv files
.env

View File

@ -17,16 +17,6 @@ public class GetAddressesPageQueryAuthorizer :
public override void BuildPolicy(GetAddressesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInAnyOfRolesRequirement
{
RequiredRoles =
[IdentityRole.Administrator, IdentityRole.CompanyOwner],
UserRoles = _sessionUserService.Roles
});
UseRequirement(new AllowAllRequirement());
}
}

View File

@ -141,11 +141,18 @@ public class SearchAllQueryHandler :
// Find paths
var departureAddress = routeAddressDetails
.First(e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid)
.RouteAddress.Address;
.FirstOrDefault(
e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid)
?.RouteAddress.Address;
var arrivalAddress = routeAddressDetails
.First(e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid)
.RouteAddress.Address;
.FirstOrDefault(
e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid)
?.RouteAddress.Address;
if (departureAddress == null || arrivalAddress == null)
{
throw new NotFoundException();
}
var paths = new List<List<TaggedEdge<Address, RouteAddressDetail>>>();

View File

@ -19,8 +19,8 @@ public static class Configuration
Environment.GetEnvironmentVariable("TravelGuide_Environment");
var configuration = new ConfigurationBuilder()
.AddJsonFile($"./appsettings.{environment}.json", optional: true)
.AddJsonFile($"./appsettings.json", optional: true)
.AddJsonFile($"./appsettings.{environment}.json", optional: true)
.AddEnvironmentVariables(prefix: "TravelGuide_")
.AddCommandLine(args)
.Build();

View File

@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore;
using cuqmbr.TravelGuide.Persistence.PostgreSql;
using cuqmbr.TravelGuide.Persistence.InMemory;
using Microsoft.EntityFrameworkCore.Diagnostics;
using cuqmbr.TravelGuide.Application.Common.Services;
namespace cuqmbr.TravelGuide.Configuration.Persistence;
@ -76,10 +77,33 @@ public static class Configuration
$"{configuration.Type} datastore is not supported.");
}
if (configuration.Seed)
if (configuration.SeedIdentity || configuration.SeedData)
{
using var serviceProvider = services.BuildServiceProvider();
DbSeeder.Seed(serviceProvider);
var unitOfWork =
serviceProvider.GetRequiredService<UnitOfWork>();
var passwordHasher =
serviceProvider.GetRequiredService<PasswordHasherService>();
using var dbSeeder = new DbSeeder(unitOfWork, passwordHasher);
if (configuration.SeedIdentity)
{
dbSeeder.SeedIdentity();
}
// Data can not be seeded without seeding identity
if (configuration.SeedData && !configuration.SeedIdentity)
{
throw new InvalidOperationException(
"Can not seed data without seeding identity.");
}
if (configuration.SeedData)
{
dbSeeder.SeedData();
}
}
return services;

View File

@ -18,8 +18,10 @@ public sealed class AspNetSessionUserService : SessionUserService
get
{
var claimValue = _httpContext.User.Claims
.FirstOrDefault(c => c.Properties
.Any(p => p.Value == JwtRegisteredClaimNames.Sub))
.FirstOrDefault(c =>
c.Type == JwtRegisteredClaimNames.Sub ||
c.Properties
.Any(p => p.Value == JwtRegisteredClaimNames.Sub))
?.Value;
var parsed = System.Guid.TryParse(claimValue, out var guid);
@ -29,18 +31,24 @@ public sealed class AspNetSessionUserService : SessionUserService
}
public string? Username => _httpContext.User.Claims
.FirstOrDefault(c => c.Properties
.Any(p => p.Value == JwtRegisteredClaimNames.Nickname))
.FirstOrDefault(c =>
c.Type == JwtRegisteredClaimNames.Nickname ||
c.Properties
.Any(p => p.Value == JwtRegisteredClaimNames.Nickname))
?.Value;
public string? Email => _httpContext.User.Claims
.FirstOrDefault(c => c.Properties
.Any(p => p.Value == JwtRegisteredClaimNames.Email))
.FirstOrDefault(c =>
c.Type == JwtRegisteredClaimNames.Email ||
c.Properties
.Any(p => p.Value == JwtRegisteredClaimNames.Email))
?.Value;
public ICollection<IdentityRole> Roles => _httpContext.User.Claims
.Where(c => c.Properties
.Any(p => p.Value == "roles"))
.Where(c =>
c.Type == "roles" ||
c.Properties
.Any(p => p.Value == "roles"))
.Select(c => IdentityRole.FromName(c.Value))
.ToArray() ?? default!;

View File

@ -7,18 +7,22 @@
},
"Localization": {
"DefaultCultureName": "en-US",
"CacheDuration": "00:30:00"
"CacheDuration": "00:00:30"
},
"JsonWebToken": {
"Issuer": "https://api.travel-guide.cuqmbr.xyz",
"Audience": "https://travel-guide.cuqmbr.xyz",
"IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc",
"AccessTokenValidity": "24:00:00",
"RefreshTokenValidity": "72:00:00"
"IssuerSigningKey": "change-this-sufficiently-logn-string",
"AccessTokenValidity": "00:03:00",
"RefreshTokenValidity": "14.00:00:00"
},
"Datastore": {
"Type": "postgresql",
"ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true"
"ConnectionString": "Host=127.0.0.1;Port=5432;Username=postgres;Password=0000;Database=travel_guide;",
"PartitionName": "application",
"Migrate": true,
"SeedIdentity": true,
"SeedData": false
},
"PaymentProcessing": {
"CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz",

View File

@ -1,42 +0,0 @@
{
"Logging": {
"Type": "SimpleConsole",
"LogLevel": "Information",
"TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK",
"UseUtcTimestamp": true
},
"Localization": {
"DefaultCultureName": "en-US",
"CacheDuration": "00:30:00"
},
"JsonWebToken": {
"Issuer": "https://api.travel-guide.cuqmbr.xyz",
"Audience": "https://travel-guide.cuqmbr.xyz",
"IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc",
"AccessTokenValidity": "24:00:00",
"RefreshTokenValidity": "72:00:00"
},
"Datastore": {
"Type": "postgresql",
"ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true"
},
"PaymentProcessing": {
"CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz",
"ResultAddressBase": "https://travel-guide.cuqmbr.xyz",
"LiqPay": {
"PublicKey": "sandbox_xxxxxxxxxxxx",
"PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
},
"Email": {
"Smtp": {
"Host": "mail.travel-guide.cuqmbr.xyz",
"Port": "465",
"UseTls": true,
"Username": "no-reply",
"Password": "super-secret-password",
"SenderAddress": "no-reply@travel-guide.cuqmbr.xyz",
"SenderName": "Travel Guide"
}
}
}

View File

@ -12,5 +12,7 @@ public sealed class ConfigurationOptions
public bool Migrate { get; set; } = true;
public bool Seed { get; set; } = false;
public bool SeedIdentity { get; set; } = true;
public bool SeedData { get; set; } = false;
}

View File

@ -4,103 +4,847 @@ using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Entities;
using cuqmbr.TravelGuide.Domain.Enums;
using Microsoft.Extensions.DependencyInjection;
namespace cuqmbr.TravelGuide.Persistence;
public static class DbSeeder
public sealed class DbSeeder : IDisposable
{
public static void Seed(IServiceProvider serviceProvider)
private readonly UnitOfWork _unitOfWork;
private readonly PasswordHasherService _passwordHasher;
public DbSeeder(UnitOfWork unitOfWork, PasswordHasherService passwordHasher)
{
var unitOfWork =
serviceProvider.GetRequiredService<UnitOfWork>();
var passwordHasher =
serviceProvider.GetRequiredService<PasswordHasherService>();
_unitOfWork = unitOfWork;
_passwordHasher = passwordHasher;
}
public void SeedIdentity()
{
// Seed Roles
var datastoreRoles = _unitOfWork.RoleRepository
.GetPageAsync(1, IdentityRole.Enumerations.Count,
CancellationToken.None)
.Result.Items.Select(r => r.Value);
var identityRoles = IdentityRole.Enumerations.Select(r => r.Value);
foreach (var role in identityRoles)
{
var datastoreRoles = unitOfWork.RoleRepository
.GetPageAsync(1, IdentityRole.Enumerations.Count,
CancellationToken.None)
.Result.Items.Select(r => r.Value);
var roles = IdentityRole.Enumerations.Select(r => r.Value);
foreach (var role in roles)
if (datastoreRoles.Contains(role))
{
if (datastoreRoles.Contains(role))
{
continue;
}
unitOfWork.RoleRepository.AddOneAsync(
new Role() { Value = role },
CancellationToken.None).Wait();
continue;
}
unitOfWork.SaveAsync(CancellationToken.None).Wait();
_unitOfWork.RoleRepository.AddOneAsync(
new Role() { Value = role },
CancellationToken.None).Wait();
}
_unitOfWork.SaveAsync(CancellationToken.None).Wait();
// Seed Accounts
var isAccountsPresent =
_unitOfWork.AccountRepository.GetPageAsync(
1, 1, CancellationToken.None)
.Result.Items.Any();
if (isAccountsPresent)
{
var accounts =
new (string Username, string Email,
string Password, IdentityRole[] Roles)[]
{
("admin", "admin", "admin",
new [] { IdentityRole.Administrator }),
};
var roles = unitOfWork.RoleRepository
.GetPageAsync(1, IdentityRole.Enumerations.Count,
CancellationToken.None)
.Result.Items;
foreach (var account in accounts)
{
var datastoreAccount =
unitOfWork.AccountRepository.GetOneAsync(
e => e.Email == account.Email, CancellationToken.None)
.Result;
if (datastoreAccount != null)
{
continue;
}
var password = Encoding.UTF8.GetBytes(account.Password);
var salt = RandomNumberGenerator.GetBytes(128 / 8);
var hash = passwordHasher
.HashAsync(password, salt, CancellationToken.None)
.Result;
var saltBase64 = Convert.ToBase64String(salt);
var hashBase64 = Convert.ToBase64String(hash);
unitOfWork.AccountRepository.AddOneAsync(
new Account()
{
Username = account.Username,
Email = account.Email,
PasswordHash = hashBase64,
PasswordSalt = saltBase64,
AccountRoles = account.Roles.Select(ar =>
new AccountRole()
{
RoleId = roles.Single(dr => dr.Value.Equals(ar)).Id
})
.ToArray()
},
CancellationToken.None).Wait();
}
unitOfWork.SaveAsync(CancellationToken.None).Wait();
return;
}
unitOfWork.Dispose();
var accounts =
new (string Username, string Email, IdentityRole[] Roles)[]
{
("admin", "admin@email.com",
new [] { IdentityRole.Administrator }),
};
var roles = _unitOfWork.RoleRepository
.GetPageAsync(1, IdentityRole.Enumerations.Count,
CancellationToken.None)
.Result.Items;
var datastoreAccounts =
_unitOfWork.AccountRepository.GetPageAsync(
e => accounts.Select(a => a.Email).Contains(e.Email),
1, accounts.Count(), CancellationToken.None)
.Result.Items;
foreach (var account in accounts)
{
var datastoreAccount = datastoreAccounts
.SingleOrDefault(a => a.Email == account.Email);
if (datastoreAccount != null)
{
continue;
}
var password = Encoding.UTF8.GetBytes(account.Username);
var salt = RandomNumberGenerator.GetBytes(128 / 8);
var hash = _passwordHasher
.HashAsync(password, salt, CancellationToken.None)
.Result;
var saltBase64 = Convert.ToBase64String(salt);
var hashBase64 = Convert.ToBase64String(hash);
_unitOfWork.AccountRepository.AddOneAsync(
new Account()
{
Username = account.Username,
Email = account.Email,
PasswordHash = hashBase64,
PasswordSalt = saltBase64,
AccountRoles = account.Roles.Select(ar =>
new AccountRole()
{
RoleId = roles.Single(dr => dr.Value.Equals(ar)).Id
})
.ToArray()
},
CancellationToken.None).Wait();
}
_unitOfWork.Save();
}
public void SeedData()
{
var isAddressesPresent =
_unitOfWork.AddressRepository.GetPageAsync(
1, 1, CancellationToken.None)
.Result.Items.Any();
if (isAddressesPresent)
{
return;
}
var countries = new Country[]
{
new Country()
{
Name = "Ukraine"
}
};
foreach (var country in countries)
{
_unitOfWork.CountryRepository
.AddOneAsync(country, CancellationToken.None)
.Wait();
}
var regions = new Region[]
{
new Region()
{
Name = "Kyiv Region",
Country = countries[0]
},
new Region()
{
Name = "Zhytomyr Region",
Country = countries[0]
}
};
foreach (var region in regions)
{
_unitOfWork.RegionRepository
.AddOneAsync(region, CancellationToken.None)
.Wait();
}
var cities = new City[]
{
new City()
{
Name = "Brovary",
Region = regions[0]
},
new City()
{
Name = "Bila Tserkva",
Region = regions[0]
},
new City()
{
Name = "Korostyshiv",
Region = regions[1]
},
new City()
{
Name = "Stanyshivka",
Region = regions[1]
}
};
foreach (var city in cities)
{
_unitOfWork.CityRepository
.AddOneAsync(city, CancellationToken.None)
.Wait();
}
var addresses = new Address[]
{
new Address()
{
Name = "Address 0",
Longitude = 0,
Latitude = 0,
VehicleType = VehicleType.Bus,
City = cities[0]
},
new Address()
{
Name = "Address 1",
Longitude = 1,
Latitude = 1,
VehicleType = VehicleType.Bus,
City = cities[0]
},
new Address()
{
Name = "Address 2",
Longitude = 2,
Latitude = 2,
VehicleType = VehicleType.Bus,
City = cities[0]
},
new Address()
{
Name = "Address 3",
Longitude = 3,
Latitude = 3,
VehicleType = VehicleType.Bus,
City = cities[1]
},
new Address()
{
Name = "Address 4",
Longitude = 4,
Latitude = 4,
VehicleType = VehicleType.Bus,
City = cities[1]
},
new Address()
{
Name = "Address 5",
Longitude = 5,
Latitude = 5,
VehicleType = VehicleType.Bus,
City = cities[1]
},
new Address()
{
Name = "Address 6",
Longitude = 6,
Latitude = 6,
VehicleType = VehicleType.Bus,
City = cities[2]
},
new Address()
{
Name = "Address 7",
Longitude = 7,
Latitude = 7,
VehicleType = VehicleType.Bus,
City = cities[2]
},
new Address()
{
Name = "Address 8",
Longitude = 8,
Latitude = 8,
VehicleType = VehicleType.Bus,
City = cities[3]
},
new Address()
{
Name = "Address 9",
Longitude = 9,
Latitude = 9,
VehicleType = VehicleType.Bus,
City = cities[3]
}
};
foreach (var address in addresses)
{
_unitOfWork.AddressRepository
.AddOneAsync(address, CancellationToken.None)
.Wait();
}
var routes = new Route[]
{
new Route()
{
Name = "Route 0",
VehicleType = VehicleType.Bus,
RouteAddresses = new RouteAddress[]
{
new RouteAddress()
{
Order = 0,
Address = addresses[3]
},
new RouteAddress()
{
Order = 1,
Address = addresses[6]
},
new RouteAddress()
{
Order = 2,
Address = addresses[2]
}
}
},
new Route()
{
Name = "Route 1",
VehicleType = VehicleType.Bus,
RouteAddresses = new RouteAddress[]
{
new RouteAddress()
{
Order = 0,
Address = addresses[1]
},
new RouteAddress()
{
Order = 1,
Address = addresses[2]
},
new RouteAddress()
{
Order = 2,
Address = addresses[7]
},
new RouteAddress()
{
Order = 3,
Address = addresses[9]
}
}
},
new Route()
{
Name = "Route 2",
VehicleType = VehicleType.Bus,
RouteAddresses = new RouteAddress[]
{
new RouteAddress()
{
Order = 0,
Address = addresses[0]
},
new RouteAddress()
{
Order = 1,
Address = addresses[1]
},
new RouteAddress()
{
Order = 2,
Address = addresses[3]
},
new RouteAddress()
{
Order = 3,
Address = addresses[4]
},
new RouteAddress()
{
Order = 4,
Address = addresses[7]
},
new RouteAddress()
{
Order = 5,
Address = addresses[8]
}
}
}
};
foreach (var route in routes)
{
_unitOfWork.RouteRepository
.AddOneAsync(route, CancellationToken.None)
.Wait();
}
var roles = _unitOfWork.RoleRepository
.GetPageAsync(1, IdentityRole.Enumerations.Count,
CancellationToken.None)
.Result.Items;
var companyOwnerRoleId = roles
.SingleOrDefault(
e => e.Value == IdentityRole.CompanyOwner)
?.Id;
if (companyOwnerRoleId == null)
{
throw new InvalidOperationException("Can not find required role.");
}
var companyOwnerAccountCredentials =
new (string Username, string Email)[]
{
("light_travel_owner", "light_travel_owner0@email.com")
};
var companyOwnerAccounts = new List<Account>();
foreach (var accountCredential in companyOwnerAccountCredentials)
{
var password = Encoding.UTF8.GetBytes(accountCredential.Username);
var salt = RandomNumberGenerator.GetBytes(128 / 8);
var hash = _passwordHasher
.HashAsync(password, salt, CancellationToken.None)
.Result;
var saltBase64 = Convert.ToBase64String(salt);
var hashBase64 = Convert.ToBase64String(hash);
companyOwnerAccounts.Add(new Account()
{
Username = accountCredential.Username,
Email = accountCredential.Email,
PasswordHash = hashBase64,
PasswordSalt = saltBase64,
AccountRoles = new AccountRole[]
{
new AccountRole()
{
RoleId = (long)companyOwnerRoleId
}
}
});
}
foreach (var account in companyOwnerAccounts)
{
_unitOfWork.AccountRepository
.AddOneAsync(account, CancellationToken.None)
.Wait();
}
var companies = new Company[]
{
new Company()
{
Name = "Light Travel",
LegalAddress = "Ukraine, Kyiv Region, Kyiv, Address 0",
ContactEmail = "light-travel@email.com",
ContactPhoneNumber = "+38000000000",
Account = companyOwnerAccounts[0]
}
};
foreach (var company in companies)
{
_unitOfWork.CompanyRepository
.AddOneAsync(company, CancellationToken.None)
.Wait();
}
var companyEmployeeRoleId = roles
.SingleOrDefault(
e => e.Value == IdentityRole.CompanyEmployee)
?.Id;
if (companyEmployeeRoleId == null)
{
throw new InvalidOperationException("Can not find required role.");
}
var companyEmployeeAccountCredentials =
new (string Username, string Email)[]
{
("company_employee0", "company_employee0@email.com"),
("company_employee1", "company_employee1@email.com"),
("company_employee2", "company_employee2@email.com")
};
var companyEmployeeAccounts = new List<Account>();
var employees = new List<Employee>();
foreach (var accountCredential in companyEmployeeAccountCredentials)
{
var password = Encoding.UTF8.GetBytes(accountCredential.Username);
var salt = RandomNumberGenerator.GetBytes(128 / 8);
var hash = _passwordHasher
.HashAsync(password, salt, CancellationToken.None)
.Result;
var saltBase64 = Convert.ToBase64String(salt);
var hashBase64 = Convert.ToBase64String(hash);
companyEmployeeAccounts.Add(new Account()
{
Username = accountCredential.Username,
Email = accountCredential.Email,
PasswordHash = hashBase64,
PasswordSalt = saltBase64,
AccountRoles = new AccountRole[]
{
new AccountRole()
{
RoleId = (long)companyEmployeeRoleId
}
}
});
}
foreach (var account in companyEmployeeAccounts)
{
_unitOfWork.AccountRepository
.AddOneAsync(account, CancellationToken.None)
.Wait();
employees.Add(new Employee()
{
FirstName = $"{account.Username}'s fname",
LastName = $"{account.Username}'s lname",
Patronymic = $"{account.Username}'s patronymic",
Sex =
DateTimeOffset.UtcNow.Ticks % 2 == 1 ?
Sex.Male : Sex.Female,
BirthDate = DateOnly.FromDateTime(
DateTimeOffset.UtcNow.Subtract(
TimeSpan.FromDays(365 * 20)).Date),
Company = companies[0],
Documents = new EmployeeDocument[]
{
new EmployeeDocument()
{
DocumentType = DocumentType.Passport,
Information = $"{account.Username}'s passport"
}
},
Account = account
});
}
foreach (var employee in employees)
{
_unitOfWork.EmployeeRepository
.AddOneAsync(employee, CancellationToken.None)
.Wait();
}
var vehicles = new Vehicle[]
{
new Bus()
{
Number = "XX0000XX",
Model = "Model 0",
Capacity = 30,
Company = companies[0]
},
new Bus()
{
Number = "XX1111XX",
Model = "Model 1",
Capacity = 30,
Company = companies[0]
},
new Bus()
{
Number = "XX2222XX",
Model = "Model 2",
Capacity = 30,
Company = companies[0]
}
};
foreach (var vehicle in vehicles)
{
_unitOfWork.VehicleRepository
.AddOneAsync(vehicle, CancellationToken.None)
.Wait();
}
// Vehicle Enrollments (binding between a vehicle and a route,
// specifying departure date and time)
// and
// Route Address Details (for earch address in the route specifying
// cost and time of travel from current to next address
// and current address stop duration)
// VE1: 4 -> 7 -> 3
// \
// VE2: 2 -> 3 -> 8 -> 10
// \
// VE3: 1 -> 2 -> 4 -> 5 -> 8 -> 9
// ---------------------------------> time
// Vehicle Enrollment 1
//
// Route: 4 -> 7 -> 3
// Departure Time: 2025-01-01 07:30:00.000
// Time to Next Address:
// 4: P0000-00-00T00:30:00.000
// 7: P0000-00-00T00:30:00.000
// 3: P0000-00-00T00:00:00.000
// Total time moving: 60 minutes
// Cost to Next Address:
// 4: 5
// 7: 11.50
// 3: 0
// Total: 16.50
// Address Stop duration:
// 4: P0000-00-00T00:00:00.000
// 7: P0000-00-00T00:10:00.000
// 3: P0000-00-00T00:00:00.000
// Total time in stops: 10 minutes
// Total time on road: 70 minutes
//
// Arriave to 3 at 8:40
// Vehicle Enrollment 2
//
// Route: 2 -> 3 -> 8 -> 10
// Departure Time: 2025-01-01 08:30:00.000
// Time to Next Address:
// 2: P0000-00-00T00:20:00.000
// 3: P0000-00-00T00:30:00.000
// 8: P0000-00-00T00:20:00.000
// 10: P0000-00-00T00:00:00.000
// Total time moving: 70 minutes
// Cost to Next Address:
// 2: 4.30
// 3: 14.71
// 8: 12.10
// 10: 0
// Total: 21.11
// Address Stop duration:
// 2: P0000-00-00T00:00:00.000
// 3: P0000-00-00T00:10:00.000
// 8: P0000-00-00T00:10:00.000
// 10: P0000-00-00T00:00:00.000
// Total time in stops: 20 minutes
// Total time on road: 90 minutes
//
// Arrive to 3 at 8:50
// Arrive to 8 at 9:30
// Vehicle Enrollment 3
//
// Route: 1 -> 2 -> 4 -> 5 -> 8 -> 9
// Departure Time: 2025-01-01 07:30:00.000
// Time to Next Address:
// 1: P0000-00-00T00:30:00.000
// 2: P0000-00-00T00:20:00.000
// 4: P0000-00-00T00:30:00.000
// 5: P0000-00-00T00:30:00.000
// 8: P0000-00-00T00:30:00.000
// 9: P0000-00-00T00:00:00.000
// Total time moving: 140 minutes
// Cost to Next Address:
// 1: 5
// 2: 10
// 4: 11.2
// 5: 5.23
// 8: 5.2
// 9: 0
// Total: 36.63
// Address Stop duration:
// 1: P0000-00-00T00:00:00.000
// 2: P0000-00-00T00:10:00.000
// 4: P0000-00-00T00:10:00.000
// 5: P0000-00-00T00:10:00.000
// 8: P0000-00-00T00:10:00.000
// 9: P0000-00-00T00:00:00.000
// Total time in stops: 40 minutes
// Total time on road: 180 minutes
//
// Arrive to 8 at 9:40
var vehicleEnrollments = new VehicleEnrollment[]
{
new VehicleEnrollment()
{
DepartureTime = DateTimeOffset.UtcNow.AddDays(1),
Currency = Currency.EUR,
Vehicle = vehicles[0],
Route = routes[0],
RouteAddressDetails = new RouteAddressDetail[]
{
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(30),
CostToNextAddress = 5,
CurrentAddressStopTime = TimeSpan.Zero,
RouteAddress = routes[0].RouteAddresses.ElementAt(0)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(30),
CostToNextAddress = 11.5M,
CurrentAddressStopTime = TimeSpan.FromMinutes(10),
RouteAddress = routes[0].RouteAddresses.ElementAt(1)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.Zero,
CostToNextAddress = 0,
CurrentAddressStopTime = TimeSpan.Zero,
RouteAddress = routes[0].RouteAddresses.ElementAt(2)
}
},
VehicleEnrollmentEmployees = new VehicleEnrollmentEmployee[]
{
new VehicleEnrollmentEmployee()
{
Employee = employees[0]
}
}
},
new VehicleEnrollment()
{
DepartureTime = DateTimeOffset.UtcNow.AddDays(1).AddHours(1),
Currency = Currency.UAH,
Vehicle = vehicles[1],
Route = routes[1],
RouteAddressDetails = new RouteAddressDetail[]
{
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(20),
CostToNextAddress = 4.3M,
CurrentAddressStopTime = TimeSpan.Zero,
RouteAddress = routes[1].RouteAddresses.ElementAt(0)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(30),
CostToNextAddress = 14.71M,
CurrentAddressStopTime = TimeSpan.FromMinutes(10),
RouteAddress = routes[1].RouteAddresses.ElementAt(1)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(20),
CostToNextAddress = 12.1M,
CurrentAddressStopTime = TimeSpan.FromMinutes(10),
RouteAddress = routes[1].RouteAddresses.ElementAt(2)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.Zero,
CostToNextAddress = 0,
CurrentAddressStopTime = TimeSpan.Zero,
RouteAddress = routes[1].RouteAddresses.ElementAt(3)
}
},
VehicleEnrollmentEmployees = new VehicleEnrollmentEmployee[]
{
new VehicleEnrollmentEmployee()
{
Employee = employees[1]
}
}
},
new VehicleEnrollment()
{
DepartureTime = DateTimeOffset.UtcNow.AddDays(1),
Currency = Currency.USD,
Vehicle = vehicles[2],
Route = routes[2],
RouteAddressDetails = new RouteAddressDetail[]
{
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(30),
CostToNextAddress = 5,
CurrentAddressStopTime = TimeSpan.Zero,
RouteAddress = routes[2].RouteAddresses.ElementAt(0)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(20),
CostToNextAddress = 10,
CurrentAddressStopTime = TimeSpan.FromMinutes(10),
RouteAddress = routes[2].RouteAddresses.ElementAt(1)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(30),
CostToNextAddress = 11.2M,
CurrentAddressStopTime = TimeSpan.FromMinutes(10),
RouteAddress = routes[2].RouteAddresses.ElementAt(2)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(30),
CostToNextAddress = 5.23M,
CurrentAddressStopTime = TimeSpan.FromMinutes(10),
RouteAddress = routes[2].RouteAddresses.ElementAt(3)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.FromMinutes(30),
CostToNextAddress = 5.2M,
CurrentAddressStopTime = TimeSpan.FromMinutes(10),
RouteAddress = routes[2].RouteAddresses.ElementAt(4)
},
new RouteAddressDetail()
{
TimeToNextAddress = TimeSpan.Zero,
CostToNextAddress = 0,
CurrentAddressStopTime = TimeSpan.Zero,
RouteAddress = routes[2].RouteAddresses.ElementAt(5)
}
},
VehicleEnrollmentEmployees = new VehicleEnrollmentEmployee[]
{
new VehicleEnrollmentEmployee()
{
Employee = employees[2]
}
}
},
};
foreach (var enrollment in vehicleEnrollments)
{
_unitOfWork.VehicleEnrollmentRepository
.AddOneAsync(enrollment, CancellationToken.None)
.Wait();
}
_unitOfWork.Save();
}
public void Dispose()
{
_unitOfWork.Dispose();
}
}

View File

@ -8,6 +8,7 @@ using Moq;
using System.Globalization;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Configuration.Infrastructure;
namespace cuqmbr.TravelGuide.Application.IntegrationTests;
@ -29,9 +30,8 @@ public abstract class TestBase : IDisposable
})
.ConfigureLogging()
.ConfigureApplication()
.ConfigureInfrastructure()
.ConfigurePersistence();
// TODO: Create InMemory configuration for Identity
// .ConfigureIdentity();
SetCulture("en-US");
SetTimeZone("Europe/Kyiv");