Files
james.maesandClaude Opus 5.5 839fb72bec chore: adopt the KofTwentyTwo standards (#2)
## 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>
2026-10-09 14:43:17 -05:00

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(" "));
}
}