Initial import

This commit is contained in:
Stein Helge Riise
2025-11-17 08:32:46 +01:00
commit ede31fbb7e
129 changed files with 9514 additions and 0 deletions
@@ -0,0 +1,61 @@
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using MinAttest.Contracts.Attests;
using MinAttest.Contracts.Employers;
using MinAttest.Contracts.Persons;
using MinAttest.Tests.Integration.Server;
using Xunit;
namespace MinAttest.Tests.Integration.Api;
[Collection("IntegrationCollection")]
public class EmployersApiTests : IClassFixture<ApiWebAppFactory>
{
private readonly HttpClient _client;
public EmployersApiTests(ApiWebAppFactory factory) => _client = factory.CreateClient();
[Fact]
public async Task Employer_issue_and_list_and_download_via_http()
{
// Upsert employer
var employerUpsert = new EmployerUpsertRequest("321654987", "ApiCo AS");
var employerResp = await _client.PostAsJsonAsync("/api/v1/employers", employerUpsert);
employerResp.StatusCode.Should().Be(HttpStatusCode.OK);
var employer = await employerResp.Content.ReadFromJsonAsync<EmployerResponse>();
employer.Should().NotBeNull();
// Upsert person
var personResp = await _client.PostAsJsonAsync("/api/v1/persons", new PersonUpsertRequest("hash-api-emp-1", "emp-person@example.com", null));
personResp.StatusCode.Should().Be(HttpStatusCode.OK);
var person = await personResp.Content.ReadFromJsonAsync<PersonResponse>();
person.Should().NotBeNull();
// Issue attest for person
var issueReq = new EmployerAttestUploadRequest(
PersonId: person!.Id,
Title: "ApiEmployerAttest",
From: new DateOnly(2024,1,1),
To: new DateOnly(2024,6,1),
Summary: null,
BlobPath: string.Empty,
BlobHash: null,
ContentBase64: Convert.ToBase64String(new byte[]{7,8,9}),
ContentType: "application/octet-stream");
var issueResp = await _client.PostAsJsonAsync($"/api/v1/employers/{employer!.Id}/attests", issueReq);
issueResp.StatusCode.Should().Be(HttpStatusCode.Created);
var created = await issueResp.Content.ReadFromJsonAsync<Dictionary<string, object>>();
created.Should().NotBeNull();
var createdId = Guid.Parse(created!["attestId"].ToString()!);
// List employer attests
var listResp = await _client.GetAsync($"/api/v1/employers/{employer.Id}/attests");
listResp.StatusCode.Should().Be(HttpStatusCode.OK);
// Download
var dlResp = await _client.GetAsync($"/api/v1/employers/{employer.Id}/attests/{createdId}/download");
dlResp.StatusCode.Should().Be(HttpStatusCode.OK);
(await dlResp.Content.ReadAsByteArrayAsync()).Length.Should().BeGreaterThan(0);
}
}
@@ -0,0 +1,41 @@
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using MinAttest.Contracts.Attests;
using MinAttest.Contracts.Persons;
using MinAttest.Tests.Integration.Server;
using Xunit;
namespace MinAttest.Tests.Integration.Api;
[Collection("IntegrationCollection")]
public class PersonsApiTests : IClassFixture<ApiWebAppFactory>
{
private readonly HttpClient _client;
public PersonsApiTests(ApiWebAppFactory factory) => _client = factory.CreateClient();
[Fact]
public async Task Upsert_person_and_upload_and_list()
{
var upsertResp = await _client.PostAsJsonAsync("/api/v1/persons", new PersonUpsertRequest("hash-api-1", "api@example.com", null));
upsertResp.StatusCode.Should().Be(HttpStatusCode.OK);
var person = await upsertResp.Content.ReadFromJsonAsync<PersonResponse>();
person.Should().NotBeNull();
var uploadReq = new PersonAttestUploadRequest(
Title: "ApiAttest",
From: new DateOnly(2024,1,1),
To: new DateOnly(2024,6,1),
Summary: "",
BlobPath: "",
BlobHash: null,
ContentBase64: Convert.ToBase64String(new byte[]{1,2,3}),
ContentType: "application/octet-stream");
var uploadResp = await _client.PostAsJsonAsync($"/api/v1/persons/{person!.Id}/attests", uploadReq);
uploadResp.StatusCode.Should().Be(HttpStatusCode.Created);
var listResp = await _client.GetAsync($"/api/v1/persons/{person.Id}/attests");
listResp.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
@@ -0,0 +1,56 @@
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using MinAttest.Contracts.Attests;
using MinAttest.Contracts.Persons;
using MinAttest.Contracts.ShareLinks;
using MinAttest.Tests.Integration.Server;
using Xunit;
namespace MinAttest.Tests.Integration.Api;
[Collection("IntegrationCollection")]
public class ShareLinksApiTests : IClassFixture<ApiWebAppFactory>
{
private readonly HttpClient _client;
public ShareLinksApiTests(ApiWebAppFactory factory) => _client = factory.CreateClient();
[Fact]
public async Task Create_list_revoke_sharelink_via_http()
{
// Upsert person
var personResp = await _client.PostAsJsonAsync("/api/v1/persons", new PersonUpsertRequest("hash-api-share-1", "share-person@example.com", null));
personResp.StatusCode.Should().Be(HttpStatusCode.OK);
var person = await personResp.Content.ReadFromJsonAsync<PersonResponse>();
// Upload attest
var upload = new PersonAttestUploadRequest(
Title: "ShareAttest",
From: new DateOnly(2024,1,1),
To: new DateOnly(2024,6,1),
Summary: null,
BlobPath: string.Empty,
BlobHash: null,
ContentBase64: Convert.ToBase64String(new byte[]{1,2,3,4}),
ContentType: "application/pdf");
var upResp = await _client.PostAsJsonAsync($"/api/v1/persons/{person!.Id}/attests", upload);
upResp.StatusCode.Should().Be(HttpStatusCode.Created);
var created = await upResp.Content.ReadFromJsonAsync<Dictionary<string, object>>();
var attestId = Guid.Parse(created!["attestId"].ToString()!);
// Create share link
var createReq = new CreateShareLinkRequest(TimeSpan.FromDays(7), false);
var slResp = await _client.PostAsJsonAsync($"/api/v1/attests/{attestId}/sharelinks", createReq);
slResp.StatusCode.Should().Be(HttpStatusCode.OK);
var link = await slResp.Content.ReadFromJsonAsync<ShareLinkResponse>();
link.Should().NotBeNull();
// List
var listResp = await _client.GetAsync($"/api/v1/attests/{attestId}/sharelinks");
listResp.StatusCode.Should().Be(HttpStatusCode.OK);
// Revoke
var delResp = await _client.DeleteAsync($"/api/v1/attests/{attestId}/sharelinks/{link!.ShareId}");
delResp.StatusCode.Should().Be(HttpStatusCode.NoContent);
}
}
@@ -0,0 +1,46 @@
using FluentAssertions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using MinAttest.Application.Features.Attests.Commands;
using MinAttest.Application.Features.Attests.Queries;
using MinAttest.Contracts.Attests;
using Xunit;
namespace MinAttest.Tests.Integration;
[Collection("IntegrationCollection")]
public class AttestsIntegrationTests
{
private readonly IntegrationTestFixture _fixture;
public AttestsIntegrationTests(IntegrationTestFixture fixture) => _fixture = fixture;
[Fact]
public async Task Person_upload_and_list_attest_success()
{
var sp = _fixture.Services;
var mediator = sp.GetRequiredService<IMediator>();
// Ensure person exists
var upsert = await mediator.Send(new MinAttest.Application.Features.Persons.Commands.UpsertPersonCommand("person-hash-123", "person@example.com", null));
var personId = upsert.Id;
var req = new PersonAttestUploadRequest(
Title: "TestAttest",
From: new DateOnly(2023,1,1),
To: new DateOnly(2023,12,31),
Summary: "Test summary",
BlobPath: string.Empty,
BlobHash: null,
ContentBase64: Convert.ToBase64String(new byte[] {1,2,3}),
ContentType: "application/octet-stream"
);
var createdId = await mediator.Send(new PersonUploadAttestCommand(personId, req));
createdId.Should().NotBeNull();
var list = await mediator.Send(new ListPersonAttestsQuery(personId, 10));
list.Should().NotBeNull();
list!.Any(a => a.Id == createdId).Should().BeTrue();
}
}
@@ -0,0 +1,45 @@
using FluentAssertions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using MinAttest.Application.Features.Employers.Commands;
using MinAttest.Application.Features.Employers.Queries;
using Xunit;
namespace MinAttest.Tests.Integration;
[Collection("IntegrationCollection")]
public class EmployerUsersIntegrationTests
{
private readonly IntegrationTestFixture _fixture;
public EmployerUsersIntegrationTests(IntegrationTestFixture fixture) => _fixture = fixture;
[Fact]
public async Task Upsert_list_get_delete_employer_user()
{
var mediator = _fixture.Services.GetRequiredService<IMediator>();
// Ensure employer exists
var employer = await mediator.Send(new UpsertEmployerCommand("999999999", "TestBedrift AS"));
// Upsert user
var user = await mediator.Send(new UpsertEmployerUserCommand(
employer.Id, "oid-123", "hr@example.com", "Hr User", "Issuer"));
user.Should().NotBeNull();
// List
var list = await mediator.Send(new ListEmployerUsersQuery(employer.Id));
list.Should().Contain(u => u.ExternalObjectId == "oid-123");
// Get
var got = await mediator.Send(new GetEmployerUserQuery(employer.Id, user!.Id));
got.Should().NotBeNull();
got!.Email.Should().Be("hr@example.com");
// Delete
var deleted = await mediator.Send(new DeleteEmployerUserCommand(employer.Id, user.Id));
deleted.Should().BeTrue();
// Delete again -> false or idempotent? we return false when not found
var deletedAgain = await mediator.Send(new DeleteEmployerUserCommand(employer.Id, user.Id));
deletedAgain.Should().BeFalse();
}
}
@@ -0,0 +1,48 @@
using FluentAssertions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using MinAttest.Application.Features.Attests.Commands;
using MinAttest.Application.Features.Attests.Queries;
using MinAttest.Application.Features.Employers.Commands;
using MinAttest.Contracts.Attests;
using Xunit;
namespace MinAttest.Tests.Integration;
[Collection("IntegrationCollection")]
public class EmployersAttestsIntegrationTests
{
private readonly IntegrationTestFixture _fixture;
public EmployersAttestsIntegrationTests(IntegrationTestFixture fixture) => _fixture = fixture;
[Fact]
public async Task Employer_issue_and_list_attests()
{
var mediator = _fixture.Services.GetRequiredService<IMediator>();
// Ensure employer & person
var employer = await mediator.Send(new UpsertEmployerCommand("555555555", "NewCo AS"));
var person = await mediator.Send(new MinAttest.Application.Features.Persons.Commands.UpsertPersonCommand("person-hash-456", "person2@example.com", null));
var req = new EmployerAttestUploadRequest(
PersonId: person.Id,
Title: "IssuedAttest",
From: new DateOnly(2023,1,1),
To: new DateOnly(2023,12,31),
Summary: null,
BlobPath: string.Empty,
BlobHash: null,
ContentBase64: Convert.ToBase64String(new byte[]{4,5,6}),
ContentType: "application/octet-stream");
var createdId = await mediator.Send(new EmployerIssueAttestCommand(employer.Id, req));
createdId.Should().NotBeNull();
var list = await mediator.Send(new ListEmployerAttestsQuery(employer.Id, 10));
list.Should().Contain(a => a.Id == createdId);
// Content
var content = await mediator.Send(new GetAttestContentQuery(createdId!.Value));
content.Should().NotBeNull();
content!.Content.Length.Should().BeGreaterThan(0);
}
}
@@ -0,0 +1,31 @@
using FluentAssertions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using MinAttest.Application.Features.Employers.Commands;
using MinAttest.Application.Features.Employers.Queries;
using Xunit;
namespace MinAttest.Tests.Integration;
[Collection("IntegrationCollection")]
public class EmployersIntegrationTests
{
private readonly IntegrationTestFixture _fixture;
public EmployersIntegrationTests(IntegrationTestFixture fixture) => _fixture = fixture;
[Fact]
public async Task Upsert_and_get_and_list_employers_work()
{
var mediator = _fixture.Services.GetRequiredService<IMediator>();
var upsert = await mediator.Send(new UpsertEmployerCommand("123456789", "Acme AS"));
upsert.OrgNumber.Should().Be("123456789");
upsert.Name.Should().Be("Acme AS");
var list = await mediator.Send(new ListEmployersQuery());
list.Should().Contain(e => e.OrgNumber == "123456789");
var get = await mediator.Send(new GetEmployerQuery(upsert.Id));
get.Should().NotBeNull();
get!.Name.Should().Be("Acme AS");
}
}
@@ -0,0 +1,9 @@
using Xunit;
namespace MinAttest.Tests.Integration;
[CollectionDefinition("IntegrationCollection")]
public class IntegrationCollection : ICollectionFixture<IntegrationTestFixture>
{
}
@@ -0,0 +1,67 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MinAttest.Application;
using MinAttest.Application.Abstractions;
using MinAttest.Application.Common.Behaviors;
using MinAttest.Infrastructure.Data;
using Xunit;
namespace MinAttest.Tests.Integration;
public class IntegrationTestFixture : IAsyncLifetime
{
public IServiceProvider Services { get; private set; } = default!;
public string ConnectionString { get; private set; } = string.Empty;
public async Task InitializeAsync()
{
await Server.TestDb.StartAsync();
ConnectionString = Server.TestDb.ConnectionString;
var services = new ServiceCollection();
services.AddDbContext<AppDbContext>(opt => opt.UseSqlServer(ConnectionString));
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ApplicationAssembly).Assembly));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
Services = services.BuildServiceProvider();
// Migrate and seed
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();
await SeedAsync(db);
}
public async Task DisposeAsync()
{
await Server.TestDb.StopAsync();
}
private static async Task SeedAsync(AppDbContext db)
{
if (!db.Persons.Any())
{
var personId = Guid.NewGuid();
db.Persons.Add(new MinAttest.Domain.Entities.Person
{
Id = personId,
NationalIdHash = "person-hash-123",
Email = "person@example.com"
});
var employerId = Guid.NewGuid();
db.Employers.Add(new MinAttest.Domain.Entities.Employer
{
Id = employerId,
OrgNumber = "999999999",
Name = "TestBedrift AS"
});
await db.SaveChangesAsync();
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MinAttest.Infrastructure.Data;
namespace MinAttest.Tests.Integration.Server;
public class ApiWebAppFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor is not null)
{
services.Remove(descriptor);
}
services.AddDbContext<AppDbContext>(options => options.UseSqlServer(TestDb.ConnectionString));
});
}
}
@@ -0,0 +1,42 @@
using Testcontainers.MsSql;
namespace MinAttest.Tests.Integration.Server;
public static class TestDb
{
private static MsSqlContainer? _container;
private static readonly object _lock = new();
private static bool _started;
public static string ConnectionString { get; private set; } = string.Empty;
public static async Task StartAsync()
{
if (_started) return;
lock (_lock)
{
if (_container == null)
{
_container = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithPassword("Your_password123")
.WithPortBinding(14333, 1433)
.Build();
}
}
await _container!.StartAsync();
ConnectionString = _container.GetConnectionString() + ";Database=MinAttest_Integration";
_started = true;
}
public static async Task StopAsync()
{
if (_container is null) return;
await _container.StopAsync();
await _container.DisposeAsync();
_container = null;
_started = false;
ConnectionString = string.Empty;
}
}
@@ -0,0 +1,52 @@
using FluentAssertions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using MinAttest.Application.Features.Attests.Commands;
using MinAttest.Application.Features.ShareLinks.Commands;
using MinAttest.Application.Features.ShareLinks.Queries;
using MinAttest.Contracts.Attests;
using MinAttest.Contracts.ShareLinks;
using Xunit;
namespace MinAttest.Tests.Integration;
[Collection("IntegrationCollection")]
public class ShareLinksIntegrationTests
{
private readonly IntegrationTestFixture _fixture;
public ShareLinksIntegrationTests(IntegrationTestFixture fixture) => _fixture = fixture;
[Fact]
public async Task Create_list_revoke_sharelink_for_attest()
{
var mediator = _fixture.Services.GetRequiredService<IMediator>();
// Ensure person
var person = await mediator.Send(new MinAttest.Application.Features.Persons.Commands.UpsertPersonCommand("person-hash-123", "person@example.com", null));
// Create attest
var attestId = await mediator.Send(new PersonUploadAttestCommand(person.Id, new PersonAttestUploadRequest(
Title: "Test",
From: new DateOnly(2023,1,1),
To: new DateOnly(2023,12,31),
Summary: null,
BlobPath: string.Empty,
BlobHash: null,
ContentBase64: Convert.ToBase64String(new byte[]{1,2,3}),
ContentType: "application/octet-stream"
)));
attestId.Should().NotBeNull();
// Create sharelink
var created = await mediator.Send(new CreateShareLinkCommand(attestId!.Value, new CreateShareLinkRequest(TimeSpan.FromDays(7), false), "https://localhost"));
created.Should().NotBeNull();
// List
var list = await mediator.Send(new ListShareLinksQuery(attestId.Value));
list.Should().Contain(sl => sl.ShareId == created!.ShareId);
// Revoke
var revoked = await mediator.Send(new RevokeShareLinkCommand(attestId.Value, created!.ShareId));
revoked.Should().BeTrue();
}
}