Files
Sharepoint-Toolbox/.planning/milestones/v1.0-phases/01-foundation/01-03-PLAN.md
Dev 724fdc550d chore: complete v1.0 milestone
Archive 5 phases (36 plans) to milestones/v1.0-phases/.
Archive roadmap, requirements, and audit to milestones/.
Evolve PROJECT.md with shipped state and validated requirements.
Collapse ROADMAP.md to one-line milestone summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:19:03 +02:00

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-foundation 03 execute 3
01-01
01-02
SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs
SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs
SharepointToolbox/Services/ProfileService.cs
SharepointToolbox/Services/SettingsService.cs
SharepointToolbox.Tests/Services/ProfileServiceTests.cs
SharepointToolbox.Tests/Services/SettingsServiceTests.cs
true
FOUND-02
FOUND-10
FOUND-12
truths artifacts key_links
ProfileService reads Sharepoint_Export_profiles.json without migration — field names are the contract
SettingsService reads Sharepoint_Settings.json preserving dataFolder and lang fields
Write operations use write-then-replace (file.tmp → validate → File.Move) with SemaphoreSlim(1)
ProfileService unit tests: SaveAndLoad round-trips, corrupt file recovery, concurrent write safety
SettingsService unit tests: SaveAndLoad round-trips, default settings when file missing
path provides contains
SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs File I/O for profiles JSON with write-then-replace SemaphoreSlim
path provides exports
SharepointToolbox/Services/ProfileService.cs CRUD operations on TenantProfile collection
AddProfile
RenameProfile
DeleteProfile
GetProfiles
path provides exports
SharepointToolbox/Services/SettingsService.cs Read/write for app settings including data folder and language
GetSettings
SaveSettings
path provides
SharepointToolbox.Tests/Services/ProfileServiceTests.cs Unit tests covering FOUND-02 and FOUND-10
from to via pattern
SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs Sharepoint_Export_profiles.json System.Text.Json deserialization of { profiles: [...] } wrapper profiles
from to via pattern
SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs Sharepoint_Settings.json System.Text.Json deserialization of { dataFolder, lang } dataFolder
Build the persistence layer: ProfileRepository and SettingsRepository (Infrastructure) plus ProfileService and SettingsService (Services layer). Implement write-then-replace safety. Write unit tests that validate the round-trip and edge cases.

Purpose: Profiles and settings are the first user-visible data. Corrupt files or wrong field names would break existing users' data on migration. Unit tests lock in the JSON schema contract. Output: 4 production files + 2 test files with passing unit tests.

<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md @.planning/phases/01-foundation/01-02-SUMMARY.md ```csharp namespace SharepointToolbox.Core.Models; public class TenantProfile { public string Name { get; set; } = string.Empty; public string TenantUrl { get; set; } = string.Empty; public string ClientId { get; set; } = string.Empty; } ```

// Sharepoint_Export_profiles.json { "profiles": [{ "name": "...", "tenantUrl": "...", "clientId": "..." }] }

// Sharepoint_Settings.json { "dataFolder": "...", "lang": "en" }

Task 1: ProfileRepository and ProfileService with write-then-replace SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs, SharepointToolbox/Services/ProfileService.cs, SharepointToolbox.Tests/Services/ProfileServiceTests.cs - Test: SaveAsync then LoadAsync round-trips a list of TenantProfiles with correct field values - Test: LoadAsync on missing file returns empty list (no exception) - Test: LoadAsync on corrupt JSON throws InvalidDataException (not silently returns empty) - Test: Concurrent SaveAsync calls don't corrupt the file (SemaphoreSlim ensures ordering) - Test: ProfileService.AddProfile assigns the new profile and persists immediately - Test: ProfileService.RenameProfile changes Name, persists, throws if profile not found - Test: ProfileService.DeleteProfile removes by Name, throws if not found - Test: Saved JSON wraps profiles in { "profiles": [...] } root object (schema compatibility) Create `Infrastructure/Persistence/` and `Services/` directories.
**ProfileRepository.cs** — handles raw file I/O:
```csharp
namespace SharepointToolbox.Infrastructure.Persistence;

public class ProfileRepository
{
    private readonly string _filePath;
    private readonly SemaphoreSlim _writeLock = new(1, 1);

    public ProfileRepository(string filePath)
    {
        _filePath = filePath;
    }

    public async Task<IReadOnlyList<TenantProfile>> LoadAsync()
    {
        if (!File.Exists(_filePath))
            return Array.Empty<TenantProfile>();

        var json = await File.ReadAllTextAsync(_filePath, Encoding.UTF8);
        var root = JsonSerializer.Deserialize<ProfilesRoot>(json,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
        return root?.Profiles ?? Array.Empty<TenantProfile>();
    }

    public async Task SaveAsync(IReadOnlyList<TenantProfile> profiles)
    {
        await _writeLock.WaitAsync();
        try
        {
            var root = new ProfilesRoot { Profiles = profiles.ToList() };
            var json = JsonSerializer.Serialize(root,
                new JsonSerializerOptions { WriteIndented = true,
                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
            var tmpPath = _filePath + ".tmp";
            Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
            await File.WriteAllTextAsync(tmpPath, json, Encoding.UTF8);
            // Validate round-trip before replacing
            JsonDocument.Parse(await File.ReadAllTextAsync(tmpPath)).Dispose();
            File.Move(tmpPath, _filePath, overwrite: true);
        }
        finally { _writeLock.Release(); }
    }

    private sealed class ProfilesRoot
    {
        public List<TenantProfile> Profiles { get; set; } = new();
    }
}
```
Note: Use `PropertyNamingPolicy = JsonNamingPolicy.CamelCase` to serialize `Name`→`name`, `TenantUrl`→`tenantUrl`, `ClientId`→`clientId` matching the existing JSON schema.

**ProfileService.cs** — CRUD on top of repository:
- Constructor takes `ProfileRepository` (inject via DI later; for now accept in constructor)
- `Task<IReadOnlyList<TenantProfile>> GetProfilesAsync()`
- `Task AddProfileAsync(TenantProfile profile)` — validates Name not empty, TenantUrl valid URL, ClientId not empty; throws `ArgumentException` for invalid inputs
- `Task RenameProfileAsync(string existingName, string newName)` — throws `KeyNotFoundException` if not found
- `Task DeleteProfileAsync(string name)` — throws `KeyNotFoundException` if not found
- All mutations load → modify in-memory list → save (single-load-modify-save to preserve order)

**ProfileServiceTests.cs** — Replace the stub with real tests using temp file paths:
```csharp
public class ProfileServiceTests : IDisposable
{
    private readonly string _tempFile = Path.GetTempFileName();
    // Dispose deletes temp file

    [Fact]
    public async Task SaveAndLoad_RoundTrips_Profiles() { ... }
    // etc.
}
```
Tests must use a temp file, not the real user data file. All tests in `[Trait("Category", "Unit")]`.
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~ProfileServiceTests" 2>&1 | tail -10 All ProfileServiceTests pass (no skips). JSON output uses camelCase field names matching existing schema. Write-then-replace with SemaphoreSlim implemented. Task 2: SettingsRepository and SettingsService SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs, SharepointToolbox/Services/SettingsService.cs, SharepointToolbox.Tests/Services/SettingsServiceTests.cs - Test: LoadAsync returns default settings (dataFolder = empty string, lang = "en") when file missing - Test: SaveAsync then LoadAsync round-trips dataFolder and lang values exactly - Test: Serialized JSON contains "dataFolder" and "lang" keys (not DataFolder/Lang — schema compatibility) - Test: SaveAsync uses write-then-replace (tmp file created, then moved) - Test: SettingsService.SetLanguageAsync("fr") persists lang="fr" - Test: SettingsService.SetDataFolderAsync("C:\\Exports") persists dataFolder path **AppSettings model** (add to `Core/Models/AppSettings.cs`): ```csharp namespace SharepointToolbox.Core.Models; public class AppSettings { public string DataFolder { get; set; } = string.Empty; public string Lang { get; set; } = "en"; } ``` Note: STJ with `PropertyNamingPolicy.CamelCase` will serialize `DataFolder`→`dataFolder`, `Lang`→`lang`.
**SettingsRepository.cs** — same write-then-replace pattern as ProfileRepository:
- `Task<AppSettings> LoadAsync()` — returns `new AppSettings()` if file missing; throws `InvalidDataException` on corrupt JSON
- `Task SaveAsync(AppSettings settings)` — write-then-replace with `SemaphoreSlim(1)` and camelCase serialization

**SettingsService.cs**:
- Constructor takes `SettingsRepository`
- `Task<AppSettings> GetSettingsAsync()`
- `Task SetLanguageAsync(string cultureCode)` — validates "en" or "fr"; throws `ArgumentException` otherwise
- `Task SetDataFolderAsync(string path)` — saves path (empty string allowed — means default)

**SettingsServiceTests.cs** — Replace stub with real tests using temp file.
All tests in `[Trait("Category", "Unit")]`.
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SettingsServiceTests" 2>&1 | tail -10 All SettingsServiceTests pass. AppSettings serializes to dataFolder/lang (camelCase). Default settings returned when file is absent. - `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "Category=Unit"` — all pass - JSON output from ProfileRepository contains `"profiles"` root key with `"name"`, `"tenantUrl"`, `"clientId"` field names - JSON output from SettingsRepository contains `"dataFolder"` and `"lang"` field names - Both repositories use `SemaphoreSlim(1)` write lock - Both repositories use write-then-replace (`.tmp` file then `File.Move`)

<success_criteria> Unit tests green for ProfileService and SettingsService. JSON schema compatibility verified by test assertions on serialized output. Write-then-replace pattern protects against crash-corruption. </success_criteria>

After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`