M5: Windows client moderation UI; add C ABI getters for account list, user mute/deafen, channel topic

This commit is contained in:
2026-06-17 16:31:29 +02:00
parent 3990f63f0f
commit 9b321d0d4f
24 changed files with 1723 additions and 30 deletions

View File

@@ -55,6 +55,22 @@ public sealed class VoiceCatClientSmokeTests : IDisposable
}
Assert.True(port is not null, "voicecat-server.exe did not report a bound TCP port within 10s.");
_port = port!.Value;
// M5: provision a known admin account so we can exercise moderation wrappers end-to-end.
string adminExe = Path.Combine(FindRepoRoot(), "build", "m1-dev", "bin", "voicecat-admin.exe");
Assert.True(File.Exists(adminExe), "voicecat-admin.exe not found — build the m1-dev preset.");
var adminPsi = new ProcessStartInfo(adminExe)
{
Arguments = $"--data-dir \"{_tempDir}\" account add admin2 --admin --password testpassword123",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
using (var adminProc = Process.Start(adminPsi) ?? throw new InvalidOperationException("failed to start voicecat-admin.exe"))
{
Assert.True(adminProc.WaitForExit(10000), "voicecat-admin.exe did not exit within 10s.");
Assert.Equal(0, adminProc.ExitCode);
}
}
public void Dispose()
@@ -124,6 +140,110 @@ public sealed class VoiceCatClientSmokeTests : IDisposable
var channels = client.ListChannels();
Assert.Contains(channels, c => c.Id == 1 && c.Name == "Lobby");
// M5: permissions getter round-trip.
var perms = client.GetPermissions();
Assert.False(perms.IsAdmin);
Assert.False(perms.CanKick);
// M5: moderation request wrappers queue without error. As a guest, account listing
// is rejected by the server with a GenericResult, which proves the wrapper path works
// end-to-end and that the new event type is delivered through P/Invoke.
Assert.Equal(VcResult.Ok, client.RequestAccountList());
Assert.True(PumpUntil(client,
() => events.Any(e => e.Type == VcEventType.GenericResult), 3000),
"did not receive VC_EVENT_GENERIC_RESULT for guest ListAccounts");
var generic = events.First(e => e.Type == VcEventType.GenericResult);
Assert.Equal(VcResult.PermissionDenied, generic.Result);
client.Disconnect();
}
[Fact]
public void Admin_ChannelCrud_AccountCrud_RoundTrips()
{
var events = new List<VoiceCatEvent>();
using var client = new VoiceCatClient("vc-csharp-admin", "0.1", VcLogLevel.Off,
tofuStorePath: Path.Combine(_tempDir, "tofu_pins_admin.txt"));
client.EventReceived += events.Add;
Assert.Equal(VcResult.Ok, client.Connect("127.0.0.1", _port));
Assert.Equal(VcResult.Ok, client.AuthenticateUser("admin2", "testpassword123"));
Assert.True(PumpUntil(client, () => events.Any(e => e.Type == VcEventType.ServerIdentity), 5000));
Assert.Equal(VcResult.Ok, client.ConfirmServerIdentity(accept: true));
Assert.True(PumpUntil(client, () => events.Any(e => e.Type == VcEventType.AuthResult), 5000));
Assert.Equal(VcResult.Ok, events.First(e => e.Type == VcEventType.AuthResult).Result);
Assert.True(PumpUntil(client, () => events.Any(e => e.Type == VcEventType.ChannelList), 3000));
var perms = client.GetPermissions();
Assert.True(perms.IsAdmin || perms.CanAdminAccounts);
// Channel CRUD
Assert.Equal(VcResult.Ok, client.CreateChannel(new ChannelEditInfo(
Id: 0,
ParentId: 0,
Name: "CSharp Test Channel",
Topic: "Created by C# smoke test",
PasswordProtected: false,
Password: null,
MaxUsers: 42,
SortOrder: 0,
Audio: new AudioConfigInfo(0, true, 48000, 64000, 20, 1, true, 5, false, 10))));
Assert.True(PumpUntil(client,
() => events.Any(e => e.Type == VcEventType.GenericResult && e.Result == VcResult.Ok), 3000),
"CreateChannel did not succeed");
var channels = client.ListChannels();
var created = channels.FirstOrDefault(c => c.Name == "CSharp Test Channel");
Assert.NotNull(created);
Assert.Equal("Created by C# smoke test", created.Topic);
Assert.True(created.PasswordProtected == false);
Assert.Equal(VcResult.Ok, client.EditChannel(new ChannelEditInfo(
created.Id,
created.ParentId,
created.Name,
"Updated topic",
created.PasswordProtected,
null,
100,
0,
new AudioConfigInfo(0, true, 48000, 64000, 20, 1, true, 5, false, 10))));
events.Clear();
Assert.True(PumpUntil(client,
() => events.Any(e => e.Type == VcEventType.GenericResult && e.Result == VcResult.Ok), 3000),
"EditChannel did not succeed");
Assert.Equal(VcResult.Ok, client.DeleteChannel(created.Id));
events.Clear();
Assert.True(PumpUntil(client,
() => events.Any(e => e.Type == VcEventType.GenericResult && e.Result == VcResult.Ok), 3000),
"DeleteChannel did not succeed");
// Account CRUD
Assert.Equal(VcResult.Ok, client.CreateAccount("csharp_smoke_user", "initialpw"));
events.Clear();
Assert.True(PumpUntil(client,
() => events.Any(e => e.Type == VcEventType.GenericResult && e.Result == VcResult.Ok), 3000),
"CreateAccount did not succeed");
Assert.Equal(VcResult.Ok, client.RequestAccountList());
Assert.True(PumpUntil(client, () => events.Any(e => e.Type == VcEventType.AccountList), 3000));
var accounts = client.ListAccounts();
Assert.Contains(accounts, a => a.Username == "csharp_smoke_user");
Assert.Equal(VcResult.Ok, client.ResetPassword("csharp_smoke_user", "newpw123"));
events.Clear();
Assert.True(PumpUntil(client,
() => events.Any(e => e.Type == VcEventType.GenericResult && e.Result == VcResult.Ok), 3000),
"ResetPassword did not succeed");
Assert.Equal(VcResult.Ok, client.DeleteAccount("csharp_smoke_user"));
events.Clear();
Assert.True(PumpUntil(client,
() => events.Any(e => e.Type == VcEventType.GenericResult && e.Result == VcResult.Ok), 3000),
"DeleteAccount did not succeed");
client.Disconnect();
}
}