Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/api-tests
diff options
context:
space:
mode:
authorLeitwolf112026-11-06 12:57:27 +0300
committerLeitwolf112026-11-06 12:57:27 +0300
commit53fb8b8bde857025d12655ae53857fc30226b186 (patch)
treed0963feb1dddeedbd0980eb242dcd7e3e7c923e6 /apps/api-tests
parent8f18eba55a8980141bd7b989378cb557ae927e2e (diff)
downloadprogmaweb-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.cs58
-rw-r--r--apps/api-tests/AccountServiceTests.cs64
-rw-r--r--apps/api-tests/AccountStoreContractTests.cs20
-rw-r--r--apps/api-tests/AccountStoreSelectionTests.cs9
-rw-r--r--apps/api-tests/OAuthTestClient.cs2
-rw-r--r--apps/api-tests/SessionLifetimeTests.cs2
-rw-r--r--apps/api-tests/UnconfiguredGoogleTests.cs2
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);