Merge branch 'feat/ui-testing' into release/1.0.8-rc3

This commit is contained in:
Paul Schneider 2026-08-23 23:26:59 +01:00
commit bcd72e17d8
Signed by: notazof
GPG key ID: 1DD5D838E5343B06
342 changed files with 1058 additions and 1358 deletions

View file

@ -5,8 +5,6 @@ using Microsoft.Extensions.DependencyInjection;
using Yavsc.Abstract.BlogSpot;
using Yavsc.Models;
using Yavsc.Models.Access;
using Yavsc.Models.Blog;
using Yavsc.Models.Relationship;
using Yavsc.Tests.Shared;
using static Yavsc.Constants;

View file

@ -79,10 +79,10 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
DateModified = DateTime.UtcNow
};
var response = await http.PostAsJsonAsync("/api/v1/blog", draft);
var response = await http.PostAsJsonAsync("/api/v1/blog", draft, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var created = await response.Content.ReadFromJsonAsync<BlogPost>();
var created = await response.Content.ReadFromJsonAsync<BlogPost>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
Assert.Equal("mapped-user", created!.AuthorId);
}
@ -101,10 +101,10 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
Article = "Contenu initial.",
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
});
}, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createdResponse.StatusCode);
var created = await createdResponse.Content.ReadFromJsonAsync<BlogPost>();
var created = await createdResponse.Content.ReadFromJsonAsync<BlogPost>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
var updateResponse = await http.PutAsJsonAsync($"/api/v1/blog/{created!.Id}", new BlogPost
@ -115,7 +115,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
Article = "Contenu mis à jour.",
DateCreated = created.DateCreated,
DateModified = DateTime.UtcNow
});
}, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, updateResponse.StatusCode);
}
@ -134,10 +134,10 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
Article = "Contenu initial.",
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
});
}, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createdResponse.StatusCode);
var created = await createdResponse.Content.ReadFromJsonAsync<BlogPost>();
var created = await createdResponse.Content.ReadFromJsonAsync<BlogPost>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
using var otherHttp = NewClient(subject: "mapped-other");
@ -149,7 +149,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
Article = "Contenu non autorisé.",
DateCreated = created.DateCreated,
DateModified = DateTime.UtcNow
});
}, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Unauthorized, updateResponse.StatusCode);
}

View file

@ -1,7 +1,4 @@
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Tests.Shared;
namespace Yavsc.Blogs.Tests;

View file

@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Json;
@ -115,11 +114,12 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
ResetDatabase();
using var http = NewClient();
var response = await http.GetAsync("/api/v1/blog");
var response = await http.GetAsync("/api/v1/blog",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
// Empty table → empty JSON array. We compare as a JsonDocument
// so a future change in formatting (whitespace, indentation)
// doesn't break the assertion.
@ -147,20 +147,26 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft);
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
// The POST returns the server-issued post (with a real Id).
var created = await postResponse.Content.ReadFromJsonAsync<BlogPost>();
var created = await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
);
Assert.NotNull(created);
Assert.NotEqual(0, created!.Id);
Assert.Equal(draft.Title, created.Title);
// The list should now contain exactly one entry.
var listResponse = await http.GetAsync("/api/v1/blog");
var listResponse = await http.GetAsync("/api/v1/blog",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync());
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
Assert.Equal(1, doc.RootElement.GetArrayLength());
Assert.Equal(created.Id, doc.RootElement[0].GetProperty("id").GetInt64());
@ -182,17 +188,23 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft);
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
var created = await postResponse.Content.ReadFromJsonAsync<BlogPost>();
var created = await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
);
Assert.NotNull(created);
Assert.Equal("tester", created!.AuthorId);
var listResponse = await http.GetAsync("/api/v1/blog");
var listResponse = await http.GetAsync("/api/v1/blog",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync());
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
Assert.Equal(1, doc.RootElement.GetArrayLength());
Assert.Equal("tester", doc.RootElement[0].GetProperty("authorId").GetString());
@ -214,21 +226,27 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft);
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
var createdPost = await postResponse.Content.ReadFromJsonAsync<BlogPost>();
var createdPost = await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
);
Assert.NotNull(createdPost);
Thread.Sleep(100);
var commentResponse = await http.PostAsJsonAsync("/api/v1/blogcomments", new
{
Article = "Premier commentaire",
ReceiverId = createdPost!.Id
});
}, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, commentResponse.StatusCode);
using var doc = JsonDocument.Parse(await commentResponse.Content.ReadAsStringAsync());
using var doc = JsonDocument.Parse(
await commentResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.True(doc.RootElement.TryGetProperty("id", out var id));
Assert.True(id.GetInt64() > 0);
Assert.True(doc.RootElement.TryGetProperty("dateCreated", out _));
@ -257,7 +275,8 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
// the framework returns 401. This is the proof that the
// production policy is wired in the test host and not
// short-circuited by a test-only auth bypass.
var response = await http.GetAsync("/api/v1/blog");
var response = await http.GetAsync("/api/v1/blog",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
@ -284,10 +303,13 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft);
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
var created = (await postResponse.Content.ReadFromJsonAsync<BlogPost>())!;
var created = (await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
))!;
// PUT with the server-issued Id; the controller rejects
// mismatched id/blog.Id with 400, so we keep them aligned.
@ -300,13 +322,19 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateCreated = created.DateCreated,
DateModified = DateTime.UtcNow
};
var putResponse = await http.PutAsJsonAsync($"/api/v1/blog/{created.Id}", update);
var putResponse = await http.PutAsJsonAsync($"/api/v1/blog/{created.Id}",
update,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, putResponse.StatusCode);
// The list should now reflect the new title.
var listResponse = await http.GetAsync("/api/v1/blog");
var listResponse = await http.GetAsync("/api/v1/blog",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync());
using var doc = JsonDocument.Parse(
await listResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
Assert.Equal(1, doc.RootElement.GetArrayLength());
Assert.Equal("Après", doc.RootElement[0].GetProperty("title").GetString());
@ -328,15 +356,23 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft);
var created = (await postResponse.Content.ReadFromJsonAsync<BlogPost>())!;
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
TestContext.Current.CancellationToken);
var created = (await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
))!;
var deleteResponse = await http.DeleteAsync($"/api/v1/blog/{created.Id}");
var deleteResponse = await http.DeleteAsync($"/api/v1/blog/{created.Id}",
TestContext.Current.CancellationToken
);
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
// The list should now be empty.
var listResponse = await http.GetAsync("/api/v1/blog");
String response = await listResponse.Content.ReadAsStringAsync();
var listResponse = await http.GetAsync("/api/v1/blog",
TestContext.Current.CancellationToken);
String response = await listResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
);
using var doc = JsonDocument.Parse(response);
Assert.Equal(0, doc.RootElement.GetArrayLength());
}
@ -375,7 +411,8 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var response = await http.PostAsJsonAsync("/api/v1/blog", draft);
var response = await http.PostAsJsonAsync("/api/v1/blog", draft,
TestContext.Current.CancellationToken);
// Dump the body on failure so the test name + the response
// payload are enough to start a fix; the framework's
@ -383,7 +420,9 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
// Created, got BadRequest").
if (response.StatusCode != HttpStatusCode.Created)
{
var body = await response.Content.ReadAsStringAsync();
var body = await response.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
);
Assert.Fail(string.Format("Expected 201 Created, got {0} {1}. Body: {2}", (int)response.StatusCode, response.StatusCode, body));
}
}
@ -417,11 +456,14 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var response = await http.PostAsJsonAsync("/api/v1/blog", draft);
var response = await http.PostAsJsonAsync("/api/v1/blog", draft,
TestContext.Current.CancellationToken);
if (response.StatusCode != HttpStatusCode.BadRequest)
{
var body = await response.Content.ReadAsStringAsync();
var body = await response.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
);
Assert.Fail(string.Format("Expected 400 BadRequest (empty Title is invalid), got {0} {1}. Body: {2}", (int)response.StatusCode, response.StatusCode, body));
}
}

View file

@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
@ -114,10 +113,10 @@ public sealed class CircleMembersApiTests : IClassFixture<BlogsWebServerFixture>
var circleId = SeedCircle("alice", "Famille");
using var http = NewClient("alice");
var response = await http.GetAsync(MembersUrl(circleId));
var response = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
Assert.Equal(0, doc.RootElement.GetArrayLength());
}
@ -131,14 +130,14 @@ public sealed class CircleMembersApiTests : IClassFixture<BlogsWebServerFixture>
var postResponse = await http.PostAsJsonAsync(
MembersUrl(circleId),
new { userId = "bob" });
new { userId = "bob" }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
var getResponse = await http.GetAsync(MembersUrl(circleId));
var getResponse = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync());
using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
Assert.Equal(1, doc.RootElement.GetArrayLength());
var member = doc.RootElement[0];
@ -156,12 +155,12 @@ public sealed class CircleMembersApiTests : IClassFixture<BlogsWebServerFixture>
var first = await http.PostAsJsonAsync(
MembersUrl(circleId),
new { userId = "bob" });
new { userId = "bob" }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, first.StatusCode);
var second = await http.PostAsJsonAsync(
MembersUrl(circleId),
new { userId = "bob" });
new { userId = "bob" }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Conflict, second.StatusCode);
}
@ -172,14 +171,14 @@ public sealed class CircleMembersApiTests : IClassFixture<BlogsWebServerFixture>
var circleId = SeedCircle("alice", "Famille");
using var http = NewClient("alice");
await http.PostAsJsonAsync(MembersUrl(circleId), new { userId = "bob" });
await http.PostAsJsonAsync(MembersUrl(circleId), new { userId = "bob" }, TestContext.Current.CancellationToken);
var deleteResponse = await http.DeleteAsync(
$"{MembersUrl(circleId)}/bob");
$"{MembersUrl(circleId)}/bob", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
var getResponse = await http.GetAsync(MembersUrl(circleId));
using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync());
var getResponse = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken);
using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal(0, doc.RootElement.GetArrayLength());
}
@ -191,7 +190,7 @@ public sealed class CircleMembersApiTests : IClassFixture<BlogsWebServerFixture>
var circleId = SeedCircle("alice", "Famille");
using var http = NewClient("bob");
var response = await http.GetAsync(MembersUrl(circleId));
var response = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken);
// 404, not 403 — the controller deliberately avoids leaking
// the existence of someone else's circle.

View file

@ -1,5 +1,3 @@
using Xunit;
namespace Yavsc.Blogs.Tests;
[CollectionDefinition("JwtClaimMapping", DisableParallelization = true)]

View file

@ -1,6 +1,5 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;

View file

@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
@ -101,12 +100,12 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
var postId = SeedPost("alice");
using var http = NewClient("alice");
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true });
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
var get = await http.GetAsync($"{BlogsUrl}/{postId}");
var get = await http.GetAsync($"{BlogsUrl}/{postId}", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, get.StatusCode);
using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync());
using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.True(doc.RootElement.GetProperty("isPublished").GetBoolean());
}
@ -117,12 +116,12 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
var postId = SeedPost("alice");
using var http = NewClient("alice");
await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true });
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = false });
await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = false }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
var get = await http.GetAsync($"{BlogsUrl}/{postId}");
using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync());
var get = await http.GetAsync($"{BlogsUrl}/{postId}", TestContext.Current.CancellationToken);
using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.False(doc.RootElement.GetProperty("isPublished").GetBoolean());
}
@ -131,7 +130,7 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
{
ResetDatabase();
using var http = NewClient("alice");
var put = await http.PutAsJsonAsync(PublishUrl(99999L), new { publish = true });
var put = await http.PutAsJsonAsync(PublishUrl(99999L), new { publish = true }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, put.StatusCode);
}
@ -142,7 +141,7 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
var postId = SeedPost("alice");
using var http = NewClient("bob");
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true });
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
// 401 Challenge (the controller returns Challenge()
// for AuthorizationFailureException). The exact code
// is framework-dependent; what matters is "not 204".

View file

@ -35,4 +35,4 @@
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>