## What Brings AppKit into conformance with [KofTwentyTwo/standards](https://github.com/KofTwentyTwo/standards). Reviewable commit by commit: | Commit | Change | | --- | --- | | build | Quality gate from `configs/dotnet` (analyzers incl. Meziantou, warnings as errors), `packages.lock.json` per project, locked-mode restore | | style | Kingsrook layout and LF line endings, produced mechanically by `dotnet format` (formatting only) | | docs | SPDX license header on every C# file | | refactor | Real code changes: analyzer findings fixed (one type per file, regex timeouts, async void, …) and header comments on every type and member | | ci | Old workflows replaced by callers of the shared standards workflows (PR gates, security, CodeQL, .NET CI, NuGet release with SBOM + attestation + trusted publishing), Scorecard | | docs | README, CONTRIBUTING (GitHub Flow, DCO, signing), SECURITY, CODEOWNERS, templates, Dependabot, threat model, ADRs, RELEASING | Also includes `dev`'s CI fix, so it supersedes #1; the `dev` branch goes away under GitHub Flow. ## Why AppKit is the first Product repository to adopt the standards. Static conformance before: 16 FAIL; after: 0 FAIL, 0 WARN (the remaining UNKNOWN rows are GitHub settings and rulesets, applied separately). ## How tested Locally with `CI=true` (lock files enforced): locked restore x64 and ARM64; zero-warning x64 build; 115 unit tests with 100% line coverage of `KofTwentyTwo.AppKit` and `KofTwentyTwo.AppKit.Updates`; 12 FlaUI UI tests (WinUI + WPF samples); `dotnet format --verify-no-changes --severity warn` clean; pack produces 4 packages + symbols; actionlint, zizmor (pedantic), gitleaks clean. The UI tests caught a regression introduced while fixing MA0147 (the WPF splash stopped dismissing because no UI context existed yet); fixed by starting the countdown through the dispatcher in both packages. ## Notes - `ci.yml` and `release.yml` are pinned to KofTwentyTwo/standards#3's head commit; they are repinned after that PR merges. - This pull request's new check names become required checks once the repository's rulesets are updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: James Maes <james@kof22.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
3.8 KiB
C#
127 lines
3.8 KiB
C#
/*
|
|
* Copyright (c) 2026 James Maes (KofTwentyTwo)
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
using System.ComponentModel;
|
|
using KofTwentyTwo.AppKit.Secrets;
|
|
|
|
|
|
namespace KofTwentyTwo.AppKit.Tests;
|
|
|
|
/// <summary>Tests for the in-memory secret vault.</summary>
|
|
public class InMemorySecretVaultTests
|
|
{
|
|
/// <summary>Store, overwrite, retrieve, and delete behave like a dictionary, and deleting twice is harmless.</summary>
|
|
[Fact]
|
|
public void StoreRetrieveDelete()
|
|
{
|
|
var vault = new InMemorySecretVault();
|
|
Assert.Null(vault.TryRetrieve("k"));
|
|
vault.Store("k", "v1");
|
|
vault.Store("k", "v2");
|
|
Assert.Equal("v2", vault.TryRetrieve("k"));
|
|
vault.Delete("k");
|
|
vault.Delete("k");
|
|
Assert.Null(vault.TryRetrieve("k"));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Empty keys and null secrets are rejected.</summary>
|
|
[Fact]
|
|
public void ValidatesArguments()
|
|
{
|
|
var vault = new InMemorySecretVault();
|
|
Assert.Throws<ArgumentException>(() => vault.Store("", "v"));
|
|
Assert.Throws<ArgumentNullException>(() => vault.Store("k", null!));
|
|
Assert.Throws<ArgumentNullException>(() => vault.TryRetrieve(null!));
|
|
Assert.Throws<ArgumentException>(() => vault.Delete(""));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Runs against the real per-user Credential Manager under a unique prefix.</summary>
|
|
public sealed class CredentialManagerVaultTests : IDisposable
|
|
{
|
|
private readonly CredentialManagerVault _vault = new("appkit-test-" + Guid.NewGuid().ToString("N"));
|
|
|
|
|
|
|
|
/// <summary>Removes the credentials the test may have created.</summary>
|
|
public void Dispose()
|
|
{
|
|
_vault.Delete("token");
|
|
_vault.Delete("empty");
|
|
}
|
|
|
|
|
|
|
|
/// <summary>A secret round-trips through the Credential Manager, overwrites in place, and deletes cleanly.</summary>
|
|
[Fact]
|
|
public void StoreRetrieveOverwriteDelete()
|
|
{
|
|
Assert.Null(_vault.TryRetrieve("token"));
|
|
_vault.Store("token", "ghp_first");
|
|
_vault.Store("token", "ghp_second ✓");
|
|
Assert.Equal("ghp_second ✓", _vault.TryRetrieve("token"));
|
|
_vault.Delete("token");
|
|
_vault.Delete("token");
|
|
Assert.Null(_vault.TryRetrieve("token"));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>EmptySecret: round trips.</summary>
|
|
[Fact]
|
|
public void EmptySecret_RoundTrips()
|
|
{
|
|
_vault.Store("empty", "");
|
|
Assert.Equal("", _vault.TryRetrieve("empty"));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>TargetName: is prefix colon key.</summary>
|
|
[Fact]
|
|
public void TargetName_IsPrefixColonKey()
|
|
{
|
|
var vault = new CredentialManagerVault("gclo");
|
|
Assert.Equal("gclo:account:abc", vault.TargetName("account:abc"));
|
|
Assert.Throws<ArgumentException>(() => vault.TargetName(""));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>OversizedSecret: fails loudly.</summary>
|
|
[Fact]
|
|
public void OversizedSecret_FailsLoudly()
|
|
{
|
|
// The Credential Manager caps blobs at 2560 bytes; silently losing a secret is worse than throwing.
|
|
Win32Exception ex = Assert.Throws<Win32Exception>(() => _vault.Store("token", new string('x', 5000)));
|
|
Assert.Contains("CredWriteW", ex.Message, StringComparison.Ordinal);
|
|
Assert.Throws<ArgumentNullException>(() => _vault.Store("token", null!));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>InvalidTargets: surface native errors.</summary>
|
|
[Fact]
|
|
public void InvalidTargets_SurfaceNativeErrors()
|
|
{
|
|
Assert.Throws<Win32Exception>(() => CredentialManagerVault.TryRetrieveTarget("", "k"));
|
|
Assert.Throws<Win32Exception>(() => CredentialManagerVault.DeleteTarget("", "k"));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Constructor: validates platform and prefix.</summary>
|
|
[Fact]
|
|
public void Constructor_ValidatesPlatformAndPrefix()
|
|
{
|
|
Assert.Throws<PlatformNotSupportedException>(() => new CredentialManagerVault("x", isWindows: false));
|
|
Assert.Throws<ArgumentException>(() => new CredentialManagerVault(" "));
|
|
}
|
|
}
|