feat(launcher): Campaign LA add Avalonia desktop shell
This commit is contained in:
parent
6c4cd2bbc6
commit
d0a9c65d85
31 changed files with 4831 additions and 16 deletions
|
|
@ -370,6 +370,41 @@ public sealed class SessionConfigComposerTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComposeProbeAndWriteWritesThePasswordFreeProbeDocument()
|
||||
{
|
||||
string root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"acdream-launcher-probe-writer-tests",
|
||||
Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
var paths = new ApplicationPathSet(
|
||||
Path.Combine(root, "cfg"),
|
||||
Path.Combine(root, "data"),
|
||||
Path.Combine(root, "cache"),
|
||||
null);
|
||||
ComposedSessionConfig composed = SessionConfigComposer.ComposeProbeAndWrite(
|
||||
Server(),
|
||||
Account(),
|
||||
Install,
|
||||
paths,
|
||||
"probe-write");
|
||||
|
||||
string json = File.ReadAllText(composed.ConfigFilePath);
|
||||
Assert.Contains("\"mode\": \"probe\"", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"character\"", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(Account().Password, json, StringComparison.Ordinal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject ParseRoot(ComposedSessionConfig composed)
|
||||
{
|
||||
string json = SessionConfigComposer.Serialize(composed.Document);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,502 @@
|
|||
using AcDream.Launcher.Core.Launching;
|
||||
using AcDream.Launcher.Core.Orchestration;
|
||||
using AcDream.Launcher.Core.Profiles;
|
||||
using AcDream.Launcher.Core.Status;
|
||||
using AcDream.Platform;
|
||||
|
||||
namespace AcDream.Launcher.Core.Tests.Orchestration;
|
||||
|
||||
public sealed class LauncherOrchestratorTests : IDisposable
|
||||
{
|
||||
private const string Password = "launcher-only-secret";
|
||||
private readonly string _root;
|
||||
private readonly ApplicationPathSet _paths;
|
||||
|
||||
public LauncherOrchestratorTests()
|
||||
{
|
||||
_root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"acdream-la4-orchestrator-tests",
|
||||
Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_root);
|
||||
_paths = new ApplicationPathSet(
|
||||
Path.Combine(_root, "config"),
|
||||
Path.Combine(_root, "data"),
|
||||
Path.Combine(_root, "cache"),
|
||||
null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_root))
|
||||
{
|
||||
Directory.Delete(_root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SnapshotProjectsTheFullHierarchyWithoutTheCredential()
|
||||
{
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator();
|
||||
|
||||
LauncherStateSnapshot snapshot = orchestrator.GetSnapshot();
|
||||
|
||||
LauncherServerSnapshot server = Assert.Single(snapshot.Servers);
|
||||
LauncherAccountSnapshot account = Assert.Single(server.Accounts);
|
||||
LauncherCharacterSnapshot character = Assert.Single(account.Characters);
|
||||
Assert.Equal("Local ACE", server.Name);
|
||||
Assert.Equal("testaccount", account.AccountName);
|
||||
Assert.Equal("+Acdream", character.Name);
|
||||
|
||||
string serialized = System.Text.Json.JsonSerializer.Serialize(snapshot);
|
||||
Assert.DoesNotContain(Password, serialized, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
typeof(LauncherAccountSnapshot).GetProperties(),
|
||||
property => property.Name.Contains("password", StringComparison.OrdinalIgnoreCase)
|
||||
|| property.Name.Contains("credential", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LaunchComposesTheSelectedModeAndSpawnsThroughTheInjectedSeam()
|
||||
{
|
||||
var config = new RecordingConfigService();
|
||||
var supervisors = new FakeSupervisorFactory();
|
||||
var statusSources = new QueueStatusSourceFactory();
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator(
|
||||
configService: config,
|
||||
supervisorFactory: supervisors,
|
||||
statusSourceFactory: statusSources);
|
||||
|
||||
LauncherSessionSnapshot launched = await orchestrator.LaunchAsync(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Acdream",
|
||||
LaunchMode.Gui);
|
||||
|
||||
Assert.Equal(LauncherActivityState.Running, launched.State);
|
||||
Assert.Equal(1, config.PlayCallCount);
|
||||
Assert.Equal(0, config.ProbeCallCount);
|
||||
Assert.Equal(LaunchMode.Gui, config.LastCharacter!.LaunchMode);
|
||||
Assert.Equal(["ExamplePlugin"], config.LastCharacter.Plugins);
|
||||
Assert.Equal(["/vt start"], config.LastCharacter.LoginCommands);
|
||||
Assert.Equal(string.Empty, config.LastAccountPassword);
|
||||
|
||||
FakeSupervisor supervisor = Assert.Single(supervisors.Created);
|
||||
Assert.Equal("gui-host", supervisor.Spec!.ExecutablePath);
|
||||
Assert.Equal(
|
||||
["--session-config", config.LastComposed!.ConfigFilePath],
|
||||
supervisor.Spec.Arguments);
|
||||
Assert.Equal(Password, supervisor.PasswordWrittenToStdin);
|
||||
Assert.DoesNotContain(
|
||||
Password,
|
||||
string.Join(' ', supervisor.Spec.Arguments),
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
Password,
|
||||
SessionConfigComposer.Serialize(config.LastComposed.Document),
|
||||
StringComparison.Ordinal);
|
||||
|
||||
QueueStatusSource source = Assert.Single(statusSources.Created);
|
||||
source.Enqueue(Connected("s1"));
|
||||
source.Enqueue(EnteredWorld("s1", "+Acdream"));
|
||||
orchestrator.PollStatus();
|
||||
|
||||
LauncherSessionSnapshot inWorld = Assert.Single(orchestrator.GetSnapshot().Sessions);
|
||||
Assert.Equal(LauncherActivityState.InWorld, inWorld.State);
|
||||
Assert.Contains("+Acdream", inWorld.Status, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeIsRefusedWhileTheAccountHasARunningLauncherActivity()
|
||||
{
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator();
|
||||
await orchestrator.LaunchAsync(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Acdream",
|
||||
LaunchMode.Headless);
|
||||
|
||||
LauncherCapability capability = orchestrator.GetProbeCapability(
|
||||
"Local ACE",
|
||||
"testaccount");
|
||||
|
||||
Assert.False(capability.IsAvailable);
|
||||
Assert.Contains("Stop", capability.Reason, StringComparison.OrdinalIgnoreCase);
|
||||
await Assert.ThrowsAsync<LauncherOperationException>(() =>
|
||||
orchestrator.ProbeAsync("Local ACE", "testaccount"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeUsesTheProbeShapeAndFoldsTheReportedRosterIntoTheStore()
|
||||
{
|
||||
var config = new RecordingConfigService();
|
||||
var statusSources = new QueueStatusSourceFactory();
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator(
|
||||
includeCharacter: false,
|
||||
configService: config,
|
||||
statusSourceFactory: statusSources);
|
||||
|
||||
LauncherSessionSnapshot probe = await orchestrator.ProbeAsync(
|
||||
"Local ACE",
|
||||
"testaccount");
|
||||
|
||||
Assert.Equal(LauncherActivityKind.Probe, probe.Kind);
|
||||
Assert.Equal(0, config.PlayCallCount);
|
||||
Assert.Equal(1, config.ProbeCallCount);
|
||||
string json = SessionConfigComposer.Serialize(config.LastComposed!.Document);
|
||||
Assert.Contains("\"mode\": \"probe\"", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"character\"", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(Password, json, StringComparison.Ordinal);
|
||||
|
||||
QueueStatusSource source = Assert.Single(statusSources.Created);
|
||||
source.Enqueue(new CharacterListStatusEvent
|
||||
{
|
||||
V = 1,
|
||||
E = "characterList",
|
||||
T = DateTimeOffset.UtcNow,
|
||||
SessionId = "s1",
|
||||
AccountName = "testaccount",
|
||||
SlotCount = 6,
|
||||
Characters =
|
||||
[
|
||||
new StatusCharacterEntry(0x5000000Au, "+Acdream", 0),
|
||||
new StatusCharacterEntry(0x5000000Bu, "+Second", 0),
|
||||
],
|
||||
});
|
||||
orchestrator.PollStatus();
|
||||
|
||||
LauncherAccountSnapshot account = Assert.Single(
|
||||
Assert.Single(orchestrator.GetSnapshot().Servers).Accounts);
|
||||
Assert.Equal(2, account.Characters.Count);
|
||||
Assert.Contains(account.Characters, character => character.Name == "+Acdream");
|
||||
Assert.Contains(account.Characters, character => character.Name == "+Second");
|
||||
|
||||
var reloaded = new LauncherProfileStore(
|
||||
Path.Combine(_paths.ConfigDirectory, "launcher-profiles.json"));
|
||||
Assert.True(reloaded.Load());
|
||||
Assert.Equal(
|
||||
2,
|
||||
reloaded.Document.Servers.Single().Accounts.Single().Characters.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LinuxRejectsBothGraphicalModesBeforeCompositionOrSpawnWithSliceLExplanation()
|
||||
{
|
||||
var config = new RecordingConfigService();
|
||||
var supervisors = new FakeSupervisorFactory();
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator(
|
||||
platform: LinuxCapabilities(),
|
||||
configService: config,
|
||||
supervisorFactory: supervisors);
|
||||
|
||||
foreach (LaunchMode mode in new[] { LaunchMode.Gui, LaunchMode.GuiSelect })
|
||||
{
|
||||
LauncherOperationException exception = await Assert.ThrowsAsync<LauncherOperationException>(() =>
|
||||
orchestrator.LaunchAsync(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Acdream",
|
||||
mode));
|
||||
Assert.Contains("Slice L", exception.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("parked at L1", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.True(orchestrator.GetLaunchCapability(LaunchMode.Headless).IsAvailable);
|
||||
Assert.Equal(0, config.PlayCallCount);
|
||||
Assert.Empty(supervisors.Created);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartFailureIsVisibleButRedactsTheCredentialEverywhere()
|
||||
{
|
||||
var supervisors = new FakeSupervisorFactory(
|
||||
startExceptionFactory: password =>
|
||||
new IOException($"simulated pipe failure containing {password}"));
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator(
|
||||
supervisorFactory: supervisors);
|
||||
|
||||
LauncherOperationException exception = await Assert.ThrowsAsync<LauncherOperationException>(
|
||||
() => orchestrator.LaunchAsync(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Acdream",
|
||||
LaunchMode.Headless));
|
||||
|
||||
Assert.DoesNotContain(Password, exception.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("[redacted]", exception.Message, StringComparison.Ordinal);
|
||||
LauncherSessionSnapshot failed = Assert.Single(orchestrator.GetSnapshot().Sessions);
|
||||
Assert.Equal(LauncherActivityState.Failed, failed.State);
|
||||
Assert.DoesNotContain(Password, failed.Error ?? string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreCancelledLaunchNeverComposesOrSpawnsAndEndsCancelled()
|
||||
{
|
||||
var config = new RecordingConfigService();
|
||||
var supervisors = new FakeSupervisorFactory();
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator(
|
||||
configService: config,
|
||||
supervisorFactory: supervisors);
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
orchestrator.LaunchAsync(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Acdream",
|
||||
LaunchMode.Headless,
|
||||
cancellation.Token));
|
||||
|
||||
Assert.Equal(0, config.PlayCallCount);
|
||||
Assert.Empty(supervisors.Created);
|
||||
Assert.Equal(
|
||||
LauncherActivityState.Cancelled,
|
||||
Assert.Single(orchestrator.GetSnapshot().Sessions).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopRunsThroughTheSupervisorOffThreadAndMakesTheAccountProbeableAgain()
|
||||
{
|
||||
var supervisors = new FakeSupervisorFactory();
|
||||
using LauncherOrchestrator orchestrator = CreateOrchestrator(
|
||||
supervisorFactory: supervisors);
|
||||
LauncherSessionSnapshot launched = await orchestrator.LaunchAsync(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Acdream",
|
||||
LaunchMode.Headless);
|
||||
|
||||
await orchestrator.StopSessionAsync(
|
||||
launched.SessionId,
|
||||
TimeSpan.FromMilliseconds(10));
|
||||
|
||||
FakeSupervisor supervisor = Assert.Single(supervisors.Created);
|
||||
Assert.Equal(1, supervisor.StopCallCount);
|
||||
Assert.Equal(
|
||||
LauncherActivityState.Exited,
|
||||
Assert.Single(orchestrator.GetSnapshot().Sessions).State);
|
||||
Assert.True(orchestrator.GetProbeCapability(
|
||||
"Local ACE",
|
||||
"testaccount").IsAvailable);
|
||||
}
|
||||
|
||||
private LauncherOrchestrator CreateOrchestrator(
|
||||
bool includeCharacter = true,
|
||||
LauncherPlatformCapabilities? platform = null,
|
||||
ILauncherSessionConfigService? configService = null,
|
||||
ILauncherProcessSupervisorFactory? supervisorFactory = null,
|
||||
IStatusEventSourceFactory? statusSourceFactory = null)
|
||||
{
|
||||
string profilePath = Path.Combine(
|
||||
_paths.ConfigDirectory,
|
||||
"launcher-profiles.json");
|
||||
var store = new LauncherProfileStore(profilePath);
|
||||
store.Load();
|
||||
store.AddServer("Local ACE", "127.0.0.1", 9000);
|
||||
store.AddAccount("Local ACE", "testaccount", Password);
|
||||
if (includeCharacter)
|
||||
{
|
||||
store.AddCharacter(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Acdream",
|
||||
"0x5000000A",
|
||||
LaunchMode.GuiSelect,
|
||||
["ExamplePlugin"],
|
||||
["/vt start"]);
|
||||
}
|
||||
store.Save();
|
||||
|
||||
int nextSession = 0;
|
||||
var orchestrator = new LauncherOrchestrator(
|
||||
store,
|
||||
_paths,
|
||||
new LauncherExecutableSet("gui-host", "headless-host"),
|
||||
new LauncherInstallRecord("dats", "pak"),
|
||||
platform ?? WindowsCapabilities(),
|
||||
configService,
|
||||
supervisorFactory ?? new FakeSupervisorFactory(),
|
||||
statusSourceFactory ?? new QueueStatusSourceFactory(),
|
||||
() => $"s{Interlocked.Increment(ref nextSession)}");
|
||||
orchestrator.LoadProfiles();
|
||||
return orchestrator;
|
||||
}
|
||||
|
||||
private static LauncherPlatformCapabilities WindowsCapabilities() =>
|
||||
new(true, false, true, true, "Windows", null);
|
||||
|
||||
private static LauncherPlatformCapabilities LinuxCapabilities() =>
|
||||
new(
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"Linux",
|
||||
LauncherPlatformCapabilities.LinuxGraphicalLaunchDisabledReason);
|
||||
|
||||
private static ConnectedStatusEvent Connected(string sessionId) =>
|
||||
new()
|
||||
{
|
||||
V = 1,
|
||||
E = "connected",
|
||||
T = DateTimeOffset.UtcNow,
|
||||
SessionId = sessionId,
|
||||
};
|
||||
|
||||
private static EnteredWorldStatusEvent EnteredWorld(
|
||||
string sessionId,
|
||||
string characterName) =>
|
||||
new()
|
||||
{
|
||||
V = 1,
|
||||
E = "enteredWorld",
|
||||
T = DateTimeOffset.UtcNow,
|
||||
SessionId = sessionId,
|
||||
CharacterId = 0x5000000Au,
|
||||
CharacterName = characterName,
|
||||
};
|
||||
|
||||
private sealed class RecordingConfigService : ILauncherSessionConfigService
|
||||
{
|
||||
public int PlayCallCount { get; private set; }
|
||||
|
||||
public int ProbeCallCount { get; private set; }
|
||||
|
||||
public CharacterProfile? LastCharacter { get; private set; }
|
||||
|
||||
public string? LastAccountPassword { get; private set; }
|
||||
|
||||
public ComposedSessionConfig? LastComposed { get; private set; }
|
||||
|
||||
public ComposedSessionConfig ComposeAndWrite(
|
||||
ServerProfile server,
|
||||
AccountProfile account,
|
||||
CharacterProfile character,
|
||||
LauncherInstallRecord install,
|
||||
ApplicationPathSet paths,
|
||||
string sessionId,
|
||||
int? loginCommandDelayMs = null)
|
||||
{
|
||||
PlayCallCount++;
|
||||
LastCharacter = character;
|
||||
LastAccountPassword = account.Password;
|
||||
LastComposed = SessionConfigComposer.Compose(
|
||||
server,
|
||||
account,
|
||||
character,
|
||||
install,
|
||||
paths,
|
||||
sessionId,
|
||||
loginCommandDelayMs);
|
||||
return LastComposed;
|
||||
}
|
||||
|
||||
public ComposedSessionConfig ComposeProbeAndWrite(
|
||||
ServerProfile server,
|
||||
AccountProfile account,
|
||||
LauncherInstallRecord install,
|
||||
ApplicationPathSet paths,
|
||||
string sessionId)
|
||||
{
|
||||
ProbeCallCount++;
|
||||
LastAccountPassword = account.Password;
|
||||
LastComposed = SessionConfigComposer.ComposeProbe(
|
||||
server,
|
||||
account,
|
||||
install,
|
||||
paths,
|
||||
sessionId);
|
||||
return LastComposed;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeSupervisorFactory(
|
||||
Func<string?, Exception>? startExceptionFactory = null)
|
||||
: ILauncherProcessSupervisorFactory
|
||||
{
|
||||
public List<FakeSupervisor> Created { get; } = [];
|
||||
|
||||
public ILauncherProcessSupervisor Create()
|
||||
{
|
||||
var supervisor = new FakeSupervisor(startExceptionFactory);
|
||||
Created.Add(supervisor);
|
||||
return supervisor;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeSupervisor(Func<string?, Exception>? startExceptionFactory)
|
||||
: ILauncherProcessSupervisor
|
||||
{
|
||||
public LauncherSessionState State { get; private set; } =
|
||||
LauncherSessionState.Starting;
|
||||
|
||||
public int? ExitCode { get; private set; }
|
||||
|
||||
public LauncherProcessSpec? Spec { get; private set; }
|
||||
|
||||
public string? PasswordWrittenToStdin { get; private set; }
|
||||
|
||||
public int StopCallCount { get; private set; }
|
||||
|
||||
public event EventHandler<LauncherSessionState>? StateChanged;
|
||||
|
||||
public void Start(LauncherProcessSpec spec, string? password)
|
||||
{
|
||||
Spec = spec;
|
||||
PasswordWrittenToStdin = password;
|
||||
if (startExceptionFactory is not null)
|
||||
{
|
||||
throw startExceptionFactory(password);
|
||||
}
|
||||
|
||||
State = LauncherSessionState.Running;
|
||||
StateChanged?.Invoke(this, State);
|
||||
}
|
||||
|
||||
public void Stop(TimeSpan timeout)
|
||||
{
|
||||
StopCallCount++;
|
||||
State = LauncherSessionState.Exited;
|
||||
ExitCode = 0;
|
||||
StateChanged?.Invoke(this, State);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class QueueStatusSourceFactory : IStatusEventSourceFactory
|
||||
{
|
||||
public List<QueueStatusSource> Created { get; } = [];
|
||||
|
||||
public IStatusEventSource Create(string path)
|
||||
{
|
||||
var source = new QueueStatusSource();
|
||||
Created.Add(source);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class QueueStatusSource : IStatusEventSource
|
||||
{
|
||||
private readonly Queue<StatusEvent> _events = [];
|
||||
|
||||
public void Enqueue(StatusEvent statusEvent) => _events.Enqueue(statusEvent);
|
||||
|
||||
public IReadOnlyList<StatusEvent> ReadNewEvents()
|
||||
{
|
||||
var result = new List<StatusEvent>();
|
||||
while (_events.TryDequeue(out StatusEvent? statusEvent))
|
||||
{
|
||||
if (statusEvent is not null)
|
||||
{
|
||||
result.Add(statusEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,6 +187,60 @@ public sealed class LauncherProfileStoreTests : IDisposable
|
|||
Assert.Equal("0x5000000A", character.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddEditAndRemoveCachedCharacterRoundTripsThroughTheCrudSurface()
|
||||
{
|
||||
var store = new LauncherProfileStore(_filePath);
|
||||
store.Load();
|
||||
store.AddServer("Local ACE", "127.0.0.1", 9000);
|
||||
store.AddAccount("Local ACE", "testaccount", "pw");
|
||||
|
||||
store.AddCharacter(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Manual",
|
||||
"0x5000000a");
|
||||
CharacterProfile character = Assert.Single(
|
||||
store.Document.Servers.Single().Accounts.Single().Characters);
|
||||
Assert.Equal("0x5000000A", character.Id);
|
||||
Assert.Equal(LaunchMode.GuiSelect, character.LaunchMode);
|
||||
|
||||
store.EditCharacter(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Manual",
|
||||
newName: "+Renamed",
|
||||
newId: "",
|
||||
launchMode: LaunchMode.Headless);
|
||||
character = Assert.Single(
|
||||
store.Document.Servers.Single().Accounts.Single().Characters);
|
||||
Assert.Equal("+Renamed", character.Name);
|
||||
Assert.Null(character.Id);
|
||||
Assert.Equal(LaunchMode.Headless, character.LaunchMode);
|
||||
|
||||
store.RemoveCharacter("Local ACE", "testaccount", "+Renamed");
|
||||
Assert.Empty(store.Document.Servers.Single().Accounts.Single().Characters);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("5000000A")]
|
||||
[InlineData("0x00000000")]
|
||||
[InlineData("not-an-id")]
|
||||
public void AddCharacterRejectsAnAmbiguousOrInvalidId(string id)
|
||||
{
|
||||
var store = new LauncherProfileStore(_filePath);
|
||||
store.Load();
|
||||
store.AddServer("Local ACE", "127.0.0.1", 9000);
|
||||
store.AddAccount("Local ACE", "testaccount", "pw");
|
||||
|
||||
Assert.Throws<LauncherProfileException>(() =>
|
||||
store.AddCharacter(
|
||||
"Local ACE",
|
||||
"testaccount",
|
||||
"+Manual",
|
||||
id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullProfileWithServersAccountsAndCharactersRoundTripsThroughDisk()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue