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

summaryrefslogtreecommitdiffhomepage
path: root/apps/api-tests/AccountCommandTests.cs
blob: 46c8e31867fab59bd204a1ac0c16b2e053529638 (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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

using Microsoft.Extensions.DependencyInjection;
using Progmasoft.Progmaweb.Api.Accounts;

namespace Progmasoft.Progmaweb.Api.Tests;

/// <summary>The operations command that provisions the reserved accounts.</summary>
public sealed class AccountCommandTests
{
    private const string Password = "a long test passphrase";

    private sealed class Input(bool terminal, params string?[] lines) : IPasswordInput
    {
        private readonly Queue<string?> remaining = new(lines);

        public bool IsTerminal { get; } = terminal;

        public List<string> Prompts { get; } = [];

        public string? Read(string prompt)
        {
            Prompts.Add(prompt);
            return remaining.Count > 0 ? remaining.Dequeue() : null;
        }
    }

    private static (ServiceProvider Services, InMemoryAccountStore Store) Services()
    {
        InMemoryAccountStore store = new();
        ServiceProvider services = new ServiceCollection()
            .AddSingleton<IAccountStore>(store)
            .AddSingleton(TimeProvider.System)
            .AddSingleton<AccountService>()
            .BuildServiceProvider();
        return (services, store);
    }

    private static async Task<(int Code, string Output, string Errors)> RunAsync(
        IServiceProvider services,
        IPasswordInput input,
        params string[] args)
    {
        StringWriter output = new();
        StringWriter errors = new();
        int code = await AccountCommand.RunAsync(
            services, args, input, output, errors, TestContext.Current.CancellationToken);
        return (code, output.ToString(), errors.ToString());
    }

    [Fact]
    public async Task AReservedAccountIsCreatedWithThePasswordTypedTwice()
    {
        (ServiceProvider services, InMemoryAccountStore store) = Services();
        Input input = new(terminal: true, Password, Password);

        (int code, string output, _) = await RunAsync(
            services, input, "provision-reserved", "Progmasoft", "support@progmasoft.com");

        Assert.Equal(0, code);
        Assert.Contains("Created the reserved account Progmasoft", output, StringComparison.Ordinal);
        Assert.DoesNotContain(Password, output, StringComparison.Ordinal);
        Assert.Equal(["Password: ", "Password again: "], input.Prompts);
        AccountRecord? account = await store.FindByAccountNameAsync("PROGMASOFT", TestContext.Current.CancellationToken);
        Assert.NotNull(account);
        Assert.Equal("Progmasoft", account.AccountName);
        // The password works for signing in.
        AccountRecord? verified = await services.GetRequiredService<AccountService>().VerifyPasswordAsync(
            "support@progmasoft.com", Password, TestContext.Current.CancellationToken);
        Assert.Equal(account.Id, verified?.Id);
    }

    [Fact]
    public async Task OnlyReservedNamesAreProvisioned()
    {
        (ServiceProvider services, _) = Services();
        (int code, _, string errors) = await RunAsync(
            services, new Input(terminal: false, Password), "provision-reserved", "Somebody1", "a@example.com");
        Assert.Equal(1, code);
        Assert.Contains("is not a reserved name", errors, StringComparison.Ordinal);
    }

    [Fact]
    public async Task DifferentPasswordsCreateNothing()
    {
        (ServiceProvider services, InMemoryAccountStore store) = Services();
        (int code, _, string errors) = await RunAsync(
            services, new Input(terminal: true, Password, Password + "!"), "provision-reserved", "Leitwolf",
            "owner@example.com");
        Assert.Equal(1, code);
        Assert.Contains("differ", errors, StringComparison.Ordinal);
        Assert.Null(await store.FindByAccountNameAsync("LEITWOLF", TestContext.Current.CancellationToken));
    }

    [Fact]
    public async Task AWeakPasswordOrAnExistingAccountIsRefused()
    {
        (ServiceProvider services, _) = Services();
        (int weak, _, string weakErrors) = await RunAsync(
            services, new Input(terminal: false, "short"), "provision-reserved", "Progmasoft", "support@progmasoft.com");
        Assert.Equal(1, weak);
        Assert.Contains("at least 12 characters", weakErrors, StringComparison.Ordinal);

        Assert.Equal(0, (await RunAsync(
            services, new Input(terminal: false, Password), "provision-reserved", "Progmasoft",
            "support@progmasoft.com")).Code);
        (int again, _, string againErrors) = await RunAsync(
            services, new Input(terminal: false, Password), "provision-reserved", "PROGMASOFT", "other@progmasoft.com");
        Assert.Equal(1, again);
        Assert.Contains("exists already", againErrors, StringComparison.Ordinal);
    }

    [Fact]
    public async Task PublicRegistrationStillRefusesReservedNames()
    {
        Assert.False(AccountNamePolicy.TryNormalize("Progmasoft", out _, out string error));
        Assert.Equal("That account name is reserved.", error);
        (int code, _, string errors) = await RunAsync(Services().Services, new Input(terminal: false), "provision");
        Assert.Equal(2, code);
        Assert.Contains("usage", errors, StringComparison.Ordinal);
    }
}