Initial curated itworx-knowledge-hub source from 3b1751d0d9dee551aa6994eee0093704dc334817
This commit is contained in:
commit
ad8ad252a9
825 files changed
+124216
No files matched your search
@@ -0,0 +1,147 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using ItWorx.KnowledgeHub.Contracts.Admin;
|
||||
using ItWorx.KnowledgeHub.Contracts.Auth;
|
||||
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
||||
|
||||
namespace ItWorx.KnowledgeHub.IntegrationTests.Auth;
|
||||
|
||||
[Collection(PostgresTestGroup.Name)]
|
||||
public sealed class RecoveryTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Administrator_issued_token_lets_the_user_set_a_new_password_and_old_sessions_stop_working()
|
||||
{
|
||||
using var adminClient = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(adminClient, OwnerEmail, OwnerPassword);
|
||||
|
||||
const string email = "vergeten@itworx.test";
|
||||
const string originalPassword = "original-strong-password";
|
||||
var createResponse = await SetupHelper.PostWithCsrfAsync(
|
||||
adminClient,
|
||||
"/api/v1/admin/users",
|
||||
new CreateUserRequest("Vergeetachtige Gebruiker", email, originalPassword, "Editor"));
|
||||
var user = await createResponse.Content.ReadFromJsonAsync<UserSummaryResponse>();
|
||||
Assert.NotNull(user);
|
||||
|
||||
// The user logs in once with the original password to prove that session gets
|
||||
// invalidated once the password is reset via recovery.
|
||||
using var userClient = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(userClient, email, originalPassword);
|
||||
var beforeReset = await userClient.GetFromJsonAsync<SessionResponse>("/api/v1/auth/session");
|
||||
Assert.NotNull(beforeReset);
|
||||
Assert.True(beforeReset.Authenticated);
|
||||
|
||||
var issueResponse = await SetupHelper.PostWithCsrfAsync<object?>(
|
||||
adminClient,
|
||||
$"/api/v1/admin/users/{user.Id}/recovery-tokens",
|
||||
null);
|
||||
Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode);
|
||||
|
||||
var issued = await issueResponse.Content.ReadFromJsonAsync<IssueRecoveryTokenResponse>();
|
||||
Assert.NotNull(issued);
|
||||
Assert.False(string.IsNullOrWhiteSpace(issued.Token));
|
||||
|
||||
const string newPassword = "brand-new-strong-password";
|
||||
using var redeemClient = Factory.CreateClient();
|
||||
var redeemResponse = await redeemClient.PostAsJsonAsync(
|
||||
"/api/v1/auth/recovery/redeem",
|
||||
new RecoveryRedeemRequest(issued.Token, newPassword));
|
||||
Assert.Equal(HttpStatusCode.OK, redeemResponse.StatusCode);
|
||||
|
||||
// The pre-reset session must be dead now (security stamp rotated).
|
||||
var afterReset = await userClient.GetFromJsonAsync<SessionResponse>("/api/v1/auth/session");
|
||||
Assert.NotNull(afterReset);
|
||||
Assert.False(afterReset.Authenticated);
|
||||
|
||||
// The old password no longer works...
|
||||
using var oldPasswordClient = Factory.CreateClient();
|
||||
var oldPasswordAttempt = await SetupHelper.LoginAsync(oldPasswordClient, email, originalPassword);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, oldPasswordAttempt.StatusCode);
|
||||
|
||||
// ...but the new one does.
|
||||
using var newPasswordClient = Factory.CreateClient();
|
||||
var newPasswordAttempt = await SetupHelper.LoginAsync(newPasswordClient, email, newPassword);
|
||||
Assert.Equal(HttpStatusCode.OK, newPasswordAttempt.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Redeeming_the_same_recovery_token_twice_only_works_once()
|
||||
{
|
||||
using var adminClient = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(adminClient, OwnerEmail, OwnerPassword);
|
||||
|
||||
const string email = "eenmalig@itworx.test";
|
||||
var createResponse = await SetupHelper.PostWithCsrfAsync(
|
||||
adminClient,
|
||||
"/api/v1/admin/users",
|
||||
new CreateUserRequest("Eenmalige Gebruiker", email, "starting-strong-password", "Reader"));
|
||||
var user = await createResponse.Content.ReadFromJsonAsync<UserSummaryResponse>();
|
||||
Assert.NotNull(user);
|
||||
|
||||
var issueResponse = await SetupHelper.PostWithCsrfAsync<object?>(
|
||||
adminClient,
|
||||
$"/api/v1/admin/users/{user.Id}/recovery-tokens",
|
||||
null);
|
||||
var issued = await issueResponse.Content.ReadFromJsonAsync<IssueRecoveryTokenResponse>();
|
||||
Assert.NotNull(issued);
|
||||
|
||||
using var client = Factory.CreateClient();
|
||||
|
||||
var first = await client.PostAsJsonAsync(
|
||||
"/api/v1/auth/recovery/redeem",
|
||||
new RecoveryRedeemRequest(issued.Token, "first-new-strong-password"));
|
||||
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
|
||||
|
||||
var second = await client.PostAsJsonAsync(
|
||||
"/api/v1/auth/recovery/redeem",
|
||||
new RecoveryRedeemRequest(issued.Token, "second-new-strong-password"));
|
||||
Assert.Equal(HttpStatusCode.BadRequest, second.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Redeeming_an_unknown_token_fails_without_revealing_anything()
|
||||
{
|
||||
using var client = Factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/api/v1/auth/recovery/redeem",
|
||||
new RecoveryRedeemRequest("ikhrec_not-a-real-token", "whatever-strong-password"));
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
// V2-M7: closes the recovery-token admin UI (issuance) — this authorization
|
||||
// boundary (matching TrashPurgeTests.Purge_is_administrator_only's exact pattern)
|
||||
// had no test at all before, even though the endpoint itself already correctly
|
||||
// requires Administrator via AdminEndpoints' group-level policy.
|
||||
[Fact]
|
||||
public async Task Issuing_a_recovery_token_is_administrator_only()
|
||||
{
|
||||
using var adminClient = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(adminClient, OwnerEmail, OwnerPassword);
|
||||
|
||||
var targetEmail = "doelgebruiker-recovery@itworx.test";
|
||||
var createResponse = await SetupHelper.PostWithCsrfAsync(
|
||||
adminClient,
|
||||
"/api/v1/admin/users",
|
||||
new CreateUserRequest("Doelgebruiker", targetEmail, "starting-strong-password", "Reader"));
|
||||
var target = await createResponse.Content.ReadFromJsonAsync<UserSummaryResponse>();
|
||||
Assert.NotNull(target);
|
||||
|
||||
const string editorEmail = "redacteur-recovery@itworx.test";
|
||||
const string editorPassword = "correct-horse-battery-staple";
|
||||
await SetupHelper.PostWithCsrfAsync(
|
||||
adminClient,
|
||||
"/api/v1/admin/users",
|
||||
new CreateUserRequest("Redacteur", editorEmail, editorPassword, "Editor"));
|
||||
|
||||
using var editorClient = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(editorClient, editorEmail, editorPassword);
|
||||
|
||||
var response = await SetupHelper.PostWithCsrfAsync<object?>(
|
||||
editorClient, $"/api/v1/admin/users/{target.Id}/recovery-tokens", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user