From c69a6cce3c14c3813785625fc9ae6eaf2909be78 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 26 Jul 2026 19:54:37 +0100 Subject: [PATCH 1/3] fix(security): remove client-side UploadThing credentials --- .github/scripts/inject-token.ps1 | 47 ----- .github/workflows/ci.yml | 10 - .github/workflows/release.yml | 10 - GenHub/GenHub.Core/Constants/ApiConstants.cs | 75 +------ GenHub/GenHub.Core/Constants/ErrorMessages.cs | 25 --- GenHub/GenHub.Core/Constants/LogMessages.cs | 10 - .../Common/IUploadHistoryService.cs | 6 +- .../GenHub.Core/Models/Tools/UploadRecord.cs | 7 +- .../Services/UploadHistoryServiceTests.cs | 79 ++++++++ .../Tools/Services/UploadThingServiceTests.cs | 38 ++++ .../ViewModels/MapManagerViewModel.cs | 12 +- .../MapManager/Views/MapManagerView.axaml | 4 +- .../ViewModels/ReplayManagerViewModel.cs | 12 +- .../Views/ReplayManagerView.axaml | 4 +- .../Tools/Services/UploadHistoryService.cs | 146 +++----------- .../Tools/Services/UploadThingService.cs | 187 ++---------------- docs/dev/constants.md | 7 +- docs/dev/uploading-api.md | 73 ++++--- 18 files changed, 218 insertions(+), 534 deletions(-) delete mode 100644 .github/scripts/inject-token.ps1 create mode 100644 GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs create mode 100644 GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadThingServiceTests.cs diff --git a/.github/scripts/inject-token.ps1 b/.github/scripts/inject-token.ps1 deleted file mode 100644 index f41d16e12..000000000 --- a/.github/scripts/inject-token.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -param( - [string]$Token -) - -if ([string]::IsNullOrEmpty($Token)) { - # For PRs from forks, secrets are not available. We use a dummy token to allow the build to pass. - if ($env:GITHUB_EVENT_NAME -eq 'pull_request') { - Write-Host "Warning: No UPLOADTHING_TOKEN provided. Using dummy token for PR build." - $Token = "DUMMY_TOKEN_FOR_CI_ONLY" - } else { - Write-Error "No UPLOADTHING_TOKEN provided. Fails for Release builds." - exit 1 - } -} - -$constantsPath = "GenHub/GenHub.Core/Constants/ApiConstants.cs" -if (-not (Test-Path $constantsPath)) { - Write-Error "Could not find $constantsPath" - exit 1 -} - -$tokenBytes = [System.Text.Encoding]::UTF8.GetBytes($Token) -$key = New-Object byte[] 32 -[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($key) - -$obfuscated = New-Object byte[] $tokenBytes.Length -for ($i = 0; $i -lt $tokenBytes.Length; $i++) { - $obfuscated[$i] = $tokenBytes[$i] -bxor $key[$i % $key.Length] -} - -$dataStr = ($obfuscated | ForEach-Object { "0x{0:x2}" -f $_ }) -join ", " -$keyStr = ($key | ForEach-Object { "0x{0:x2}" -f $_ }) -join ", " - -$content = Get-Content $constantsPath -Raw - -# Use regex replace to be robust against whitespace -# Pattern: byte[] data = []; // [PLACEHOLDER_DATA] -$content = [System.Text.RegularExpressions.Regex]::Replace($content, 'byte\[\]\s+data\s*=\s*\[\];\s*//\s*\[PLACEHOLDER_DATA\]', "byte[] data = [$dataStr];") -$content = [System.Text.RegularExpressions.Regex]::Replace($content, 'byte\[\]\s+key\s*=\s*\[\];\s*//\s*\[PLACEHOLDER_KEY\]', "byte[] key = [$keyStr];") - -if ($content -notmatch "0x") { - Write-Error "Token injection failed! Placeholders were not found or replaced." - exit 1 -} - -Set-Content $constantsPath $content -Write-Host "Successfully injected and obfuscated UPLOADTHING_TOKEN into ApiConstants.cs" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 837e9ae40..3020a15a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,11 +123,6 @@ jobs: echo "VERSION=$version" >> $env:GITHUB_OUTPUT echo "CHANNEL=$channel" >> $env:GITHUB_OUTPUT - - name: Inject Secrets - shell: pwsh - run: | - ./.github/scripts/inject-token.ps1 -Token "${{ secrets.UPLOADTHING_TOKEN }}" - - name: Build Projects shell: pwsh run: | @@ -285,11 +280,6 @@ jobs: echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - name: Inject Secrets - shell: pwsh - run: | - ./.github/scripts/inject-token.ps1 -Token "${{ secrets.UPLOADTHING_TOKEN }}" - - name: Build Projects run: | BUILD_PROPS="-p:Version=${{ steps.buildinfo.outputs.VERSION }} -p:GitShortHash=${{ steps.buildinfo.outputs.SHORT_HASH }} -p:PullRequestNumber=${{ steps.buildinfo.outputs.PR_NUMBER }} -p:BuildChannel=${{ steps.buildinfo.outputs.CHANNEL }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cee9e205..8ab2b06a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,11 +40,6 @@ jobs: $version = "0.0.${{ github.run_number }}" echo "VERSION=$version" >> $env:GITHUB_OUTPUT - - name: Inject Secrets - shell: pwsh - run: | - ./.github/scripts/inject-token.ps1 -Token "${{ secrets.UPLOADTHING_TOKEN }}" - - name: Publish Windows App shell: pwsh run: | @@ -116,11 +111,6 @@ jobs: - name: Install Linux Dependencies run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libx11-dev - - name: Inject Secrets - shell: pwsh - run: | - ./.github/scripts/inject-token.ps1 -Token "${{ secrets.UPLOADTHING_TOKEN }}" - - name: Publish Linux App run: | dotnet publish "${{ env.LINUX_PROJECT }}" \ diff --git a/GenHub/GenHub.Core/Constants/ApiConstants.cs b/GenHub/GenHub.Core/Constants/ApiConstants.cs index 9a93c95f5..7ab83ccb9 100644 --- a/GenHub/GenHub.Core/Constants/ApiConstants.cs +++ b/GenHub/GenHub.Core/Constants/ApiConstants.cs @@ -59,84 +59,13 @@ public static class ApiConstants /// public const string GitHubApiRunArtifactsFormat = "https://api.github.com/repos/{0}/{1}/actions/runs/{2}/artifacts"; - // UploadThing - - /// - /// UploadThing API version. - /// - public const string UploadThingApiVersion = "7.7.4"; - - /// - /// UploadThing prepare upload URL. - /// - public const string UploadThingPrepareUrl = "https://api.uploadthing.com/v7/prepareUpload"; - - /// - /// UploadThing delete file URL. - /// - public const string UploadThingDeleteUrl = "https://api.uploadthing.com/v6/deleteFiles"; - - /// - /// UploadThing public file URL format. - /// - public const string UploadThingPublicUrlFormat = "https://utfs.io/f/{0}"; + // UploadThing links /// /// UploadThing URL fragment for identification. /// public const string UploadThingUrlFragment = "utfs.io/f/"; - /// - /// UploadThing token environment variable. - /// - public const string UploadThingTokenEnvVar = "UPLOADTHING_TOKEN"; - - /// - /// Alternative UploadThing token environment variable. - /// - public const string UploadThingTokenEnvVarAlt = "GENHUB_UPLOADTHING_TOKEN"; - - /// - /// Gets the default UploadThing token injected at build time (Obfuscated). - /// - public static string BuildTimeUploadThingToken - { - get - { - // This is a simple XOR obfuscation to prevent the raw token from appearing in strings/debuggers. - // The actual values are injected during the GitHub Actions build process. - byte[] data = []; // [PLACEHOLDER_DATA] - byte[] key = []; // [PLACEHOLDER_KEY] - - if (data.Length == 0 || key.Length == 0) return string.Empty; - - var result = new byte[data.Length]; - for (int i = 0; i < data.Length; i++) - { - result[i] = (byte)(data[i] ^ key[i % key.Length]); - } - - return System.Text.Encoding.UTF8.GetString(result); - } - } - - /// - /// UploadThing API key header. - /// - public const string UploadThingApiKeyHeader = "x-uploadthing-api-key"; - - /// - /// UploadThing version header. - /// - public const string UploadThingVersionHeader = "x-uploadthing-version"; - - // Media Types - - /// - /// Media type for ZIP files. - /// - public const string MediaTypeZip = "application/zip"; - // GenTool /// @@ -167,4 +96,4 @@ public static string BuildTimeUploadThingToken /// UserAgent string that mimics a standard web browser. /// public const string BrowserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; -} \ No newline at end of file +} diff --git a/GenHub/GenHub.Core/Constants/ErrorMessages.cs b/GenHub/GenHub.Core/Constants/ErrorMessages.cs index 7e5371288..ecbbe6be5 100644 --- a/GenHub/GenHub.Core/Constants/ErrorMessages.cs +++ b/GenHub/GenHub.Core/Constants/ErrorMessages.cs @@ -5,11 +5,6 @@ namespace GenHub.Core.Constants; /// public static class ErrorMessages { - /// - /// Error message for missing UploadThing token. - /// - public const string UploadThingTokenMissing = "UploadThing V7 Token is missing. Ensure UPLOADTHING_TOKEN is in your .env file."; - /// /// Error message for ZIP validation failure. /// @@ -20,26 +15,6 @@ public static class ErrorMessages /// public const string FileExceedsSizeLimit = "File exceeds size limit: {Path}"; - /// - /// Error message for failed prepare upload. - /// - public const string V7PrepareUploadFailed = "V7 PrepareUpload failed: {StatusCode} - {Error}"; - - /// - /// Error message for missing required fields in UploadThing response. - /// - public const string UploadThingMissingFields = "UploadThing V7 returned 200 OK but missing required fields. Response: {Response}"; - - /// - /// Error message for failed binary upload. - /// - public const string V7BinaryUploadFailed = "V7 PUT Binary Upload failed: {StatusCode} - {Error}"; - - /// - /// Error message for exception in UploadThing flow. - /// - public const string ExceptionInUploadThingFlow = "Exception in UploadThing V7 flow"; - /// /// Error message for could not extract download URL. /// diff --git a/GenHub/GenHub.Core/Constants/LogMessages.cs b/GenHub/GenHub.Core/Constants/LogMessages.cs index 290e58e97..aeaf5ad27 100644 --- a/GenHub/GenHub.Core/Constants/LogMessages.cs +++ b/GenHub/GenHub.Core/Constants/LogMessages.cs @@ -40,16 +40,6 @@ public static class LogMessages /// public const string FailedToDeleteReplay = "Failed to delete replay: {Path}"; - /// - /// Log message for uploading to UploadThing. - /// - public const string UploadingToUploadThing = "Uploading to UploadThing V7: {Path}"; - - /// - /// Log message for successful UploadThing upload. - /// - public const string UploadThingSuccessful = "UploadThing V7 successful. Public URL: {Url}"; - /// /// Log message for failed ZIP creation. /// diff --git a/GenHub/GenHub.Core/Interfaces/Common/IUploadHistoryService.cs b/GenHub/GenHub.Core/Interfaces/Common/IUploadHistoryService.cs index 3bfca422a..7af1df63a 100644 --- a/GenHub/GenHub.Core/Interfaces/Common/IUploadHistoryService.cs +++ b/GenHub/GenHub.Core/Interfaces/Common/IUploadHistoryService.cs @@ -42,15 +42,15 @@ public interface IUploadHistoryService Task> GetUploadHistoryAsync(); /// - /// Removes a history item. + /// Removes an item from local history without deleting the hosted file. /// /// The URL. /// A task representing the asynchronous operation. Task RemoveHistoryItemAsync(string url); /// - /// Clears the history. + /// Clears local history without deleting hosted files. /// /// A task representing the asynchronous operation. Task ClearHistoryAsync(); -} \ No newline at end of file +} diff --git a/GenHub/GenHub.Core/Models/Tools/UploadRecord.cs b/GenHub/GenHub.Core/Models/Tools/UploadRecord.cs index 30733b0c5..00aa94789 100644 --- a/GenHub/GenHub.Core/Models/Tools/UploadRecord.cs +++ b/GenHub/GenHub.Core/Models/Tools/UploadRecord.cs @@ -24,9 +24,4 @@ public sealed class UploadRecord /// Gets or sets the name of the uploaded file. /// public string? FileName { get; set; } - - /// - /// Gets or sets a value indicating whether this item is queued for deletion. - /// - public bool IsPendingDeletion { get; set; } -} \ No newline at end of file +} diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs new file mode 100644 index 000000000..f4ab50c58 --- /dev/null +++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs @@ -0,0 +1,79 @@ +using GenHub.Core.Interfaces.Common; +using GenHub.Features.Tools.Services; +using Microsoft.Extensions.Logging; +using Moq; + +namespace GenHub.Tests.Core.Features.Tools.Services; + +/// +/// Tests for local upload history behavior while cloud deletion is disabled. +/// +public sealed class UploadHistoryServiceTests : IDisposable +{ + private readonly string _tempDirectory; + + /// + /// Initializes a new instance of the class. + /// + public UploadHistoryServiceTests() + { + _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDirectory); + } + + /// + /// Removes temporary test data. + /// + public void Dispose() + { + if (Directory.Exists(_tempDirectory)) + { + Directory.Delete(_tempDirectory, recursive: true); + } + + GC.SuppressFinalize(this); + } + + /// + /// Verifies that removing an item deletes its local record immediately. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task RemoveHistoryItemAsync_WhenItemExists_RemovesLocalRecord() + { + var service = CreateService(); + service.RecordUpload(1024, "https://utfs.io/f/example", "example.zip"); + + await service.RemoveHistoryItemAsync("https://utfs.io/f/example"); + + var reloadedService = CreateService(); + Assert.Empty(await reloadedService.GetUploadHistoryAsync()); + } + + /// + /// Verifies that clearing history deletes every local record immediately. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task ClearHistoryAsync_WhenItemsExist_RemovesAllLocalRecords() + { + var service = CreateService(); + service.RecordUpload(1024, "https://utfs.io/f/first", "first.zip"); + service.RecordUpload(2048, "https://utfs.io/f/second", "second.zip"); + + await service.ClearHistoryAsync(); + + var reloadedService = CreateService(); + Assert.Empty(await reloadedService.GetUploadHistoryAsync()); + } + + private UploadHistoryService CreateService() + { + var appConfig = new Mock(); + appConfig.Setup(config => config.GetConfiguredDataPath()).Returns(_tempDirectory); + + return new UploadHistoryService( + Mock.Of>(), + appConfig.Object); + } +} diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadThingServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadThingServiceTests.cs new file mode 100644 index 000000000..8bb511fed --- /dev/null +++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadThingServiceTests.cs @@ -0,0 +1,38 @@ +using GenHub.Features.Tools.Services; +using Microsoft.Extensions.Logging; +using Moq; + +namespace GenHub.Tests.Core.Features.Tools.Services; + +/// +/// Tests for the disabled UploadThing integration. +/// +public class UploadThingServiceTests +{ + private readonly UploadThingService _service = + new(Mock.Of>()); + + /// + /// Verifies that uploads fail closed while short-lived credentials are unavailable. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task UploadFileAsync_WhenCredentialsAreUnavailable_ReturnsNull() + { + var result = await _service.UploadFileAsync("unused.zip"); + + Assert.Null(result); + } + + /// + /// Verifies that authenticated deletion also fails closed. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task DeleteFileAsync_WhenCredentialsAreUnavailable_ReturnsFalse() + { + var result = await _service.DeleteFileAsync("unused-key"); + + Assert.False(result); + } +} diff --git a/GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs b/GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs index cf6736c64..4803d69ca 100644 --- a/GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs +++ b/GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs @@ -1081,7 +1081,7 @@ private async Task RemoveHistoryItemAsync(UploadHistoryItemViewModel item) { _notificationService.ShowInfo( "Remove From History", - "Removes the item from your local history. This frees up your upload quota immediately."); + "Removes the item from local history without deleting the hosted file."); return; } @@ -1089,7 +1089,9 @@ private async Task RemoveHistoryItemAsync(UploadHistoryItemViewModel item) { await _uploadHistoryService.RemoveHistoryItemAsync(item.Url); await LoadHistoryAsync(); - _notificationService.ShowSuccess("Removed", "History item removed."); + _notificationService.ShowSuccess( + "Removed", + "Removed from local history. The hosted file was not deleted."); } catch (Exception ex) { @@ -1107,7 +1109,7 @@ private async Task ClearHistoryAsync() { _notificationService.ShowInfo( "Clear History", - "Clears your entire local upload history. This frees up all your upload quota."); + "Clears local upload history without deleting hosted files."); return; } @@ -1115,7 +1117,9 @@ private async Task ClearHistoryAsync() { await _uploadHistoryService.ClearHistoryAsync(); await LoadHistoryAsync(); - _notificationService.ShowSuccess("Cleared", "All upload history cleared."); + _notificationService.ShowSuccess( + "Cleared", + "Local history cleared. Hosted files were not deleted."); } catch (Exception ex) { diff --git a/GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml b/GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml index c530da3be..a8d8b2a79 100644 --- a/GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml +++ b/GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml @@ -189,9 +189,9 @@ - - diff --git a/GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml b/GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml index 11ba14842..15c8f1273 100644 --- a/GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml +++ b/GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml @@ -182,8 +182,6 @@ ToolTip.Tip="View the history of your uploaded replays."> - - diff --git a/GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs b/GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs index c16ae2b02..ba7c855e4 100644 --- a/GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs +++ b/GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs @@ -175,10 +175,20 @@ private List LoadHistoryInternal() // Clean up old entries (expired retention) var retentionCutoff = DateTime.UtcNow.AddDays(-HistoryRetentionDays); + var hasPendingDeletionRecords = history.Any(r => r.IsPendingDeletion); - _cache = history.Where(r => r.Timestamp >= retentionCutoff).OrderByDescending(r => r.Timestamp).ToList(); + var migratedHistory = history + .Where(r => !r.IsPendingDeletion && r.Timestamp >= retentionCutoff) + .OrderByDescending(r => r.Timestamp) + .ToList(); + _cache = migratedHistory; - return new List(_cache); + if (hasPendingDeletionRecords) + { + SaveHistoryInternal(migratedHistory); + } + + return new List(migratedHistory); } catch (Exception ex) { From 3c4472df49eae5b933829eb3d0dc638f82602715 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 28 Jul 2026 14:17:37 +0100 Subject: [PATCH 3/3] test(upload-history): cover removal edge cases --- .../Services/UploadHistoryServiceTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs index e003fb9a0..1d6cc74bf 100644 --- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs +++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/Services/UploadHistoryServiceTests.cs @@ -50,6 +50,41 @@ public async Task RemoveHistoryItemAsync_WhenItemExists_RemovesLocalRecord() Assert.Empty(await reloadedService.GetUploadHistoryAsync()); } + /// + /// Verifies that removing one item preserves the other local records. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task RemoveHistoryItemAsync_WhenOtherItemsExist_PreservesOtherRecords() + { + var service = CreateService(); + service.RecordUpload(1024, "https://utfs.io/f/first", "first.zip"); + service.RecordUpload(2048, "https://utfs.io/f/second", "second.zip"); + + await service.RemoveHistoryItemAsync("https://utfs.io/f/first"); + + var reloadedService = CreateService(); + var item = Assert.Single(await reloadedService.GetUploadHistoryAsync()); + Assert.Equal("https://utfs.io/f/second", item.Url); + } + + /// + /// Verifies that removing a non-matching URL leaves local history unchanged. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task RemoveHistoryItemAsync_WhenUrlDoesNotMatch_PreservesHistory() + { + var service = CreateService(); + service.RecordUpload(1024, "https://utfs.io/f/example", "example.zip"); + + await service.RemoveHistoryItemAsync("https://utfs.io/f/missing"); + + var reloadedService = CreateService(); + var item = Assert.Single(await reloadedService.GetUploadHistoryAsync()); + Assert.Equal("https://utfs.io/f/example", item.Url); + } + /// /// Verifies that clearing history deletes every local record immediately. /// @@ -67,6 +102,21 @@ public async Task ClearHistoryAsync_WhenItemsExist_RemovesAllLocalRecords() Assert.Empty(await reloadedService.GetUploadHistoryAsync()); } + /// + /// Verifies that clearing empty history completes without creating records. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task ClearHistoryAsync_WhenHistoryIsEmpty_RemainsEmpty() + { + var service = CreateService(); + + await service.ClearHistoryAsync(); + + var reloadedService = CreateService(); + Assert.Empty(await reloadedService.GetUploadHistoryAsync()); + } + /// /// Verifies that legacy pending-deletion records are removed during migration. ///