Fixes ACL backend support
This commit is contained in:
parent
0034a311f2
commit
e62adedc17
14 changed files with 205 additions and 56 deletions
|
|
@ -1,10 +1,12 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Yavsc.Abstract.BlogSpot;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Access;
|
||||
using Yavsc.Models.Blog;
|
||||
using Yavsc.Tests.Shared;
|
||||
using static Yavsc.Constants;
|
||||
|
||||
|
|
@ -38,15 +40,16 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
{
|
||||
private readonly BlogsWebServerFixture _fixture;
|
||||
|
||||
|
||||
public BlogAclApiTests(BlogsWebServerFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
|
||||
private string BlogUrl()
|
||||
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogSpotPath}";
|
||||
private string BlogAclUrl()
|
||||
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/blogacl";
|
||||
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogAclPath}";
|
||||
|
||||
/// <summary>Delete any ACL rows tied to the fixture's seeded
|
||||
/// <c>(CircleId, BlogPostId)</c> pair. The shared SQLite store
|
||||
|
|
@ -120,7 +123,7 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
// owned by the caller. We seed the same shape pre-POST so the
|
||||
// test reproduces the prod scenario end-to-end.
|
||||
CleanupAcl();
|
||||
using var http = NewClient("alice");
|
||||
using var http = NewClient(_fixture.DefaultUserLogin);
|
||||
|
||||
var payload = new PostAccessControlRulePayload
|
||||
{
|
||||
|
|
@ -178,7 +181,7 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[MemberData(nameof(BlogAclPayloadsForNever500))]
|
||||
public async Task PostCircleAuthorization_never_returns_500(PostAccessControlRulePayload payload)
|
||||
{
|
||||
using var http = NewClient("alice");
|
||||
using var http = NewClient(_fixture.DefaultUserLogin);
|
||||
|
||||
var response = await http.PostAsJsonAsync(
|
||||
BlogAclUrl(), payload,
|
||||
|
|
@ -216,4 +219,83 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostBlog_with_ACL_creates_a_post_and_Get_returns_it_in_the_list()
|
||||
{
|
||||
CleanupAcl();
|
||||
_fixture.SeedUser(_fixture.DefaultUserLogin);
|
||||
_fixture.SeedUser("tester");
|
||||
_fixture.SeedCircle(_fixture.DefaultUserLogin, "test",
|
||||
false,
|
||||
new String[]
|
||||
{
|
||||
_fixture.DefaultUserLogin,
|
||||
"tester"
|
||||
});
|
||||
using var http = NewClient(_fixture.DefaultUserLogin );
|
||||
|
||||
// Create a minimal BlogPost. The server assigns Id, so we
|
||||
// send 0 + an explicit AuthorId; the production
|
||||
// BlogSpotService.Create() tolerates that.
|
||||
var draft = new BlogPost
|
||||
{
|
||||
Id = 0,
|
||||
Title = "Premier billet",
|
||||
AuthorId = "tester",
|
||||
Article = "Contenu de test.",
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
ACL = new List<CircleAuthorizationToBlogPost>(
|
||||
new CircleAuthorizationToBlogPost[]
|
||||
{
|
||||
new CircleAuthorizationToBlogPost
|
||||
{
|
||||
CircleId = _fixture.CircleId,
|
||||
BlogPostId = _fixture.PostId
|
||||
}
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
var postResponse = await http.PostAsJsonAsync(
|
||||
BlogUrl(),
|
||||
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>(
|
||||
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(
|
||||
BlogUrl(),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
|
||||
|
||||
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync(
|
||||
TestContext.Current.CancellationToken
|
||||
));
|
||||
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
|
||||
Assert.Equal(2, doc.RootElement.GetArrayLength());
|
||||
Assert.Equal(created.Id, doc.RootElement[0].GetProperty("id").GetInt64());
|
||||
|
||||
// detail should return the same post, with ACL and tags.
|
||||
var detailResponse = await http.GetAsync(
|
||||
$"{BlogUrl()}/{created.Id}",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode);
|
||||
using var detailDoc = JsonDocument.Parse(await detailResponse.Content.ReadAsStringAsync(
|
||||
TestContext.Current.CancellationToken
|
||||
));
|
||||
Assert.Equal(JsonValueKind.Object, detailDoc.RootElement.ValueKind);
|
||||
Assert.Equal(created.Id, detailDoc.RootElement.GetProperty("id").GetInt64());
|
||||
Assert.Equal(1, detailDoc.RootElement.GetProperty("acl").GetArrayLength());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System.Net;
|
|||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Blog;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
|
@ -31,18 +30,6 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Reset the in-memory database to a known empty state.
|
||||
/// <c>UseInMemoryDatabase</c> shares its store across the
|
||||
/// lifetime of the <see cref="BlogsWebServerFixture"/> instance,
|
||||
/// so without a per-test reset the test order would leak
|
||||
/// state between tests.</summary>
|
||||
private void ResetDatabase()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.Database.EnsureDeleted();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
/// <summary>Reset the database and seed the
|
||||
/// <c>tester</c> <see cref="ApplicationUser"/> row. Required
|
||||
|
|
@ -55,7 +42,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
/// at <c>SaveChanges</c> and the controller returns 500.</summary>
|
||||
private void ResetAndSeedDefaultUser()
|
||||
{
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
_fixture.SeedUser("tester");
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +98,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task GetBlogs_returns_200_with_empty_list_when_no_posts()
|
||||
{
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
using var http = NewClient();
|
||||
|
||||
var response = await http.GetAsync("/api/v1/blog",
|
||||
|
|
@ -266,7 +253,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task GetBlog_returns_401_when_no_token_is_provided()
|
||||
{
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
using var http = NewAnonymousClient();
|
||||
|
||||
// No Authorization header → the JwtBearer middleware
|
||||
|
|
@ -443,7 +430,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
// behaviour so a future change that, say, makes Title
|
||||
// nullable in the model or drops [Required], triggers a
|
||||
// conscious update of the test (and probably of the VM).
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
using var http = NewClient(subject: "tester");
|
||||
|
||||
var draft = new BlogPost
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
|
||||
public long CircleId { get; private set; }
|
||||
public long PostId { get; private set; }
|
||||
public string DefaultUserLogin { get => "alice"; }
|
||||
|
||||
// A single SqliteConnection held open at the static level,
|
||||
// mirroring how Yavsc.Org.Tests.WebServerFixture hoists its
|
||||
|
|
@ -169,7 +170,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
// PermissionHandler ownership check sees a null
|
||||
// user id and rejects every PUT.
|
||||
options.MapInboundClaims = false;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
options.TokenValidationParameters
|
||||
= new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = TestTokenIssuer.Issuer,
|
||||
|
|
@ -263,6 +265,27 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
await Task.CompletedTask;
|
||||
return app;
|
||||
}
|
||||
/// <summary>Reset the in-memory database to a known empty state.
|
||||
/// <c>UseInMemoryDatabase</c> shares its store across the
|
||||
/// lifetime of the <see cref="BlogsWebServerFixture"/> instance,
|
||||
/// so without a per-test reset the test order would leak
|
||||
/// state between tests.</summary>
|
||||
public void ResetDatabase()
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.Database.EnsureDeleted();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
public void CleanupAcl()
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.CircleAuthorizationToBlogPost
|
||||
.Where(a => a.CircleId == CircleId
|
||||
&& a.BlogPostId == PostId)
|
||||
.ExecuteDelete();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
|
|
@ -310,7 +333,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
/// <param name="configure">Optional hook to fill in fields
|
||||
/// like <c>FullName</c> / <c>Avatar</c> / <c>EmailConfirmed</c>
|
||||
/// that downstream tests assert on.</param>
|
||||
public ApplicationUser SeedUser(string userName, Action<ApplicationUser>? configure = null)
|
||||
public ApplicationUser SeedUser(string userName,
|
||||
Action<ApplicationUser>? configure = null)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
|
|
@ -351,20 +375,31 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
/// <summary>Create a circle owned by <paramref name="ownerId"/>
|
||||
/// directly in the SQLite store and return its server-assigned
|
||||
/// id.</summary>
|
||||
private long SeedCircle(string ownerId, string name, bool isPublic = false)
|
||||
public long SeedCircle(string ownerId, string name, bool isPublic = false,
|
||||
ICollection<String> members = null
|
||||
)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
var circle = new Circle { OwnerId = ownerId, Name = name, Public = isPublic };
|
||||
db.Circle.Add(circle);
|
||||
db.SaveChanges();
|
||||
if (members != null && members.Count > 0)
|
||||
{
|
||||
foreach (String memberId in members)
|
||||
{
|
||||
var member = new CircleMember { CircleId = circle.Id, MemberId = memberId };
|
||||
db.CircleMembers.Add(member);
|
||||
}
|
||||
db.SaveChanges();
|
||||
}
|
||||
return circle.Id;
|
||||
}
|
||||
|
||||
/// <summary>Create a blog post owned by <paramref name="authorId"/>
|
||||
/// directly in the SQLite store and return its server-assigned
|
||||
/// id.</summary>
|
||||
private long SeedBlogPost(string authorId, string title)
|
||||
public long SeedBlogPost(string authorId, string title)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue