diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3ac28b5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: "CI" + +on: + push: + branches: [ develop, main ] + +jobs: + build: + runs-on: windows-latest + + env: + Solution: "src/CleanMyPosts.sln" + UnitTest_Project: "src/UnitTests/UnitTests.csproj" + IntegrationTest_Project: "src/IntegrationTests/IntegrationTests.csproj" + FORCE_COLOR: "true" + DOTNET_LOGGING__CONSOLE__COLORBEHAVIOR: Enabled + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.x + + - name: Install .NET Tools (local) + run: | + dotnet new tool-manifest + dotnet tool install dotnet-reportgenerator-globaltool + dotnet tool install dotnet-sonarscanner + + - name: Restore + run: dotnet restore "${{ env.Solution }}" + + - name: Build and Test with SonarQube + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + dotnet tool run dotnet-sonarscanner begin ` + /k:"thorstenalpers_CleanMyPosts" ` + /o:"thorstenalpers" ` + /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` + /d:sonar.host.url="https://sonarcloud.io" ` + /d:sonar.sources="src" ` + /d:sonar.tests="src/UnitTests;src/IntegrationTests" ` + /d:sonar.test.inclusions="**/*Tests.cs" ` + /d:sonar.coverageReportPaths="TestResults/Reports/SonarQube.xml" + + dotnet build "${{ env.Solution }}" --configuration Release --no-restore + + # Run Unit Tests + dotnet test "${{ env.UnitTest_Project }}" ` + --collect:"XPlat Code Coverage" ` + --results-directory TestResults/UnitTests ` + --configuration Release ` + --logger "console;verbosity=detailed" ` + --filter "TestCategory!=Long-Running" + + # Run Integration Tests + dotnet test "${{ env.IntegrationTest_Project }}" ` + --collect:"XPlat Code Coverage" ` + --results-directory TestResults/IntegrationTests ` + --configuration Release ` + --logger "console;verbosity=detailed" + + # Generate coverage reports + dotnet tool run reportgenerator ` + -reports:TestResults/**/coverage.cobertura.xml ` + -targetdir:TestResults/Reports ` + -reporttypes:"Html;lcov;SonarQube;Cobertura" ` + + dotnet tool run dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + + - name: Upload to Coveralls + uses: coverallsapp/github-action@v2 + with: + path-to-lcov: TestResults/Reports/lcov.info + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + + - name: Upload Test Coverage Report + uses: actions/upload-artifact@v4 + with: + name: test-coverage-report + path: TestResults/Reports diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml new file mode 100644 index 0000000..ff381b9 --- /dev/null +++ b/.github/workflows/deploy-release.yml @@ -0,0 +1,142 @@ +name: "Deploy Release" + +on: + workflow_dispatch: # manual trigger + +jobs: + build: + runs-on: windows-latest + + env: + Solution: "src/CleanMyPosts.sln" + UI_Project: "src/UI/UI.csproj" + UnitTest_Project: "src/UnitTests/UnitTests.csproj" + IntegrationTest_Project: "src/IntegrationTests/IntegrationTests.csproj" + Installer_Script: "installer/Installer.iss" + FORCE_COLOR: "true" + DOTNET_LOGGING__CONSOLE__COLORBEHAVIOR: Enabled + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Important to fetch full history for git tag and branches + + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.x + + - name: Restore + run: dotnet restore "${{ env.Solution }}" + + - name: Build + run: dotnet build "${{ env.Solution }}" --configuration Release --no-restore + + - name: Run Unit Tests + run: dotnet test "${{ env.UnitTest_Project }}" --configuration Release --logger "console;verbosity=detailed" --filter "TestCategory!=Long-Running" + + - name: Run Integration Tests + run: dotnet test "${{ env.IntegrationTest_Project }}" --configuration Release --logger "console;verbosity=detailed" + + - name: Extract Version + id: get_version + shell: pwsh + run: | + $content = Get-Content "${{ env.UI_Project }}" + if ($content -match '(.+)') { + $version = $matches[1] + echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append + } else { + throw "Version not found in project file" + } + + - name: Publish Single EXE + run: | + dotnet publish "${{ env.UI_Project }}" -c Release -r win-x64 --self-contained true ` + /p:PublishSingleFile=true ` + /p:IncludeAllContentForSelfExtract=true ` + /p:EnableCompressionInSingleFile=true ` + -o artifacts/single-exe + + - name: Install Inno Setup + run: choco install innosetup --yes + + - name: Build Setup EXE + run: | + iscc "/DMyAppVersion=${{ env.VERSION }}" "/DMyAppExePath=artifacts\\single-exe\\*" "${{ env.Installer_Script }}" + + - name: Copy Setup EXE to Artifacts + run: | + mkdir -p artifacts/setup + copy installer\Output\CleanMyPosts-Setup-${{ env.VERSION }}.exe artifacts\setup\ + + - name: Rename Standalone EXE for Release + run: | + Rename-Item "artifacts/single-exe/CleanMyPosts.exe" "artifacts/single-exe/CleanMyPosts-standalone.exe" + + - name: Generate update.xml + shell: pwsh + run: | + $version = "${{ env.VERSION }}" + $repo = "${{ github.repository }}" + $baseUrl = "https://github.com/$repo/releases/download/v$version" + $installerUrl = "$baseUrl/CleanMyPosts-Setup-$version.exe" + $changelogUrl = "https://github.com/$repo/releases/tag/v$version" + $xmlContent = @" + + + + CleanMyPosts + $version + $installerUrl + $changelogUrl + + + "@ + $xmlContent | Set-Content -Path "artifacts/update.xml" -Encoding UTF8 + + - name: Configure Git Credentials + run: | + echo "https://${{ secrets.GH_APIKEY }}@github.com" > $env:USERPROFILE\.git-credentials + git config --global credential.helper store + git config --global user.name "github-actions" + git config --global user.email "actions@github.com" + + - name: Push update.xml to update-feed branch + run: | + git fetch origin update-feed || echo "No update-feed branch yet" + if git show-ref --verify --quiet refs/heads/update-feed; then + git checkout update-feed + else + git checkout --orphan update-feed + git rm -rf . + fi + + cp artifacts/update.xml update.xml + git add update.xml + + if git diff --cached --quiet; then + echo "No changes in update.xml; skipping commit" + else + git commit -m "Update appcast for version ${{ env.VERSION }}" + git push origin update-feed --force + fi + + - name: Create Git Tag + run: | + git tag -a "v${{ env.VERSION }}" -m "Release v${{ env.VERSION }}" + git push origin "v${{ env.VERSION }}" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: "v${{ env.VERSION }}" + name: "CleanMyPosts ${{ env.VERSION }}" + body_path: ./release-notes/v${{ env.VERSION }}.md + files: | + artifacts/single-exe/CleanMyPosts-standalone.exe + artifacts/setup/CleanMyPosts-Setup-${{ env.VERSION }}.exe + artifacts/update.xml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index d240649..1b31fff 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ -# CleanMyPosts +![Banner](./src/UI/Assets/banner.png) + + +[![Windows](https://img.shields.io/badge/platform-Windows-blue)](#) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE.txt) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=thorstenalpers_CleanMyPosts&metric=alert_status)](https://sonarcloud.io/project/issues?issueStatuses=OPEN%2CCONFIRMED&id=thorstenalpers_CleanMyPosts) +[![CI Tests](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml/badge.svg)](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml) +[![Coverage Status](https://coveralls.io/repos/github/thorstenalpers/CleanMyPosts/badge.svg?branch=develop)](https://coveralls.io/github/thorstenalpers/CleanMyPosts?branch=develop) +[![Star this repo](https://img.shields.io/github/stars/thorstenalpers/CleanMyPosts.svg?style=social&label=Star&maxAge=60)](https://github.com/thorstenalpers/CleanMyPosts) + > ⚠️ **Warning:** Development in progress – the application has not been released. @@ -21,7 +30,7 @@ * bookmark bar, hideable via settings -# 🧹 CleanMyPosts +--- **CleanMyPosts** is a lightweight Windows desktop application that securely deletes all tweets from your X (formerly Twitter) account in bulk. Designed for privacy-focused users, social media managers, or anyone looking to start fresh. diff --git a/installer/Installer.iss b/installer/Installer.iss index 167cc16..acfb0bc 100644 --- a/installer/Installer.iss +++ b/installer/Installer.iss @@ -1,40 +1,32 @@ -; Script generated by the Inno Setup Script Wizard. -; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! - #define MyAppName "CleanMyPosts" -#define MyAppVersion "0.0.1" #define MyAppPublisher "Thorsten Alpers" #define MyAppURL "https://github.com/thorstenalpers/CleanMyPosts" #define MyAppExeName "CleanMyPosts.exe" #define MyIconPath "..\src\UI\Assets\logo.ico" -#define MyAppExePath "..\src\UI\bin\Release\net9.0-windows10.0.19041.0\win-x64\publish\*" + +; dynamically set in github actions, ifndef use local values +#ifndef MyAppVersion + #define MyAppVersion "0.0.1" +#endif +#ifndef MyAppExePath + #define MyAppExePath "..\src\UI\bin\Release\net9.0-windows10.0.19041.0\win-x64\publish\*" +#endif [Setup] -; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{AEE32610-58A5-4785-98B0-B651865B30D2}} AppName={#MyAppName} AppVersion={#MyAppVersion} -;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} UninstallDisplayIcon={app}\{#MyAppExeName} -; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run -; on anything but x64 and Windows 11 on Arm. ArchitecturesAllowed=x64compatible -; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the -; install be done in "64-bit mode" on x64 or Windows 11 on Arm, -; meaning it should use the native 64-bit Program Files directory and -; the 64-bit view of the registry. ArchitecturesInstallIn64BitMode=x64compatible DisableProgramGroupPage=yes -; Uncomment the following line to run in non administrative install mode (install for current user only). PrivilegesRequired=admin -; PrivilegesRequiredOverridesAllowed=no -OutputBaseFilename=CleanMyPosts_Setup_{#MyAppVersion} +OutputBaseFilename=CleanMyPosts-Setup-{#MyAppVersion} SolidCompression=yes WizardStyle=modern SetupIconFile={#MyIconPath} @@ -49,13 +41,10 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ Source: "{#MyAppExePath}"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#MyIconPath}"; DestDir: "{app}"; Flags: ignoreversion -; NOTE: Don't use "Flags: ignoreversion" on any shared system files - [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\logo.ico" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\logo.ico"; Tasks: desktopicon - [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/release-notes/v0.0.1.md b/release-notes/v0.0.1.md new file mode 100644 index 0000000..8bf8e03 --- /dev/null +++ b/release-notes/v0.0.1.md @@ -0,0 +1,3 @@ +### What's Changed + +* First Release diff --git a/src/CleanMyPosts.sln b/src/CleanMyPosts.sln index e2176a9..31cbeab 100644 --- a/src/CleanMyPosts.sln +++ b/src/CleanMyPosts.sln @@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UI", "UI\UI.csproj", "{48A1 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{A65F1C77-11C1-DC4F-0A69-6EE789D29685}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" ProjectSection(SolutionItems) = preProject @@ -19,6 +19,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\README.md = ..\README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "IntegrationTests\IntegrationTests.csproj", "{59FDAA84-6701-32D0-9E5C-CA30D0B3B987}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml + ..\.github\workflows\deploy-release.yml = ..\.github\workflows\deploy-release.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,10 +45,17 @@ Global {F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}.Release|Any CPU.Build.0 = Release|Any CPU + {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C5A00240-64B9-4718-9682-317A36915078} EndGlobalSection diff --git a/src/IntegrationTests/IntegrationTests.csproj b/src/IntegrationTests/IntegrationTests.csproj new file mode 100644 index 0000000..31e1348 --- /dev/null +++ b/src/IntegrationTests/IntegrationTests.csproj @@ -0,0 +1,33 @@ + + + + net9.0-windows10.0.19041.0 + false + uap10.0.18362 + x64;x86;AnyCPU + CleanMyPosts.IntegrationTests + CleanMyPosts.IntegrationTests + CleanMyPosts.IntegrationTests + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/src/IntegrationTests/PagesTests.cs b/src/IntegrationTests/PagesTests.cs new file mode 100644 index 0000000..9269c3f --- /dev/null +++ b/src/IntegrationTests/PagesTests.cs @@ -0,0 +1,53 @@ +using CleanMyPosts.UI.Contracts.Services; +using CleanMyPosts.UI.ViewModels; +using CleanMyPosts.UI.Views; +using Microsoft.Extensions.Hosting; +using NUnit.Framework; + +namespace CleanMyPosts.IntegrationTests; + +[Category("Integration")] +public class PagesTests +{ + private IHost _host; + + [SetUp] + public void Setup() + { + _host = TestHelper.SetUpHost(); + } + + [Test] + public void TestWebViewViewModelCreation() + { + var vm = _host.Services.GetService(typeof(XViewModel)); + Assert.That(vm, Is.Not.Null); + } + + [Test] + public void TestGetMainPageType() + { + var pageService = _host.Services.GetService(typeof(IPageService)) as IPageService; + Assert.That(pageService, Is.Not.Null); + + var pageType = pageService.GetPageType(typeof(XViewModel).FullName); + Assert.That(typeof(XPage), Is.EqualTo(pageType)); + } + + [Test] + public void TestSettingsViewModelCreation() + { + var vm = _host.Services.GetService(typeof(SettingsViewModel)); + Assert.That(vm, Is.Not.Null); + } + + [Test] + public void TestGetSettingsPageType() + { + var pageService = _host.Services.GetService(typeof(IPageService)) as IPageService; + Assert.That(pageService, Is.Not.Null); + + var pageType = pageService.GetPageType(typeof(SettingsViewModel).FullName); + Assert.That(typeof(SettingsPage), Is.EqualTo(pageType)); + } +} diff --git a/src/IntegrationTests/TestHelper.cs b/src/IntegrationTests/TestHelper.cs new file mode 100644 index 0000000..6ab724f --- /dev/null +++ b/src/IntegrationTests/TestHelper.cs @@ -0,0 +1,64 @@ +using CleanMyPosts.UI.Helpers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Moq; +using NetSparkleUpdater.Interfaces; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace CleanMyPosts.IntegrationTests; + +internal static class TestHelper +{ + public static IHost SetUpHost() + { + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Converters = { new StringEnumConverter() }, + Formatting = Formatting.Indented + }; + + var cfgBuilder = new ConfigurationBuilder(); + cfgBuilder.AddInMemoryCollection(new Dictionary + { + ["Updater:AppCastUrl"] = "https://example.com/appcast.xml", + ["Updater:SecurityMode"] = "Unsafe", + ["Updater:IconUri"] = "https://raw.githubusercontent.com/thorstenalpers/CleanMyPosts/refs/heads/main/src/UI/Assets/logo.ico" + }); + cfgBuilder.AddUserSecrets(); + cfgBuilder.AddEnvironmentVariables(); + var cfg = cfgBuilder.Build(); + + var host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + { + config.AddConfiguration(cfg); + }) + .ConfigureServices((context, services) => + { + services.AddCleanMyPosts(cfg); + var mockUIFactory = new Mock(); + services.AddSingleton(mockUIFactory.Object); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddConsole(); + logging.SetMinimumLevel(LogLevel.Information); + logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning); + logging.AddFilter("CleanMyPosts", LogLevel.Information); + + logging.AddSimpleConsole(options => + { + options.UseUtcTimestamp = true; + options.SingleLine = true; + options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; + }); + }) + .Build(); + + return host; + } +} diff --git a/src/Tests/PagesTests.cs b/src/Tests/PagesTests.cs deleted file mode 100644 index 46bcb58..0000000 --- a/src/Tests/PagesTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Reflection; -using CleanMyPosts.Core.Contracts.Services; -using CleanMyPosts.Core.Services; -using CleanMyPosts.UI.Contracts.Services; -using CleanMyPosts.UI.Models; -using CleanMyPosts.UI.Services; -using CleanMyPosts.UI.ViewModels; -using CleanMyPosts.UI.Views; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using NUnit.Framework; - -namespace CleanMyPosts.Tests; - -[Category("Unit")] -public class PagesTests -{ - private IHost _host; - - [SetUp] - public void Setup() - { - var appLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); - _host = Host.CreateDefaultBuilder() - .ConfigureAppConfiguration(c => c.SetBasePath(appLocation)) - .ConfigureServices(ConfigureServices) - .Build(); - } - - private void ConfigureServices(HostBuilderContext context, IServiceCollection services) - { - // Core Services - services.AddSingleton(); - - // Services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // ViewModels - services.AddTransient(); - services.AddTransient(); - - // Configuration - services.Configure(context.Configuration.GetSection(nameof(AppConfig))); - } - - [Test] - public void TestWebViewViewModelCreation() - { - var vm = _host.Services.GetService(typeof(XViewModel)); - Assert.That(vm, Is.Not.Null); - } - - [Test] - public void TestGetMainPageType() - { - if (_host.Services.GetService(typeof(IPageService)) is IPageService pageService) - { - var pageType = pageService.GetPageType(typeof(XViewModel).FullName); - Assert.That(typeof(XPage), Is.EqualTo(pageType)); - } - else - { - Assert.Fail($"Can't resolve {nameof(IPageService)}"); - } - } - - [Test] - public void TestSettingsViewModelCreation() - { - var vm = _host.Services.GetService(typeof(SettingsViewModel)); - Assert.That(vm, Is.Not.Null); - } - - [Test] - public void TestGetSettingsPageType() - { - if (_host.Services.GetService(typeof(IPageService)) is IPageService pageService) - { - var pageType = pageService.GetPageType(typeof(SettingsViewModel).FullName); - Assert.That(typeof(SettingsPage), Is.EqualTo(pageType)); - } - else - { - Assert.Fail($"Can't resolve {nameof(IPageService)}"); - } - } -} diff --git a/src/UI/App.xaml.cs b/src/UI/App.xaml.cs index 3ad67c4..fbd7f2a 100644 --- a/src/UI/App.xaml.cs +++ b/src/UI/App.xaml.cs @@ -1,14 +1,8 @@ using System.Windows; using System.Windows.Threading; -using CleanMyPosts.Core.Contracts.Services; -using CleanMyPosts.Core.Services; -using CleanMyPosts.UI.Contracts.Services; -using CleanMyPosts.UI.Contracts.Views; +using CleanMyPosts.Core.Exception; using CleanMyPosts.UI.Helpers; -using CleanMyPosts.UI.Models; -using CleanMyPosts.UI.Services; using CleanMyPosts.UI.ViewModels; -using CleanMyPosts.UI.Views; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -52,13 +46,12 @@ private async void OnStartup(object sender, StartupEventArgs e) }) .Build(); - // 🔧 LogViewModel must be resolved **after** the host is built var logViewModel = _host.Services.GetRequiredService(); Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(config) .WriteTo.Debug() - .WriteTo.LogViewModelSink(logViewModel) // custom sink + .WriteTo.LogViewModelSink(logViewModel) .CreateLogger(); await _host.StartAsync(); @@ -69,45 +62,13 @@ private async void OnStartup(object sender, StartupEventArgs e) catch (Exception ex) { Log.Fatal(ex, "Application start-up failed."); - throw; + throw new CleanMyPostsException("Application start-up failed.", ex); } } internal static void ConfigureServices(HostBuilderContext context, IServiceCollection services) { - // Your existing registrations - services.AddHostedService(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - - services.AddHttpClient(); - - services.Configure(context.Configuration.GetSection(nameof(AppConfig))); - services.Configure(context.Configuration.GetSection("UpdateSettings")); + services.AddCleanMyPosts(context.Configuration); } private async void OnExit(object sender, ExitEventArgs e) @@ -116,7 +77,7 @@ private async void OnExit(object sender, ExitEventArgs e) logger.LogInformation("Application is stopping."); await _host.StopAsync(); _host.Dispose(); - Log.CloseAndFlush(); + await Log.CloseAndFlushAsync(); _host = null; } diff --git a/src/UI/AssemblyInfo.cs b/src/UI/AssemblyInfo.cs new file mode 100644 index 0000000..2aa049f --- /dev/null +++ b/src/UI/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CleanMyPosts.UnitTests")] +[assembly: InternalsVisibleTo("CleanMyPosts.IntegrationTests")] + +// Moq: ILogger needs it to mock internal classes +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/UI/Assets/banner.png b/src/UI/Assets/banner.png new file mode 100644 index 0000000..afbdec3 Binary files /dev/null and b/src/UI/Assets/banner.png differ diff --git a/src/UI/Converters/EnumToBooleanConverter.cs b/src/UI/Converters/EnumToBooleanConverter.cs index 38df352..d049659 100644 --- a/src/UI/Converters/EnumToBooleanConverter.cs +++ b/src/UI/Converters/EnumToBooleanConverter.cs @@ -9,26 +9,16 @@ public class EnumToBooleanConverter : IValueConverter public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (parameter is string enumString) + if (parameter is string enumString && Enum.IsDefined(EnumType, value)) { - if (Enum.IsDefined(EnumType, value)) - { - var enumValue = Enum.Parse(EnumType, enumString); - - return enumValue.Equals(value); - } + var enumValue = Enum.Parse(EnumType, enumString); + return enumValue.Equals(value); } - return false; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - if (parameter is string enumString) - { - return Enum.Parse(EnumType, enumString); - } - - return null; + return parameter is string enumString ? Enum.Parse(EnumType, enumString) : null; } } diff --git a/src/UI/Helpers/Helper.cs b/src/UI/Helpers/Helper.cs index 6f7d081..a18137d 100644 --- a/src/UI/Helpers/Helper.cs +++ b/src/UI/Helpers/Helper.cs @@ -3,6 +3,6 @@ public static class Helper { public static string CleanJsonResult(string json) { - return json?.Replace("\\\"", "\"")?.Trim('\"') ?? ""; + return json.Replace("\\\"", "\"").Trim('\"'); } } diff --git a/src/UI/Helpers/ServiceCollectionExtensions.cs b/src/UI/Helpers/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d9b8106 --- /dev/null +++ b/src/UI/Helpers/ServiceCollectionExtensions.cs @@ -0,0 +1,68 @@ +using System.Windows; +using System.Windows.Media.Imaging; +using CleanMyPosts.Core.Contracts.Services; +using CleanMyPosts.Core.Services; +using CleanMyPosts.UI.Contracts.Services; +using CleanMyPosts.UI.Contracts.Views; +using CleanMyPosts.UI.Models; +using CleanMyPosts.UI.Services; +using CleanMyPosts.UI.ViewModels; +using CleanMyPosts.UI.Views; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NetSparkleUpdater.Interfaces; +using NetSparkleUpdater.UI.WPF; + +namespace CleanMyPosts.UI.Helpers; + +public static class ServiceCollectionExtensions +{ + public static void AddCleanMyPosts(this IServiceCollection services, IConfiguration configuration) + { + services.AddHostedService(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => + { + UIFactory factory = null; + Application.Current.Dispatcher.Invoke(() => + { + var options = sp.GetRequiredService>().Value; + var uri = new Uri(options.IconUri, UriKind.Absolute); + var imageSource = new BitmapImage(uri); + factory = new UIFactory(imageSource); + }); + return factory; + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + services.AddHttpClient(); + + services.Configure(configuration.GetSection(nameof(AppConfig))); + services.Configure(configuration.GetSection("Updater")); + } +} diff --git a/src/UI/Models/AppConfig.cs b/src/UI/Models/AppConfig.cs index 7fa747f..8c8479e 100644 --- a/src/UI/Models/AppConfig.cs +++ b/src/UI/Models/AppConfig.cs @@ -4,4 +4,5 @@ public class AppConfig { public string ConfigurationsFolder { get; set; } public string AppPropertiesFileName { get; set; } + public string XBaseUrl { get; set; } } diff --git a/src/UI/Models/UpdaterOptions.cs b/src/UI/Models/UpdaterOptions.cs index bb6f7cf..46434d6 100644 --- a/src/UI/Models/UpdaterOptions.cs +++ b/src/UI/Models/UpdaterOptions.cs @@ -1,7 +1,10 @@ -namespace CleanMyPosts.UI.Models; +using NetSparkleUpdater.Enums; + +namespace CleanMyPosts.UI.Models; public class UpdaterOptions { public string AppCastUrl { get; set; } - public string SecurityMode { get; set; } + public SecurityMode SecurityMode { get; set; } + public string IconUri { get; set; } } \ No newline at end of file diff --git a/src/UI/Properties/Resources.Designer.cs b/src/UI/Properties/Resources.Designer.cs index e487eca..c92d32b 100644 --- a/src/UI/Properties/Resources.Designer.cs +++ b/src/UI/Properties/Resources.Designer.cs @@ -61,7 +61,7 @@ internal Resources() { } /// - /// Looks up a localized string similar to Clean My Posts. + /// Looks up a localized string similar to CleanMyPosts. /// public static string AppDisplayName { get { diff --git a/src/UI/Properties/Resources.resx b/src/UI/Properties/Resources.resx index 39e52fd..58c7ea8 100644 --- a/src/UI/Properties/Resources.resx +++ b/src/UI/Properties/Resources.resx @@ -172,7 +172,7 @@ Open or close navigation - Clean My Posts + CleanMyPosts Log diff --git a/src/UI/Services/ApplicationHostService.cs b/src/UI/Services/ApplicationHostService.cs index f0a6f4b..d0fe5ef 100644 --- a/src/UI/Services/ApplicationHostService.cs +++ b/src/UI/Services/ApplicationHostService.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Hosting; -using CleanMyPosts.UI.Contracts.Activation; +using CleanMyPosts.UI.Contracts.Activation; using CleanMyPosts.UI.Contracts.Services; using CleanMyPosts.UI.Contracts.Views; using CleanMyPosts.UI.ViewModels; +using Microsoft.Extensions.Hosting; namespace CleanMyPosts.UI.Services; @@ -17,7 +17,6 @@ public class ApplicationHostService(IServiceProvider serviceProvider, private readonly IPersistAndRestoreService _persistAndRestoreService = persistAndRestoreService; private readonly IThemeSelectorService _themeSelectorService = themeSelectorService; private readonly IEnumerable _activationHandlers = activationHandlers; - private IShellWindow _shellWindow; private bool _isInitialized; public async Task StartAsync(CancellationToken cancellationToken) @@ -65,9 +64,9 @@ private async Task HandleActivationAsync() if (!System.Windows.Application.Current.Windows.OfType().Any()) { - _shellWindow = _serviceProvider.GetService(typeof(IShellWindow)) as IShellWindow; - _navigationService.Initialize(_shellWindow.GetNavigationFrame()); - _shellWindow.ShowWindow(); + var shellWindow = _serviceProvider.GetService(typeof(IShellWindow)) as IShellWindow; + _navigationService.Initialize(shellWindow.GetNavigationFrame()); + shellWindow.ShowWindow(); _navigationService.NavigateTo(typeof(XViewModel).FullName); await Task.CompletedTask; } diff --git a/src/UI/Services/UpdateService.cs b/src/UI/Services/UpdateService.cs index 1c75cca..f887b50 100644 --- a/src/UI/Services/UpdateService.cs +++ b/src/UI/Services/UpdateService.cs @@ -1,29 +1,29 @@ -using System.Windows.Media.Imaging; +using Ardalis.GuardClauses; using CleanMyPosts.UI.Contracts.Services; using CleanMyPosts.UI.Models; using Microsoft.Extensions.Options; using NetSparkleUpdater; -using NetSparkleUpdater.Enums; +using NetSparkleUpdater.Interfaces; using NetSparkleUpdater.SignatureVerifiers; -using NetSparkleUpdater.UI.WPF; namespace CleanMyPosts.UI.Services; public class UpdateService : IUpdateService { private readonly SparkleUpdater _sparkle; - public UpdateService(IOptions options) - { - var securityMode = options.Value.SecurityMode == "Strict" ? SecurityMode.Strict : SecurityMode.Unsafe; - var uri = new Uri("pack://application:,,,/CleanMyPosts;component/Assets/logo.ico", UriKind.Absolute); - var imageSource = new BitmapImage(uri); + public UpdateService(IOptions options, IUIFactory uIFactory) + { + var opts = options.Value; + Guard.Against.Null(opts); + Guard.Against.NullOrWhiteSpace(opts.AppCastUrl); + Guard.Against.NullOrWhiteSpace(opts.SecurityMode.ToString()); - var verifier = new DSAChecker(securityMode); - _sparkle = new SparkleUpdater(options.Value.AppCastUrl, verifier) + var verifier = new DSAChecker(opts.SecurityMode); + _sparkle = new SparkleUpdater(opts.AppCastUrl, verifier) { - UIFactory = new UIFactory(imageSource), + UIFactory = uIFactory, RelaunchAfterUpdate = true, UseNotificationToast = true }; diff --git a/src/UI/Services/WindowManagerService.cs b/src/UI/Services/WindowManagerService.cs index af313f8..e4ec40e 100644 --- a/src/UI/Services/WindowManagerService.cs +++ b/src/UI/Services/WindowManagerService.cs @@ -15,9 +15,9 @@ public class WindowManagerService(IServiceProvider serviceProvider, IPageService private readonly IPageService _pageService = pageService; public Window MainWindow => Application.Current.MainWindow; - public void OpenInNewWindow(string key, object parameter = null) + public void OpenInNewWindow(string pageKey, object parameter = null) { - var existingWindow = GetWindow(key); + var existingWindow = GetWindow(pageKey); if (existingWindow is not null) { existingWindow.Activate(); @@ -39,11 +39,11 @@ public void OpenInNewWindow(string key, object parameter = null) frame.Navigated += OnNavigated; newWindow.Closed += OnWindowClosed; - frame.Navigate(_pageService.GetPage(key), parameter); + frame.Navigate(_pageService.GetPage(pageKey), parameter); newWindow.Show(); } - public bool? OpenInDialog(string key, object parameter = null) + public bool? OpenInDialog(string pageKey, object parameter = null) { if (_serviceProvider.GetService(typeof(IShellDialogWindow)) is not IShellDialogWindow shellWindow) { @@ -54,17 +54,17 @@ public void OpenInNewWindow(string key, object parameter = null) frame.Navigated += OnNavigated; ((Window)shellWindow).Closed += OnWindowClosed; - frame.Navigate(_pageService.GetPage(key), parameter); + frame.Navigate(_pageService.GetPage(pageKey), parameter); return ((Window)shellWindow).ShowDialog(); } - public Window GetWindow(string key) + public Window GetWindow(string pageKey) { foreach (Window window in Application.Current.Windows) { var dataContext = window.GetDataContext(); - if (dataContext?.GetType().FullName == key) + if (dataContext?.GetType().FullName == pageKey) { return window; } @@ -72,7 +72,7 @@ public Window GetWindow(string key) return null; } - private void OnNavigated(object sender, NavigationEventArgs e) + private static void OnNavigated(object sender, NavigationEventArgs e) { if (sender is Frame frame && frame.GetDataContext() is INavigationAware navigationAware) diff --git a/src/UI/Services/XWebViewScriptService.cs b/src/UI/Services/XWebViewScriptService.cs index 02afb43..f9339ba 100644 --- a/src/UI/Services/XWebViewScriptService.cs +++ b/src/UI/Services/XWebViewScriptService.cs @@ -15,9 +15,8 @@ public class XWebViewScriptService(ILogger logger, IWebVi public async Task ShowPostsAsync() { - Guard.Against.Null(_userName, nameof(_userName)); + Guard.Against.Null(_userName); - //var searchQuery = $"from:{_userName} since:2000-01-01"; var searchQuery = $"from:{_userName}"; var encodedQuery = WebUtility.UrlEncode(searchQuery); var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query"); @@ -27,17 +26,15 @@ public async Task ShowPostsAsync() _webViewHostService.Source = url; if (!await WaitForFullDocumentReadyAsync()) { - _logger.LogWarning("Navigation to {url} failed.", url); - return; + _logger.LogWarning("Navigation to {Url} failed.", url); } } } public async Task DeletePostsAsync() { - Guard.Against.Null(_userName, nameof(_userName)); + Guard.Against.Null(_userName); - //var searchQuery = $"from:{_userName} since:2000-01-01"; var searchQuery = $"from:{_userName}"; var encodedQuery = WebUtility.UrlEncode(searchQuery); var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query"); @@ -85,7 +82,7 @@ public async Task DeletePostsAsync() public async Task ShowLikesAsync() { - Guard.Against.Null(_userName, nameof(_userName)); + Guard.Against.Null(_userName); var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/likes"); if (_webViewHostService.Source != url) @@ -93,8 +90,7 @@ public async Task ShowLikesAsync() _webViewHostService.Source = url; if (!await WaitForFullDocumentReadyAsync()) { - _logger.LogWarning("Navigation to {url} failed.", url); - return; + _logger.LogWarning("Navigation to {Url} failed.", url); } } } @@ -106,7 +102,7 @@ public Task DeleteStarredAsync() public async Task ShowFollowingAsync() { - Guard.Against.Null(_userName, nameof(_userName)); + Guard.Against.Null(_userName); var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/following"); if (_webViewHostService.Source != url) @@ -114,8 +110,7 @@ public async Task ShowFollowingAsync() _webViewHostService.Source = url; if (!await WaitForFullDocumentReadyAsync()) { - _logger.LogWarning("Navigation to {url} failed.", url); - return; + _logger.LogWarning("Navigation to {Url} failed.", url); } } } diff --git a/src/UI/ViewModels/XViewModel.cs b/src/UI/ViewModels/XViewModel.cs index 3a19a0e..d10a3dc 100644 --- a/src/UI/ViewModels/XViewModel.cs +++ b/src/UI/ViewModels/XViewModel.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Web.WebView2.Wpf; namespace CleanMyPosts.UI.ViewModels; @@ -14,10 +15,9 @@ public partial class XViewModel : ObservableObject private readonly IWebViewHostService _webViewHostService; private readonly ILogger _logger; private readonly IXWebViewScriptService _xWebViewScriptService; - //private readonly IWindowManagerService _windowManagerService; private OverlayWindow _overlayWindow; - private const string XBaseUrl = "https://x.com"; + private readonly string xBaseUrl; [ObservableProperty] private bool _areButtonsEnabled; @@ -26,17 +26,16 @@ public partial class XViewModel : ObservableObject private string _userName; public XViewModel(ILogger logger, - //IWindowManagerService windowManagerService, IWebViewHostService webViewHostService, + IOptions options, IXWebViewScriptService xWebViewScriptService) { _webViewHostService = webViewHostService ?? throw new ArgumentNullException(nameof(webViewHostService)); _xWebViewScriptService = xWebViewScriptService ?? throw new ArgumentNullException(nameof(xWebViewScriptService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - //_windowManagerService = windowManagerService ?? throw new ArgumentNullException(nameof(windowManagerService)); - _xWebViewScriptService = xWebViewScriptService ?? throw new ArgumentNullException(nameof(xWebViewScriptService)); _webViewHostService.NavigationCompleted += OnNavigationCompleted; _webViewHostService.WebMessageReceived += OnWebMessageReceived; + xBaseUrl = options.Value.XBaseUrl; } public async Task InitializeAsync(WebView2 webView) @@ -48,7 +47,7 @@ public async Task InitializeAsync(WebView2 webView) await _webViewHostService.InitializeAsync(webView); - _webViewHostService.Source = new Uri(XBaseUrl); + _webViewHostService.Source = new Uri(xBaseUrl); var jsScript = @" window.onerror = function(message, source, lineno, colno, error) { @@ -163,8 +162,7 @@ private async Task ShowPosts() private async Task DeletePosts() { EnableUserInteractions(false); - await Task.Delay(10000); - //await _xWebViewScriptService.DeleteAllPostsAsync(_webView); + await _xWebViewScriptService.DeletePostsAsync(); EnableUserInteractions(true); } @@ -180,8 +178,7 @@ private async Task ShowLikes() private async Task DeleteLikes() { EnableUserInteractions(false); - await Task.Delay(10000); - //await _xWebViewScriptService.DeleteAllPostsAsync(_webView); + await Task.Delay(10002); EnableUserInteractions(true); } @@ -197,8 +194,7 @@ private async Task ShowFollowing() private async Task DeleteFollowing() { EnableUserInteractions(false); - await Task.Delay(10000); - //await _xWebViewScriptService.DeleteAllPostsAsync(_webView); + await Task.Delay(10001); EnableUserInteractions(true); } diff --git a/src/UI/Views/OverlayWindow.xaml.cs b/src/UI/Views/OverlayWindow.xaml.cs index 4555872..c7879d6 100644 --- a/src/UI/Views/OverlayWindow.xaml.cs +++ b/src/UI/Views/OverlayWindow.xaml.cs @@ -22,7 +22,7 @@ private void Window_MouseDown(object sender, MouseButtonEventArgs e) try { // Try dragging the main window instead - ((Window)mainWindow).DragMove(); + mainWindow.DragMove(); } catch (InvalidOperationException) { diff --git a/src/UI/Views/ShellWindow.xaml b/src/UI/Views/ShellWindow.xaml index 17809a0..e5f2cc1 100644 --- a/src/UI/Views/ShellWindow.xaml +++ b/src/UI/Views/ShellWindow.xaml @@ -4,9 +4,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:fa="http://metro.mahapps.com/winfx/xaml/iconpacks/fontawesome" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:material="http://metro.mahapps.com/winfx/xaml/iconpacks/material" - xmlns:fa="http://metro.mahapps.com/winfx/xaml/iconpacks/fontawesome" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:properties="clr-namespace:CleanMyPosts.UI.Properties" xmlns:templateSelectors="clr-namespace:CleanMyPosts.UI.TemplateSelectors" diff --git a/src/UI/appsettings.json b/src/UI/appsettings.json index 4b3fc62..ea35664 100644 --- a/src/UI/appsettings.json +++ b/src/UI/appsettings.json @@ -1,11 +1,13 @@ { "AppConfig": { "configurationsFolder": "CleanMyPosts\\Configurations", - "appPropertiesFileName": "AppProperties.json" + "appPropertiesFileName": "AppProperties.json", + "XBaseUrl": "https://x.com" }, "Updater": { - "AppCastUrl": "https://raw.githubusercontent.com/youruser/yourrepo/main/AutoUpdater.xml", - "SecurityMode": "Strict" + "AppCastUrl": "https://raw.githubusercontent.com/thorstenalpers/CleanMyPosts/refs/heads/update-feed/update.xml", + "SecurityMode": "Strict", + "IconUri": "pack://application:,,,/CleanMyPosts;component/Assets/logo.ico" }, "Serilog": { "MinimumLevel": { diff --git a/src/Tests/SettingsViewModelTests.cs b/src/UnitTests/SettingsViewModelTests.cs similarity index 97% rename from src/Tests/SettingsViewModelTests.cs rename to src/UnitTests/SettingsViewModelTests.cs index b9c096f..5d7ffa5 100644 --- a/src/Tests/SettingsViewModelTests.cs +++ b/src/UnitTests/SettingsViewModelTests.cs @@ -5,7 +5,7 @@ using Moq; using NUnit.Framework; -namespace CleanMyPosts.Tests; +namespace CleanMyPosts.UnitTests; [Category("Unit")] public class SettingsViewModelTests @@ -40,7 +40,7 @@ public void TestSettingsViewModel_SetCurrentVersion() Mock mockUpdateService = new(); Mock> mockLogger = new(); - Version testVersion = new(1, 2, 3, 4); + Version testVersion = new(1, 2, 3); mockApplicationInfoService.Setup(mock => mock.GetVersion()).Returns(testVersion); var settingsVm = new SettingsViewModel(mockThemeSelectorService.Object, diff --git a/src/Tests/Tests.csproj b/src/UnitTests/UnitTests.csproj similarity index 54% rename from src/Tests/Tests.csproj rename to src/UnitTests/UnitTests.csproj index d62e402..7815e87 100644 --- a/src/Tests/Tests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -5,24 +5,20 @@ false uap10.0.18362 x64;x86;AnyCPU - CleanMyPosts.Tests - CleanMyPosts.UI + CleanMyPosts.UnitTests + CleanMyPosts.UnitTests enable - - x86 - - - - x64 - - - - AnyCPU - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +