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

summaryrefslogtreecommitdiffhomepage
path: root/apps/api-tests/PostgresTestDatabase.cs
blob: 401a69c3cab6bc4e2caf27ce6e0c9cbe47971e8a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

using Npgsql;

namespace Progmasoft.Progmaweb.Api.Tests;

/// <summary>A schema of its own in the PostgreSQL database of the test run, removed when the test ends.</summary>
/// <remarks>
/// <para>
/// The tests that need PostgreSQL read its connection string from the environment variable
/// <see cref="VariableName"/>. The user it names must be allowed to create schemas in its database. Without the
/// variable those tests are skipped, and <see cref="PostgresConfigurationTests"/> fails the run where skipping
/// them would hide a gap: in continuous integration.
/// </para>
/// <para>
/// Each test works in a schema with a random name, selected through the search path of its connection string, so
/// tests neither see nor wait for one another and nothing is left behind.
/// </para>
/// </remarks>
internal sealed class PostgresTestDatabase : IAsyncDisposable
{
    /// <summary>Environment variable that holds the connection string of the test database.</summary>
    public const string VariableName = "PROGMAWEB_TEST_POSTGRES";

    private readonly string administrativeConnectionString;
    private readonly string schema;

    private PostgresTestDatabase(string administrativeConnectionString, string schema)
    {
        this.administrativeConnectionString = administrativeConnectionString;
        this.schema = schema;
        ConnectionString = new NpgsqlConnectionStringBuilder(administrativeConnectionString) { SearchPath = schema }
            .ConnectionString;
    }

    /// <summary>The connection string of the test database, or <see langword="null"/> when none is configured.</summary>
    public static string? ConfiguredConnectionString
    {
        get
        {
            string? value = Environment.GetEnvironmentVariable(VariableName);
            return string.IsNullOrWhiteSpace(value) ? null : value;
        }
    }

    /// <summary>A connection string whose statements run in the schema of this test.</summary>
    public string ConnectionString { get; }

    /// <summary>Creates the schema of a test; skips the test when no database is configured.</summary>
    public static async Task<PostgresTestDatabase> CreateAsync()
    {
        string? configured = ConfiguredConnectionString;
        Assert.SkipUnless(configured is not null, $"{VariableName} names no PostgreSQL database.");

        string schema = "test_" + Guid.NewGuid().ToString("N");
        await using NpgsqlDataSource dataSource = NpgsqlDataSource.Create(configured!);
        await using NpgsqlCommand command = dataSource.CreateCommand($"CREATE SCHEMA {schema}");
        await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
        return new PostgresTestDatabase(configured!, schema);
    }

    /// <summary>Opens connections to the schema of this test. The caller disposes the data source.</summary>
    public NpgsqlDataSource OpenDataSource() => NpgsqlDataSource.Create(ConnectionString);

    public async ValueTask DisposeAsync()
    {
        // Pooled connections of disposed data sources are closed, so nothing holds the schema.
        await using NpgsqlDataSource dataSource = NpgsqlDataSource.Create(administrativeConnectionString);
        await using NpgsqlCommand command = dataSource.CreateCommand($"DROP SCHEMA IF EXISTS {schema} CASCADE");
        await command.ExecuteNonQueryAsync(CancellationToken.None);
    }
}

public sealed class PostgresConfigurationTests
{
    [Fact]
    public void ContinuousIntegrationRunsTheDatabaseTests()
    {
        // GitHub Actions sets CI. A run there without the database would report the PostgreSQL tests as skipped and
        // the run as passed, which is how an untested store would be merged.
        if (string.Equals(Environment.GetEnvironmentVariable("CI"), "true", StringComparison.OrdinalIgnoreCase))
        {
            Assert.NotNull(PostgresTestDatabase.ConfiguredConnectionString);
        }
    }
}