yavsc/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs

506 lines
16 KiB
C#
Raw Normal View History

2026-08-31 02:14:04 +01:00
using Microsoft.AspNetCore.Builder;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
2026-09-13 03:09:12 +01:00
using Npgsql;
using System.Runtime.Loader;
2026-08-31 02:14:04 +01:00
using Yavsc.Controllers;
2026-09-12 13:57:25 +01:00
using Yavsc.Interfaces.Workflow;
2026-08-31 02:14:04 +01:00
using Yavsc.Models;
2026-09-12 13:57:25 +01:00
using Yavsc.Models.Google.Messaging;
2026-08-31 04:15:10 +01:00
using Yavsc.Models.Haircut;
2026-09-12 13:57:25 +01:00
using Yavsc.Models.Messaging;
2026-08-31 02:14:04 +01:00
using Yavsc.Models.Relationship;
using Yavsc.Models.Workflow;
2026-09-12 13:57:25 +01:00
using Yavsc.Services;
2026-08-31 02:14:04 +01:00
using Yavsc.Tests.Shared;
namespace Yavsc.Api.Test.Fixtures;
public sealed class ApiWebServerFixture : WebHostFixture
{
2026-09-13 03:09:12 +01:00
private const string DbProviderEnvVar = "YAVSC_API_TEST_DB_PROVIDER";
private const string NpgsqlAdminConnectionEnvVar = "YAVSC_API_TEST_NPGSQL_ADMIN_CONNECTION";
private const string DefaultDevelopmentConnectionString = "Server=localhost;Port=5432;Database=yavscdev;Username=yavscdev;Password=8*5idas;Include Error Detail=true";
2026-08-31 02:14:04 +01:00
protected override int HttpsPort => 5104;
private static SqliteConnection? _sharedSqliteConnection;
private static readonly object _sqliteLock = new();
2026-09-13 03:09:12 +01:00
private static readonly object _npgsqlLock = new();
private static string? _sharedNpgsqlConnectionString;
private static string? _sharedNpgsqlAdminConnectionString;
private static string? _sharedNpgsqlDatabaseName;
private static bool _npgsqlCleanupRegistered;
2026-08-31 02:14:04 +01:00
protected override WebApplication BuildApp(WebApplicationBuilder builder)
{
2026-09-13 03:09:12 +01:00
if (UseNpgsqlProvider())
{
var npgsqlConnectionString = EnsureNpgsqlDatabaseCreated();
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseNpgsql(npgsqlConnectionString));
}
else
2026-08-31 02:14:04 +01:00
{
2026-09-13 03:09:12 +01:00
SqliteConnection sharedConnection;
lock (_sqliteLock)
2026-08-31 02:14:04 +01:00
{
2026-09-13 03:09:12 +01:00
if (_sharedSqliteConnection is null)
{
_sharedSqliteConnection = new SqliteConnection(
"Data Source=YavscApiTests;Mode=Memory;Cache=Shared");
_sharedSqliteConnection.Open();
}
sharedConnection = _sharedSqliteConnection;
2026-08-31 02:14:04 +01:00
}
2026-09-13 03:09:12 +01:00
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlite(sharedConnection));
}
2026-08-31 02:14:04 +01:00
builder.Services.AddControllers()
.AddApplicationPart(typeof(ActivityApiController).Assembly);
2026-09-12 13:57:25 +01:00
builder.Services.AddLocalization();
builder.Services.Configure<GoogleAuthSettings>(_ => { });
builder.Services.AddTransient<IBillingService, BillingService>();
builder.Services.AddTransient<IYavscMessageSender, NoopMessageSender>();
2026-08-31 02:14:04 +01:00
builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.IncludeErrorDetails = true;
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = TestTokenIssuer.Issuer,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = TestTokenIssuer.SigningKey,
NameClaimType = "sub",
RoleClaimType = Yavsc.Constants.RoleClaimType,
};
});
return builder.Build();
}
protected override async Task<WebApplication> ConfigurePipelineAsync(WebApplication app)
{
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
db.Database.EnsureCreated();
}
await Task.CompletedTask;
return app;
}
public string BaseAddress => Addresses.First(a => a.StartsWith("https://", StringComparison.Ordinal));
2026-09-13 03:09:12 +01:00
public static bool UseNpgsqlProvider()
=> string.Equals(
Environment.GetEnvironmentVariable(DbProviderEnvVar),
"npgsql",
StringComparison.OrdinalIgnoreCase);
private static string EnsureNpgsqlDatabaseCreated()
{
lock (_npgsqlLock)
{
if (!string.IsNullOrWhiteSpace(_sharedNpgsqlConnectionString))
{
return _sharedNpgsqlConnectionString;
}
var adminConnectionString = BuildAdminConnectionString();
var databaseName = $"yavsc_api_test_{Guid.NewGuid():N}";
using (var adminConnection = new NpgsqlConnection(adminConnectionString))
{
adminConnection.Open();
using var createCommand = adminConnection.CreateCommand();
createCommand.CommandText = $"CREATE DATABASE \"{databaseName}\"";
createCommand.ExecuteNonQuery();
}
var testConnectionBuilder = new NpgsqlConnectionStringBuilder(adminConnectionString)
{
Database = databaseName,
Pooling = false,
IncludeErrorDetail = true
};
_sharedNpgsqlAdminConnectionString = adminConnectionString;
_sharedNpgsqlDatabaseName = databaseName;
_sharedNpgsqlConnectionString = testConnectionBuilder.ToString();
RegisterNpgsqlCleanup();
return _sharedNpgsqlConnectionString;
}
}
private static string BuildAdminConnectionString()
{
var configured = Environment.GetEnvironmentVariable(NpgsqlAdminConnectionEnvVar);
var source = string.IsNullOrWhiteSpace(configured)
? DefaultDevelopmentConnectionString
: configured;
var builder = new NpgsqlConnectionStringBuilder(source)
{
Pooling = false,
IncludeErrorDetail = true
};
if (string.IsNullOrWhiteSpace(configured))
{
builder.Database = "postgres";
}
else if (string.IsNullOrWhiteSpace(builder.Database))
{
builder.Database = "postgres";
}
return builder.ToString();
}
private static void RegisterNpgsqlCleanup()
{
if (_npgsqlCleanupRegistered)
{
return;
}
AppDomain.CurrentDomain.ProcessExit += (_, __) => DropTemporaryNpgsqlDatabase();
AssemblyLoadContext.Default.Unloading += _ => DropTemporaryNpgsqlDatabase();
_npgsqlCleanupRegistered = true;
}
private static void DropTemporaryNpgsqlDatabase()
{
lock (_npgsqlLock)
{
if (string.IsNullOrWhiteSpace(_sharedNpgsqlDatabaseName)
|| string.IsNullOrWhiteSpace(_sharedNpgsqlAdminConnectionString))
{
return;
}
try
{
using var adminConnection = new NpgsqlConnection(_sharedNpgsqlAdminConnectionString);
adminConnection.Open();
using (var terminateCommand = adminConnection.CreateCommand())
{
terminateCommand.CommandText = @"
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = @databaseName
AND pid <> pg_backend_pid();";
terminateCommand.Parameters.AddWithValue("databaseName", _sharedNpgsqlDatabaseName);
terminateCommand.ExecuteNonQuery();
}
using var dropCommand = adminConnection.CreateCommand();
dropCommand.CommandText = $"DROP DATABASE IF EXISTS \"{_sharedNpgsqlDatabaseName}\"";
dropCommand.ExecuteNonQuery();
}
catch
{
// Best-effort cleanup only.
}
finally
{
_sharedNpgsqlConnectionString = null;
_sharedNpgsqlAdminConnectionString = null;
_sharedNpgsqlDatabaseName = null;
}
}
}
2026-09-12 13:57:25 +01:00
private sealed class NoopMessageSender : IYavscMessageSender
{
public Task<MessageWithPayloadResponse> NotifyBookQueryAsync(IEnumerable<string> connectionIds, RdvQueryEvent ev)
=> Task.FromResult(new MessageWithPayloadResponse());
public Task<MessageWithPayloadResponse> NotifyEstimateAsync(IEnumerable<string> connectionIds, EstimationEvent ev)
=> Task.FromResult(new MessageWithPayloadResponse());
public Task<MessageWithPayloadResponse> NotifyHairCutQueryAsync(IEnumerable<string> connectionIds, HairCutQueryEvent ev)
=> Task.FromResult(new MessageWithPayloadResponse());
public Task<MessageWithPayloadResponse> NotifyAsync(IEnumerable<string> connectionIds, IEvent yaev)
=> Task.FromResult(new MessageWithPayloadResponse());
}
2026-08-31 02:14:04 +01:00
public void ResetAndSeedActivityGraph()
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var user = new ApplicationUser
{
Id = "alice",
UserName = "alice",
Email = "alice@example.test",
EmailConfirmed = true,
FullName = "Alice",
};
db.Users.Add(user);
var location = new Location
{
Address = "1 rue du Test",
Latitude = 48.8566,
Longitude = 2.3522,
};
db.Add(location);
db.SaveChanges();
var activity = new Activity
{
Code = "dev",
Name = "Dev",
Hidden = false,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
};
db.Activities.Add(activity);
db.Activities.Add(new Activity
{
Code = "ghost",
Name = "Ghost",
Hidden = false,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
});
var performer = new PerformerProfile
{
PerformerId = "alice",
SIREN = "123456789",
OrganizationAddressId = location.Id,
AcceptNotifications = true,
AcceptPublicContact = true,
Active = true,
Rate = 5,
WebSite = "https://alice.dev",
};
db.Performers.Add(performer);
db.UserActivities.Add(new UserActivity
{
UserId = "alice",
DoesCode = "dev",
Weight = 100,
});
db.Users.Add(new ApplicationUser
{
Id = "bob",
UserName = "bob",
Email = "bob@example.test",
EmailConfirmed = true,
FullName = "Bob",
});
db.Activities.Add(new Activity
{
Code = "declared-only",
Name = "Declared Only",
Hidden = false,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
});
db.Performers.Add(new PerformerProfile
{
PerformerId = "bob",
SIREN = "987654321",
OrganizationAddressId = location.Id,
AcceptNotifications = false,
AcceptPublicContact = false,
Active = false,
Rate = 0,
WebSite = "",
});
db.UserActivities.Add(new UserActivity
{
UserId = "bob",
DoesCode = "declared-only",
Weight = 10,
});
db.SaveChanges();
}
2026-08-31 03:37:09 +01:00
public void ResetAndSeedRdvQueryGraph()
{
ResetAndSeedActivityGraph();
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var location = db.Locations.Single(l => l.Address == "1 rue du Test");
db.RdvQueries.Add(new RdvQuery
{
ActivityCode = "dev",
ClientId = "alice",
PerformerId = "alice",
Consent = true,
UserCreated = "alice",
UserModified = "alice",
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
2026-08-31 03:37:09 +01:00
EventDate = DateTime.UtcNow.AddDays(1),
Location = location,
Reason = "Initial rendez-vous",
Status = Yavsc.QueryStatus.Inserted,
});
db.SaveChanges();
}
2026-08-31 04:15:10 +01:00
public void ResetAndSeedHaircutGraph()
{
ResetAndSeedActivityGraph();
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var location = db.Locations.Single(l => l.Address == "1 rue du Test");
if (!db.Activities.Any(a => a.Code == "brush"))
{
db.Activities.Add(new Activity
{
Code = "brush",
Name = "Brush",
Hidden = false,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
});
}
if (!db.Activities.Any(a => a.Code == "mbrush"))
{
db.Activities.Add(new Activity
{
Code = "mbrush",
Name = "MBrush",
Hidden = false,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
});
}
if (!db.BrusherProfile.Any(p => p.UserId == "alice"))
{
db.BrusherProfile.Add(new BrusherProfile
{
UserId = "alice",
ActionDistance = 25,
WomenLongCutPrice = 50m,
WomenHalfCutPrice = 40m,
WomenShortCutPrice = 30m,
ManCutPrice = 20m,
KidCutPrice = 15m,
ShampooPrice = 5m,
});
}
var prestation1 = new HairPrestation
{
Gender = HairCutGenders.Women,
Length = HairLength.HalfLong,
Cut = true,
Shampoo = true,
Dressing = HairDressings.Brushing,
Tech = HairTechnos.NoTech,
Cares = false,
Taints = new List<HairTaintInstance>(),
};
var prestation2 = new HairPrestation
{
Gender = HairCutGenders.Man,
Length = HairLength.Short,
Cut = true,
Shampoo = false,
Dressing = HairDressings.Brushing,
Tech = HairTechnos.NoTech,
Cares = false,
Taints = new List<HairTaintInstance>(),
};
db.HairPrestation.AddRange(prestation1, prestation2);
db.SaveChanges();
db.HairCutQueries.Add(new HairCutQuery
{
ActivityCode = "brush",
ClientId = "alice",
PerformerId = "alice",
Consent = true,
UserCreated = "alice",
UserModified = "alice",
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
2026-08-31 04:15:10 +01:00
EventDate = DateTime.UtcNow.AddDays(3),
Location = location,
PrestationId = prestation1.Id,
Prestation = prestation1,
AdditionalInfo = "Coupe test",
Status = Yavsc.QueryStatus.Inserted,
Description = "Haircut seed",
});
db.HairMultiCutQueries.Add(new HairMultiCutQuery
{
ActivityCode = "mbrush",
ClientId = "alice",
PerformerId = "alice",
Consent = true,
UserCreated = "alice",
UserModified = "alice",
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
2026-08-31 04:15:10 +01:00
EventDate = DateTime.UtcNow.AddDays(4),
Location = location,
Prestations = new List<HairPrestationCollectionItem>
{
new() { PrestationId = prestation1.Id, Prestation = prestation1 },
new() { PrestationId = prestation2.Id, Prestation = prestation2 },
},
Status = Yavsc.QueryStatus.Inserted,
});
db.SaveChanges();
}
2026-08-31 02:14:04 +01:00
public override void Dispose()
{
2026-09-12 17:31:18 +01:00
// Keep the shared in-memory SQLite connection alive for the
// whole test process. Closing it from one fixture instance can
// drop the schema while other tests are still running.
base.Dispose();
2026-08-31 02:14:04 +01:00
}
}