diff options
| author | Leitwolf | 2026-11-07 11:43:30 +0300 |
|---|---|---|
| committer | GitHub | 2026-11-07 11:43:30 +0300 |
| commit | d28c8c216708c3bb59773722e6235c4c8cbb2b73 (patch) | |
| tree | 00101b2c540df4f683a0aae890f8366f473f2be4 /apps/api-tests | |
| parent | 097fb096966117fe259bc2cd721527bbec04e57d (diff) | |
| parent | 5379c74171c0ef0d08c999ce93e2a03c37d25889 (diff) | |
| download | progmaweb-d28c8c216708c3bb59773722e6235c4c8cbb2b73.tar.gz | |
Merge pull request #58 from Progmasoft/feature/policies-consent
Ask for the Privacy and Security Policies at registration
Diffstat (limited to 'apps/api-tests')
| -rw-r--r-- | apps/api-tests/AccountEndpointTests.cs | 58 | ||||
| -rw-r--r-- | apps/api-tests/AccountServiceTests.cs | 64 | ||||
| -rw-r--r-- | apps/api-tests/AccountStoreContractTests.cs | 20 | ||||
| -rw-r--r-- | apps/api-tests/AccountStoreSelectionTests.cs | 9 | ||||
| -rw-r--r-- | apps/api-tests/OAuthTestClient.cs | 2 | ||||
| -rw-r--r-- | apps/api-tests/SessionLifetimeTests.cs | 2 | ||||
| -rw-r--r-- | apps/api-tests/UnconfiguredGoogleTests.cs | 2 |
7 files changed, 144 insertions, 13 deletions
diff --git a/apps/api-tests/AccountEndpointTests.cs b/apps/api-tests/AccountEndpointTests.cs index c5c305e..50d43d5 100644 --- a/apps/api-tests/AccountEndpointTests.cs +++ b/apps/api-tests/AccountEndpointTests.cs @@ -22,7 +22,7 @@ public sealed class AccountEndpointTests using HttpClient client = factory.CreateAccountClient(handleCookies: false); HttpResponseMessage response = await client.GetAsync( - "/api/v1/accounts/oauth/google/start?accountName=GoogleUser", + "/api/v1/accounts/oauth/google/start?accountName=GoogleUser&acceptPolicies=true", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); @@ -62,6 +62,47 @@ public sealed class AccountEndpointTests Assert.Equal("/register?google=invalid-account-name", response.Headers.Location?.OriginalString); } + [Theory] + [InlineData("/api/v1/accounts/oauth/google/start?accountName=GoogleUser")] + [InlineData("/api/v1/accounts/oauth/google/start?accountName=GoogleUser&acceptPolicies=false")] + public async Task GoogleRegistrationWithoutThePoliciesStaysOnTheSite(string start) + { + await using AccountApiFactory factory = new(); + using HttpClient client = factory.CreateAccountClient(handleCookies: false); + + HttpResponseMessage response = await client.GetAsync(start, TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/register?google=policies-required", response.Headers.Location?.OriginalString); + } + + [Theory] + [InlineData(null)] + [InlineData(false)] + public async Task RegistrationWithoutThePoliciesIsRefused(bool? acceptPolicies) + { + await using AccountApiFactory factory = new(); + using HttpClient client = factory.CreateAccountClient(); + + HttpResponseMessage response = await client.PostAsJsonAsync("/api/v1/accounts/register", new + { + accountName = "PolicyUser", + email = "policy@example.com", + password = "a-long-development-password", + acceptPolicies + }, TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + using JsonDocument problem = JsonDocument.Parse( + await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + JsonElement messages = problem.RootElement.GetProperty("errors").GetProperty("acceptPolicies"); + Assert.Equal([PolicyAcceptance.RequiredMessage], messages.EnumerateArray().Select(message => message.GetString())); + + // The refusal kept neither the name nor the address. + HttpResponseMessage accepted = await RegisterAsync(client, "PolicyUser", "policy@example.com"); + Assert.Equal(HttpStatusCode.Created, accepted.StatusCode); + } + [Fact] public async Task GoogleCompleteDoesNotIssueASessionForAnExistingPasswordAccountWithTheSameEmail() { @@ -109,7 +150,8 @@ public sealed class AccountEndpointTests { accountName = "PrimaryUser", email = "leitwolf1112@gmail.com", - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Created, registration.StatusCode); @@ -139,7 +181,8 @@ public sealed class AccountEndpointTests { accountName = "LogoutUser", email = "logout@example.com", - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Created, registration.StatusCode); @@ -165,7 +208,8 @@ public sealed class AccountEndpointTests { accountName, email = $"{Guid.NewGuid():N}@example.com", - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); @@ -229,7 +273,8 @@ public sealed class AccountEndpointTests HttpResponseMessage missing = await client.PostAsJsonAsync("/api/v1/accounts/login", new { email = "missing@example.com", - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); HttpResponseMessage wrong = await client.PostAsJsonAsync("/api/v1/accounts/login", new { @@ -284,6 +329,7 @@ public sealed class AccountEndpointTests { accountName, email, - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); } diff --git a/apps/api-tests/AccountServiceTests.cs b/apps/api-tests/AccountServiceTests.cs index 0ff7b33..27863bf 100644 --- a/apps/api-tests/AccountServiceTests.cs +++ b/apps/api-tests/AccountServiceTests.cs @@ -23,6 +23,7 @@ public sealed class AccountServiceTests null, "google@example.com", "GoogleUser", + PolicyAcceptance.CurrentVersion, sessions, TestContext.Current.CancellationToken); @@ -44,7 +45,8 @@ public sealed class AccountServiceTests new RegisterAccountRequest( "PasswordUser", "unverified@example.com", - "a-long-development-password"), + "a-long-development-password", + true), sessions, TestContext.Current.CancellationToken); Assert.NotNull(registration); @@ -54,6 +56,7 @@ public sealed class AccountServiceTests "google-subject-victim", "unverified@example.com", null, + null, sessions, TestContext.Current.CancellationToken); @@ -85,6 +88,7 @@ public sealed class AccountServiceTests "google-subject-race", "race@example.com", "GoogleUser", + PolicyAcceptance.CurrentVersion, sessions, TestContext.Current.CancellationToken); @@ -107,6 +111,7 @@ public sealed class AccountServiceTests "google-subject-stable", "google@example.com", "GoogleUser", + PolicyAcceptance.CurrentVersion, sessions, TestContext.Current.CancellationToken); @@ -115,6 +120,7 @@ public sealed class AccountServiceTests "google-subject-stable", "changed@example.com", null, + null, sessions, TestContext.Current.CancellationToken); @@ -125,6 +131,62 @@ public sealed class AccountServiceTests Assert.Equal(first.Account.Id, second.Account.Id); } + [Fact] + public async Task RegistrationKeepsTheVersionOfThePoliciesAndTheMoment() + { + InMemoryAccountStore store = new(); + AccountService accounts = new(store, TimeProvider.System); + SessionService sessions = new( + store, + TimeProvider.System, + Options.Create(new AccountSessionOptions())); + + (AuthenticatedAccount? password, _) = await accounts.RegisterAsync( + new RegisterAccountRequest("PasswordUser", "password@example.com", "a-long-development-password", true), + sessions, + TestContext.Current.CancellationToken); + (AuthenticatedAccount? google, _) = await accounts.AuthenticateGoogleAsync( + "google-subject-policies", + "google@example.com", + "GoogleUser", + PolicyAcceptance.CurrentVersion, + sessions, + TestContext.Current.CancellationToken); + + foreach (AuthenticatedAccount? authentication in new[] { password, google }) + { + Assert.NotNull(authentication); + Assert.Equal(PolicyAcceptance.CurrentVersion, authentication.Account.PoliciesVersion); + Assert.Equal(authentication.Account.CreatedAt, authentication.Account.PoliciesAcceptedAt); + } + } + + [Theory] + [InlineData(null)] + [InlineData("2020-01-01")] + public async Task GoogleAuthenticationCreatesNoAccountWithoutTheCurrentPolicies(string? policies) + { + InMemoryAccountStore store = new(); + AccountService accounts = new(store, TimeProvider.System); + SessionService sessions = new( + store, + TimeProvider.System, + Options.Create(new AccountSessionOptions())); + + (AuthenticatedAccount? authentication, GoogleAccountFailure failure) = + await accounts.AuthenticateGoogleAsync( + "google-subject-new", + "new@example.com", + "GoogleUser", + policies, + sessions, + TestContext.Current.CancellationToken); + + Assert.Null(authentication); + Assert.Equal(GoogleAccountFailure.PoliciesRequired, failure); + Assert.Null(await store.FindByGoogleSubjectAsync("google-subject-new", TestContext.Current.CancellationToken)); + } + private sealed class ConcurrentPasswordRegistrationStore(AccountRecord passwordAccount) : IAccountStore { private bool createAttempted; diff --git a/apps/api-tests/AccountStoreContractTests.cs b/apps/api-tests/AccountStoreContractTests.cs index 2266273..f9f36be 100644 --- a/apps/api-tests/AccountStoreContractTests.cs +++ b/apps/api-tests/AccountStoreContractTests.cs @@ -65,6 +65,26 @@ public abstract class AccountStoreContractTests Assert.Equal("AQAAAA-hash", found.PasswordHash); Assert.Equal("Grace", found.AccountName); Assert.Equal("grace@example.com", found.Email); + Assert.Null(found.PoliciesVersion); + Assert.Null(found.PoliciesAcceptedAt); + } + + [Fact] + public async Task AnAccountKeepsThePoliciesItAccepted() + { + IAccountStore store = await CreateStoreAsync(); + AccountRecord account = Account("Hopper", "hopper@example.com") with + { + PoliciesVersion = PolicyAcceptance.CurrentVersion, + PoliciesAcceptedAt = Moment + }; + + AccountRecord stored = Assert.IsType<AccountRecord>((await store.CreateAsync(account, Cancellation)).Account); + + AccountRecord found = Assert.IsType<AccountRecord>(await store.FindByIdAsync(account.Id, Cancellation)); + Assert.Equal(stored, found); + Assert.Equal(PolicyAcceptance.CurrentVersion, found.PoliciesVersion); + Assert.True((Moment - found.PoliciesAcceptedAt!.Value).Duration() < TimeSpan.FromMicroseconds(1)); } [Fact] diff --git a/apps/api-tests/AccountStoreSelectionTests.cs b/apps/api-tests/AccountStoreSelectionTests.cs index 1087575..f1153dd 100644 --- a/apps/api-tests/AccountStoreSelectionTests.cs +++ b/apps/api-tests/AccountStoreSelectionTests.cs @@ -223,7 +223,8 @@ public sealed class DurableAccountApiTests { accountName = "Survivor", email = "survivor@example.com", - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Created, registration.StatusCode); cookie = await SessionCookieAsync(registration); @@ -244,7 +245,8 @@ public sealed class DurableAccountApiTests HttpResponseMessage login = await restarted.PostAsJsonAsync("/api/v1/accounts/login", new { email = "survivor@example.com", - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, login.StatusCode); @@ -252,7 +254,8 @@ public sealed class DurableAccountApiTests { accountName = "survivor", email = "other@example.com", - password = "a-long-development-password" + password = "a-long-development-password", + acceptPolicies = true }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, again.StatusCode); diff --git a/apps/api-tests/OAuthTestClient.cs b/apps/api-tests/OAuthTestClient.cs index a176c04..a5d342d 100644 --- a/apps/api-tests/OAuthTestClient.cs +++ b/apps/api-tests/OAuthTestClient.cs @@ -37,7 +37,7 @@ internal sealed class OAuthTestClient(AccountApiFactory factory) { using HttpClient accounts = factory.CreateAccountClient(handleCookies: false); HttpResponseMessage registered = await accounts.PostAsJsonAsync("/api/v1/accounts/register", - new { accountName, email, password = "correct horse battery staple" }, Cancel); + new { accountName, email, password = "correct horse battery staple", acceptPolicies = true }, Cancel); Assert.Equal(HttpStatusCode.Created, registered.StatusCode); // The console knows the account of the Progmasoft account session; it has no sign-in of its own. string session = registered.Headers.GetValues("Set-Cookie") diff --git a/apps/api-tests/SessionLifetimeTests.cs b/apps/api-tests/SessionLifetimeTests.cs index 8626186..aa9d9ea 100644 --- a/apps/api-tests/SessionLifetimeTests.cs +++ b/apps/api-tests/SessionLifetimeTests.cs @@ -22,7 +22,7 @@ public sealed class SessionLifetimeTests AccountService accounts = new(store, clock); SessionService sessions = new(store, clock, Options.Create(options)); (AuthenticatedAccount? registration, _) = await accounts.RegisterAsync( - new RegisterAccountRequest("LifetimeUser", "lifetime@example.com", "a-long-development-password"), + new RegisterAccountRequest("LifetimeUser", "lifetime@example.com", "a-long-development-password", true), sessions, TestContext.Current.CancellationToken); return Assert.IsType<AuthenticatedAccount>(registration).ExpiresAt; diff --git a/apps/api-tests/UnconfiguredGoogleTests.cs b/apps/api-tests/UnconfiguredGoogleTests.cs index d4e8c53..9a7ca77 100644 --- a/apps/api-tests/UnconfiguredGoogleTests.cs +++ b/apps/api-tests/UnconfiguredGoogleTests.cs @@ -36,7 +36,7 @@ public sealed class UnconfiguredGoogleTests HttpResponseMessage registration = await client.PostAsJsonAsync( "/api/v1/accounts/register", - new RegisterAccountRequest("LocalUser1", "local@example.com", password), + new RegisterAccountRequest("LocalUser1", "local@example.com", password, true), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Created, registration.StatusCode); |