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

summaryrefslogtreecommitdiffhomepage
path: root/apps/api-tests
diff options
context:
space:
mode:
authorLeitwolf112026-10-03 18:46:35 +0300
committerLeitwolf112026-10-03 18:46:35 +0300
commitec774db15976385a6d5ea6f48d3622fe507d3843 (patch)
tree285451ccb4ff35b37f89805415003e372fda02ec /apps/api-tests
parentbc1b0b080b398e52b3201034f92398750f415ed2 (diff)
downloadprogmaweb-ec774db15976385a6d5ea6f48d3622fe507d3843.tar.gz
Run the account API without a Google client
With an empty Google client identifier or secret the API answered every request with 500, including registration, password sign-in and the status route. The Google scheme was always registered, and the authentication middleware initializes every remote scheme for each request; with empty credentials the validation of the Google options threw there, before any endpoint ran. The API could not be started locally without credentials, and the "Google sign-in is unavailable" answer of the start endpoint was never reached. The Google scheme is now registered only when both values are configured. Without it the service runs normally, the start endpoint answers 503, the completion endpoint redirects to the failed sign-in page without a session, and the callback path is not a route. With both values nothing changes. The test factory passes the two values as host settings. The program makes the decision while it registers services, and configuration sources a test factory adds are applied only when the host is built, after that point. New tests run the service without a Google client: the ordinary routes, a full register, sign-out and sign-in round trip, and the three Google paths. They failed with 500 against the previous program and pass now, also when the environment of the test run carries Google credentials. The guides no longer list this as a limitation or ask for placeholder credentials. Deploying this change restarts the API, which deletes every account while the store is in memory.
Diffstat (limited to 'apps/api-tests')
-rw-r--r--apps/api-tests/AccountApiFactory.cs19
-rw-r--r--apps/api-tests/UnconfiguredGoogleTests.cs97
2 files changed, 112 insertions, 4 deletions
diff --git a/apps/api-tests/AccountApiFactory.cs b/apps/api-tests/AccountApiFactory.cs
index 373cd84..89faa3e 100644
--- a/apps/api-tests/AccountApiFactory.cs
+++ b/apps/api-tests/AccountApiFactory.cs
@@ -7,19 +7,30 @@ using Microsoft.Extensions.Configuration;
namespace Progmasoft.Progmaweb.Api.Tests;
-internal sealed class AccountApiFactory : WebApplicationFactory<Program>
+/// <summary>Starts the account API in memory for a test.</summary>
+/// <param name="configureGoogle">
+/// Whether the Google client is configured. Without it the service has to run and refuse only Google sign-in.
+/// </param>
+internal sealed class AccountApiFactory(bool configureGoogle = true) : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
+
+ // The program decides while it registers services whether the Google scheme exists, so these two values
+ // have to be host settings: configuration sources added below are applied only when the host is built,
+ // after that decision. An empty value also overrides credentials the environment of the test run carries.
+ builder.UseSetting(
+ "Authentication:Google:ClientId",
+ configureGoogle ? "test-client.apps.googleusercontent.com" : string.Empty);
+ builder.UseSetting("Authentication:Google:ClientSecret", configureGoogle ? "test-secret" : string.Empty);
+
builder.ConfigureAppConfiguration((_, configuration) =>
{
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["AccountSessions:LifetimeHours"] = "2",
- ["AccountSessions:CookieName"] = "__Host-ProgmasoftSession",
- ["Authentication:Google:ClientId"] = "test-client.apps.googleusercontent.com",
- ["Authentication:Google:ClientSecret"] = "test-secret"
+ ["AccountSessions:CookieName"] = "__Host-ProgmasoftSession"
});
});
}
diff --git a/apps/api-tests/UnconfiguredGoogleTests.cs b/apps/api-tests/UnconfiguredGoogleTests.cs
new file mode 100644
index 0000000..d4e8c53
--- /dev/null
+++ b/apps/api-tests/UnconfiguredGoogleTests.cs
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
+
+using System.Net;
+using System.Net.Http.Json;
+using Progmasoft.Progmaweb.Api.Accounts;
+
+namespace Progmasoft.Progmaweb.Api.Tests;
+
+/// <summary>
+/// The service without a Google client. It used to answer every request with 500, because the Google scheme was
+/// registered with empty credentials and failed its option validation in the authentication middleware.
+/// </summary>
+public sealed class UnconfiguredGoogleTests
+{
+ [Theory]
+ [InlineData("/api/v1/status", HttpStatusCode.OK)]
+ [InlineData("/health", HttpStatusCode.OK)]
+ [InlineData("/api/v1/accounts/me", HttpStatusCode.Unauthorized)]
+ public async Task OrdinaryRoutesAnswerWithoutGoogle(string path, HttpStatusCode expected)
+ {
+ await using AccountApiFactory factory = new(configureGoogle: false);
+ using HttpClient client = factory.CreateAccountClient();
+
+ HttpResponseMessage response = await client.GetAsync(path, TestContext.Current.CancellationToken);
+
+ Assert.Equal(expected, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PasswordAccountsWorkWithoutGoogle()
+ {
+ await using AccountApiFactory factory = new(configureGoogle: false);
+ using HttpClient client = factory.CreateAccountClient();
+ const string password = "a sufficiently long passphrase";
+
+ HttpResponseMessage registration = await client.PostAsJsonAsync(
+ "/api/v1/accounts/register",
+ new RegisterAccountRequest("LocalUser1", "local@example.com", password),
+ TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.Created, registration.StatusCode);
+
+ // The registration set the session cookie, so the same client is now signed in.
+ HttpResponseMessage me = await client.GetAsync("/api/v1/accounts/me", TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.OK, me.StatusCode);
+
+ HttpResponseMessage logout = await client.PostAsync(
+ "/api/v1/accounts/logout", null, TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.NoContent, logout.StatusCode);
+
+ HttpResponseMessage login = await client.PostAsJsonAsync(
+ "/api/v1/accounts/login",
+ new LoginAccountRequest("local@example.com", password),
+ TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.OK, login.StatusCode);
+ }
+
+ [Fact]
+ public async Task GoogleStartReportsThatSignInIsUnavailable()
+ {
+ await using AccountApiFactory factory = new(configureGoogle: false);
+ using HttpClient client = factory.CreateAccountClient(handleCookies: false);
+
+ HttpResponseMessage response = await client.GetAsync(
+ "/api/v1/accounts/oauth/google/start?accountName=GoogleUser",
+ TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ Assert.Null(response.Headers.Location);
+ }
+
+ [Fact]
+ public async Task GoogleCompleteFailsClosedWithoutGoogle()
+ {
+ await using AccountApiFactory factory = new(configureGoogle: false);
+ using HttpClient client = factory.CreateAccountClient(handleCookies: false);
+
+ HttpResponseMessage response = await client.GetAsync(
+ "/api/v1/accounts/oauth/google/complete", TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
+ Assert.Equal("/login?google=failed", response.Headers.Location?.OriginalString);
+ Assert.False(response.Headers.Contains("Set-Cookie"));
+ }
+
+ [Fact]
+ public async Task GoogleCallbackIsNotARouteWithoutGoogle()
+ {
+ await using AccountApiFactory factory = new(configureGoogle: false);
+ using HttpClient client = factory.CreateAccountClient(handleCookies: false);
+
+ HttpResponseMessage response = await client.GetAsync(
+ "/api/v1/accounts/oauth/google/callback", TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}