mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-21 21:13:05 +01:00
2124 lines
75 KiB
C#
2124 lines
75 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Abstractions;
|
|
using System.Linq;
|
|
using System.Management;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Mime;
|
|
using System.Net.Sockets;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using Microsoft.Data.SqlClient;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
using Moq;
|
|
|
|
using MySqlConnector;
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
using Npgsql;
|
|
|
|
using StrawberryShake;
|
|
|
|
using Tgstation.Server.Api;
|
|
using Tgstation.Server.Api.Extensions;
|
|
using Tgstation.Server.Api.Models;
|
|
using Tgstation.Server.Api.Models.Request;
|
|
using Tgstation.Server.Api.Models.Response;
|
|
using Tgstation.Server.Api.Rights;
|
|
using Tgstation.Server.Client;
|
|
using Tgstation.Server.Client.Components;
|
|
using Tgstation.Server.Client.GraphQL;
|
|
using Tgstation.Server.Common.Extensions;
|
|
using Tgstation.Server.Host.Components;
|
|
using Tgstation.Server.Host.Configuration;
|
|
using Tgstation.Server.Host.Database;
|
|
using Tgstation.Server.Host.Extensions;
|
|
using Tgstation.Server.Host.Jobs;
|
|
using Tgstation.Server.Host.System;
|
|
using Tgstation.Server.Tests.Live.Instance;
|
|
|
|
namespace Tgstation.Server.Tests.Live
|
|
{
|
|
[TestClass]
|
|
[TestCategory("SkipWhenLiveUnitTesting")]
|
|
[TestCategory("RequiresDatabase")]
|
|
public sealed class TestLiveServer
|
|
{
|
|
public static readonly Version TestUpdateVersion = new(5, 11, 0);
|
|
|
|
static readonly Lazy<ushort> odDMPort = new(() => FreeTcpPort());
|
|
static readonly Lazy<ushort> odDDPort = new(() => FreeTcpPort(odDMPort.Value));
|
|
static readonly Lazy<ushort> compatDMPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value));
|
|
static readonly Lazy<ushort> compatDDPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value));
|
|
static readonly Lazy<ushort> mainDDPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value));
|
|
static readonly Lazy<ushort> mainDMPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value, mainDDPort.Value));
|
|
|
|
static void InitializePorts()
|
|
{
|
|
_ = odDMPort.Value;
|
|
_ = odDDPort.Value;
|
|
_ = compatDMPort.Value;
|
|
_ = compatDDPort.Value;
|
|
_ = mainDDPort.Value;
|
|
_ = mainDMPort.Value;
|
|
}
|
|
|
|
readonly RestServerClientFactory restClientFactory;
|
|
readonly GraphQLServerClientFactory graphQLClientFactory;
|
|
|
|
public TestLiveServer()
|
|
{
|
|
restClientFactory = new(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
|
graphQLClientFactory = new GraphQLServerClientFactory(restClientFactory);
|
|
}
|
|
|
|
public static List<System.Diagnostics.Process> GetEngineServerProcessesOnPort(EngineType engineType, ushort? port)
|
|
{
|
|
var result = new List<System.Diagnostics.Process>();
|
|
|
|
switch (engineType)
|
|
{
|
|
case EngineType.Byond:
|
|
result.AddRange(System.Diagnostics.Process.GetProcessesByName("DreamDaemon"));
|
|
if (new PlatformIdentifier().IsWindows)
|
|
result.AddRange(System.Diagnostics.Process.GetProcessesByName("dd"));
|
|
break;
|
|
case EngineType.OpenDream:
|
|
var potentialProcesses = System.Diagnostics.Process.GetProcessesByName("dotnet")
|
|
.Where(process =>
|
|
{
|
|
if (GetCommandLine(process)?.Contains("Robust.Server") == true)
|
|
return true;
|
|
|
|
process.Dispose();
|
|
return false;
|
|
});
|
|
|
|
result.AddRange(potentialProcesses);
|
|
break;
|
|
default:
|
|
Assert.Fail($"Unknown engine type: {engineType}");
|
|
return null;
|
|
}
|
|
|
|
if (port.HasValue)
|
|
result = result.Where(x =>
|
|
{
|
|
string portString = null;
|
|
switch (engineType)
|
|
{
|
|
case EngineType.OpenDream:
|
|
portString = $"--cvar net.port={port.Value}";
|
|
break;
|
|
case EngineType.Byond:
|
|
portString = $"-port {port.Value}";
|
|
break;
|
|
default:
|
|
Assert.Fail($"Unknown engine type: {engineType}");
|
|
break;
|
|
}
|
|
|
|
if (GetCommandLine(x)?.Contains(portString) ?? false)
|
|
return true;
|
|
|
|
x.Dispose();
|
|
return false;
|
|
}).ToList();
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string GetCommandLine(System.Diagnostics.Process process)
|
|
{
|
|
if (new PlatformIdentifier().IsWindows)
|
|
{
|
|
var searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id);
|
|
var objects = searcher.Get();
|
|
var commandLine = objects.Cast<ManagementBaseObject>().SingleOrDefault()?["CommandLine"]?.ToString();
|
|
return commandLine;
|
|
}
|
|
|
|
try
|
|
{
|
|
var cmdlineFile = File.ReadAllText($"/proc/{process.Id}/cmdline");
|
|
var parsed = cmdlineFile.Replace('\0', ' ');
|
|
return parsed;
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static bool TerminateAllEngineServers()
|
|
{
|
|
var result = false;
|
|
foreach (var enumValue in Enum.GetValues<EngineType>())
|
|
foreach (var proc in GetEngineServerProcessesOnPort(enumValue, null))
|
|
using (proc)
|
|
{
|
|
proc.Kill();
|
|
proc.WaitForExit();
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static ushort FreeTcpPort(params ushort[] usedPorts)
|
|
{
|
|
ushort result;
|
|
var listeners = new List<TcpListener>();
|
|
|
|
try
|
|
{
|
|
do
|
|
{
|
|
var l = new TcpListener(IPAddress.Any, 0);
|
|
l.Start();
|
|
try
|
|
{
|
|
listeners.Add(l);
|
|
}
|
|
catch
|
|
{
|
|
using (l)
|
|
l.Stop();
|
|
throw;
|
|
}
|
|
|
|
result = (ushort)((IPEndPoint)l.LocalEndpoint).Port;
|
|
}
|
|
while (usedPorts.Contains(result) || result < 20000);
|
|
}
|
|
finally
|
|
{
|
|
foreach (var l in listeners)
|
|
using (l)
|
|
l.Stop();
|
|
}
|
|
|
|
Console.WriteLine($"Allocated port: {result}");
|
|
return result;
|
|
}
|
|
|
|
[ClassInitialize]
|
|
public static async Task Initialize(TestContext _)
|
|
{
|
|
// Clear problematic environment variables
|
|
Environment.SetEnvironmentVariable("MSBuildExtensionsPath", null);
|
|
Environment.SetEnvironmentVariable("MSBuildSDKsPath", null);
|
|
|
|
if (TestingUtils.RunningInGitHubActions || String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")))
|
|
await TestingGitHubService.InitializeAndInject(default);
|
|
|
|
await CachingFileDownloader.InitializeAndInjectForLiveTests(default);
|
|
|
|
DummyChatProvider.RandomDisconnections(true);
|
|
RestServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory();
|
|
|
|
var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_CONNECTION_STRING");
|
|
if (String.IsNullOrWhiteSpace(connectionString))
|
|
return;
|
|
// In CI we've run into issues with the sql services not starting
|
|
// check they're ready
|
|
var databaseType = Enum.Parse<DatabaseType>(Environment.GetEnvironmentVariable("TGS_TEST_DATABASE_TYPE"));
|
|
switch (databaseType)
|
|
{
|
|
case DatabaseType.MariaDB:
|
|
case DatabaseType.MySql:
|
|
var mySqlBuilder = new MySqlConnectionStringBuilder(connectionString)
|
|
{
|
|
Database = new MySqlConnectionStringBuilder().Database
|
|
};
|
|
connectionString = mySqlBuilder.ConnectionString;
|
|
break;
|
|
case DatabaseType.PostgresSql:
|
|
var pgSqlBuilder = new NpgsqlConnectionStringBuilder(connectionString)
|
|
{
|
|
Database = new NpgsqlConnectionStringBuilder().Database
|
|
};
|
|
connectionString = pgSqlBuilder.ConnectionString;
|
|
break;
|
|
case DatabaseType.SqlServer:
|
|
var msSqlBuilder = new SqlConnectionStringBuilder(connectionString)
|
|
{
|
|
InitialCatalog = new SqlConnectionStringBuilder().InitialCatalog
|
|
};
|
|
connectionString = msSqlBuilder.ConnectionString;
|
|
break;
|
|
case DatabaseType.Sqlite:
|
|
return; // no test required
|
|
default:
|
|
Assert.Fail($"Unknown DatabaseType {databaseType}!");
|
|
return;
|
|
}
|
|
|
|
var connectionFactory = new DatabaseConnectionFactory();
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var cancellationToken = cts.Token;
|
|
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection(connectionString, databaseType);
|
|
try
|
|
{
|
|
await connection.OpenAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"TEST ERROR INIT: Could not connect to database. Retrying after 3s. Exception: {ex}");
|
|
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
|
|
continue;
|
|
}
|
|
|
|
await connection.CloseAsync();
|
|
break;
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested && TestingUtils.RunningInGitHubActions)
|
|
{
|
|
Assert.Fail("Could not connect to the test database! Try re-running failed jobs.");
|
|
}
|
|
}
|
|
|
|
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
|
|
public static void Cleanup()
|
|
{
|
|
CachingFileDownloader.Cleanup();
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestUpdateProtocolAndDisabledOAuth()
|
|
{
|
|
using var server = new LiveTestingServer(null, false);
|
|
using var serverCts = new CancellationTokenSource();
|
|
var cancellationToken = serverCts.Token;
|
|
var serverTask = server.Run(cancellationToken).AsTask();
|
|
try
|
|
{
|
|
async ValueTask<ServerUpdateResponse> TestWithoutAndWithPermission(Func<ValueTask<ServerUpdateResponse>> action, IRestServerClient client, AdministrationRights right)
|
|
{
|
|
var ourUser = await client.Users.Read(cancellationToken);
|
|
var update = new UserUpdateRequest
|
|
{
|
|
Id = ourUser.Id,
|
|
PermissionSet = ourUser.PermissionSet,
|
|
};
|
|
|
|
update.PermissionSet.AdministrationRights &= ~right;
|
|
await client.Users.Update(update, cancellationToken);
|
|
|
|
await ApiAssert.ThrowsExactly<InsufficientPermissionsException, ServerUpdateResponse>(action);
|
|
|
|
update.PermissionSet.AdministrationRights |= right;
|
|
await client.Users.Update(update, cancellationToken);
|
|
|
|
return await action();
|
|
}
|
|
|
|
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
|
|
{
|
|
// Disabled OAuth test
|
|
using (var httpClient = new HttpClient())
|
|
using (var request = new HttpRequestMessage(HttpMethod.Post, server.ApiUrl.ToString()))
|
|
{
|
|
request.Headers.Accept.Clear();
|
|
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
|
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
|
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
|
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, adminClient.RestClient.Token.Bearer);
|
|
request.Headers.Add(ApiHeaders.OAuthProviderHeader, Api.Models.OAuthProvider.GitHub.ToString());
|
|
using var response = await httpClient.SendAsync(request, cancellationToken);
|
|
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var message = JsonConvert.DeserializeObject<ErrorMessageResponse>(content);
|
|
Assert.AreEqual(Api.Models.ErrorCode.OAuthProviderDisabled, message.ErrorCode);
|
|
}
|
|
|
|
//attempt to update to stable
|
|
await adminClient.Execute(
|
|
async restClient =>
|
|
{
|
|
var responseModel = await TestWithoutAndWithPermission(
|
|
() => restClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = TestUpdateVersion,
|
|
UploadZip = false,
|
|
},
|
|
null,
|
|
cancellationToken),
|
|
adminClient.RestClient,
|
|
AdministrationRights.ChangeVersion);
|
|
|
|
Assert.IsNotNull(responseModel);
|
|
Assert.IsNull(responseModel.FileTicket);
|
|
Assert.AreEqual(TestUpdateVersion, responseModel.NewVersion);
|
|
},
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RepositoryBasedServerUpdate.ExecuteAsync(TestUpdateVersion, cancellationToken),
|
|
result => result.ChangeServerNodeVersionViaTrackedRepository,
|
|
cancellationToken));
|
|
|
|
try
|
|
{
|
|
var serverInfoTask = adminClient.RestClient.ServerInformation(cancellationToken).AsTask();
|
|
var completedTask = await Task.WhenAny(serverTask, serverInfoTask);
|
|
if (completedTask == serverInfoTask)
|
|
{
|
|
var serverInfo = await serverInfoTask;
|
|
Assert.IsTrue(serverInfo.UpdateInProgress);
|
|
}
|
|
}
|
|
catch (ServiceUnavailableException) { }
|
|
catch (HttpRequestException) { }
|
|
}
|
|
|
|
async Task CheckUpdate()
|
|
{
|
|
//wait up to 3 minutes for the dl and install
|
|
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(3), cancellationToken));
|
|
|
|
Assert.IsTrue(serverTask.IsCompleted, "Server still running!");
|
|
|
|
await serverTask;
|
|
|
|
Assert.IsTrue(Directory.Exists(server.UpdatePath), "Update directory not present!");
|
|
|
|
var updatedAssemblyPath = Path.Combine(server.UpdatePath, "Tgstation.Server.Host.dll");
|
|
Assert.IsTrue(File.Exists(updatedAssemblyPath), "Updated assembly missing!");
|
|
|
|
var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath);
|
|
Assert.AreEqual(TestUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver());
|
|
}
|
|
|
|
await CheckUpdate();
|
|
|
|
// Second pass, uploaded updates
|
|
var downloader = CachingFileDownloader.CreateRealDownloader(Mock.Of<ILogger<Host.IO.FileDownloader>>());
|
|
var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN");
|
|
if (String.IsNullOrWhiteSpace(gitHubToken))
|
|
gitHubToken = null;
|
|
await new Host.IO.DefaultIOManager(new FileSystem()).DeleteDirectory(server.UpdatePath, cancellationToken);
|
|
serverTask = server.Run(cancellationToken).AsTask();
|
|
|
|
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
|
|
{
|
|
// test we can't do this without the correct permission
|
|
|
|
await using var download = downloader.DownloadFile(
|
|
new Uri($"https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v{TestLiveServer.TestUpdateVersion}/ServerUpdatePackage.zip"),
|
|
gitHubToken);
|
|
|
|
var downloadStream = await download.GetResult(cancellationToken);
|
|
var responseModel = await TestWithoutAndWithPermission(
|
|
() => adminClient.RestClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = TestUpdateVersion,
|
|
UploadZip = true,
|
|
},
|
|
downloadStream,
|
|
cancellationToken),
|
|
adminClient.RestClient,
|
|
AdministrationRights.UploadVersion);
|
|
|
|
Assert.IsNotNull(responseModel);
|
|
Assert.IsNotNull(responseModel.FileTicket);
|
|
Assert.AreEqual(TestUpdateVersion, responseModel.NewVersion);
|
|
}
|
|
|
|
await CheckUpdate();
|
|
}
|
|
finally
|
|
{
|
|
serverCts.Cancel();
|
|
try
|
|
{
|
|
await serverTask;
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (AggregateException ex)
|
|
{
|
|
if (ex.InnerException is NotSupportedException notSupportedException)
|
|
Assert.Inconclusive(notSupportedException.Message);
|
|
}
|
|
}
|
|
|
|
Assert.IsTrue(server.RestartRequested, "Server not requesting restart!");
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestUpdateBadVersion()
|
|
{
|
|
using var server = new LiveTestingServer(null, false);
|
|
using var serverCts = new CancellationTokenSource();
|
|
var cancellationToken = serverCts.Token;
|
|
var serverTask = server.Run(cancellationToken);
|
|
try
|
|
{
|
|
var testUpdateVersion = new Version(5, 11, 20);
|
|
await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken);
|
|
await ApiAssert.ThrowsExactly<ConflictException, ServerUpdateResponse>(
|
|
() => adminClient.RestClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = testUpdateVersion
|
|
},
|
|
null,
|
|
cancellationToken),
|
|
Api.Models.ErrorCode.ResourceNotPresent);
|
|
}
|
|
finally
|
|
{
|
|
serverCts.Cancel();
|
|
try
|
|
{
|
|
await serverTask;
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (AggregateException ex)
|
|
{
|
|
if (ex.InnerException is NotSupportedException notSupportedException)
|
|
Assert.Inconclusive(notSupportedException.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestOneServerSwarmUpdate()
|
|
{
|
|
// cleanup existing directories
|
|
new LiveTestingServer(null, false).Dispose();
|
|
|
|
const string PrivateKey = "adlfj73ywifhks7iwrgfegjs";
|
|
|
|
var controllerAddress = new Uri("http://localhost:15011");
|
|
using (var controller = new LiveTestingServer(new SwarmConfiguration
|
|
{
|
|
Address = controllerAddress,
|
|
Identifier = "controller",
|
|
PrivateKey = PrivateKey
|
|
}, false, 15011))
|
|
{
|
|
using var serverCts = new CancellationTokenSource();
|
|
serverCts.CancelAfter(TimeSpan.FromHours(3));
|
|
var cancellationToken = serverCts.Token;
|
|
var serverTask = controller.Run(cancellationToken).AsTask();
|
|
|
|
try
|
|
{
|
|
await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken);
|
|
|
|
var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
|
|
|
|
static void CheckInfo(ServerInformationResponse serverInformation)
|
|
{
|
|
Assert.IsNotNull(serverInformation.SwarmServers);
|
|
Assert.AreEqual(1, serverInformation.SwarmServers.Count);
|
|
var controller = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "controller");
|
|
Assert.IsNotNull(controller);
|
|
Assert.AreEqual(controller.Address, new Uri("http://localhost:15011"));
|
|
Assert.IsTrue(controller.Controller);
|
|
}
|
|
|
|
CheckInfo(controllerInfo);
|
|
|
|
// test update
|
|
await controllerClient.Execute(
|
|
async restClient =>
|
|
{
|
|
var responseModel = await restClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = TestUpdateVersion
|
|
},
|
|
null,
|
|
cancellationToken);
|
|
|
|
Assert.IsNotNull(responseModel);
|
|
Assert.IsNull(responseModel.FileTicket);
|
|
Assert.AreEqual(TestUpdateVersion, responseModel.NewVersion);
|
|
},
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RepositoryBasedServerUpdate.ExecuteAsync(TestUpdateVersion, cancellationToken),
|
|
result => result.ChangeServerNodeVersionViaTrackedRepository,
|
|
cancellationToken));
|
|
|
|
await Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(2)), serverTask);
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
|
|
static void CheckServerUpdated(LiveTestingServer server)
|
|
{
|
|
Assert.IsTrue(Directory.Exists(server.UpdatePath), "Update directory not present!");
|
|
|
|
var updatedAssemblyPath = Path.Combine(server.UpdatePath, "Tgstation.Server.Host.dll");
|
|
Assert.IsTrue(File.Exists(updatedAssemblyPath), "Updated assembly missing!");
|
|
|
|
var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath);
|
|
Assert.AreEqual(TestUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver());
|
|
Directory.Delete(server.UpdatePath, true);
|
|
}
|
|
|
|
CheckServerUpdated(controller);
|
|
}
|
|
finally
|
|
{
|
|
serverCts.Cancel();
|
|
await serverTask;
|
|
}
|
|
}
|
|
|
|
new LiveTestingServer(null, false).Dispose();
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestCreateServerWithNoArguments()
|
|
{
|
|
using var server = new LiveTestingServer(null, false);
|
|
await server.RunNoArgumentsTest(default);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestSwarmSynchronizationAndUpdates()
|
|
{
|
|
// cleanup existing directories
|
|
new LiveTestingServer(null, false).Dispose();
|
|
const string PrivateKey = "adlfj73ywifhks7iwrgfegjs";
|
|
|
|
var controllerAddress = new Uri("http://localhost:15011");
|
|
using (var controller = new LiveTestingServer(new SwarmConfiguration
|
|
{
|
|
Address = controllerAddress,
|
|
Identifier = "controller",
|
|
PrivateKey = PrivateKey
|
|
}, false, 15011))
|
|
{
|
|
using var node1 = new LiveTestingServer(new SwarmConfiguration
|
|
{
|
|
Address = new Uri("http://localhost:15012"),
|
|
ControllerAddress = controllerAddress,
|
|
Identifier = "node1",
|
|
PrivateKey = PrivateKey
|
|
}, false, 15012);
|
|
using var node2 = new LiveTestingServer(new SwarmConfiguration
|
|
{
|
|
Address = new Uri("http://localhost:15013"),
|
|
ControllerAddress = controllerAddress,
|
|
Identifier = "node2",
|
|
PrivateKey = PrivateKey
|
|
}, false, 15013);
|
|
using var serverCts = new CancellationTokenSource();
|
|
var cancellationToken = serverCts.Token;
|
|
var serverTask = Task.WhenAll(
|
|
node1.Run(cancellationToken).AsTask(),
|
|
node2.Run(cancellationToken).AsTask(),
|
|
controller.Run(cancellationToken).AsTask());
|
|
|
|
try
|
|
{
|
|
await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken);
|
|
await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken);
|
|
await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken);
|
|
|
|
var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
|
|
|
|
async Task WaitForSwarmServerUpdate()
|
|
{
|
|
ServerInformationResponse serverInformation;
|
|
do
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(10));
|
|
serverInformation = await node1Client.RestClient.ServerInformation(cancellationToken);
|
|
}
|
|
while (serverInformation.SwarmServers.Count == 1);
|
|
}
|
|
|
|
static void CheckInfo(ServerInformationResponse serverInformation)
|
|
{
|
|
Assert.IsNotNull(serverInformation.SwarmServers);
|
|
Assert.AreEqual(3, serverInformation.SwarmServers.Count);
|
|
|
|
var node1 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node1");
|
|
Assert.IsNotNull(node1);
|
|
Assert.AreEqual(node1.Address, new Uri("http://localhost:15012"));
|
|
Assert.IsFalse(node1.Controller);
|
|
|
|
var node2 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node2");
|
|
Assert.IsNotNull(node2);
|
|
Assert.AreEqual(node2.Address, new Uri("http://localhost:15013"));
|
|
Assert.IsFalse(node2.Controller);
|
|
|
|
var controller = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "controller");
|
|
Assert.IsNotNull(controller);
|
|
Assert.AreEqual(controller.Address, new Uri("http://localhost:15011"));
|
|
Assert.IsTrue(controller.Controller);
|
|
}
|
|
|
|
CheckInfo(controllerInfo);
|
|
|
|
// wait a few minutes for the updated server list to dispatch
|
|
await Task.WhenAny(
|
|
WaitForSwarmServerUpdate(),
|
|
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
|
|
|
|
var node2Info = await node2Client.RestClient.ServerInformation(cancellationToken);
|
|
var node1Info = await node1Client.RestClient.ServerInformation(cancellationToken);
|
|
CheckInfo(node1Info);
|
|
CheckInfo(node2Info);
|
|
|
|
// check user info is shared
|
|
var newUser = await node2Client.RestClient.Users.Create(new UserCreateRequest
|
|
{
|
|
Name = "asdf",
|
|
Password = "asdfasdfasdfasdf",
|
|
Enabled = true,
|
|
PermissionSet = new PermissionSet
|
|
{
|
|
AdministrationRights = AdministrationRights.ChangeVersion
|
|
}
|
|
}, cancellationToken);
|
|
|
|
var node1User = await node1Client.RestClient.Users.GetId(newUser, cancellationToken);
|
|
Assert.AreEqual(newUser.Name, node1User.Name);
|
|
Assert.AreEqual(newUser.Enabled, node1User.Enabled);
|
|
|
|
await using var controllerUserClient = await restClientFactory.CreateFromLogin(
|
|
controllerAddress,
|
|
newUser.Name,
|
|
"asdfasdfasdfasdf");
|
|
|
|
await using var node1TokenCopiedClient = restClientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token);
|
|
await node1TokenCopiedClient.Administration.Read(false, cancellationToken);
|
|
|
|
// check instance info is not shared
|
|
var controllerInstance = await controllerClient.RestClient.Instances.CreateOrAttach(
|
|
new InstanceCreateRequest
|
|
{
|
|
Name = "ControllerInstance",
|
|
Path = Path.Combine(controller.Directory, "ControllerInstance")
|
|
},
|
|
cancellationToken);
|
|
|
|
var node2Instance = await node2Client.RestClient.Instances.CreateOrAttach(
|
|
new InstanceCreateRequest
|
|
{
|
|
Name = "Node2Instance",
|
|
Path = Path.Combine(node2.Directory, "Node2Instance")
|
|
},
|
|
cancellationToken);
|
|
var node2InstanceList = await node2Client.RestClient.Instances.List(null, cancellationToken);
|
|
Assert.AreEqual(1, node2InstanceList.Count);
|
|
Assert.AreEqual(node2Instance.Id, node2InstanceList[0].Id);
|
|
Assert.IsNotNull(await node2Client.RestClient.Instances.GetId(node2Instance, cancellationToken));
|
|
var controllerInstanceList = await controllerClient.RestClient.Instances.List(null, cancellationToken);
|
|
Assert.AreEqual(1, controllerInstanceList.Count);
|
|
Assert.AreEqual(controllerInstance.Id, controllerInstanceList[0].Id);
|
|
Assert.IsNotNull(await controllerClient.RestClient.Instances.GetId(controllerInstance, cancellationToken));
|
|
|
|
await ApiAssert.ThrowsExactly<ConflictException, InstanceResponse>(() => controllerClient.RestClient.Instances.GetId(node2Instance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent);
|
|
await ApiAssert.ThrowsExactly<ConflictException, InstanceResponse>(() => node1Client.RestClient.Instances.GetId(controllerInstance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent);
|
|
|
|
// test update
|
|
await node1Client.Execute(
|
|
async restClient => await restClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = TestUpdateVersion
|
|
},
|
|
null,
|
|
cancellationToken),
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RepositoryBasedServerUpdate.ExecuteAsync(TestUpdateVersion, cancellationToken),
|
|
result => result.ChangeServerNodeVersionViaTrackedRepository,
|
|
cancellationToken));
|
|
await Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(2)), serverTask);
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
|
|
void CheckServerUpdated(LiveTestingServer server)
|
|
{
|
|
Assert.IsTrue(Directory.Exists(server.UpdatePath), "Update directory not present!");
|
|
|
|
var updatedAssemblyPath = Path.Combine(server.UpdatePath, "Tgstation.Server.Host.dll");
|
|
Assert.IsTrue(File.Exists(updatedAssemblyPath), "Updated assembly missing!");
|
|
|
|
var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath);
|
|
Assert.AreEqual(TestUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver());
|
|
Directory.Delete(server.UpdatePath, true);
|
|
}
|
|
|
|
CheckServerUpdated(controller);
|
|
CheckServerUpdated(node1);
|
|
CheckServerUpdated(node2);
|
|
|
|
// test it respects the update configuration
|
|
controller.UpdateSwarmArguments(new SwarmConfiguration
|
|
{
|
|
Address = controllerAddress,
|
|
Identifier = "controller",
|
|
PrivateKey = PrivateKey,
|
|
UpdateRequiredNodeCount = 2,
|
|
});
|
|
serverTask = Task.WhenAll(
|
|
controller.Run(cancellationToken).AsTask(),
|
|
node1.Run(cancellationToken).AsTask());
|
|
|
|
await using var controllerClient2 = await CreateAdminClient(controller.ApiUrl, cancellationToken);
|
|
await using var node1Client2 = await CreateAdminClient(node1.ApiUrl, cancellationToken);
|
|
|
|
await controllerClient2.Execute(
|
|
async restClient => await ApiAssert.ThrowsExactly<ApiConflictException, ServerUpdateResponse>(
|
|
() => restClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = TestUpdateVersion
|
|
},
|
|
null,
|
|
cancellationToken),
|
|
Api.Models.ErrorCode.SwarmIntegrityCheckFailed),
|
|
async gqlClient => await ApiAssert.OperationFails(
|
|
gqlClient,
|
|
gql => gql.RepositoryBasedServerUpdate.ExecuteAsync(TestUpdateVersion, cancellationToken),
|
|
result => result.ChangeServerNodeVersionViaTrackedRepository,
|
|
Client.GraphQL.ErrorCode.SwarmIntegrityCheckFailed,
|
|
cancellationToken));
|
|
|
|
// regression: test updating also works from the controller
|
|
serverTask = Task.WhenAll(
|
|
serverTask,
|
|
node2.Run(cancellationToken).AsTask());
|
|
|
|
await using var node2Client2 = await CreateAdminClient(node2.ApiUrl, cancellationToken);
|
|
|
|
async Task WaitForSwarmServerUpdate2()
|
|
{
|
|
ServerInformationResponse serverInformation;
|
|
do
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(10));
|
|
serverInformation = await node2Client2.RestClient.ServerInformation(cancellationToken);
|
|
}
|
|
while (serverInformation.SwarmServers.Count == 1);
|
|
}
|
|
|
|
await Task.WhenAny(
|
|
WaitForSwarmServerUpdate2(),
|
|
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
|
|
|
|
var node2Info2 = await node2Client2.RestClient.ServerInformation(cancellationToken);
|
|
var node1Info2 = await node1Client2.RestClient.ServerInformation(cancellationToken);
|
|
CheckInfo(node1Info2);
|
|
CheckInfo(node2Info2);
|
|
|
|
// also test with uploaded updates this time
|
|
var downloader = CachingFileDownloader.CreateRealDownloader(Mock.Of<ILogger<Host.IO.FileDownloader>>());
|
|
var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN");
|
|
if (String.IsNullOrWhiteSpace(gitHubToken))
|
|
gitHubToken = null;
|
|
await using var download = downloader.DownloadFile(
|
|
new Uri($"https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v{TestLiveServer.TestUpdateVersion}/ServerUpdatePackage.zip"),
|
|
gitHubToken);
|
|
|
|
var downloadStream = await download.GetResult(cancellationToken);
|
|
var responseModel = await controllerClient2.RestClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = TestUpdateVersion,
|
|
UploadZip = true,
|
|
},
|
|
downloadStream,
|
|
cancellationToken);
|
|
|
|
Assert.IsNotNull(responseModel);
|
|
Assert.IsNotNull(responseModel.FileTicket);
|
|
Assert.AreEqual(TestUpdateVersion, responseModel.NewVersion);
|
|
|
|
await Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(2)), serverTask);
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
|
|
CheckServerUpdated(controller);
|
|
CheckServerUpdated(node1);
|
|
CheckServerUpdated(node2);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}");
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
serverCts.Cancel();
|
|
await serverTask;
|
|
}
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestSwarmReconnection()
|
|
{
|
|
// cleanup existing directories
|
|
new LiveTestingServer(null, false).Dispose();
|
|
|
|
const string PrivateKey = "adlfj73ywifhks7iwrgfegjs";
|
|
|
|
var controllerAddress = new Uri("http://localhost:15011");
|
|
using (var controller = new LiveTestingServer(new SwarmConfiguration
|
|
{
|
|
Address = controllerAddress,
|
|
Identifier = "controller",
|
|
PublicAddress = new Uri("http://fakecontroller.com"),
|
|
PrivateKey = PrivateKey,
|
|
UpdateRequiredNodeCount = 2,
|
|
}, false, 15011))
|
|
{
|
|
using var node1 = new LiveTestingServer(new SwarmConfiguration
|
|
{
|
|
Address = new Uri("http://localhost:15012"),
|
|
ControllerAddress = controllerAddress,
|
|
PublicAddress = new Uri("http://fakenode1.com"),
|
|
Identifier = "node1",
|
|
PrivateKey = PrivateKey
|
|
}, false, 15012);
|
|
using var node2 = new LiveTestingServer(new SwarmConfiguration
|
|
{
|
|
Address = new Uri("http://localhost:15013"),
|
|
ControllerAddress = controllerAddress,
|
|
Identifier = "node2",
|
|
PrivateKey = PrivateKey
|
|
}, false, 15013);
|
|
using var serverCts = new CancellationTokenSource();
|
|
|
|
var cancellationToken = serverCts.Token;
|
|
using var node1Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
|
|
Task node1Task, node2Task, controllerTask;
|
|
var serverTask = Task.WhenAll(
|
|
node1Task = node1.Run(node1Cts.Token).AsTask(),
|
|
node2Task = node2.Run(cancellationToken).AsTask(),
|
|
controllerTask = controller.Run(cancellationToken).AsTask());
|
|
|
|
try
|
|
{
|
|
await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken);
|
|
await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken);
|
|
await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken);
|
|
|
|
// test a token signed from any one node will work on another
|
|
var token = node2Client.RestClient.Token;
|
|
var testNode1Client = restClientFactory.CreateFromToken(node1.ApiUrl, token);
|
|
|
|
await testNode1Client.ServerInformation(cancellationToken);
|
|
|
|
var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
|
|
|
|
async Task WaitForSwarmServerUpdate(IRestServerClient client, int currentServerCount)
|
|
{
|
|
ServerInformationResponse serverInformation;
|
|
do
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(10));
|
|
serverInformation = await client.ServerInformation(cancellationToken);
|
|
}
|
|
while (serverInformation.SwarmServers.Count == currentServerCount);
|
|
}
|
|
|
|
static void CheckInfo(ServerInformationResponse serverInformation)
|
|
{
|
|
Assert.IsNotNull(serverInformation.SwarmServers);
|
|
Assert.AreEqual(3, serverInformation.SwarmServers.Count);
|
|
|
|
var node1 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node1");
|
|
Assert.IsNotNull(node1);
|
|
Assert.AreEqual(node1.Address, new Uri("http://localhost:15012"));
|
|
Assert.IsFalse(node1.Controller);
|
|
Assert.AreEqual(node1.PublicAddress, new Uri("http://fakenode1.com"));
|
|
|
|
var node2 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node2");
|
|
Assert.IsNotNull(node2);
|
|
Assert.AreEqual(node2.Address, new Uri("http://localhost:15013"));
|
|
Assert.IsFalse(node2.Controller);
|
|
Assert.IsNull(node2.PublicAddress);
|
|
|
|
var controller = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "controller");
|
|
Assert.IsNotNull(controller);
|
|
Assert.AreEqual(controller.Address, new Uri("http://localhost:15011"));
|
|
Assert.IsTrue(controller.Controller);
|
|
Assert.AreEqual(controller.PublicAddress, new Uri("http://fakecontroller.com"));
|
|
}
|
|
|
|
CheckInfo(controllerInfo);
|
|
|
|
// wait a few minutes for the updated server list to dispatch
|
|
await Task.WhenAny(
|
|
WaitForSwarmServerUpdate(node1Client.RestClient, 1),
|
|
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
|
|
|
|
var node2Info = await node2Client.RestClient.ServerInformation(cancellationToken);
|
|
var node1Info = await node1Client.RestClient.ServerInformation(cancellationToken);
|
|
CheckInfo(node1Info);
|
|
CheckInfo(node2Info);
|
|
|
|
// kill node1
|
|
node1Cts.Cancel();
|
|
await Task.WhenAny(
|
|
node1Task,
|
|
Task.Delay(TimeSpan.FromMinutes(1)));
|
|
Assert.IsTrue(node1Task.IsCompleted);
|
|
|
|
// it should unregister
|
|
controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
|
|
Assert.AreEqual(2, controllerInfo.SwarmServers.Count);
|
|
Assert.IsFalse(controllerInfo.SwarmServers.Any(x => x.Identifier == "node1"));
|
|
|
|
// wait a few minutes for the updated server list to dispatch
|
|
await Task.WhenAny(
|
|
WaitForSwarmServerUpdate(node2Client.RestClient, 3),
|
|
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
|
|
|
|
node2Info = await node2Client.RestClient.ServerInformation(cancellationToken);
|
|
Assert.AreEqual(2, node2Info.SwarmServers.Count);
|
|
Assert.IsFalse(node2Info.SwarmServers.Any(x => x.Identifier == "node1"));
|
|
|
|
// restart the controller
|
|
await controllerClient.Execute(
|
|
restClient => restClient.Administration.Restart(cancellationToken),
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RestartServer.ExecuteAsync(cancellationToken),
|
|
result => result.RestartServerNode,
|
|
cancellationToken));
|
|
|
|
await Task.WhenAny(
|
|
controllerTask,
|
|
Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
|
|
Assert.IsTrue(controllerTask.IsCompleted);
|
|
|
|
controllerTask = controller.Run(cancellationToken).AsTask();
|
|
await using var controllerClient2 = await CreateAdminClient(controller.ApiUrl, cancellationToken);
|
|
|
|
// node 2 should reconnect once it's health check triggers
|
|
await Task.WhenAny(
|
|
WaitForSwarmServerUpdate(controllerClient2.RestClient, 1),
|
|
Task.Delay(TimeSpan.FromMinutes(5), cancellationToken));
|
|
|
|
controllerInfo = await controllerClient2.RestClient.ServerInformation(cancellationToken);
|
|
Assert.AreEqual(2, controllerInfo.SwarmServers.Count);
|
|
Assert.IsNotNull(controllerInfo.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"));
|
|
|
|
// wait a few seconds to dispatch the updated list to node2
|
|
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
|
|
|
|
// restart node2
|
|
await node2Client.Execute(
|
|
restClient => restClient.Administration.Restart(cancellationToken),
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RestartServer.ExecuteAsync(cancellationToken),
|
|
result => result.RestartServerNode,
|
|
cancellationToken));
|
|
await Task.WhenAny(
|
|
node2Task,
|
|
Task.Delay(TimeSpan.FromMinutes(1)));
|
|
Assert.IsTrue(node1Task.IsCompleted);
|
|
|
|
// should have unregistered
|
|
controllerInfo = await controllerClient2.RestClient.ServerInformation(cancellationToken);
|
|
Assert.AreEqual(1, controllerInfo.SwarmServers.Count);
|
|
Assert.IsNull(controllerInfo.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"));
|
|
|
|
// update should fail
|
|
await controllerClient2.Execute(
|
|
async restClient => await ApiAssert.ThrowsExactly<ApiConflictException, ServerUpdateResponse>(
|
|
() => restClient.Administration.Update(
|
|
new ServerUpdateRequest
|
|
{
|
|
NewVersion = TestUpdateVersion
|
|
},
|
|
null,
|
|
cancellationToken),
|
|
Api.Models.ErrorCode.SwarmIntegrityCheckFailed),
|
|
async gqlClient => await ApiAssert.OperationFails(
|
|
gqlClient,
|
|
gql => gql.RepositoryBasedServerUpdate.ExecuteAsync(TestUpdateVersion, cancellationToken),
|
|
result => result.ChangeServerNodeVersionViaTrackedRepository,
|
|
Client.GraphQL.ErrorCode.SwarmIntegrityCheckFailed,
|
|
cancellationToken));
|
|
|
|
node2Task = node2.Run(cancellationToken).AsTask();
|
|
await using var node2Client2 = await CreateAdminClient(node2.ApiUrl, cancellationToken);
|
|
|
|
// should re-register
|
|
await Task.WhenAny(
|
|
WaitForSwarmServerUpdate(node2Client2.RestClient, 1),
|
|
Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
|
|
|
|
node2Info = await node2Client2.RestClient.ServerInformation(cancellationToken);
|
|
Assert.AreEqual(2, node2Info.SwarmServers.Count);
|
|
Assert.IsNotNull(node2Info.SwarmServers.SingleOrDefault(x => x.Identifier == "controller"));
|
|
}
|
|
finally
|
|
{
|
|
serverCts.Cancel();
|
|
await serverTask;
|
|
}
|
|
}
|
|
|
|
new LiveTestingServer(null, false).Dispose();
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestTgstationHeadless() => await TestTgstation(false);
|
|
|
|
async ValueTask TestTgstation(bool interactive)
|
|
{
|
|
// i'm only running this on dev machines, actions is too taxed
|
|
if (TestingUtils.RunningInGitHubActions)
|
|
Assert.Inconclusive("lol. lmao.");
|
|
|
|
var discordConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN");
|
|
|
|
var procs = System.Diagnostics.Process.GetProcessesByName("byond");
|
|
if (procs.Length != 0)
|
|
{
|
|
foreach (var proc in procs)
|
|
proc.Dispose();
|
|
|
|
// Inconclusive and not fail because we don't want to unexpectedly kill a dev's BYOND.exe
|
|
Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!");
|
|
}
|
|
|
|
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddConsole();
|
|
builder.SetMinimumLevel(LogLevel.Trace);
|
|
});
|
|
using var server = new LiveTestingServer(null, true);
|
|
|
|
TerminateAllEngineServers();
|
|
|
|
using var serverCts = new CancellationTokenSource();
|
|
var cancellationToken = serverCts.Token;
|
|
var serverTask = server.Run(cancellationToken);
|
|
try
|
|
{
|
|
await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken);
|
|
var restAdminClient = adminClient.RestClient;
|
|
var instanceManagerTest = new InstanceManagerTest(restAdminClient, server.Directory);
|
|
var instance = await instanceManagerTest.CreateTestInstance("TgTestInstance", cancellationToken);
|
|
var instanceClient = restAdminClient.Instances.CreateClient(instance);
|
|
|
|
|
|
var ddUpdateTask = instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
|
{
|
|
LogOutput = true,
|
|
}, cancellationToken);
|
|
var dmUpdateTask = instanceClient.DreamMaker.Update(new DreamMakerRequest
|
|
{
|
|
ApiValidationSecurityLevel = DreamDaemonSecurity.Trusted,
|
|
}, cancellationToken);
|
|
|
|
var ioManager = new Host.IO.DefaultIOManager(new FileSystem());
|
|
var repoPath = ioManager.ConcatPath(instance.Path, "Repository");
|
|
await using var jobsTest = new JobsRequiredTest(instanceClient.Jobs);
|
|
var postWriteHandler = (Host.IO.IPostWriteHandler)(new PlatformIdentifier().IsWindows
|
|
? new Host.IO.WindowsPostWriteHandler()
|
|
: new Host.IO.PosixPostWriteHandler(loggerFactory.CreateLogger<Host.IO.PosixPostWriteHandler>()));
|
|
var localRepoPath = Environment.GetEnvironmentVariable("TGS_LOCAL_TG_REPO");
|
|
Task jobWaitTask;
|
|
if (!String.IsNullOrWhiteSpace(localRepoPath))
|
|
{
|
|
await ioManager.CopyDirectory(
|
|
[],
|
|
(src, dest) =>
|
|
{
|
|
if (postWriteHandler.NeedsPostWrite(src))
|
|
postWriteHandler.HandleWrite(dest);
|
|
|
|
return ValueTask.CompletedTask;
|
|
},
|
|
ioManager.ConcatPath(
|
|
localRepoPath,
|
|
".git"),
|
|
ioManager.ConcatPath(
|
|
repoPath,
|
|
".git"),
|
|
null,
|
|
cancellationToken);
|
|
|
|
ProcessExecutor processExecutor = null;
|
|
processExecutor = new ProcessExecutor(
|
|
new PlatformIdentifier().IsWindows
|
|
? new WindowsProcessFeatures(loggerFactory.CreateLogger<WindowsProcessFeatures>())
|
|
: new PosixProcessFeatures(new Lazy<IProcessExecutor>(() => processExecutor), ioManager, loggerFactory.CreateLogger<PosixProcessFeatures>()),
|
|
ioManager,
|
|
loggerFactory.CreateLogger<ProcessExecutor>(),
|
|
loggerFactory);
|
|
|
|
async ValueTask RunGitCommand(string args)
|
|
{
|
|
await using var gitRemoteOriginFixProc = await processExecutor.LaunchProcess(
|
|
"git",
|
|
repoPath,
|
|
args,
|
|
cancellationToken,
|
|
null,
|
|
null,
|
|
true,
|
|
true,
|
|
false);
|
|
|
|
int? exitCode;
|
|
using (cancellationToken.Register(gitRemoteOriginFixProc.Terminate))
|
|
exitCode = await gitRemoteOriginFixProc.Lifetime;
|
|
|
|
loggerFactory.CreateLogger("TgTest").LogInformation("git {args} output:{newLine}{output}", args, Environment.NewLine, await gitRemoteOriginFixProc.GetCombinedOutput(cancellationToken));
|
|
Assert.AreEqual(0, exitCode);
|
|
}
|
|
|
|
await RunGitCommand("remote set-url origin https://github.com/tgstation/tgstation");
|
|
await RunGitCommand("checkout -f master");
|
|
await RunGitCommand("reset --hard origin/master");
|
|
|
|
jobWaitTask = Task.CompletedTask;
|
|
}
|
|
else
|
|
{
|
|
var repoResponse = await instanceClient.Repository.Clone(new RepositoryCreateRequest
|
|
{
|
|
Origin = new Uri("https://github.com/tgstation/tgstation"),
|
|
}, cancellationToken);
|
|
jobWaitTask = jobsTest.WaitForJob(repoResponse.ActiveJob, 300, false, null, cancellationToken);
|
|
}
|
|
|
|
await Task.WhenAll(jobWaitTask, ddUpdateTask.AsTask(), dmUpdateTask.AsTask());
|
|
|
|
var depsBytesTask = ioManager.ReadAllBytes(
|
|
ioManager.ConcatPath(repoPath, "dependencies.sh"),
|
|
cancellationToken);
|
|
|
|
var scriptsCopyTask = ioManager.CopyDirectory(
|
|
[],
|
|
(src, dest) =>
|
|
{
|
|
if (postWriteHandler.NeedsPostWrite(src))
|
|
postWriteHandler.HandleWrite(dest);
|
|
|
|
return ValueTask.CompletedTask;
|
|
},
|
|
ioManager.ConcatPath(
|
|
repoPath,
|
|
"tools",
|
|
"tgs_scripts"),
|
|
ioManager.ConcatPath(
|
|
instance.Path,
|
|
"Configuration",
|
|
"EventScripts"),
|
|
null,
|
|
cancellationToken);
|
|
|
|
var dependenciesSh = Encoding.UTF8.GetString(await depsBytesTask);
|
|
var lines = dependenciesSh.Split("\n", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
|
const string MajorPrefix = "export BYOND_MAJOR=";
|
|
var major = Int32.Parse(lines.First(x => x.StartsWith(MajorPrefix))[MajorPrefix.Length..]);
|
|
const string MinorPrefix = "export BYOND_MINOR=";
|
|
var minor = Int32.Parse(lines.First(x => x.StartsWith(MinorPrefix))[MinorPrefix.Length..]);
|
|
|
|
var byondJob = await instanceClient.Engine.SetActiveVersion(new EngineVersionRequest
|
|
{
|
|
EngineVersion = new EngineVersion
|
|
{
|
|
Version = new Version(major, minor),
|
|
Engine = EngineType.Byond,
|
|
},
|
|
}, null, cancellationToken);
|
|
|
|
var byondJobTask = jobsTest.WaitForJob(byondJob.InstallJob, 60, false, null, cancellationToken);
|
|
|
|
await Task.WhenAll(scriptsCopyTask.AsTask(), byondJobTask);
|
|
|
|
var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken);
|
|
|
|
await jobsTest.WaitForJob(compileJob, 180, false, null, cancellationToken);
|
|
|
|
var startJob = await instanceClient.DreamDaemon.Start(cancellationToken);
|
|
await jobsTest.WaitForJob(startJob, 30, false, null, cancellationToken);
|
|
|
|
var compileJob2 = await instanceClient.DreamMaker.Compile(cancellationToken);
|
|
|
|
await jobsTest.WaitForJob(compileJob2, 360, false, null, cancellationToken);
|
|
|
|
if (interactive)
|
|
{
|
|
bool updated = false;
|
|
while (true)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
|
|
|
var status = await instanceClient.DreamDaemon.Read(cancellationToken);
|
|
|
|
if (updated)
|
|
{
|
|
if (status.Status == WatchdogStatus.Offline)
|
|
break;
|
|
}
|
|
else if (status.StagedCompileJob == null)
|
|
{
|
|
updated = true;
|
|
await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
|
{
|
|
SoftShutdown = true
|
|
}, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
serverCts.Cancel();
|
|
await serverTask;
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public Task TestStandardTgsOperation() => TestStandardTgsOperation(false);
|
|
|
|
[TestMethod]
|
|
public Task TestOpenDreamExclusiveTgsOperation()
|
|
{
|
|
if (!Boolean.TryParse(Environment.GetEnvironmentVariable("TGS_TEST_OD_EXCLUSIVE"), out var odExclusive) || !odExclusive)
|
|
Assert.Inconclusive("This test is covered by TestStandardTgsOperation");
|
|
|
|
return TestStandardTgsOperation(true);
|
|
}
|
|
|
|
async Task TestStandardTgsOperation(bool openDreamOnly)
|
|
{
|
|
using (var currentProcess = System.Diagnostics.Process.GetCurrentProcess())
|
|
{
|
|
var currentPriorityClass = currentProcess.PriorityClass;
|
|
if (currentPriorityClass != ProcessPriorityClass.Normal)
|
|
{
|
|
// attempt to adjust it
|
|
Console.WriteLine($"TEST PROCESS PRIORITY: Attempting to normalize process priority from {currentPriorityClass}...");
|
|
currentProcess.PriorityClass = ProcessPriorityClass.Normal;
|
|
}
|
|
}
|
|
|
|
using (var currentProcess = System.Diagnostics.Process.GetCurrentProcess())
|
|
Assert.AreEqual(ProcessPriorityClass.Normal, currentProcess.PriorityClass);
|
|
|
|
InitializePorts();
|
|
|
|
var maximumTestMinutes = TestingUtils.RunningInGitHubActions ? 90 : 20;
|
|
using var hardCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(maximumTestMinutes));
|
|
var hardCancellationToken = hardCancellationTokenSource.Token;
|
|
|
|
hardCancellationToken.Register(() => Console.WriteLine("TGS TEST CANCELLED TOKEN DUE TO TIMEOUT"));
|
|
|
|
ServiceCollectionExtensions.UseAdditionalLoggerProvider<HardFailLoggerProvider>();
|
|
|
|
var failureTask = HardFailLoggerProvider.FailureSource;
|
|
var internalTask = TestTgsInternal(openDreamOnly, hardCancellationToken);
|
|
await Task.WhenAny(
|
|
internalTask,
|
|
failureTask);
|
|
|
|
if (!internalTask.IsCompleted)
|
|
{
|
|
Console.WriteLine("TGS TEST CANCELLING TOKEN DUE TO ERROR");
|
|
hardCancellationTokenSource.Cancel();
|
|
try
|
|
{
|
|
await failureTask;
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
await internalTask;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Console.WriteLine("TEST CANCELLED!");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"ADDITIONAL TEST ERROR: {ex}");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
await internalTask;
|
|
}
|
|
|
|
async Task TestTgsInternal(bool openDreamOnly, CancellationToken hardCancellationToken)
|
|
{
|
|
var discordConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN");
|
|
var ircConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CONNECTION_STRING");
|
|
var missingChatVarsCount = Convert.ToInt32(String.IsNullOrWhiteSpace(discordConnectionString))
|
|
+ Convert.ToInt32(String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_CHANNEL")))
|
|
+ Convert.ToInt32(String.IsNullOrWhiteSpace(ircConnectionString))
|
|
+ Convert.ToInt32(String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_IRC_CHANNEL")));
|
|
|
|
const int TotalChatVars = 4;
|
|
|
|
// uncomment to force this test to run with DummyChatProviders
|
|
// missingChatVarsCount = TotalChatVars;
|
|
|
|
// uncomment to force this test to run with basic watchdog
|
|
// Environment.SetEnvironmentVariable("General__UseBasicWatchdog", "true");
|
|
|
|
if (missingChatVarsCount != 0)
|
|
{
|
|
if (missingChatVarsCount != TotalChatVars)
|
|
Assert.Fail("All TGS_TEST_* chat environment variables must be present or none at all!");
|
|
|
|
ServiceCollectionExtensions.UseChatProviderFactory<DummyChatProviderFactory>();
|
|
}
|
|
else
|
|
{
|
|
// prevalidate
|
|
Assert.IsTrue(new DiscordConnectionStringBuilder(discordConnectionString).Valid);
|
|
Assert.IsTrue(new IrcConnectionStringBuilder(ircConnectionString).Valid);
|
|
}
|
|
|
|
var procs = System.Diagnostics.Process.GetProcessesByName("byond");
|
|
if (procs.Length != 0)
|
|
{
|
|
foreach (var proc in procs)
|
|
proc.Dispose();
|
|
|
|
// Inconclusive and not fail because we don't want to unexpectedly kill a dev's BYOND.exe
|
|
Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!");
|
|
}
|
|
|
|
if (TerminateAllEngineServers())
|
|
await Task.Delay(TimeSpan.FromSeconds(5), hardCancellationToken);
|
|
|
|
using var server = new LiveTestingServer(null, true);
|
|
|
|
using var serverCts = CancellationTokenSource.CreateLinkedTokenSource(hardCancellationToken);
|
|
var cancellationToken = serverCts.Token;
|
|
|
|
for (var i = 0; i < 50; ++i)
|
|
await Task.Yield();
|
|
|
|
async ValueTask<IHost> WaitForHost()
|
|
{
|
|
while (true)
|
|
{
|
|
var host = ((Host.Server)server.RealServer).Host;
|
|
if (host != null)
|
|
return host;
|
|
|
|
await Task.Yield();
|
|
}
|
|
}
|
|
|
|
async ValueTask<InstanceManager> GetInstanceManager()
|
|
{
|
|
var host = await WaitForHost();
|
|
return host.Services.GetRequiredService<InstanceManager>();
|
|
}
|
|
|
|
async ValueTask<ILogger> GetLogger()
|
|
{
|
|
var host = await WaitForHost();
|
|
return host.Services.GetRequiredService<ILogger<TestLiveServer>>();
|
|
}
|
|
|
|
async ValueTask<Host.IO.IFileDownloader> GetFileDownloader()
|
|
{
|
|
var host = await WaitForHost();
|
|
return host.Services.GetRequiredService<Host.IO.IFileDownloader>();
|
|
}
|
|
|
|
// main run
|
|
var serverTask = server.Run(cancellationToken).AsTask();
|
|
if (serverTask.IsFaulted)
|
|
await serverTask;
|
|
|
|
var fileDownloader = ((Host.Server)server.RealServer).Host.Services.GetRequiredService<Host.IO.IFileDownloader>();
|
|
var graphQLClientFactory = new GraphQLServerClientFactory(restClientFactory);
|
|
try
|
|
{
|
|
Api.Models.Instance instance;
|
|
long initialStaged, initialActive, initialSessionId;
|
|
|
|
await using var firstAdminMultiClient = await CreateAdminClient(server.ApiUrl, cancellationToken);
|
|
|
|
var firstAdminRestClient = firstAdminMultiClient.RestClient;
|
|
|
|
if (MultiServerClient.UseGraphQL)
|
|
await using (var tokenOnlyGraphQLClient = graphQLClientFactory.CreateFromToken(server.RootUrl, firstAdminRestClient.Token.Bearer))
|
|
{
|
|
// just testing auth works the same here
|
|
var result = await tokenOnlyGraphQLClient.RunOperation(client => client.ServerVersion.ExecuteAsync(cancellationToken), cancellationToken);
|
|
Assert.IsTrue(result.IsSuccessResult());
|
|
}
|
|
|
|
await using (var tokenOnlyRestClient = restClientFactory.CreateFromToken(server.RootUrl, firstAdminRestClient.Token))
|
|
{
|
|
// regression test for password change issue
|
|
var currentUser = await tokenOnlyRestClient.Users.Read(cancellationToken);
|
|
var updatedUser = await tokenOnlyRestClient.Users.Update(new UserUpdateRequest
|
|
{
|
|
Id = currentUser.Id,
|
|
Password = DefaultCredentials.DefaultAdminUserPassword,
|
|
}, cancellationToken);
|
|
|
|
await ApiAssert.ThrowsExactly<UnauthorizedException, UserResponse>(() => tokenOnlyRestClient.Users.Read(cancellationToken), null);
|
|
}
|
|
|
|
// basic graphql test, to be used everywhere eventually
|
|
if (MultiServerClient.UseGraphQL)
|
|
await using (var unauthenticatedGraphQLClient = graphQLClientFactory.CreateUnauthenticated(server.RootUrl))
|
|
{
|
|
// check auth works as expected
|
|
var result = await unauthenticatedGraphQLClient.RunOperation(client => client.ServerVersion.ExecuteAsync(cancellationToken), cancellationToken);
|
|
Assert.IsTrue(result.IsErrorResult());
|
|
|
|
// test getting server info
|
|
var unAuthedMultiClient = new MultiServerClient(firstAdminRestClient, unauthenticatedGraphQLClient);
|
|
|
|
await unauthenticatedGraphQLClient.RunQueryEnsureNoErrors(
|
|
gqlClient => gqlClient.UnauthenticatedServerInformation.ExecuteAsync(cancellationToken),
|
|
cancellationToken);
|
|
|
|
var testObserver = new HoldLastObserver<IOperationResult<ISessionInvalidationResult>>();
|
|
using var subscription = await unauthenticatedGraphQLClient.Subscribe(
|
|
gql => gql.SessionInvalidation.Watch(),
|
|
testObserver,
|
|
cancellationToken);
|
|
|
|
await Task.Delay(1000, cancellationToken);
|
|
|
|
Assert.AreEqual(0U, testObserver.ErrorCount);
|
|
Assert.AreEqual(1U, testObserver.ResultCount);
|
|
Assert.IsTrue(testObserver.LastValue.IsAuthenticationError());
|
|
Assert.IsTrue(testObserver.Completed);
|
|
}
|
|
|
|
async ValueTask<MultiServerClient> CreateUserWithNoInstancePerms()
|
|
{
|
|
var createRequest = new UserCreateRequest()
|
|
{
|
|
Name = "SomePermlessChum",
|
|
Password = "alidfjuwh84322r4yrkajhfdqh38hrfiouw4",
|
|
Enabled = true,
|
|
PermissionSet = new PermissionSet
|
|
{
|
|
InstanceManagerRights = InstanceManagerRights.Read,
|
|
}
|
|
};
|
|
|
|
var user = await firstAdminRestClient.Users.Create(createRequest, cancellationToken);
|
|
Assert.IsTrue(user.Enabled);
|
|
|
|
return await CreateClient(server.RootUrl, createRequest.Name, createRequest.Password, false, cancellationToken);
|
|
}
|
|
|
|
var restartObserver = new HoldLastObserver<IOperationResult<ISessionInvalidationResult>>();
|
|
IDisposable restartSubscription;
|
|
var jobsHubTest = new JobsHubTests(firstAdminMultiClient, await CreateUserWithNoInstancePerms());
|
|
Task jobsHubTestTask;
|
|
{
|
|
if (server.DumpOpenApiSpecpath)
|
|
{
|
|
// Dump swagger to disk
|
|
// This is purely for CI
|
|
using var httpClient = new HttpClient();
|
|
var webRequestTask = httpClient.GetAsync(server.ApiUrl.ToString() + "doc/tgs_api.json", cancellationToken);
|
|
using var response = await webRequestTask;
|
|
response.EnsureSuccessStatusCode();
|
|
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
await using var output = new FileStream(@"C:\tgs_api.json", FileMode.Create);
|
|
await content.CopyToAsync(output, cancellationToken);
|
|
}
|
|
|
|
async Task FailFast(Task task)
|
|
{
|
|
try
|
|
{
|
|
await task;
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}");
|
|
serverCts.Cancel();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
Task nonInstanceTests;
|
|
IInstanceClient instanceClient = null;
|
|
InstanceResponse odInstance, compatInstance;
|
|
if (!openDreamOnly)
|
|
{
|
|
// force a session refresh if necessary
|
|
if (MultiServerClient.UseGraphQL)
|
|
await firstAdminMultiClient.GraphQLClient.RunQueryEnsureNoErrors(
|
|
gql => gql.ReadCurrentUser.ExecuteAsync(cancellationToken),
|
|
cancellationToken);
|
|
|
|
jobsHubTestTask = FailFast(await jobsHubTest.Run(cancellationToken)); // returns Task<Task>
|
|
var rootTest = FailFast(RawRequestTests.Run(restClientFactory, firstAdminRestClient, cancellationToken));
|
|
var adminTest = FailFast(new AdministrationTest(firstAdminMultiClient).Run(cancellationToken));
|
|
var usersTest = FailFast(new UsersTest(firstAdminMultiClient).Run(cancellationToken).AsTask());
|
|
|
|
var instanceManagerTest = new InstanceManagerTest(firstAdminRestClient, server.Directory);
|
|
var compatInstanceTask = instanceManagerTest.CreateTestInstance("CompatTestsInstance", cancellationToken);
|
|
var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken);
|
|
var byondApiCompatInstanceTask = instanceManagerTest.CreateTestInstance("BCAPITestsInstance", cancellationToken);
|
|
instance = await instanceManagerTest.CreateTestInstance("LiveTestsInstance", cancellationToken);
|
|
compatInstance = await compatInstanceTask;
|
|
odInstance = await odInstanceTask;
|
|
var byondApiCompatInstance = await byondApiCompatInstanceTask;
|
|
var instancesTest = FailFast(instanceManagerTest.RunPreTest(cancellationToken));
|
|
Assert.IsTrue(Directory.Exists(instance.Path));
|
|
instanceClient = firstAdminRestClient.Instances.CreateClient(instance);
|
|
|
|
Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path));
|
|
nonInstanceTests = Task.WhenAll(instancesTest, adminTest, rootTest, usersTest);
|
|
}
|
|
else
|
|
{
|
|
compatInstance = null;
|
|
nonInstanceTests = Task.CompletedTask;
|
|
jobsHubTestTask = null;
|
|
instance = null;
|
|
var instanceManagerTest = new InstanceManagerTest(firstAdminRestClient, server.Directory);
|
|
var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken);
|
|
odInstance = await odInstanceTask;
|
|
}
|
|
|
|
var instanceTest = new InstanceTest(
|
|
firstAdminRestClient.Instances,
|
|
await GetFileDownloader(),
|
|
await GetInstanceManager(),
|
|
(ushort)server.ApiUrl.Port);
|
|
|
|
async Task RunInstanceTests()
|
|
{
|
|
var testSerialized = TestingUtils.RunningInGitHubActions; // they only have 2 cores, can't handle intense parallelization
|
|
async Task ODCompatTests()
|
|
{
|
|
var fileDownloader = await GetFileDownloader();
|
|
var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, await GetLogger(), fileDownloader, cancellationToken);
|
|
|
|
var ex = await Assert.ThrowsExactlyAsync<JobException>(
|
|
() => InstanceTest.DownloadEngineVersion(
|
|
new EngineVersion
|
|
{
|
|
Engine = EngineType.OpenDream,
|
|
SourceSHA = "f1dc153caf9d84cd1d0056e52286cc0163e3f4d3", // 1 before verified version
|
|
},
|
|
fileDownloader,
|
|
server.OpenDreamUrl,
|
|
cancellationToken).AsTask());
|
|
|
|
Assert.AreEqual(Api.Models.ErrorCode.OpenDreamTooOld, ex.ErrorCode);
|
|
|
|
await instanceTest
|
|
.RunCompatTests(
|
|
await edgeODVersionTask,
|
|
server.OpenDreamUrl,
|
|
firstAdminRestClient.Instances.CreateClient(odInstance),
|
|
odDMPort.Value,
|
|
odDDPort.Value,
|
|
server.HighPriorityDreamDaemon,
|
|
server.UsingBasicWatchdog,
|
|
cancellationToken);
|
|
}
|
|
|
|
var odCompatTests = Task.CompletedTask ?? FailFast(ODCompatTests());
|
|
|
|
if (openDreamOnly || testSerialized)
|
|
await odCompatTests;
|
|
|
|
if (openDreamOnly)
|
|
return;
|
|
|
|
var windowsMinCompat = new Version(510, 1346);
|
|
var linuxMinCompat = new Version(512, 1451); // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451
|
|
await CachingFileDownloader.InitializeByondVersion(
|
|
await GetLogger(),
|
|
new PlatformIdentifier().IsWindows
|
|
? windowsMinCompat
|
|
: linuxMinCompat,
|
|
new PlatformIdentifier().IsWindows,
|
|
cancellationToken);
|
|
|
|
var compatTests = Task.CompletedTask ?? FailFast(
|
|
instanceTest
|
|
.RunCompatTests(
|
|
new EngineVersion
|
|
{
|
|
Engine = EngineType.Byond,
|
|
Version = new PlatformIdentifier().IsWindows
|
|
? windowsMinCompat
|
|
: linuxMinCompat,
|
|
},
|
|
server.OpenDreamUrl,
|
|
firstAdminRestClient.Instances.CreateClient(compatInstance),
|
|
compatDMPort.Value,
|
|
compatDDPort.Value,
|
|
server.HighPriorityDreamDaemon,
|
|
server.UsingBasicWatchdog,
|
|
cancellationToken));
|
|
|
|
if (testSerialized)
|
|
await compatTests;
|
|
|
|
await FailFast(
|
|
instanceTest
|
|
.RunTests(
|
|
await GetLogger(),
|
|
instanceClient,
|
|
mainDMPort.Value,
|
|
mainDDPort.Value,
|
|
server.HighPriorityDreamDaemon,
|
|
server.LowPriorityDeployments,
|
|
server.UsingBasicWatchdog,
|
|
cancellationToken));
|
|
|
|
await compatTests;
|
|
await odCompatTests;
|
|
}
|
|
|
|
var instanceTests = RunInstanceTests();
|
|
|
|
await Task.WhenAll(nonInstanceTests, instanceTests);
|
|
|
|
if (openDreamOnly)
|
|
return;
|
|
|
|
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
|
|
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
|
|
Assert.IsNotNull(dd.StagedCompileJob);
|
|
Assert.AreNotEqual(dd.StagedCompileJob.Id, dd.ActiveCompileJob.Id);
|
|
initialActive = dd.ActiveCompileJob.Id.Value;
|
|
initialStaged = dd.StagedCompileJob.Id.Value;
|
|
initialSessionId = dd.SessionId.Value;
|
|
|
|
// force a session refresh if necessary
|
|
if (MultiServerClient.UseGraphQL)
|
|
{
|
|
await firstAdminMultiClient.GraphQLClient.RunQueryEnsureNoErrors(
|
|
gql => gql.ReadCurrentUser.ExecuteAsync(cancellationToken),
|
|
cancellationToken);
|
|
|
|
restartSubscription = await firstAdminMultiClient.GraphQLClient.Subscribe(
|
|
gql => gql.SessionInvalidation.Watch(),
|
|
restartObserver,
|
|
cancellationToken);
|
|
}
|
|
else
|
|
restartSubscription = null;
|
|
|
|
try
|
|
{
|
|
await Task.Delay(1000, cancellationToken);
|
|
|
|
jobsHubTest.ExpectShutdown();
|
|
await firstAdminMultiClient.Execute(
|
|
restClient => restClient.Administration.Restart(cancellationToken),
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RestartServer.ExecuteAsync(cancellationToken),
|
|
result => result.RestartServerNode,
|
|
cancellationToken));
|
|
}
|
|
catch
|
|
{
|
|
restartSubscription?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
|
|
if (MultiServerClient.UseGraphQL)
|
|
{
|
|
Assert.AreEqual(0U, restartObserver.ErrorCount);
|
|
Assert.AreEqual(1U, restartObserver.ResultCount);
|
|
restartObserver.LastValue.EnsureNoErrors();
|
|
Assert.IsTrue(restartObserver.Completed);
|
|
Assert.AreEqual(SessionInvalidationReason.ServerShutdown, restartObserver.LastValue.Data.SessionInvalidated);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
restartSubscription?.Dispose();
|
|
}
|
|
|
|
// test the reattach message queueing
|
|
// for the code coverage really...
|
|
var topicRequestResult = await WatchdogTest.SendTestTopic(
|
|
"tgs_integration_test_tactics6=1",
|
|
WatchdogTest.StaticTopicClient,
|
|
null,
|
|
mainDDPort.Value,
|
|
cancellationToken);
|
|
|
|
Assert.IsNotNull(topicRequestResult);
|
|
Assert.AreEqual("queued", topicRequestResult.StringData);
|
|
|
|
// http bind test https://github.com/tgstation/tgstation-server/issues/1065
|
|
if (new PlatformIdentifier().IsWindows)
|
|
{
|
|
HardFailLoggerProvider.BlockFails = true;
|
|
try
|
|
{
|
|
using var blockingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
|
blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true);
|
|
blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false);
|
|
blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.ApiUrl.Port));
|
|
// bind test run
|
|
await server.Run(cancellationToken);
|
|
Assert.Fail("Expected server task to end with a SocketException");
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
Assert.AreEqual(SocketError.AddressAlreadyInUse, ex.SocketErrorCode);
|
|
}
|
|
finally
|
|
{
|
|
HardFailLoggerProvider.BlockFails = false;
|
|
}
|
|
}
|
|
|
|
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
|
|
var preStartupTime = DateTimeOffset.UtcNow;
|
|
|
|
// chat bot start and DD reattach test
|
|
serverTask = server.Run(cancellationToken).AsTask();
|
|
await using (var multiClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
|
|
{
|
|
var adminClient = multiClient.RestClient;
|
|
|
|
await jobsHubTest.WaitForReconnect(cancellationToken);
|
|
var instanceClient = adminClient.Instances.CreateClient(instance);
|
|
|
|
var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken);
|
|
if (jobs.Count == 0)
|
|
{
|
|
var entities = await instanceClient.Jobs.List(null, cancellationToken);
|
|
var getTasks = entities
|
|
.Select(e => instanceClient.Jobs.GetId(e, cancellationToken))
|
|
.ToList();
|
|
|
|
jobs = (await ValueTaskExtensions.WhenAll(getTasks))
|
|
.Where(x => x.StartedAt.Value >= preStartupTime)
|
|
.ToList();
|
|
}
|
|
|
|
await using var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
|
foreach (var job in jobs)
|
|
{
|
|
Assert.IsTrue(job.StartedAt.Value >= preStartupTime);
|
|
await jrt.WaitForJob(job, 130, job.Description.Contains("Reconnect chat bot") ? null : false, null, cancellationToken);
|
|
}
|
|
|
|
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
|
|
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
|
|
Assert.IsNotNull(dd.StagedCompileJob);
|
|
Assert.AreNotEqual(dd.StagedCompileJob.Id, dd.ActiveCompileJob.Id);
|
|
Assert.AreEqual(initialStaged, dd.StagedCompileJob.Id);
|
|
Assert.AreEqual(initialActive, dd.ActiveCompileJob.Id);
|
|
Assert.AreEqual(initialSessionId, dd.SessionId);
|
|
|
|
var chatReadTask = instanceClient.ChatBots.List(null, cancellationToken);
|
|
|
|
// Check the DMAPI got the channels again https://github.com/tgstation/tgstation-server/issues/1490
|
|
topicRequestResult = await WatchdogTest.SendTestTopic(
|
|
"tgs_integration_test_tactics7=1",
|
|
WatchdogTest.StaticTopicClient,
|
|
(await GetInstanceManager()).GetInstanceReference(instanceClient.Metadata),
|
|
mainDDPort.Value,
|
|
cancellationToken);
|
|
|
|
Assert.IsNotNull(topicRequestResult);
|
|
Assert.IsTrue(topicRequestResult.FloatData.HasValue);
|
|
|
|
var currentChatBots = await chatReadTask;
|
|
var connectedChannelCount = currentChatBots.Where(x => x.Enabled.Value).SelectMany(x => x.Channels).Count();
|
|
|
|
Assert.AreEqual(connectedChannelCount, topicRequestResult.FloatData.Value);
|
|
|
|
dd = await WatchdogTest.TellWorldToReboot2(
|
|
instanceClient,
|
|
await GetInstanceManager(),
|
|
WatchdogTest.StaticTopicClient,
|
|
mainDDPort.Value,
|
|
true,
|
|
cancellationToken);
|
|
|
|
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); // if this assert fails, you likely have to crack open the debugger and read test_fail_reason.txt manually
|
|
Assert.IsNull(dd.StagedCompileJob);
|
|
Assert.AreEqual(initialStaged, dd.ActiveCompileJob.Id);
|
|
|
|
await instanceClient.DreamDaemon.Shutdown(cancellationToken);
|
|
dd = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
|
{
|
|
AutoStart = true
|
|
}, cancellationToken);
|
|
|
|
Assert.AreEqual(WatchdogStatus.Offline, dd.Status);
|
|
|
|
jobsHubTest.ExpectShutdown();
|
|
await multiClient.Execute(
|
|
restClient => restClient.Administration.Restart(cancellationToken),
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RestartServer.ExecuteAsync(cancellationToken),
|
|
result => result.RestartServerNode,
|
|
cancellationToken));
|
|
}
|
|
|
|
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
|
|
preStartupTime = DateTimeOffset.UtcNow;
|
|
|
|
async Task WaitForInitialJobs(IInstanceClient instanceClient)
|
|
{
|
|
var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken);
|
|
if (jobs.Count == 0)
|
|
jobs = (await instanceClient.Jobs.List(null, cancellationToken))
|
|
.Where(x => x.StartedAt.Value > preStartupTime)
|
|
.ToList();
|
|
else
|
|
jobs = jobs.Where(x => x.JobCode.Value.IsServerStartupJob()).ToList();
|
|
|
|
await using var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
|
foreach (var job in jobs)
|
|
{
|
|
Assert.IsTrue(job.StartedAt.Value >= preStartupTime);
|
|
await jrt.WaitForJob(job, 140, job.JobCode == JobCode.ReconnectChatBot ? null : false, null, cancellationToken);
|
|
}
|
|
}
|
|
|
|
// chat bot start, dd autostart, and reboot with different initial job test
|
|
preStartupTime = DateTimeOffset.UtcNow;
|
|
serverTask = server.Run(cancellationToken).AsTask();
|
|
long expectedCompileJobId, expectedStaged;
|
|
var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, await GetLogger(), await GetFileDownloader(), cancellationToken);
|
|
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
|
|
{
|
|
var restAdminClient = adminClient.RestClient;
|
|
await jobsHubTest.WaitForReconnect(cancellationToken);
|
|
var instanceClient = restAdminClient.Instances.CreateClient(instance);
|
|
await WaitForInitialJobs(instanceClient);
|
|
|
|
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
|
|
|
|
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
|
|
|
|
var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken);
|
|
await using var wdt = new WatchdogTest(edgeVersion, instanceClient, await GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog);
|
|
await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken);
|
|
|
|
dd = await instanceClient.DreamDaemon.Read(cancellationToken);
|
|
Assert.AreEqual(dd.StagedCompileJob.Job.Id, compileJob.Id);
|
|
|
|
expectedCompileJobId = compileJob.Id.Value;
|
|
dd = await wdt.TellWorldToReboot(true, cancellationToken);
|
|
|
|
Assert.AreEqual(expectedCompileJobId, dd.ActiveCompileJob.Job.Id);
|
|
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
|
|
|
|
expectedCompileJobId = dd.ActiveCompileJob.Id.Value;
|
|
|
|
await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
|
{
|
|
AutoStart = false,
|
|
}, cancellationToken);
|
|
|
|
compileJob = await instanceClient.DreamMaker.Compile(cancellationToken);
|
|
await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken);
|
|
expectedStaged = compileJob.Id.Value;
|
|
|
|
jobsHubTest.ExpectShutdown();
|
|
await adminClient.Execute(
|
|
restClient => restClient.Administration.Restart(cancellationToken),
|
|
async gqlClient => await gqlClient.RunMutationEnsureNoErrors(
|
|
gql => gql.RestartServer.ExecuteAsync(cancellationToken),
|
|
result => result.RestartServerNode,
|
|
cancellationToken));
|
|
}
|
|
|
|
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
|
|
// post/entity deletion tests
|
|
serverTask = server.Run(cancellationToken).AsTask();
|
|
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
|
|
{
|
|
var restAdminClient = adminClient.RestClient;
|
|
await jobsHubTest.WaitForReconnect(cancellationToken);
|
|
var instanceClient = restAdminClient.Instances.CreateClient(instance);
|
|
await WaitForInitialJobs(instanceClient);
|
|
|
|
var currentDD = await instanceClient.DreamDaemon.Read(cancellationToken);
|
|
Assert.AreEqual(expectedCompileJobId, currentDD.ActiveCompileJob.Id.Value);
|
|
Assert.AreEqual(WatchdogStatus.Online, currentDD.Status);
|
|
Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value);
|
|
|
|
await using var wdt = new WatchdogTest(edgeVersion, instanceClient, await GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog);
|
|
currentDD = await wdt.TellWorldToReboot(false, cancellationToken);
|
|
Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value);
|
|
Assert.IsNull(currentDD.StagedCompileJob);
|
|
|
|
await using var repoTestObj = new RepositoryTest(instanceClient, instanceClient.Repository, instanceClient.Jobs);
|
|
var repoTest = repoTestObj.RunPostTest(cancellationToken);
|
|
await using var chatTestObj = new ChatTest(instanceClient.ChatBots, restAdminClient.Instances, instanceClient.Jobs, instance);
|
|
await chatTestObj.RunPostTest(cancellationToken);
|
|
await repoTest;
|
|
|
|
DummyChatProvider.RandomDisconnections(false);
|
|
|
|
jobsHubTest.CompleteNow();
|
|
await jobsHubTestTask;
|
|
|
|
await new InstanceManagerTest(restAdminClient, server.Directory).RunPostTest(instance, cancellationToken);
|
|
}
|
|
}
|
|
catch (ApiException ex)
|
|
{
|
|
Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex.ErrorCode}: {ex.Message}\n{ex.AdditionalServerData}");
|
|
throw;
|
|
}
|
|
catch (OperationCanceledException ex)
|
|
{
|
|
Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ABORTED: {ex}");
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}");
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
Console.WriteLine("TGS TEST CANCELLING TOKEN AS FINAL STEP");
|
|
serverCts.Cancel();
|
|
try
|
|
{
|
|
await serverTask.WaitAsync(hardCancellationToken);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
|
|
TerminateAllEngineServers();
|
|
}
|
|
|
|
Assert.IsTrue(serverTask.IsCompleted);
|
|
await serverTask;
|
|
}
|
|
|
|
ValueTask<MultiServerClient> CreateAdminClient(Uri url, CancellationToken cancellationToken)
|
|
=> CreateClient(url, DefaultCredentials.AdminUserName, DefaultCredentials.DefaultAdminUserPassword, true, cancellationToken);
|
|
|
|
async ValueTask<MultiServerClient> CreateClient(
|
|
Uri url,
|
|
string username,
|
|
string password,
|
|
bool retry,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
url = new Uri(url.ToString().Replace(Routes.ApiRoot, String.Empty));
|
|
var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2);
|
|
for (var I = 1; ; ++I)
|
|
{
|
|
ValueTask<IRestServerClient> restClientTask;
|
|
ValueTask<IAuthenticatedGraphQLServerClient> graphQLClientTask;
|
|
try
|
|
{
|
|
Console.WriteLine($"TEST: CreateAdminClient attempt {I}...");
|
|
|
|
restClientTask = restClientFactory.CreateFromLogin(
|
|
url,
|
|
username,
|
|
password,
|
|
cancellationToken: cancellationToken);
|
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
graphQLClientTask = MultiServerClient.UseGraphQL
|
|
? graphQLClientFactory.CreateFromLogin(
|
|
url,
|
|
username,
|
|
password,
|
|
cancellationToken: cts.Token)
|
|
: ValueTask.FromResult<IAuthenticatedGraphQLServerClient>(null);
|
|
|
|
IRestServerClient restClient;
|
|
try
|
|
{
|
|
restClient = await restClientTask;
|
|
}
|
|
catch (Exception restException) when (restException is not HttpRequestException && restException is not ServiceUnavailableException)
|
|
{
|
|
cts.Cancel();
|
|
try
|
|
{
|
|
await (await graphQLClientTask).DisposeAsync();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (Exception graphQLException)
|
|
{
|
|
throw new AggregateException(restException, graphQLException);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
try
|
|
{
|
|
return new MultiServerClient(
|
|
restClient,
|
|
await graphQLClientTask);
|
|
}
|
|
catch
|
|
{
|
|
await restClient.DisposeAsync();
|
|
throw;
|
|
}
|
|
}
|
|
catch (HttpRequestException)
|
|
{
|
|
//migrating, to be expected
|
|
if (DateTimeOffset.UtcNow > giveUpAt || !retry)
|
|
throw;
|
|
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
|
}
|
|
catch (ServiceUnavailableException)
|
|
{
|
|
// migrating, to be expected
|
|
if (DateTimeOffset.UtcNow > giveUpAt || !retry)
|
|
throw;
|
|
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|