diff options
| author | Leitwolf11 | 2026-10-03 18:46:35 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-03 18:46:35 +0300 |
| commit | ec774db15976385a6d5ea6f48d3622fe507d3843 (patch) | |
| tree | 285451ccb4ff35b37f89805415003e372fda02ec /apps/api-tests | |
| parent | bc1b0b080b398e52b3201034f92398750f415ed2 (diff) | |
| download | progmaweb-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.cs | 19 | ||||
| -rw-r--r-- | apps/api-tests/UnconfiguredGoogleTests.cs | 97 |
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); + } +} |