diff options
| author | Leitwolf11 | 2026-11-06 12:57:27 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-06 12:57:27 +0300 |
| commit | 53fb8b8bde857025d12655ae53857fc30226b186 (patch) | |
| tree | d0963feb1dddeedbd0980eb242dcd7e3e7c923e6 /apps/api-tests | |
| parent | 8f18eba55a8980141bd7b989378cb557ae927e2e (diff) | |
| download | progmaweb-53fb8b8bde857025d12655ae53857fc30226b186.tar.gz | |
Ask for the Privacy and Security Policies at registration
progmasoft.com/privacy and progmasoft.com/security publish the two
policies in en-US, de-DE, ru-RU and he-IL, and the footer links both.
The Privacy Policy says what an account keeps and that Progmasoft keeps
an IP address for one purpose only: with a ViGet upload, to establish
where an upload that turns out to be malicious software came from and
to give it to the competent authorities. It says that no data, the IP
address included, is sold, rented or traded. The Security Policy covers
passwords, sessions, HTTPS, the ban on malicious software in ViGet and
how to report a weakness.
The registration form asks the person to accept both policies, and the
API refuses a registration without that acceptance, with a password
(acceptPolicies, a validation error of that field) or through Google:
the start of a Google registration without acceptPolicies=true goes
back to /register?google=policies-required, and the completion creates
no account unless the authentication state carries the current version.
An account keeps the version it accepted and the moment, in two new
columns that schema step 2 adds; accounts created before it keep null.
PolicyAcceptance.CurrentVersion and policyVersion on the web side name
the same date, and a web test reads the C# constant to keep them equal.
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); |