Files
MarkdownViewr.Windows/tests/MarkdownViewr.Windows.Core.Tests/EditorConfigurationTests.cs
james.maesandCodex GPT-6 38dd5c1488 feat: implement Windows MarkdownViewr (#35)
This replaces the blocked history in PR #3 with one properly attributed,
signed/DCO commit. The owner personally reviewed and approved PR #3 in
the release session.

**No file changes from the approved candidate:** both
baee14f492 and
e605df62c4 have exact Git tree
a85912cbad. Original PR #3 and its commits
are preserved for review history. Historical commits were locally signed
but GitHub reported no_user; protected merge checks those introduced
commits even for squash merging. This commit resolves attribution
without bypassing or weakening signature rules.

The reader, Settings, themes, source highlighting/split, live reload,
file associations, Print/PDF, About credits, toolbar/footer/editor
fixes, user-first README/screenshots and developer guides are exactly
the approved implementation. Stable/dev separation and protected Azure
signing caller are unchanged.

Evidence:
- Approved tree baee14f / CI38285775463 passes all checks. Downloaded
focused29/29, published111/111, stable110/110 and dev110/110 TRXs have
zero failures/skips. Core coverage100% lines/98.86% branches; actual
update/restart/coexistence/default-preservation receipts verify.
- New single-commit hooks, online workflow security, signing and DCO
pass. GitHub must rerun all required checks on this commit; results are
not copied across SHAs.
- Live repository setup validates18 Actions-bound checks, with only
fuzzing added and all prior protections preserved.
- OpenSSF project15417 is now owner-reviewed and saved at70% under
owner-confirmed CDLA-Permissive2.0. This is an in-progress assessment,
not a passing badge.
- Production CI OIDC and signed1.0/dev publication remain required;
local representative signing is not CI proof.

Substantial AI assistance: Codex (GPT-6). Supersedes PR #3. Release work
remains tracked in #33.

Closes #4, #6, #7, #8, #9, #10, #11, #12, #13, #14, #15, #16, #17, #18,
#19, #20, #21, #22, #23, #24, #25, #26, #27, #28, #29, #32, #34.
Refs #1, #5, #30, #31, #33.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex GPT-6 <noreply@openai.com>
2026-10-12 14:01:42 -05:00

122 lines
5.4 KiB
C#

/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using MarkdownViewr.Windows.Core.Editors;
namespace MarkdownViewr.Windows.Core.Tests;
/// <summary>Proves bounded named editor settings, stable migration/order and one literal parent-folder argument.</summary>
public sealed class EditorConfigurationTests
{
/// <summary>Corrupt labels, paths and identities cannot become a native launch choice.</summary>
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("bad\nname")]
public void Sanitize_InvalidNames_RejectsChoice(string name)
{
var editor = new EditorConfiguration(Guid.NewGuid(), name, @"C:\Editor\editor.exe");
Assert.False(EditorConfiguration.IsValid(editor));
Assert.Empty(EditorConfiguration.Sanitize([editor]));
}
/// <summary>Valid order and folder modes survive sanitation; duplicate/empty identities and invalid choices are discarded.</summary>
[Fact]
public void Sanitize_StableIdentityAndOrder_PreservesValidChoices()
{
var first = new EditorConfiguration(Guid.NewGuid(), " File editor ", @"C:\Editor\editor.exe");
var second = new EditorConfiguration(Guid.NewGuid(), "Folder editor", @"C:\Editor\other.exe", true);
EditorConfiguration[] choices = EditorConfiguration.Sanitize([null, first, first, second,
first with { Id = Guid.Empty }, first with { Id = Guid.NewGuid(), Name = new string('x', 81) },
first with { Id = Guid.NewGuid(), Executable = "editor.cmd" }, first with { Id = Guid.NewGuid(), Name = null! }]);
Assert.Equal(new[] { first.Id, second.Id }, choices.Select(editor => editor.Id));
Assert.Equal("File editor", choices[0].Name);
Assert.True(choices[1].OpensFolder);
Assert.Empty(EditorConfiguration.Sanitize(null));
Assert.False(EditorConfiguration.IsValid(null));
}
/// <summary>Large saved lists are bounded before being exposed as native commands.</summary>
[Fact]
public void Sanitize_TooManyEditors_BoundsList()
{
EditorConfiguration[] choices = EditorConfiguration.Sanitize(Enumerable.Range(0, 40).Select(index => new EditorConfiguration(Guid.NewGuid(), "Editor " + index, @"C:\Editor\editor.exe")));
Assert.Equal(EditorConfiguration.MaximumEditors, choices.Length);
Assert.Equal("Editor 31", choices[^1].Name);
}
/// <summary>Existing single-path settings migrate once; repeated sanitation preserves IDs and deletion does not resurrect a choice.</summary>
[Fact]
public void Sanitize_LegacyPath_MigratesStableChoiceAndAllowsRemoval()
{
var settings = new AppSettings { ExternalEditor = @"C:\Editor\editor.exe", ConfiguredEditors = null! };
settings.Sanitize();
EditorConfiguration choice = Assert.Single(settings.ConfiguredEditors);
Assert.NotEqual(Guid.Empty, choice.Id);
Assert.Equal("Editor", choice.Name);
Assert.False(choice.OpensFolder);
Assert.Equal(settings.ExternalEditor, choice.Executable);
settings.Sanitize();
Assert.Equal(choice.Id, Assert.Single(settings.ConfiguredEditors).Id);
settings.ConfiguredEditors = [];
settings.ExternalEditor = "";
settings.Sanitize();
Assert.Empty(settings.ConfiguredEditors);
Assert.Empty(settings.ExternalEditor);
}
/// <summary>Atomic AppKit persistence retains named IDs, order and folder mode using the trimmed serializer.</summary>
[Fact]
public void SaveAndLoad_EditorList_PreservesIdentityOrderAndMode()
{
using var temporary = new TempDirectory();
var store = new KofTwentyTwo.AppKit.Settings.SettingsStore<AppSettings>(temporary.File("settings.json"), AppSettingsContext.Default.AppSettings);
var first = new EditorConfiguration(Guid.NewGuid(), "Folder editor", @"C:\Editor\folder.exe", true);
var second = new EditorConfiguration(Guid.NewGuid(), "File editor", @"C:\Editor\file.exe");
Assert.True(store.Save(new AppSettings { ConfiguredEditors = [first, second], ExternalEditor = @"C:\Other\old.exe" }));
AppSettings loaded = store.Load();
Assert.Equal(new[] { first, second }, loaded.ConfiguredEditors);
Assert.Equal(first.Executable, loaded.ExternalEditor);
loaded.ConfiguredEditors = [];
loaded.ExternalEditor = "";
Assert.True(store.Save(loaded));
Assert.Empty(store.Load().ConfiguredEditors);
}
/// <summary>Folder mode supplies one literal parent directory, preserving punctuation and Unicode without shell execution.</summary>
[Theory]
[InlineData(@"C:\日本語 & $HOME\notes.md", @"C:\日本語 & $HOME")]
[InlineData(@"C:\notes.md", @"C:\")]
[InlineData(@"\\server\share\notes.md", @"\\server\share")]
public void Create_FolderMode_PassesOneLiteralParent(string document, string parent)
{
System.Diagnostics.ProcessStartInfo start = EditorLaunch.Create(@"C:\Editor\editor.exe", document, opensFolder: true);
Assert.Equal(parent, Assert.Single(start.ArgumentList));
Assert.Equal(parent, start.WorkingDirectory);
Assert.False(start.UseShellExecute);
Assert.Empty(start.Arguments);
}
/// <summary>A root directory cannot masquerade as a file when deriving its parent.</summary>
[Theory]
[InlineData(@"C:\")]
[InlineData(@"\\server\share\")]
public void Create_DirectoryRoot_RejectsDocument(string document)
=> Assert.Throws<ArgumentException>(() => EditorLaunch.Create(@"C:\Editor\editor.exe", document, opensFolder: true));
}