Files
tgstation-server/tests/Tgstation.Server.Tests/TestVersions.cs
2026-01-11 15:31:44 -05:00

570 lines
19 KiB
C#

using System;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Newtonsoft.Json.Linq;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Client;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host;
using Tgstation.Server.Host.Components.Engine;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Controllers;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Properties;
using Tgstation.Server.Host.System;
using Tgstation.Server.Tests.Live;
namespace Tgstation.Server.Tests
{
[TestClass]
[TestCategory("SkipWhenLiveUnitTesting")]
public sealed class TestVersions
{
static XNamespace xmlNamespace;
static XElement versionsPropertyGroup;
[ClassInitialize]
public static void Init(TestContext _)
{
var doc = XDocument.Load("../../../../../build/Version.props");
var project = doc.Root;
xmlNamespace = project.GetDefaultNamespace();
versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup");
Assert.IsNotNull(versionsPropertyGroup);
}
[TestMethod]
public void TestCoreVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsCoreVersion").Value + ".0";
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var actual = typeof(Program).Assembly.GetName().Version;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestConfigVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsConfigVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var actual = GeneralConfiguration.CurrentConfigVersion;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestRestVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsRestVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
Assert.AreEqual(expected, ApiHeaders.Version);
}
[TestMethod]
public void TestGraphQLVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsGraphQLVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
Assert.AreEqual(expected, Version.Parse(MasterVersionsAttribute.Instance.RawGraphQLVersion));
}
[TestMethod]
public void TestApiLibraryVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsApiLibraryVersion").Value + ".0";
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var actual = typeof(ApiHeaders).Assembly.GetName().Version;
Assert.AreEqual(expected, actual);
}
[TestMethod]
[TestCategory("RequiresDatabase")]
public async Task TestDDExeByondVersion()
{
var mockGeneralConfigurationOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockGeneralConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration
{
ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate,
});
var mockSessionConfigurationOptions = new Mock<IOptionsMonitor<SessionConfiguration>>();
mockSessionConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new SessionConfiguration());
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Trace);
});
var logger = loggerFactory.CreateLogger<CachingFileDownloader>();
// windows only BYOND but can be checked on any system
var init1 = CachingFileDownloader.InitializeByondVersion(
logger,
WindowsByondInstaller.DDExeVersion,
true,
CancellationToken.None);
await CachingFileDownloader.InitializeByondVersion(
logger,
new Version(WindowsByondInstaller.DDExeVersion.Major, WindowsByondInstaller.DDExeVersion.Minor - 1),
true,
CancellationToken.None);
await init1;
using var byondInstaller = new WindowsByondInstaller(
Mock.Of<IProcessExecutor>(),
Mock.Of<IIOManager>(),
new CachingFileDownloader(Mock.Of<ILogger<CachingFileDownloader>>()),
mockGeneralConfigurationOptions.Object,
mockSessionConfigurationOptions.Object,
Mock.Of<ILogger<WindowsByondInstaller>>());
const string ArchiveEntryPath = "byond/bin/dd.exe";
var hasEntry = ArchiveHasFileEntry(
await TestingUtils.ExtractMemoryStreamFromInstallationData(
await byondInstaller.DownloadVersion(
new EngineVersion
{
Engine = EngineType.Byond,
Version = WindowsByondInstaller.DDExeVersion
},
null,
default),
CancellationToken.None),
ArchiveEntryPath);
Assert.IsTrue(hasEntry);
var (byondBytes, _) = await GetByondVersionPriorTo(byondInstaller, WindowsByondInstaller.DDExeVersion);
hasEntry = ArchiveHasFileEntry(
byondBytes,
ArchiveEntryPath);
Assert.IsFalse(hasEntry);
}
static Version MapThreadsVersion() => (Version)typeof(ByondInstallerBase).GetField("MapThreadsVersion", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) ?? throw new InvalidOperationException("Couldn't find MapThreadsVersion");
[TestMethod]
[TestCategory("RequiresDatabase")]
public async Task TestMapThreadsByondVersion()
{
var mockGeneralConfigurationOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockGeneralConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration
{
SkipAddingByondFirewallException = true,
ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate,
});
var mockSessionConfigurationOptions = new Mock<IOptionsMonitor<SessionConfiguration>>();
mockSessionConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new SessionConfiguration());
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Trace);
});
var platformIdentifier = new PlatformIdentifier();
var logger = loggerFactory.CreateLogger<CachingFileDownloader>();
var init1 = CachingFileDownloader.InitializeByondVersion(
logger,
MapThreadsVersion(),
platformIdentifier.IsWindows,
CancellationToken.None);
await CachingFileDownloader.InitializeByondVersion(
logger,
new Version(MapThreadsVersion().Major, MapThreadsVersion().Minor - 1),
platformIdentifier.IsWindows,
CancellationToken.None);
await init1;
var fileDownloader = new CachingFileDownloader(Mock.Of<ILogger<CachingFileDownloader>>());
var mockIOManager = new Mock<IIOManager>();
mockIOManager.Setup(x => x.FileExists(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(true);
mockIOManager.Setup(x => x.CreateResolverForSubdirectory(It.IsNotNull<string>())).Returns(mockIOManager.Object);
mockIOManager.Setup(x => x.ConcatPath(It.IsNotNull<string[]>())).Returns<string[]>(Path.Combine);
mockIOManager.Setup(x => x.ResolvePath(It.IsNotNull<string>())).Returns<string>(path => path);
ByondInstallerBase byondInstaller = platformIdentifier.IsWindows
? new WindowsByondInstaller(
Mock.Of<IProcessExecutor>(),
mockIOManager.Object,
fileDownloader,
mockGeneralConfigurationOptions.Object,
mockSessionConfigurationOptions.Object,
loggerFactory.CreateLogger<WindowsByondInstaller>())
: new PosixByondInstaller(
new PosixPostWriteHandler(loggerFactory.CreateLogger<PosixPostWriteHandler>()),
new DefaultIOManager(new FileSystem()),
fileDownloader,
mockGeneralConfigurationOptions.Object,
loggerFactory.CreateLogger<PosixByondInstaller>());
using var disposable = byondInstaller as IDisposable;
var processExecutor = new ProcessExecutor(
platformIdentifier.IsWindows
? new WindowsProcessFeatures(Mock.Of<ILogger<WindowsProcessFeatures>>())
: new PosixProcessFeatures(
new Lazy<IProcessExecutor>(() => null),
new DefaultIOManager(new FileSystem()),
loggerFactory.CreateLogger<PosixProcessFeatures>()),
mockIOManager.Object,
loggerFactory.CreateLogger<ProcessExecutor>(),
loggerFactory);
var ioManager = new DefaultIOManager(new FileSystem());
var tempPath = ioManager.ConcatPath(LiveTestingServer.BaseDirectory, "mapthreads");
await ioManager.CreateDirectory(tempPath, default);
try
{
await TestMapThreadsVersion(
new EngineVersion
{
Engine = EngineType.Byond,
Version = MapThreadsVersion(),
},
await TestingUtils.ExtractMemoryStreamFromInstallationData(
await byondInstaller.DownloadVersion(
new EngineVersion
{
Engine = EngineType.Byond,
Version = MapThreadsVersion()
},
null,
default),
CancellationToken.None),
byondInstaller,
ioManager,
processExecutor,
tempPath);
await ioManager.DeleteDirectory(tempPath, default);
var (byondBytes, version) = await GetByondVersionPriorTo(byondInstaller, MapThreadsVersion());
await TestMapThreadsVersion(
version,
byondBytes,
byondInstaller,
ioManager,
processExecutor,
tempPath);
}
finally
{
await ioManager.DeleteDirectory(tempPath, default);
}
}
[ClassCleanup]
public static void Cleanup()
{
CachingFileDownloader.Cleanup();
}
[TestMethod]
public void TestClientVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsClientVersion").Value + ".0";
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var actual = typeof(RestServerClientFactory).Assembly.GetName().Version;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestWatchdogVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsHostWatchdogVersion").Value + ".0";
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var actual = typeof(Host.Watchdog.WatchdogFactory).Assembly.GetName().Version;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestDmapiVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsDmapiVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var lines = File.ReadAllLines("../../../../../src/DMAPI/tgs.dm");
const string Prefix = "#define TGS_DMAPI_VERSION ";
var versionLine = lines.FirstOrDefault(l => l.StartsWith(Prefix));
Assert.IsNotNull(versionLine);
versionLine = versionLine.Substring(Prefix.Length + 1, expected.ToString().Length);
Assert.IsTrue(Version.TryParse(versionLine, out var actual));
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestInteropVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsInteropVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
Assert.AreEqual(expected, DMApiConstants.InteropVersion);
}
[TestMethod]
public void TestControlPanelVersion()
{
var doc = XDocument.Load("../../../../../build/WebpanelVersion.props");
var project = doc.Root;
var controlPanelXmlNamespace = project.GetDefaultNamespace();
var controlPanelVersionsPropertyGroup = project.Elements().First(x => x.Name == controlPanelXmlNamespace + "PropertyGroup");
var versionString = controlPanelVersionsPropertyGroup.Element(controlPanelXmlNamespace + "TgsWebpanelVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var jsonText = File.ReadAllText("../../../../../src/Tgstation.Server.Host/ClientApp/package.json");
dynamic json = JObject.Parse(jsonText);
string cpVersionString = json.version;
Assert.IsTrue(Version.TryParse(cpVersionString, out var actual));
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestWatchdogClientVersion()
{
var expected = typeof(Host.Watchdog.WatchdogFactory).Assembly.GetName().Version;
var actual = Program.HostWatchdogVersion;
Assert.AreEqual(expected.Major, actual.Major);
Assert.AreEqual(expected.Minor, actual.Minor);
Assert.AreEqual(expected.Build, actual.Build);
Assert.AreEqual(-1, actual.Revision);
}
[TestMethod]
public async Task TestContainerScriptVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsContainerScriptVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
var scriptLines = await File.ReadAllLinesAsync("../../../../../build/tgs.docker.sh");
var line = scriptLines.FirstOrDefault(x => x.Trim().Contains($"SCRIPT_VERSION=\"{expected.Semver()}\""));
Assert.IsNotNull(line);
}
[TestMethod]
public void TestDowngradeMigrations()
{
static string GetMigrationTimestampString(Type type) => type
?.GetCustomAttributes(typeof(MigrationAttribute), false)
.OfType<MigrationAttribute>()
.SingleOrDefault()
?.Id
.Split('_')
.First()
?? String.Empty;
var allTypesWithMigrationAttributes = typeof(Program)
.Assembly
.GetTypes()
.ToDictionary(
x => x,
x => GetMigrationTimestampString(x));
Type latestMigrationMS = null;
Type latestMigrationMY = null;
Type latestMigrationPG = null;
Type latestMigrationSL = null;
foreach (var kvp in allTypesWithMigrationAttributes)
{
var migrationType = kvp.Key;
var migrationTimestamp = kvp.Value;
switch (migrationType.Name[..2])
{
case "MS":
if (String.Compare(GetMigrationTimestampString(latestMigrationMS), migrationTimestamp) < 0)
latestMigrationMS = migrationType;
break;
case "MY":
if (String.Compare(GetMigrationTimestampString(latestMigrationMY), migrationTimestamp) < 0)
latestMigrationMY = migrationType;
break;
case "PG":
if (String.Compare(GetMigrationTimestampString(latestMigrationPG), migrationTimestamp) < 0)
latestMigrationPG = migrationType;
break;
case "SL":
if (String.Compare(GetMigrationTimestampString(latestMigrationSL), migrationTimestamp) < 0)
latestMigrationSL = migrationType;
break;
}
}
Assert.AreEqual(latestMigrationMS, DatabaseContext.MSLatestMigration);
Assert.AreEqual(latestMigrationMY, DatabaseContext.MYLatestMigration);
Assert.AreEqual(latestMigrationPG, DatabaseContext.PGLatestMigration);
Assert.AreEqual(latestMigrationSL, DatabaseContext.SLLatestMigration);
}
[TestMethod]
public async Task CheckWebRootPathForTgsLogo()
{
var directory = Path.GetFullPath("../../../../../src/Tgstation.Server.Host/wwwroot");
if (!Directory.Exists(directory))
Assert.Inconclusive("Webpanel not built?");
static string GetConstField(string name) => (string)typeof(RootController).GetField(name, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
var logo = new PlatformIdentifier().IsWindows
? GetConstField("LogoSvgWindowsName")
: GetConstField("LogoSvgLinuxName");
var path = $"../../../../../src/Tgstation.Server.Host/wwwroot/{logo}.svg";
Assert.IsTrue(File.Exists(path));
var content = await File.ReadAllBytesAsync(path);
var hash = String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
Assert.AreEqual(
new PlatformIdentifier().IsWindows
? "c5e4709774c14a6f376dbb5100bd80a0114a2287"
: "9eba2fac24c5c7e0008721690d07c3df575a00d6",
hash);
}
static async Task<Tuple<Stream, EngineVersion>> GetByondVersionPriorTo(ByondInstallerBase byondInstaller, Version version)
{
var minusOneMinor = new Version(version.Major, version.Minor - 1);
var byondVersion = new EngineVersion
{
Engine = EngineType.Byond,
Version = minusOneMinor
};
try
{
return Tuple.Create(await TestingUtils.ExtractMemoryStreamFromInstallationData(await byondInstaller.DownloadVersion(
byondVersion,
null,
CancellationToken.None), CancellationToken.None), byondVersion);
}
catch (HttpRequestException)
{
var minusOneMajor = new Version(minusOneMinor.Major - 1, minusOneMinor.Minor);
byondVersion.Version = minusOneMajor;
return Tuple.Create(await TestingUtils.ExtractMemoryStreamFromInstallationData(await byondInstaller.DownloadVersion(
byondVersion,
null,
CancellationToken.None), CancellationToken.None), byondVersion);
}
}
static async Task TestMapThreadsVersion(
EngineVersion engineVersion,
Stream byondBytes,
ByondInstallerBase byondInstaller,
DefaultIOManager ioManager,
ProcessExecutor processExecutor,
string tempPath)
{
using (byondBytes)
await ioManager.ZipToDirectory(tempPath, byondBytes, default);
// HAAAAAAAX
var installerType = byondInstaller.GetType();
if (byondInstaller is WindowsByondInstaller)
typeof(WindowsByondInstaller).GetField("installedDirectX", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(byondInstaller, true);
await byondInstaller.Install(engineVersion, tempPath, false, default);
var binPath = (string)typeof(ByondInstallerBase).GetField("ByondBinPath", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
var ddNameFunc = installerType.GetMethod("GetDreamDaemonName", BindingFlags.Instance | BindingFlags.NonPublic);
var supportsCli = false;
var argArray = new object[] { engineVersion.Version, supportsCli };
// https://stackoverflow.com/questions/2438065/how-can-i-invoke-a-method-with-an-out-parameter
var ddPath = ioManager.ConcatPath(
tempPath,
binPath,
(string)ddNameFunc.Invoke(byondInstaller, argArray));
Assert.IsTrue((bool)argArray[1]);
var shouldSupportMapThreads = engineVersion.Version >= MapThreadsVersion();
await File.WriteAllBytesAsync("fake.dmb", [], CancellationToken.None);
try
{
await using var process = await processExecutor.LaunchProcess(
ddPath,
Environment.CurrentDirectory,
"fake.dmb -map-threads 3 -close",
CancellationToken.None,
null,
null,
true,
true,
false);
try
{
await process.Startup;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await process.Lifetime.WaitAsync(cts.Token);
var output = await process.GetCombinedOutput(cts.Token);
var supportsMapThreads = !output.Contains("invalid option '-map-threads'");
Assert.AreEqual(shouldSupportMapThreads, supportsMapThreads, $"DD Output:{Environment.NewLine}{output}");
}
finally
{
process.Terminate();
}
}
finally
{
File.Delete("fake.dmb");
}
}
static bool ArchiveHasFileEntry(Stream byondBytes, string entryPath)
{
using (byondBytes)
{
using var archive = new ZipArchive(byondBytes, ZipArchiveMode.Read);
var entry = archive.Entries.FirstOrDefault(entry => entry.FullName == entryPath);
return entry != null;
}
}
}
}