Exploration des activités

This commit is contained in:
Paul Schneider 2026-08-31 02:14:04 +01:00
commit 3c51e66e50
20 changed files with 743 additions and 61 deletions

View file

@ -0,0 +1,161 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Test.Fixtures;
using Yavsc.Tests.Shared;
namespace Yavsc.Api.Test;
[Collection("Yavsc Api")]
public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixture>
{
private readonly ApiWebServerFixture _fixture;
public ActivityApiControllerTests(ApiWebServerFixture fixture)
{
_fixture = fixture;
}
private HttpClient NewClient(string subject = "alice", string scope = "api")
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
var http = new HttpClient(handler)
{
BaseAddress = new Uri(_fixture.BaseAddress)
};
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
return http;
}
[Fact]
public async Task GetUsers_returns_declared_user_for_exact_activity_code()
{
_fixture.ResetAndSeedActivityGraph();
using var http = NewClient();
var response = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
Assert.Single(payload!);
Assert.Equal("alice", payload[0].PerformerId);
Assert.Equal("alice", payload[0].UserName);
Assert.True(payload[0].HasPerformerProfile);
Assert.True(payload[0].Active);
Assert.Equal("dev", payload[0].ActivityCode);
}
[Fact]
public async Task GetUsers_returns_user_even_when_performer_inactive()
{
_fixture.ResetAndSeedActivityGraph();
using (var scope = _fixture.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
var performer = db.Performers.Single(p => p.PerformerId == "alice");
performer.Active = false;
db.SaveChanges();
}
using var http = NewClient();
var response = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
Assert.Single(payload!);
Assert.Equal("alice", payload[0].PerformerId);
Assert.False(payload[0].Active);
}
[Fact]
public async Task Catalog_and_Performers_are_consistent_for_dev_activity()
{
_fixture.ResetAndSeedActivityGraph();
using var http = NewClient();
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(catalog);
var dev = catalog!.Single(a => a.Code == "dev");
Assert.True(dev.PerformerCount > 0);
var performersResponse = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, performersResponse.StatusCode);
var performers = await performersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(performers);
Assert.Equal(dev.PerformerCount, performers!.Count);
Assert.Contains(performers, p => p.PerformerId == "alice");
}
[Fact]
public async Task Catalog_does_not_list_activity_without_declaration()
{
_fixture.ResetAndSeedActivityGraph();
using var http = NewClient();
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(catalog);
Assert.DoesNotContain(catalog!, a => a.Code == "ghost");
Assert.Contains(catalog!, a => a.Code == "dev");
}
[Fact]
public async Task Catalog_lists_declared_activity_even_when_performer_inactive()
{
_fixture.ResetAndSeedActivityGraph();
using var http = NewClient();
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(catalog);
Assert.Contains(catalog!, a => a.Code == "declared-only");
var response = await http.GetAsync("/api/v1/activity/declared-only/users", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
Assert.Single(payload!);
Assert.Equal("bob", payload[0].PerformerId);
Assert.Equal("bob", payload[0].UserName);
Assert.True(payload[0].HasPerformerProfile);
Assert.False(payload[0].Active);
}
[Fact]
public async Task Performers_alias_returns_same_payload_as_users_endpoint()
{
_fixture.ResetAndSeedActivityGraph();
using var http = NewClient();
var usersResponse = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
var performersResponse = await http.GetAsync("/api/v1/activity/dev/performers", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, usersResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, performersResponse.StatusCode);
var usersPayload = await usersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
var performersPayload = await performersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
Assert.NotNull(usersPayload);
Assert.NotNull(performersPayload);
Assert.Equal(usersPayload!.Count, performersPayload!.Count);
Assert.Equal(usersPayload[0].PerformerId, performersPayload[0].PerformerId);
Assert.Equal(usersPayload[0].UserName, performersPayload[0].UserName);
}
}

View file

@ -0,0 +1,6 @@
namespace Yavsc.Api.Test;
[CollectionDefinition("Yavsc Api")]
public sealed class ApiCollection
{
}

View file

@ -0,0 +1,7 @@
<Project>
<!--
Yavsc.Api.Test has no project-specific package versions. All
package versions are declared at the repository root.
-->
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Packages.props', '$(MSBuildThisFileDirectory)../'))" />
</Project>

View file

@ -0,0 +1,210 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Yavsc.Controllers;
using Yavsc.Models;
using Yavsc.Models.Relationship;
using Yavsc.Models.Workflow;
using Yavsc.Tests.Shared;
namespace Yavsc.Api.Test.Fixtures;
public sealed class ApiWebServerFixture : WebHostFixture
{
protected override int HttpsPort => 5104;
private static SqliteConnection? _sharedSqliteConnection;
private static readonly object _sqliteLock = new();
protected override WebApplication BuildApp(WebApplicationBuilder builder)
{
SqliteConnection sharedConnection;
lock (_sqliteLock)
{
if (_sharedSqliteConnection is null)
{
_sharedSqliteConnection = new SqliteConnection(
"Data Source=YavscApiTests;Mode=Memory;Cache=Shared");
_sharedSqliteConnection.Open();
}
sharedConnection = _sharedSqliteConnection;
}
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlite(sharedConnection));
builder.Services.AddControllers()
.AddApplicationPart(typeof(ActivityApiController).Assembly);
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));
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();
}
public override void Dispose()
{
try
{
base.Dispose();
}
finally
{
lock (_sqliteLock)
{
if (_sharedSqliteConnection is not null)
{
_sharedSqliteConnection.Close();
_sharedSqliteConnection.Dispose();
_sharedSqliteConnection = null;
}
}
}
}
}

View file

@ -0,0 +1,69 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Yavsc.Api.Test.Fixtures;
using Yavsc.Controllers;
using Yavsc.Models.Workflow;
namespace Yavsc.Api.Test;
[Collection("Yavsc Api")]
public sealed class HomeControllerTests : IClassFixture<ApiWebServerFixture>
{
private readonly ApiWebServerFixture _fixture;
public HomeControllerTests(ApiWebServerFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task Index_does_not_list_activity_without_declaration()
{
_fixture.ResetAndSeedActivityGraph();
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
var controller = new HomeController(
NullLogger<HomeController>.Instance,
localizer: null!,
context: db,
settingsOptions: Options.Create(new SiteSettings()),
env: new TestEnvironment());
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim("sub", "alice"),
new Claim(ClaimTypes.NameIdentifier, "alice")
}, "Bearer"))
}
};
var result = await controller.Index(id: null);
var view = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<Activity>>(view.Model);
Assert.Contains(model, a => a.Code == "dev");
Assert.DoesNotContain(model, a => a.Code == "ghost");
}
private sealed class TestEnvironment : IWebHostEnvironment
{
public string ApplicationName { get; set; } = "Yavsc.Api.Test";
public IFileProvider WebRootFileProvider { get; set; } = null!;
public string WebRootPath { get; set; } = string.Empty;
public string EnvironmentName { get; set; } = "Development";
public string ContentRootPath { get; set; } = string.Empty;
public IFileProvider ContentRootFileProvider { get; set; } = null!;
}
}

View file

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Yavsc.Api.Test</RootNamespace>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.v3.common" />
<PackageReference Include="xunit.v3.extensibility.core" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Yavsc.Abstract\Yavsc.Abstract.csproj" />
<ProjectReference Include="..\Yavsc.Server\Yavsc.Server.csproj" />
<ProjectReference Include="..\Yavsc.Api\Yavsc.Api.csproj" />
<ProjectReference Include="..\Yavsc.Org\Yavsc.Org.csproj" />
<ProjectReference Include="..\Yavsc.Tests.Shared\Yavsc.Tests.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>