Merge branch 'master' into dev

This commit is contained in:
tgstation-server
2023-04-29 21:52:29 +00:00
13 changed files with 359 additions and 310 deletions
@@ -4,8 +4,7 @@
name: "Auto-Approve Dominion's PRs"
on:
pull_request:
types: [opened, synchronize]
pull_request_target:
branches:
- dev
- master
+24 -24
View File
@@ -21,7 +21,7 @@ env:
TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
TGS_TEST_IRC_CHANNEL: ${{ secrets.IRC_CHANNEL }}
TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }}
TGS_TEST_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
concurrency:
@@ -32,7 +32,7 @@ jobs:
security-checkpoint:
name: Check CI Clearance
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id && github.event.pull_request.state == 'open'
steps:
- name: Comment on new Fork PR
if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared')
@@ -47,14 +47,14 @@ jobs:
labels: CI Cleared
- name: Fail Clearance Check if PR has Unlabeled new Commits from Fork
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared')
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
run: exit 1
start-ci-run-gate:
name: Start CI Run Gate
needs: security-checkpoint
runs-on: ubuntu-latest
if: always() && (needs.security-checkpoint.result == 'success' || (needs.security-checkpoint.result == 'skipped' && (github.event_name == 'push' || github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id)))
if: "!(cancelled() || failure()) && (needs.security-checkpoint.result == 'success' || (needs.security-checkpoint.result == 'skipped' && (github.event_name == 'push' || (github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id && github.event_name != 'pull_request_target'))))"
steps:
- name: GitHub Requires at Least One Step for a Job
run: exit 0
@@ -62,7 +62,7 @@ jobs:
analyze:
name: Code Scanning
needs: start-ci-run-gate
if: always() && needs.start-ci-run-gate.result == 'success'
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
runs-on: ubuntu-latest
steps:
- name: Install Node 12.X
@@ -100,7 +100,7 @@ jobs:
dmapi-build:
name: Build DMAPI
needs: start-ci-run-gate
if: always() && needs.start-ci-run-gate.result == 'success'
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
env:
BYOND_MAJOR: 515
BYOND_MINOR: 1592
@@ -158,7 +158,7 @@ jobs:
name: Build Doxygen Site
runs-on: ubuntu-latest
needs: start-ci-run-gate
if: always() && needs.start-ci-run-gate.result == 'success'
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
steps:
- name: Checkout (Branch Push)
uses: actions/checkout@v3
@@ -208,7 +208,7 @@ jobs:
name: Build Docker Image
runs-on: ubuntu-latest
needs: start-ci-run-gate
if: always() && needs.start-ci-run-gate.result == 'success'
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
steps:
- name: Checkout (Branch Push)
uses: actions/checkout@v3
@@ -226,7 +226,7 @@ jobs:
linux-unit-tests:
name: Linux Tests
needs: start-ci-run-gate
if: always() && needs.start-ci-run-gate.result == 'success'
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
strategy:
fail-fast: false
matrix:
@@ -266,7 +266,7 @@ jobs:
windows-unit-tests:
name: Windows Tests
needs: start-ci-run-gate
if: always() && needs.start-ci-run-gate.result == 'success'
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
strategy:
fail-fast: false
matrix:
@@ -306,7 +306,7 @@ jobs:
windows-integration-test:
name: Windows Live Tests
needs: dmapi-build
if: always() && needs.dmapi-build.result == 'success'
if: "!(cancelled() || failure()) && needs.dmapi-build.result == 'success'"
env:
TGS_TEST_DATABASE_TYPE: SqlServer
TGS_TEST_DUMP_API_SPEC: yes
@@ -386,7 +386,7 @@ jobs:
linux-integration-tests:
name: Linux Live Tests
needs: dmapi-build
if: always() && needs.dmapi-build.result == 'success'
if: "!(cancelled() || failure()) && needs.dmapi-build.result == 'success'"
services: # We start all dbs here so we can just code the stuff once
postgres:
image: cyberboss/postgres-max-connections # Fork of _/postgres:latest with max_connections=500 becuase GitHub actions service containers have no way to set command lines. Rebuilds with updates.
@@ -538,7 +538,7 @@ jobs:
validate-openapi-spec:
name: OpenAPI Spec Validation
needs: windows-integration-test
if: always() && needs.windows-integration-test.result == 'success'
if: "!(cancelled() || failure()) && needs.windows-integration-test.result == 'success'"
runs-on: ubuntu-latest
steps:
- name: Install Node 12.X
@@ -571,7 +571,7 @@ jobs:
upload-code-coverage:
name: Upload Code Coverage
needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test]
if: always() && needs.linux-unit-tests.result == 'success' && needs.linux-integration-tests.result == 'success' && needs.windows-unit-tests.result == 'success' && needs.windows-integration-test.result == 'success'
if: "!(cancelled() || failure()) && needs.linux-unit-tests.result == 'success' && needs.linux-integration-tests.result == 'success' && needs.windows-unit-tests.result == 'success' && needs.windows-integration-test.result == 'success'"
runs-on: ubuntu-latest
steps:
- name: Checkout (Branch Push)
@@ -661,7 +661,7 @@ jobs:
name: Deploy HTTP API
needs: [upload-code-coverage, validate-openapi-spec]
runs-on: windows-latest
if: always() && needs.upload-code-coverage.result == 'success' && needs.validate-openapi-spec.result == 'success' && github.event_name == 'push' && contains(github.event.head_commit.message, '[APIDeploy]')
if: "!(cancelled() || failure()) && needs.upload-code-coverage.result == 'success' && needs.validate-openapi-spec.result == 'success' && github.event_name == 'push' && contains(github.event.head_commit.message, '[APIDeploy]')"
steps:
- name: Checkout (Branch Push)
uses: actions/checkout@v3
@@ -690,7 +690,7 @@ jobs:
uses: juitnow/github-action-create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
with:
tag_name: api-v${{ env.TGS_API_VERSION }}
release_name: tgstation-server API v${{ env.TGS_API_VERSION }}
@@ -700,7 +700,7 @@ jobs:
- name: Upload OpenApi Spec
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./swagger/swagger.json
@@ -711,7 +711,7 @@ jobs:
name: Deploy DreamMaker API
needs: [upload-code-coverage, validate-openapi-spec]
runs-on: windows-latest
if: always() && needs.upload-code-coverage.result == 'success' && needs.validate-openapi-spec.result == 'success' && github.event_name == 'push' && contains(github.event.head_commit.message, '[DMDeploy]')
if: "!(cancelled() || failure()) && needs.upload-code-coverage.result == 'success' && needs.validate-openapi-spec.result == 'success' && github.event_name == 'push' && contains(github.event.head_commit.message, '[DMDeploy]')"
steps:
- name: Checkout (Branch Push)
uses: actions/checkout@v3
@@ -739,7 +739,7 @@ jobs:
uses: juitnow/github-action-create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
with:
tag_name: dmapi-v${{ env.TGS_DM_VERSION }}
release_name: tgstation-server DMAPI v${{ env.TGS_DM_VERSION }}
@@ -749,7 +749,7 @@ jobs:
- name: Upload DMAPI Artifact
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./DMAPI.zip
@@ -760,7 +760,7 @@ jobs:
name: Deploy Nuget Packages
needs: [upload-code-coverage, validate-openapi-spec]
runs-on: ubuntu-latest
if: always() && needs.upload-code-coverage.result == 'success' && needs.validate-openapi-spec.result == 'success' && github.event_name == 'push' && contains(github.event.head_commit.message, '[NugetDeploy]')
if: "!(cancelled() || failure()) && needs.upload-code-coverage.result == 'success' && needs.validate-openapi-spec.result == 'success' && github.event_name == 'push' && contains(github.event.head_commit.message, '[NugetDeploy]')"
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v2
@@ -797,7 +797,7 @@ jobs:
name: Ensure TGS Release is Latest GitHub Release
needs: [deploy-dm, deploy-http]
runs-on: ubuntu-latest
if: always() && (needs.deploy-dm.result == 'success' || needs.deploy-http.result == 'success') && !contains(github.event.head_commit.message, '[TGSDeploy]')
if: "!(cancelled() || failure()) && (needs.deploy-dm.result == 'success' || needs.deploy-http.result == 'success') && !contains(github.event.head_commit.message, '[TGSDeploy]')"
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v2
@@ -821,7 +821,7 @@ jobs:
name: Deploy TGS
needs: [deploy-dm, deploy-http, upload-code-coverage, validate-openapi-spec]
runs-on: windows-latest
if: always() && (needs.upload-code-coverage.result == 'success' || needs.validate-openapi-spec.result == 'success') && github.event_name == 'push' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]')
if: "!(cancelled() || failure()) && (needs.upload-code-coverage.result == 'success' || needs.validate-openapi-spec.result == 'success') && github.event_name == 'push' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]')"
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v2
@@ -944,7 +944,7 @@ jobs:
deploy-docker:
name: Deploy TGS (Docker)
needs: [deploy-tgs]
if: always() && needs.deploy-tgs.result == 'success'
if: "!(cancelled() || failure()) && needs.deploy-tgs.result == 'success'"
runs-on: ubuntu-latest
steps:
- name: Checkout (Branch Push)
+1 -1
View File
@@ -7,7 +7,7 @@
<TgsConfigVersion>4.6.0</TgsConfigVersion>
<TgsApiVersion>9.10.2</TgsApiVersion>
<TgsApiLibraryVersion>10.4.1</TgsApiLibraryVersion>
<TgsClientVersion>11.4.1</TgsClientVersion>
<TgsClientVersion>11.4.2</TgsClientVersion>
<TgsDmapiVersion>6.4.2</TgsDmapiVersion>
<TgsInteropVersion>5.6.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.2.2</TgsHostWatchdogVersion>
+76 -72
View File
@@ -25,7 +25,7 @@ using Tgstation.Server.Common;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class ApiClient : IApiClient
class ApiClient : IApiClient
{
/// <summary>
/// PATCH <see cref="HttpMethod"/>.
@@ -248,7 +248,9 @@ namespace Tgstation.Server.Client
using (memoryStream)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
var streamContent = new StreamContent(uploadStream ?? memoryStream);
#pragma warning restore CA2000 // Dispose objects before losing scope
try
{
await RunRequest<object>(
@@ -259,7 +261,7 @@ namespace Tgstation.Server.Client
false,
cancellationToken)
.ConfigureAwait(false);
streamContent = null; // CA2000
streamContent = null;
}
finally
{
@@ -268,75 +270,6 @@ namespace Tgstation.Server.Client
}
}
/// <summary>
/// Attempt to refresh the bearer token in the <see cref="headers"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the refresh was successful, <see langword="false"/> otherwise.</returns>
async Task<bool> RefreshToken(CancellationToken cancellationToken)
{
if (tokenRefreshHeaders == null)
return false;
var startingToken = headers.Token;
await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (startingToken != headers.Token)
return true;
var token = await RunRequest<object, TokenResponse>(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken);
headers = new ApiHeaders(headers.UserAgent!, token.Bearer!);
}
catch (ClientException)
{
return false;
}
finally
{
semaphoreSlim.Release();
}
return true;
}
/// <summary>
/// Main request method.
/// </summary>
/// <typeparam name="TBody">The body <see cref="Type"/>.</typeparam>
/// <typeparam name="TResult">The resulting POCO type.</typeparam>
/// <param name="route">The route to run.</param>
/// <param name="body">The body of the request.</param>
/// <param name="method">The method of the request.</param>
/// <param name="instanceId">The optional instance <see cref="EntityId.Id"/> for the request.</param>
/// <param name="tokenRefresh">If this is a token refresh operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response on success.</returns>
Task<TResult> RunRequest<TBody, TResult>(
string route,
TBody? body,
HttpMethod method,
long? instanceId,
bool tokenRefresh,
CancellationToken cancellationToken)
where TBody : class
{
HttpContent? content = null;
if (body != null)
content = new StringContent(
JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, GetSerializerSettings()),
Encoding.UTF8,
ApiHeaders.ApplicationJsonMime);
return RunRequest<TResult>(
route,
content,
method,
instanceId,
tokenRefresh,
cancellationToken);
}
/// <summary>
/// Main request method.
/// </summary>
@@ -348,7 +281,7 @@ namespace Tgstation.Server.Client
/// <param name="tokenRefresh">If this is a token refresh operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response on success.</returns>
async Task<TResult> RunRequest<TResult>(
protected virtual async Task<TResult> RunRequest<TResult>(
string route,
HttpContent? content,
HttpMethod method,
@@ -427,5 +360,76 @@ namespace Tgstation.Server.Client
}
}
}
/// <summary>
/// Attempt to refresh the bearer token in the <see cref="headers"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the refresh was successful, <see langword="false"/> otherwise.</returns>
async Task<bool> RefreshToken(CancellationToken cancellationToken)
{
if (tokenRefreshHeaders == null)
return false;
var startingToken = headers.Token;
await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (startingToken != headers.Token)
return true;
var token = await RunRequest<object, TokenResponse>(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false);
headers = new ApiHeaders(headers.UserAgent!, token.Bearer!);
}
catch (ClientException)
{
return false;
}
finally
{
semaphoreSlim.Release();
}
return true;
}
/// <summary>
/// Main request method.
/// </summary>
/// <typeparam name="TBody">The body <see cref="Type"/>.</typeparam>
/// <typeparam name="TResult">The resulting POCO type.</typeparam>
/// <param name="route">The route to run.</param>
/// <param name="body">The body of the request.</param>
/// <param name="method">The method of the request.</param>
/// <param name="instanceId">The optional instance <see cref="EntityId.Id"/> for the request.</param>
/// <param name="tokenRefresh">If this is a token refresh operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response on success.</returns>
async Task<TResult> RunRequest<TBody, TResult>(
string route,
TBody? body,
HttpMethod method,
long? instanceId,
bool tokenRefresh,
CancellationToken cancellationToken)
where TBody : class
{
HttpContent? content = null;
if (body != null)
content = new StringContent(
JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, GetSerializerSettings()),
Encoding.UTF8,
ApiHeaders.ApplicationJsonMime);
using (content)
return await RunRequest<TResult>(
route,
content,
method,
instanceId,
tokenRefresh,
cancellationToken)
.ConfigureAwait(false);
}
}
}
@@ -17,13 +17,21 @@ namespace Tgstation.Server.Client
/// <summary>
/// The <see cref="IApiClientFactory"/> for the <see cref="ServerClientFactory"/>.
/// </summary>
static readonly IApiClientFactory ApiClientFactory = new ApiClientFactory();
internal static IApiClientFactory ApiClientFactory { get; set; }
/// <summary>
/// The <see cref="ProductHeaderValue"/> for the <see cref="ServerClientFactory"/>.
/// </summary>
readonly ProductHeaderValue productHeaderValue;
/// <summary>
/// Initializes static members of the <see cref="ServerClientFactory"/> class.
/// </summary>
static ServerClientFactory()
{
ApiClientFactory = new ApiClientFactory();
}
/// <summary>
/// Initializes a new instance of the <see cref="ServerClientFactory"/> class.
/// </summary>
@@ -16,7 +16,7 @@
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
<Copyright>2018-2023</Copyright>
<PackageTags>json web api tgstation-server tgstation ss13 byond client</PackageTags>
<PackageReleaseNotes>Updated to latest API library patch.</PackageReleaseNotes>
<PackageReleaseNotes>Added missing Dispose() call to the StringContents for requests with bodies and added missing ConfigureAwait(false) to async call.</PackageReleaseNotes>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
@@ -257,7 +257,8 @@ namespace Tgstation.Server.Host.Controllers
throw new ArgumentNullException(nameof(rateLimitException));
Logger.LogWarning(rateLimitException, "Exceeded GitHub rate limit!");
var secondsString = Math.Ceiling((rateLimitException.Reset - DateTimeOffset.UtcNow).TotalSeconds).ToString(CultureInfo.InvariantCulture);
var secondsString = Math.Ceiling(rateLimitException.GetRetryAfterTimeSpan().TotalSeconds).ToString(CultureInfo.InvariantCulture);
Response.Headers.Add(HeaderNames.RetryAfter, secondsString);
return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessageResponse(ErrorCode.GitHubApiRateLimit));
}
@@ -261,8 +261,7 @@ namespace Tgstation.Server.Host.Controllers
return BadRequest(new ErrorMessageResponse(ErrorCode.OAuthProviderDisabled));
externalUserId = await validator
.ValidateResponseCode(ApiHeaders.Token, cancellationToken)
;
.ValidateResponseCode(ApiHeaders.Token, cancellationToken);
Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, externalUserId);
}
@@ -53,24 +53,10 @@ namespace Tgstation.Server.Tests.Live
async Task TestRead(CancellationToken cancellationToken)
{
AdministrationResponse model;
try
{
model = await client.Read(cancellationToken);
}
catch (RateLimitException)
{
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")))
{
Assert.Inconclusive("GitHub rate limit hit while testing administration endpoint. Set environment variable TGS_TEST_GITHUB_TOKEN to fix this!");
}
var model = await client.Read(cancellationToken);
// CI fails all the time b/c of this, ignore it
return;
}
//we've released a few 4.x versions now, check the release checker is at least somewhat functional
Assert.IsTrue(4 <= model.LatestVersion.Major);
//we've released a few 5.x versions now, check the release checker is at least somewhat functional
Assert.IsTrue(4 < model.LatestVersion.Major);
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Client;
using Tgstation.Server.Common;
namespace Tgstation.Server.Tests.Live
{
sealed class RateLimitRetryingApiClient : ApiClient
{
public RateLimitRetryingApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders tokenRefreshHeaders, bool authless)
: base(httpClient, url, apiHeaders, tokenRefreshHeaders, authless)
{
}
protected override async Task<TResult> RunRequest<TResult>(string route, HttpContent content, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
{
var hasGitHubToken = !String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"));
while (true)
try
{
return await base.RunRequest<TResult>(route, content, method, instanceId, tokenRefresh, cancellationToken);
}
catch (RateLimitException ex) when (hasGitHubToken && ex.RetryAfter.HasValue)
{
var now = DateTimeOffset.UtcNow;
Console.WriteLine($"TEST ERROR RATE LIMITED: {ex}");
var sleepTime = ex.RetryAfter.Value - now;
Console.WriteLine($"Sleeping for {sleepTime.TotalMinutes} minutes and retrying...");
await Task.Delay(sleepTime, cancellationToken);
}
}
}
}
@@ -0,0 +1,19 @@
using System;
using Tgstation.Server.Api;
using Tgstation.Server.Client;
using Tgstation.Server.Common;
namespace Tgstation.Server.Tests.Live
{
sealed class RateLimitRetryingApiClientFactory : IApiClientFactory
{
public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders, ApiHeaders tokenRefreshHeaders, bool authless)
=> new RateLimitRetryingApiClient(
new HttpClient(),
url,
apiHeaders,
tokenRefreshHeaders,
authless);
}
}
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -12,9 +11,6 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -29,8 +25,6 @@ using Tgstation.Server.Client;
using Tgstation.Server.Client.Components;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Database.Migrations;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.System;
using Tgstation.Server.Tests.Live.Instance;
@@ -77,6 +71,12 @@ namespace Tgstation.Server.Tests.Live
}
}
[TestInitialize]
public void Initialize()
{
ServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory();
}
[TestMethod]
public async Task TestUpdateProtocolAndDisabledOAuth()
{
@@ -128,13 +128,6 @@ namespace Tgstation.Server.Tests.Live
var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath);
Assert.AreEqual(testUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver());
}
catch (RateLimitException ex)
{
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")))
throw;
Assert.Inconclusive("GitHub rate limit hit: {0}", ex);
}
finally
{
serverCts.Cancel();
@@ -170,7 +163,7 @@ namespace Tgstation.Server.Tests.Live
}, false, 5011))
{
using var serverCts = new CancellationTokenSource();
serverCts.CancelAfter(TimeSpan.FromMinutes(3));
serverCts.CancelAfter(TimeSpan.FromHours(3));
var cancellationToken = serverCts.Token;
var serverTask = controller.Run(cancellationToken);
@@ -217,13 +210,6 @@ namespace Tgstation.Server.Tests.Live
CheckServerUpdated(controller);
}
catch (RateLimitException ex)
{
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")))
throw;
Assert.Inconclusive("GitHub rate limit hit: {0}", ex);
}
finally
{
serverCts.Cancel();
@@ -452,13 +438,6 @@ namespace Tgstation.Server.Tests.Live
CheckServerUpdated(node1);
CheckServerUpdated(node2);
}
catch (RateLimitException ex)
{
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")))
throw;
Assert.Inconclusive("GitHub rate limit hit: {0}", ex);
}
finally
{
serverCts.Cancel();
@@ -636,13 +615,6 @@ namespace Tgstation.Server.Tests.Live
Assert.AreEqual(2, node2Info.SwarmServers.Count);
Assert.IsNotNull(node2Info.SwarmServers.SingleOrDefault(x => x.Identifier == "controller"));
}
catch (RateLimitException ex)
{
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")))
throw;
Assert.Inconclusive("GitHub rate limit hit: {0}", ex);
}
finally
{
serverCts.Cancel();
@@ -653,157 +625,6 @@ namespace Tgstation.Server.Tests.Live
new LiveTestingServer(null, false).Dispose();
}
[TestMethod]
public async Task TestDownMigrations()
{
var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_CONNECTION_STRING");
if (string.IsNullOrEmpty(connectionString))
Assert.Inconclusive("No connection string configured in env var TGS_TEST_CONNECTION_STRING!");
var databaseTypeString = Environment.GetEnvironmentVariable("TGS_TEST_DATABASE_TYPE");
if (!Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
Assert.Inconclusive("No/invalid database type configured in env var TGS_TEST_DATABASE_TYPE!");
string migrationName = null;
DatabaseContext CreateContext()
{
string serverVersion = Environment.GetEnvironmentVariable($"{DatabaseConfiguration.Section}__{nameof(DatabaseConfiguration.ServerVersion)}");
if (string.IsNullOrWhiteSpace(serverVersion))
serverVersion = null;
switch (databaseType)
{
case DatabaseType.MySql:
case DatabaseType.MariaDB:
migrationName = nameof(MYInitialCreate);
return new MySqlDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<MySqlDatabaseContext>(
databaseType,
connectionString,
serverVersion));
case DatabaseType.PostgresSql:
migrationName = nameof(PGCreate);
return new PostgresSqlDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<PostgresSqlDatabaseContext>(
databaseType,
connectionString,
serverVersion));
case DatabaseType.SqlServer:
migrationName = nameof(MSInitialCreate);
return new SqlServerDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<SqlServerDatabaseContext>(
databaseType,
connectionString,
serverVersion));
case DatabaseType.Sqlite:
migrationName = nameof(SLRebuild);
return new SqliteDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<SqliteDatabaseContext>(
databaseType,
connectionString,
serverVersion));
}
return null;
}
using var context = CreateContext();
await context.Database.EnsureDeletedAsync();
await context.Database.MigrateAsync(default);
// add usergroups and dummy instances for testing purposes
var group = new Host.Models.UserGroup
{
PermissionSet = new Host.Models.PermissionSet
{
AdministrationRights = AdministrationRights.ChangeVersion,
InstanceManagerRights = InstanceManagerRights.GrantPermissions
},
Name = "TestGroup",
};
const string TestUserName = "TestUser42";
var user = new Host.Models.User
{
Name = TestUserName,
CreatedAt = DateTimeOffset.UtcNow,
OAuthConnections = new List<Host.Models.OAuthConnection>(),
CanonicalName = Host.Models.User.CanonicalizeName(TestUserName),
Enabled = false,
Group = group,
PasswordHash = "_",
};
var instance = new Host.Models.Instance
{
AutoUpdateInterval = 0,
ChatBotLimit = 1,
ChatSettings = new List<Host.Models.ChatBot>(),
ConfigurationType = ConfigurationType.HostWrite,
DreamDaemonSettings = new Host.Models.DreamDaemonSettings
{
AllowWebClient = false,
AutoStart = false,
HeartbeatSeconds = 0,
DumpOnHeartbeatRestart = false,
Port = 1447,
SecurityLevel = DreamDaemonSecurity.Safe,
Visibility = DreamDaemonVisibility.Public,
StartupTimeout = 1000,
TopicRequestTimeout = 1000,
AdditionalParameters = string.Empty,
StartProfiler = false,
LogOutput = true,
},
DreamMakerSettings = new Host.Models.DreamMakerSettings
{
ApiValidationPort = 1557,
ApiValidationSecurityLevel = DreamDaemonSecurity.Trusted,
RequireDMApiValidation = false,
Timeout = TimeSpan.FromSeconds(13),
},
InstancePermissionSets = new List<Host.Models.InstancePermissionSet>
{
new Host.Models.InstancePermissionSet
{
ByondRights = ByondRights.InstallCustomVersion,
ChatBotRights = ChatBotRights.None,
ConfigurationRights = ConfigurationRights.Read,
DreamDaemonRights = DreamDaemonRights.ReadRevision,
DreamMakerRights = DreamMakerRights.SetApiValidationPort,
InstancePermissionSetRights = InstancePermissionSetRights.Write,
PermissionSet = group.PermissionSet,
RepositoryRights = RepositoryRights.SetReference
}
},
Name = "sfdsadfsa",
Online = false,
Path = "/a/b/c/d",
RepositorySettings = new Host.Models.RepositorySettings
{
AutoUpdatesKeepTestMerges = false,
AutoUpdatesSynchronize = false,
CommitterEmail = "email@eample.com",
CommitterName = "blubluh",
CreateGitHubDeployments = false,
PostTestMergeComment = false,
PushTestMergeCommits = false,
ShowTestMergeCommitters = false,
UpdateSubmodules = false,
},
};
context.Users.Add(user);
context.Groups.Add(group);
context.Instances.Add(instance);
await context.Save(default);
var dbServiceProvider = ((IInfrastructure<IServiceProvider>)context.Database).Instance;
var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
await migrator.MigrateAsync(migrationName, default);
await context.Database.EnsureDeletedAsync();
}
[TestMethod]
public async Task TestStandardTgsOperation()
{
@@ -817,7 +638,7 @@ namespace Tgstation.Server.Tests.Live
using var server = new LiveTestingServer(null, true);
const int MaximumTestMinutes = 20;
const int MaximumTestMinutes = 180;
using var hardTimeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaximumTestMinutes));
var hardCancellationToken = hardTimeoutCancellationTokenSource.Token;
using var serverCts = CancellationTokenSource.CreateLinkedTokenSource(hardCancellationToken);
@@ -1066,12 +887,12 @@ namespace Tgstation.Server.Tests.Live
}
catch (ApiException ex)
{
System.Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex.ErrorCode}: {ex.Message}\n{ex.AdditionalServerData}");
Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex.ErrorCode}: {ex.Message}\n{ex.AdditionalServerData}");
throw;
}
catch (Exception ex)
{
System.Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}");
Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}");
throw;
}
finally
@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Database.Migrations;
using Tgstation.Server.Host.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Tgstation.Server.Tests
{
[TestClass]
public sealed class TestDatabase
{
[TestMethod]
public async Task TestDownMigrations()
{
var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_CONNECTION_STRING");
if (string.IsNullOrEmpty(connectionString))
Assert.Inconclusive("No connection string configured in env var TGS_TEST_CONNECTION_STRING!");
var databaseTypeString = Environment.GetEnvironmentVariable("TGS_TEST_DATABASE_TYPE");
if (!Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
Assert.Inconclusive("No/invalid database type configured in env var TGS_TEST_DATABASE_TYPE!");
string migrationName = null;
DatabaseContext CreateContext()
{
string serverVersion = Environment.GetEnvironmentVariable($"{DatabaseConfiguration.Section}__{nameof(DatabaseConfiguration.ServerVersion)}");
if (string.IsNullOrWhiteSpace(serverVersion))
serverVersion = null;
switch (databaseType)
{
case DatabaseType.MySql:
case DatabaseType.MariaDB:
migrationName = nameof(MYInitialCreate);
return new MySqlDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<MySqlDatabaseContext>(
databaseType,
connectionString,
serverVersion));
case DatabaseType.PostgresSql:
migrationName = nameof(PGCreate);
return new PostgresSqlDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<PostgresSqlDatabaseContext>(
databaseType,
connectionString,
serverVersion));
case DatabaseType.SqlServer:
migrationName = nameof(MSInitialCreate);
return new SqlServerDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<SqlServerDatabaseContext>(
databaseType,
connectionString,
serverVersion));
case DatabaseType.Sqlite:
migrationName = nameof(SLRebuild);
return new SqliteDatabaseContext(
Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<SqliteDatabaseContext>(
databaseType,
connectionString,
serverVersion));
}
return null;
}
using var context = CreateContext();
await context.Database.EnsureDeletedAsync();
await context.Database.MigrateAsync(default);
// add usergroups and dummy instances for testing purposes
var group = new Host.Models.UserGroup
{
PermissionSet = new Host.Models.PermissionSet
{
AdministrationRights = AdministrationRights.ChangeVersion,
InstanceManagerRights = InstanceManagerRights.GrantPermissions
},
Name = "TestGroup",
};
const string TestUserName = "TestUser42";
var user = new Host.Models.User
{
Name = TestUserName,
CreatedAt = DateTimeOffset.UtcNow,
OAuthConnections = new List<Host.Models.OAuthConnection>(),
CanonicalName = Host.Models.User.CanonicalizeName(TestUserName),
Enabled = false,
Group = group,
PasswordHash = "_",
};
var instance = new Host.Models.Instance
{
AutoUpdateInterval = 0,
ChatBotLimit = 1,
ChatSettings = new List<Host.Models.ChatBot>(),
ConfigurationType = ConfigurationType.HostWrite,
DreamDaemonSettings = new Host.Models.DreamDaemonSettings
{
AllowWebClient = false,
AutoStart = false,
HeartbeatSeconds = 0,
DumpOnHeartbeatRestart = false,
Port = 1447,
SecurityLevel = DreamDaemonSecurity.Safe,
Visibility = DreamDaemonVisibility.Public,
StartupTimeout = 1000,
TopicRequestTimeout = 1000,
AdditionalParameters = string.Empty,
StartProfiler = false,
LogOutput = true,
},
DreamMakerSettings = new Host.Models.DreamMakerSettings
{
ApiValidationPort = 1557,
ApiValidationSecurityLevel = DreamDaemonSecurity.Trusted,
RequireDMApiValidation = false,
Timeout = TimeSpan.FromSeconds(13),
},
InstancePermissionSets = new List<Host.Models.InstancePermissionSet>
{
new Host.Models.InstancePermissionSet
{
ByondRights = ByondRights.InstallCustomVersion,
ChatBotRights = ChatBotRights.None,
ConfigurationRights = ConfigurationRights.Read,
DreamDaemonRights = DreamDaemonRights.ReadRevision,
DreamMakerRights = DreamMakerRights.SetApiValidationPort,
InstancePermissionSetRights = InstancePermissionSetRights.Write,
PermissionSet = group.PermissionSet,
RepositoryRights = RepositoryRights.SetReference
}
},
Name = "sfdsadfsa",
Online = false,
Path = "/a/b/c/d",
RepositorySettings = new Host.Models.RepositorySettings
{
AutoUpdatesKeepTestMerges = false,
AutoUpdatesSynchronize = false,
CommitterEmail = "email@eample.com",
CommitterName = "blubluh",
CreateGitHubDeployments = false,
PostTestMergeComment = false,
PushTestMergeCommits = false,
ShowTestMergeCommitters = false,
UpdateSubmodules = false,
},
};
context.Users.Add(user);
context.Groups.Add(group);
context.Instances.Add(instance);
await context.Save(default);
var dbServiceProvider = ((IInfrastructure<IServiceProvider>)context.Database).Instance;
var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
await migrator.MigrateAsync(migrationName, default);
await context.Database.EnsureDeletedAsync();
}
}
}