Public Access
Main built vault key rotation while this branch was reshaping the screen that would drive it, so the two met in the same three files. Every other conflict was textual and resolved by taking both; these are the ones where a decision had to be made. **The view model.** Main taught TeamsViewModel three things and this branch had renamed and rewritten it into VaultsViewModel. All three are ported rather than dropped, because each is a behaviour rather than wording: adding somebody now wraps the vault to them on the spot instead of leaving SHARE KEY to be pressed, removing somebody rotates the vault and hands the new key to whoever is left, and a share reports how many generations were wrapped. The session calls they reach — ShareTeamVaultsAsync and RekeyTeamVaultsAsync — are scoped to a membership list rather than to one vault, and they are called that way here rather than narrowed: adding somebody is a change to the list, so every vault the list carries is one they can now fetch. This screen makes lists that carry one vault, so the sentences name one; where a list carries several, naming them all is the honest report, and the members section already says the list is shared. AddMemberAsync ran two lines over the length limit once the sharing was in it, so the calls behind it moved to AddOrInviteAsync and the three-way refusal to WhyNobodyCanBeAdded — the command reads as its guards now, which is what it was before the sharing arrived. **The tests.** Main's four new cases are ported to the vault-first API, including the one that matters most: the tampered key log is corrupted *before* the add, because the add is now a route to a wrap and a test that corrupted it afterwards would be asserting about the manual route only. SelectingAVault_ListsWhoHoldsAKey now expects two holders rather than one — main's fake records the creator's own self-grant, and a key-holder list that omitted it would show the one person who can certainly open a new vault as somebody who cannot. **The README.** The limits list is six rather than four or five: main's rotation entries and this branch's "a vault cannot be deleted" describe different things and both are true. "The rekey is flagged, never performed" is gone, since it is now performed, and M3 reads *Done* rather than *Done, except rekey*. One thing worth writing down that neither side had. An invitation claimed at sign-in still leaves the key owed, where an add does not: at the moment an invitation is issued there is no account and no published key to wrap to, and the claim happens on the invitee's machine, which holds nothing. Manual check 12.1 says so, because a reader who knows adding shares would otherwise read that step as stale. 1561 tests pass.
276 lines
10 KiB
C#
276 lines
10 KiB
C#
using DodoSSH.Client.Api;
|
|
using DodoSSH.Client.Auth;
|
|
using DodoSSH.Client.Session;
|
|
using DodoSSH.Client.Sync;
|
|
using DodoSSH.Contracts;
|
|
|
|
namespace DodoSSH.Client.App.Layout.Tests;
|
|
|
|
/// <summary>
|
|
/// The least server a <c>VaultsViewModel</c> needs in order to be laid out with something in it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The vaults screen draws its list from the session and everything under it from the server: who is in a
|
|
/// vault, who has been invited, and who holds a key are all read on open, and the suite's
|
|
/// <c>FakeAccountServer</c> implements <see cref="IAccountApi"/> and nothing else. Rather than teach that
|
|
/// fake five more interfaces for one screen, this serves fixed rows and refuses everything a layout test
|
|
/// has no business calling.
|
|
/// </para>
|
|
/// <para>
|
|
/// It does answer <see cref="CreateTeamVaultAsync"/>, unlike the other writes, because that is how the
|
|
/// suite gets a shared vault into the session at all — an offline layout test has no other way to reach
|
|
/// the state this screen exists to draw.
|
|
/// </para>
|
|
/// <para>
|
|
/// The rows are deliberately the <em>long</em> ones. A layout suite that measured "Bob" in a column sized
|
|
/// for an email address would certify a shape no real team produces — so the names, addresses and status
|
|
/// sentences here are at or near the length the interface can really be handed, which is what makes an
|
|
/// overflow show up at the minimum window rather than on somebody's screen.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal sealed class StubTeamServer : IVaultServer, ITeamApi, IVaultGrantApi
|
|
{
|
|
private static readonly Guid OwnerId = Guid.CreateVersion7();
|
|
private static readonly Guid ColleagueId = Guid.CreateVersion7();
|
|
private static readonly Guid TeamId = Guid.CreateVersion7();
|
|
private static readonly Guid VaultId = Guid.CreateVersion7();
|
|
|
|
/// <inheritdoc />
|
|
public Uri ServerUrl { get; } = new("https://dodossh.example");
|
|
|
|
/// <inheritdoc />
|
|
public ITeamApi Teams => this;
|
|
|
|
/// <inheritdoc />
|
|
public IVaultGrantApi Grants => this;
|
|
|
|
/// <inheritdoc />
|
|
public IAccountApi Account => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public ISyncApi Sync => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public IDirectoryApi Directory => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public IKeyBindingAuthorizer KeyBinding => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public SyncOptions SyncOptions => new();
|
|
|
|
/// <inheritdoc />
|
|
public string? RefreshToken => null;
|
|
|
|
/// <summary>The vault whose key holders are listed, so a test can select it.</summary>
|
|
internal static Guid TeamVaultId => VaultId;
|
|
|
|
/// <summary>The membership list behind that vault, so a test can create it in the session.</summary>
|
|
internal static Guid SharedTeamId => TeamId;
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<TeamSummary>> ListTeamsAsync(CancellationToken cancellationToken) =>
|
|
Task.FromResult<IReadOnlyList<TeamSummary>>(
|
|
[
|
|
new TeamSummary(
|
|
TeamId,
|
|
"Platform Engineering",
|
|
"platform-engineering",
|
|
"Everything that runs the estate.",
|
|
TeamMemberRole.Owner,
|
|
MemberCount: 2,
|
|
VaultCount: 1,
|
|
DateTimeOffset.UnixEpoch),
|
|
]);
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<TeamMemberSummary>> ListTeamMembersAsync(
|
|
Guid teamId,
|
|
CancellationToken cancellationToken) =>
|
|
Task.FromResult<IReadOnlyList<TeamMemberSummary>>(
|
|
[
|
|
new TeamMemberSummary(
|
|
OwnerId,
|
|
"alexandra.hollingsworth@dodotech.example",
|
|
"Alexandra Hollingsworth",
|
|
TeamMemberRole.Owner,
|
|
TeamMemberStatus.Active,
|
|
IsEnrolled: true,
|
|
DateTimeOffset.UnixEpoch,
|
|
DateTimeOffset.UnixEpoch),
|
|
|
|
// The unenrolled case on purpose: its key-state phrase is the longest the column ever
|
|
// carries, and it is the row that decides whether that column is wide enough.
|
|
new TeamMemberSummary(
|
|
ColleagueId,
|
|
"bartholomew.fotheringay@dodotech.example",
|
|
"Bartholomew Fotheringay",
|
|
TeamMemberRole.Member,
|
|
TeamMemberStatus.Active,
|
|
IsEnrolled: false,
|
|
DateTimeOffset.UnixEpoch,
|
|
LastActiveAt: null),
|
|
]);
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<TeamInvitationSummary>> ListTeamInvitationsAsync(
|
|
Guid teamId,
|
|
CancellationToken cancellationToken) =>
|
|
Task.FromResult<IReadOnlyList<TeamInvitationSummary>>(
|
|
[
|
|
// Pending, because its sentence is the long one — it has to carry the whole mechanism,
|
|
// since nothing was sent and there is nothing else on the screen that could say so.
|
|
new TeamInvitationSummary(
|
|
Guid.CreateVersion7(),
|
|
"wilhelmina.ashworth-blake@dodotech.example",
|
|
TeamMemberRole.Admin,
|
|
TeamInvitationState.Pending,
|
|
OwnerId,
|
|
DateTimeOffset.UnixEpoch,
|
|
DateTimeOffset.UnixEpoch.AddDays(14),
|
|
AcceptedAt: null),
|
|
]);
|
|
|
|
/// <inheritdoc />
|
|
public Task<VaultGrantsResponse> ListVaultGrantsAsync(
|
|
Guid vaultId,
|
|
CancellationToken cancellationToken) =>
|
|
Task.FromResult(new VaultGrantsResponse(
|
|
vaultId,
|
|
KeyGeneration: 2,
|
|
RekeyRequired: true,
|
|
Grants:
|
|
[
|
|
new VaultGrantSummary(
|
|
OwnerId,
|
|
"alexandra.hollingsworth@dodotech.example",
|
|
"Alexandra Hollingsworth",
|
|
KeyGeneration: 2,
|
|
VaultGrantState.Active,
|
|
OwnerId,
|
|
DateTimeOffset.UnixEpoch,
|
|
RevokedAt: null),
|
|
|
|
// A generation behind, so the "stale" phrasing is the one being measured rather than
|
|
// the two-word happy case.
|
|
new VaultGrantSummary(
|
|
ColleagueId,
|
|
"bartholomew.fotheringay@dodotech.example",
|
|
"Bartholomew Fotheringay",
|
|
KeyGeneration: 1,
|
|
VaultGrantState.Active,
|
|
OwnerId,
|
|
DateTimeOffset.UnixEpoch,
|
|
RevokedAt: null),
|
|
]));
|
|
|
|
/// <inheritdoc />
|
|
public Task<TeamSummary> CreateTeamAsync(
|
|
CreateTeamRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<TeamSummary> UpdateTeamAsync(
|
|
Guid teamId,
|
|
UpdateTeamRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> ArchiveTeamAsync(Guid teamId, CancellationToken cancellationToken) =>
|
|
throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task TransferTeamOwnershipAsync(
|
|
Guid teamId,
|
|
TransferTeamOwnershipRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<TeamMemberSummary> AddTeamMemberAsync(
|
|
Guid teamId,
|
|
AddTeamMemberRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<TeamMemberSummary> ChangeTeamMemberRoleAsync(
|
|
Guid teamId,
|
|
Guid userId,
|
|
ChangeTeamMemberRoleRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> RemoveTeamMemberAsync(
|
|
Guid teamId,
|
|
Guid userId,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<TeamInvitationSummary> CreateTeamInvitationAsync(
|
|
Guid teamId,
|
|
CreateTeamInvitationRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> RevokeTeamInvitationAsync(
|
|
Guid teamId,
|
|
Guid invitationId,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <summary>
|
|
/// Accepts the vault, so a layout test can put a shared one into the session it is drawing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The client's own id and wrapped key are echoed back, exactly as the real endpoint answers: the key
|
|
/// was generated on this machine and the session adopts its own copy, so anything else here would be
|
|
/// either discarded or a vault nobody could open.
|
|
/// <para>
|
|
/// It comes back owing a rekey, which is not decoration: that is the longer of the two lines a vault row
|
|
/// can carry, and this suite exists to measure the long one.
|
|
/// </para>
|
|
/// </remarks>
|
|
public Task<VaultSummary> CreateTeamVaultAsync(
|
|
Guid teamId,
|
|
CreateTeamVaultRequest request,
|
|
CancellationToken cancellationToken) =>
|
|
Task.FromResult(new VaultSummary(
|
|
request.VaultId,
|
|
request.Name,
|
|
IsPersonal: false,
|
|
TeamId: teamId,
|
|
KeyGeneration: 1,
|
|
Permissions: 31,
|
|
request.WrappedVaultKey,
|
|
RekeyRequired: true));
|
|
|
|
/// <inheritdoc />
|
|
public Task<VaultSummary> RenameVaultAsync(
|
|
Guid vaultId,
|
|
UpdateVaultRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task IssueVaultGrantAsync(
|
|
Guid vaultId,
|
|
IssueVaultGrantRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> RevokeVaultGrantAsync(
|
|
Guid vaultId,
|
|
Guid userId,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public Task<VaultSummary> RekeyVaultAsync(
|
|
Guid vaultId,
|
|
RekeyVaultRequest request,
|
|
CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
// Nothing held.
|
|
}
|
|
}
|