feat(toolbox): preconfig store (load fail-open, clear-pin, configured marker)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
namespace SilverOS.Welcome.Core.Preconfig;
|
||||||
|
|
||||||
|
public interface IPreconfigStore
|
||||||
|
{
|
||||||
|
Preconfig? Load(); // null if missing/corrupt (fail-open)
|
||||||
|
void ClearPin(); // rewrite preconfig without the BitLocker pin
|
||||||
|
bool IsConfigured(); // configured marker present?
|
||||||
|
void MarkConfigured(); // write the configured marker
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SilverOS.Welcome.Core.Preconfig;
|
||||||
|
|
||||||
|
public sealed record BitlockerConfig { public bool Enable { get; init; } public string? Pin { get; init; } }
|
||||||
|
public sealed record AppsConfig { public bool UseFlavourDefaults { get; init; } = true; public IReadOnlyList<string>? Selected { get; init; } }
|
||||||
|
|
||||||
|
public sealed record Preconfig
|
||||||
|
{
|
||||||
|
public int SchemaVersion { get; init; } = 1;
|
||||||
|
public string Flavour { get; init; } = "";
|
||||||
|
public BitlockerConfig Bitlocker { get; init; } = new();
|
||||||
|
public AppsConfig Apps { get; init; } = new();
|
||||||
|
|
||||||
|
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SilverOS.Welcome.Core.Preconfig;
|
||||||
|
|
||||||
|
public sealed class PreconfigStore(string dir) : IPreconfigStore
|
||||||
|
{
|
||||||
|
private string File_ => Path.Combine(dir, "preconfig.json");
|
||||||
|
private string Marker => Path.Combine(dir, "configured");
|
||||||
|
|
||||||
|
public Preconfig? Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(File_)) return null;
|
||||||
|
return JsonSerializer.Deserialize<Preconfig>(File.ReadAllText(File_), Preconfig.JsonOptions);
|
||||||
|
}
|
||||||
|
catch (JsonException) { return null; } // fail-open
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearPin()
|
||||||
|
{
|
||||||
|
var p = Load();
|
||||||
|
if (p is null) return;
|
||||||
|
var stripped = p with { Bitlocker = p.Bitlocker with { Pin = null } };
|
||||||
|
File.WriteAllText(File_, JsonSerializer.Serialize(stripped, Preconfig.JsonOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConfigured() => File.Exists(Marker);
|
||||||
|
public void MarkConfigured() { Directory.CreateDirectory(dir); File.WriteAllText(Marker, "1"); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using SilverOS.Welcome.Core.Preconfig;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class PreconfigTests
|
||||||
|
{
|
||||||
|
static string TempDir()
|
||||||
|
{
|
||||||
|
var d = Path.Combine(Path.GetTempPath(), "smpre-" + System.Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(d);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string Sample = """
|
||||||
|
{ "schemaVersion":1, "flavour":"developer",
|
||||||
|
"bitlocker":{"enable":true,"pin":"246810"},
|
||||||
|
"apps":{"useFlavourDefaults":true} }
|
||||||
|
""";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Loads_flavour_and_pin()
|
||||||
|
{
|
||||||
|
var dir = TempDir();
|
||||||
|
File.WriteAllText(Path.Combine(dir, "preconfig.json"), Sample);
|
||||||
|
var p = new PreconfigStore(dir).Load();
|
||||||
|
Assert.NotNull(p);
|
||||||
|
Assert.Equal("developer", p!.Flavour);
|
||||||
|
Assert.True(p.Bitlocker.Enable);
|
||||||
|
Assert.Equal("246810", p.Bitlocker.Pin);
|
||||||
|
Assert.True(p.Apps.UseFlavourDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Missing_or_bad_file_returns_null_not_throw()
|
||||||
|
{
|
||||||
|
Assert.Null(new PreconfigStore(TempDir()).Load()); // missing
|
||||||
|
var dir = TempDir();
|
||||||
|
File.WriteAllText(Path.Combine(dir, "preconfig.json"), "{ not json");
|
||||||
|
Assert.Null(new PreconfigStore(dir).Load()); // corrupt
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearPin_rewrites_without_the_pin()
|
||||||
|
{
|
||||||
|
var dir = TempDir();
|
||||||
|
File.WriteAllText(Path.Combine(dir, "preconfig.json"), Sample);
|
||||||
|
var store = new PreconfigStore(dir);
|
||||||
|
store.ClearPin();
|
||||||
|
var reread = store.Load();
|
||||||
|
Assert.True(string.IsNullOrEmpty(reread!.Bitlocker.Pin));
|
||||||
|
Assert.Equal("developer", reread.Flavour); // rest preserved
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Configured_marker_roundtrips()
|
||||||
|
{
|
||||||
|
var dir = TempDir();
|
||||||
|
var store = new PreconfigStore(dir);
|
||||||
|
Assert.False(store.IsConfigured());
|
||||||
|
store.MarkConfigured();
|
||||||
|
Assert.True(store.IsConfigured());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user