chore: complete v1.0 milestone
All checks were successful
Release zip package / release (push) Successful in 10s
All checks were successful
Release zip package / release (push) Successful in 10s
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>
This commit is contained in:
815
.planning/milestones/v1.0-phases/03-storage/03-01-PLAN.md
Normal file
815
.planning/milestones/v1.0-phases/03-storage/03-01-PLAN.md
Normal file
@@ -0,0 +1,815 @@
|
||||
---
|
||||
phase: 03
|
||||
plan: 01
|
||||
title: Wave 0 — Test Scaffolds, Stub Interfaces, and Core Models
|
||||
status: pending
|
||||
wave: 0
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- SharepointToolbox/Core/Models/StorageNode.cs
|
||||
- SharepointToolbox/Core/Models/StorageScanOptions.cs
|
||||
- SharepointToolbox/Core/Models/SearchResult.cs
|
||||
- SharepointToolbox/Core/Models/SearchOptions.cs
|
||||
- SharepointToolbox/Core/Models/DuplicateGroup.cs
|
||||
- SharepointToolbox/Core/Models/DuplicateItem.cs
|
||||
- SharepointToolbox/Core/Models/DuplicateScanOptions.cs
|
||||
- SharepointToolbox/Services/IStorageService.cs
|
||||
- SharepointToolbox/Services/ISearchService.cs
|
||||
- SharepointToolbox/Services/IDuplicatesService.cs
|
||||
- SharepointToolbox/Services/Export/StorageCsvExportService.cs
|
||||
- SharepointToolbox/Services/Export/StorageHtmlExportService.cs
|
||||
- SharepointToolbox/Services/Export/SearchCsvExportService.cs
|
||||
- SharepointToolbox/Services/Export/SearchHtmlExportService.cs
|
||||
- SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs
|
||||
- SharepointToolbox.Tests/Services/StorageServiceTests.cs
|
||||
- SharepointToolbox.Tests/Services/SearchServiceTests.cs
|
||||
- SharepointToolbox.Tests/Services/DuplicatesServiceTests.cs
|
||||
- SharepointToolbox.Tests/Services/Export/StorageCsvExportServiceTests.cs
|
||||
- SharepointToolbox.Tests/Services/Export/StorageHtmlExportServiceTests.cs
|
||||
- SharepointToolbox.Tests/Services/Export/SearchExportServiceTests.cs
|
||||
- SharepointToolbox.Tests/Services/Export/DuplicatesHtmlExportServiceTests.cs
|
||||
autonomous: true
|
||||
requirements:
|
||||
- STOR-01
|
||||
- STOR-02
|
||||
- STOR-03
|
||||
- STOR-04
|
||||
- STOR-05
|
||||
- SRCH-01
|
||||
- SRCH-02
|
||||
- SRCH-03
|
||||
- SRCH-04
|
||||
- DUPL-01
|
||||
- DUPL-02
|
||||
- DUPL-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "dotnet build produces 0 errors after all 7 models, 3 interfaces, and 5 stub export classes are created"
|
||||
- "All 7 test files exist and are discovered by dotnet test (test count > 0)"
|
||||
- "StorageServiceTests, SearchServiceTests, DuplicatesServiceTests compile but skip (stubs referencing types that exist after this plan)"
|
||||
- "The pure-logic tests in DuplicatesServiceTests (MakeKey composite key) are real [Fact] tests — not skipped — and pass"
|
||||
- "Export service tests compile but fail (types exist as stubs with no real implementation yet) — expected until Plans 03/05"
|
||||
artifacts:
|
||||
- path: "SharepointToolbox/Core/Models/StorageNode.cs"
|
||||
provides: "Tree node model for storage metrics display"
|
||||
- path: "SharepointToolbox/Core/Models/SearchResult.cs"
|
||||
provides: "Flat result record for file search output"
|
||||
- path: "SharepointToolbox/Core/Models/DuplicateGroup.cs"
|
||||
provides: "Group record for duplicate detection output"
|
||||
- path: "SharepointToolbox/Services/IStorageService.cs"
|
||||
provides: "Interface enabling ViewModel mocking for storage"
|
||||
- path: "SharepointToolbox/Services/ISearchService.cs"
|
||||
provides: "Interface enabling ViewModel mocking for search"
|
||||
- path: "SharepointToolbox/Services/IDuplicatesService.cs"
|
||||
provides: "Interface enabling ViewModel mocking for duplicates"
|
||||
key_links:
|
||||
- from: "StorageServiceTests.cs"
|
||||
to: "IStorageService"
|
||||
via: "mock interface"
|
||||
pattern: "IStorageService"
|
||||
- from: "SearchServiceTests.cs"
|
||||
to: "ISearchService"
|
||||
via: "mock interface"
|
||||
pattern: "ISearchService"
|
||||
- from: "DuplicatesServiceTests.cs"
|
||||
to: "MakeKey"
|
||||
via: "static pure function"
|
||||
pattern: "MakeKey"
|
||||
---
|
||||
|
||||
# Plan 03-01: Wave 0 — Test Scaffolds, Stub Interfaces, and Core Models
|
||||
|
||||
## Goal
|
||||
|
||||
Create all data models, service interfaces, export service stubs, and test scaffolds needed so every subsequent plan has a working `dotnet test --filter` verify command pointing at a real test class. Interfaces and models define the contracts; implementation plans (03-02 through 03-05) fill them in. One set of pure-logic tests (the `MakeKey` composite key function for duplicate detection) are real `[Fact]` tests that pass immediately since the logic is pure and has no CSOM dependencies.
|
||||
|
||||
## Context
|
||||
|
||||
Phase 2 created `PermissionEntry`, `ScanOptions`, `IPermissionsService`, and test scaffolds in exactly this pattern. Phase 3 follows the same Wave 0 approach: models + interfaces first, implementation in subsequent plans. The test project at `SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` already has xUnit 2.9.3 + Moq. The export service stubs must compile (the test files reference them) even though their `BuildCsv`/`BuildHtml` methods return empty strings until implemented.
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 1: Create all 7 core models and 3 service interfaces
|
||||
|
||||
**Files:**
|
||||
- `SharepointToolbox/Core/Models/StorageNode.cs`
|
||||
- `SharepointToolbox/Core/Models/StorageScanOptions.cs`
|
||||
- `SharepointToolbox/Core/Models/SearchResult.cs`
|
||||
- `SharepointToolbox/Core/Models/SearchOptions.cs`
|
||||
- `SharepointToolbox/Core/Models/DuplicateGroup.cs`
|
||||
- `SharepointToolbox/Core/Models/DuplicateItem.cs`
|
||||
- `SharepointToolbox/Core/Models/DuplicateScanOptions.cs`
|
||||
- `SharepointToolbox/Services/IStorageService.cs`
|
||||
- `SharepointToolbox/Services/ISearchService.cs`
|
||||
- `SharepointToolbox/Services/IDuplicatesService.cs`
|
||||
|
||||
**Action:** Create | Write
|
||||
|
||||
**Why:** All subsequent plans depend on these contracts. Tests must compile against them. Interfaces enable Moq-based unit tests.
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Core/Models/StorageNode.cs
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class StorageNode
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public string SiteTitle { get; set; } = string.Empty;
|
||||
public string Library { get; set; } = string.Empty;
|
||||
public long TotalSizeBytes { get; set; }
|
||||
public long FileStreamSizeBytes { get; set; }
|
||||
public long VersionSizeBytes => Math.Max(0L, TotalSizeBytes - FileStreamSizeBytes);
|
||||
public long TotalFileCount { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
public int IndentLevel { get; set; }
|
||||
public List<StorageNode> Children { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Core/Models/StorageScanOptions.cs
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public record StorageScanOptions(
|
||||
bool PerLibrary = true,
|
||||
bool IncludeSubsites = false,
|
||||
int FolderDepth = 0 // 0 = library root only; >0 = recurse N levels
|
||||
);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Core/Models/SearchResult.cs
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string FileExtension { get; set; } = string.Empty;
|
||||
public DateTime? Created { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string ModifiedBy { get; set; } = string.Empty;
|
||||
public long SizeBytes { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Core/Models/SearchOptions.cs
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public record SearchOptions(
|
||||
string[] Extensions,
|
||||
string? Regex,
|
||||
DateTime? CreatedAfter,
|
||||
DateTime? CreatedBefore,
|
||||
DateTime? ModifiedAfter,
|
||||
DateTime? ModifiedBefore,
|
||||
string? CreatedBy,
|
||||
string? ModifiedBy,
|
||||
string? Library,
|
||||
int MaxResults,
|
||||
string SiteUrl
|
||||
);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Core/Models/DuplicateItem.cs
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class DuplicateItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string Library { get; set; } = string.Empty;
|
||||
public long? SizeBytes { get; set; }
|
||||
public DateTime? Created { get; set; }
|
||||
public DateTime? Modified { get; set; }
|
||||
public int? FolderCount { get; set; }
|
||||
public int? FileCount { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Core/Models/DuplicateGroup.cs
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class DuplicateGroup
|
||||
{
|
||||
public string GroupKey { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<DuplicateItem> Items { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Core/Models/DuplicateScanOptions.cs
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public record DuplicateScanOptions(
|
||||
string Mode = "Files", // "Files" or "Folders"
|
||||
bool MatchSize = true,
|
||||
bool MatchCreated = false,
|
||||
bool MatchModified = false,
|
||||
bool MatchSubfolderCount = false,
|
||||
bool MatchFileCount = false,
|
||||
bool IncludeSubsites = false,
|
||||
string? Library = null
|
||||
);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/IStorageService.cs
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface IStorageService
|
||||
{
|
||||
Task<IReadOnlyList<StorageNode>> CollectStorageAsync(
|
||||
ClientContext ctx,
|
||||
StorageScanOptions options,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/ISearchService.cs
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface ISearchService
|
||||
{
|
||||
Task<IReadOnlyList<SearchResult>> SearchFilesAsync(
|
||||
ClientContext ctx,
|
||||
SearchOptions options,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/IDuplicatesService.cs
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface IDuplicatesService
|
||||
{
|
||||
Task<IReadOnlyList<DuplicateGroup>> ScanDuplicatesAsync(
|
||||
ClientContext ctx,
|
||||
DuplicateScanOptions options,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx
|
||||
```
|
||||
|
||||
Expected: 0 errors
|
||||
|
||||
### Task 2: Create 5 export service stubs and 7 test scaffold files
|
||||
|
||||
**Files:**
|
||||
- `SharepointToolbox/Services/Export/StorageCsvExportService.cs`
|
||||
- `SharepointToolbox/Services/Export/StorageHtmlExportService.cs`
|
||||
- `SharepointToolbox/Services/Export/SearchCsvExportService.cs`
|
||||
- `SharepointToolbox/Services/Export/SearchHtmlExportService.cs`
|
||||
- `SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs`
|
||||
- `SharepointToolbox.Tests/Services/StorageServiceTests.cs`
|
||||
- `SharepointToolbox.Tests/Services/SearchServiceTests.cs`
|
||||
- `SharepointToolbox.Tests/Services/DuplicatesServiceTests.cs`
|
||||
- `SharepointToolbox.Tests/Services/Export/StorageCsvExportServiceTests.cs`
|
||||
- `SharepointToolbox.Tests/Services/Export/StorageHtmlExportServiceTests.cs`
|
||||
- `SharepointToolbox.Tests/Services/Export/SearchExportServiceTests.cs`
|
||||
- `SharepointToolbox.Tests/Services/Export/DuplicatesHtmlExportServiceTests.cs`
|
||||
|
||||
**Action:** Create | Write
|
||||
|
||||
**Why:** Stubs enable test files to compile. The `MakeKey` helper and `VersionSizeBytes` derived property can be unit tested immediately without any CSOM. Export service tests will fail until plans 03-03 and 03-05 implement the real logic — that is the expected state.
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/Export/StorageCsvExportService.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services.Export;
|
||||
|
||||
public class StorageCsvExportService
|
||||
{
|
||||
public string BuildCsv(IReadOnlyList<StorageNode> nodes) => string.Empty; // implemented in Plan 03-03
|
||||
|
||||
public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, string filePath, CancellationToken ct)
|
||||
{
|
||||
var csv = BuildCsv(nodes);
|
||||
await System.IO.File.WriteAllTextAsync(filePath, csv, new System.Text.UTF8Encoding(true), ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/Export/StorageHtmlExportService.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services.Export;
|
||||
|
||||
public class StorageHtmlExportService
|
||||
{
|
||||
public string BuildHtml(IReadOnlyList<StorageNode> nodes) => string.Empty; // implemented in Plan 03-03
|
||||
|
||||
public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, string filePath, CancellationToken ct)
|
||||
{
|
||||
var html = BuildHtml(nodes);
|
||||
await System.IO.File.WriteAllTextAsync(filePath, html, System.Text.Encoding.UTF8, ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/Export/SearchCsvExportService.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services.Export;
|
||||
|
||||
public class SearchCsvExportService
|
||||
{
|
||||
public string BuildCsv(IReadOnlyList<SearchResult> results) => string.Empty; // implemented in Plan 03-05
|
||||
|
||||
public async Task WriteAsync(IReadOnlyList<SearchResult> results, string filePath, CancellationToken ct)
|
||||
{
|
||||
var csv = BuildCsv(results);
|
||||
await System.IO.File.WriteAllTextAsync(filePath, csv, new System.Text.UTF8Encoding(true), ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/Export/SearchHtmlExportService.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services.Export;
|
||||
|
||||
public class SearchHtmlExportService
|
||||
{
|
||||
public string BuildHtml(IReadOnlyList<SearchResult> results) => string.Empty; // implemented in Plan 03-05
|
||||
|
||||
public async Task WriteAsync(IReadOnlyList<SearchResult> results, string filePath, CancellationToken ct)
|
||||
{
|
||||
var html = BuildHtml(results);
|
||||
await System.IO.File.WriteAllTextAsync(filePath, html, System.Text.Encoding.UTF8, ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services.Export;
|
||||
|
||||
public class DuplicatesHtmlExportService
|
||||
{
|
||||
public string BuildHtml(IReadOnlyList<DuplicateGroup> groups) => string.Empty; // implemented in Plan 03-05
|
||||
|
||||
public async Task WriteAsync(IReadOnlyList<DuplicateGroup> groups, string filePath, CancellationToken ct)
|
||||
{
|
||||
var html = BuildHtml(groups);
|
||||
await System.IO.File.WriteAllTextAsync(filePath, html, System.Text.Encoding.UTF8, ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now the test scaffold files. The `DuplicatesServiceTests` includes a real pure-logic test for `MakeKey` — define the helper class inline in the same file so it compiles without depending on `DuplicatesService`:
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox.Tests/Services/StorageServiceTests.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
public class StorageServiceTests
|
||||
{
|
||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 03-02 implementation")]
|
||||
public Task CollectStorageAsync_ReturnsLibraryNodes_ForDocumentLibraries()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 03-02 implementation")]
|
||||
public Task CollectStorageAsync_WithFolderDepth1_ReturnsSubfolderNodes()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void StorageNode_VersionSizeBytes_IsNonNegative()
|
||||
{
|
||||
// VersionSizeBytes = TotalSizeBytes - FileStreamSizeBytes (never negative)
|
||||
var node = new StorageNode { TotalSizeBytes = 1000L, FileStreamSizeBytes = 1200L };
|
||||
Assert.Equal(0L, node.VersionSizeBytes); // Math.Max(0, -200) = 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StorageNode_VersionSizeBytes_IsCorrectWhenPositive()
|
||||
{
|
||||
var node = new StorageNode { TotalSizeBytes = 5000L, FileStreamSizeBytes = 3000L };
|
||||
Assert.Equal(2000L, node.VersionSizeBytes);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox.Tests/Services/SearchServiceTests.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
public class SearchServiceTests
|
||||
{
|
||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 03-04 implementation")]
|
||||
public Task SearchFilesAsync_WithExtensionFilter_BuildsCorrectKql()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 03-04 implementation")]
|
||||
public Task SearchFilesAsync_PaginationStopsAt50000()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 03-04 implementation")]
|
||||
public Task SearchFilesAsync_FiltersVersionHistoryPaths()
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox.Tests/Services/DuplicatesServiceTests.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Pure-logic tests for the MakeKey composite key function (no CSOM needed).
|
||||
/// Inline helper matches the implementation DuplicatesService will produce in Plan 03-04.
|
||||
/// </summary>
|
||||
public class DuplicatesServiceTests
|
||||
{
|
||||
// Inline copy of MakeKey to test logic before Plan 03-04 creates the real class
|
||||
private static string MakeKey(DuplicateItem item, DuplicateScanOptions opts)
|
||||
{
|
||||
var parts = new System.Collections.Generic.List<string> { item.Name.ToLowerInvariant() };
|
||||
if (opts.MatchSize && item.SizeBytes.HasValue) parts.Add(item.SizeBytes.Value.ToString());
|
||||
if (opts.MatchCreated && item.Created.HasValue) parts.Add(item.Created.Value.Date.ToString("yyyy-MM-dd"));
|
||||
if (opts.MatchModified && item.Modified.HasValue) parts.Add(item.Modified.Value.Date.ToString("yyyy-MM-dd"));
|
||||
if (opts.MatchSubfolderCount && item.FolderCount.HasValue) parts.Add(item.FolderCount.Value.ToString());
|
||||
if (opts.MatchFileCount && item.FileCount.HasValue) parts.Add(item.FileCount.Value.ToString());
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MakeKey_NameOnly_ReturnsLowercaseName()
|
||||
{
|
||||
var item = new DuplicateItem { Name = "Report.docx", SizeBytes = 1000 };
|
||||
var opts = new DuplicateScanOptions(MatchSize: false);
|
||||
Assert.Equal("report.docx", MakeKey(item, opts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MakeKey_WithSizeMatch_AppendsSizeToKey()
|
||||
{
|
||||
var item = new DuplicateItem { Name = "Report.docx", SizeBytes = 1024 };
|
||||
var opts = new DuplicateScanOptions(MatchSize: true);
|
||||
Assert.Equal("report.docx|1024", MakeKey(item, opts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MakeKey_WithCreatedAndModified_AppendsDateStrings()
|
||||
{
|
||||
var item = new DuplicateItem
|
||||
{
|
||||
Name = "file.pdf",
|
||||
SizeBytes = 500,
|
||||
Created = new DateTime(2024, 3, 15),
|
||||
Modified = new DateTime(2024, 6, 1)
|
||||
};
|
||||
var opts = new DuplicateScanOptions(MatchSize: false, MatchCreated: true, MatchModified: true);
|
||||
Assert.Equal("file.pdf|2024-03-15|2024-06-01", MakeKey(item, opts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MakeKey_SameKeyForSameItems_GroupsCorrectly()
|
||||
{
|
||||
var opts = new DuplicateScanOptions(MatchSize: true);
|
||||
var item1 = new DuplicateItem { Name = "Budget.xlsx", SizeBytes = 2048 };
|
||||
var item2 = new DuplicateItem { Name = "BUDGET.xlsx", SizeBytes = 2048 };
|
||||
Assert.Equal(MakeKey(item1, opts), MakeKey(item2, opts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MakeKey_DifferentSize_ProducesDifferentKeys()
|
||||
{
|
||||
var opts = new DuplicateScanOptions(MatchSize: true);
|
||||
var item1 = new DuplicateItem { Name = "file.docx", SizeBytes = 100 };
|
||||
var item2 = new DuplicateItem { Name = "file.docx", SizeBytes = 200 };
|
||||
Assert.NotEqual(MakeKey(item1, opts), MakeKey(item2, opts));
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 03-04 implementation")]
|
||||
public Task ScanDuplicatesAsync_Files_GroupsByCompositeKey()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 03-04 implementation")]
|
||||
public Task ScanDuplicatesAsync_Folders_UsesCamlFSObjType1()
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox.Tests/Services/Export/StorageCsvExportServiceTests.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services.Export;
|
||||
using Xunit;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services.Export;
|
||||
|
||||
public class StorageCsvExportServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildCsv_WithKnownNodes_ProducesHeaderRow()
|
||||
{
|
||||
var svc = new StorageCsvExportService();
|
||||
var nodes = new List<StorageNode>
|
||||
{
|
||||
new() { Name = "Shared Documents", Library = "Shared Documents", SiteTitle = "MySite",
|
||||
TotalSizeBytes = 1024, FileStreamSizeBytes = 800, TotalFileCount = 5,
|
||||
LastModified = new DateTime(2024, 1, 15) }
|
||||
};
|
||||
var csv = svc.BuildCsv(nodes);
|
||||
Assert.Contains("Library", csv);
|
||||
Assert.Contains("Site", csv);
|
||||
Assert.Contains("Files", csv);
|
||||
Assert.Contains("Total Size", csv);
|
||||
Assert.Contains("Version Size", csv);
|
||||
Assert.Contains("Last Modified", csv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_WithEmptyList_ReturnsHeaderOnly()
|
||||
{
|
||||
var svc = new StorageCsvExportService();
|
||||
var csv = svc.BuildCsv(new List<StorageNode>());
|
||||
Assert.NotEmpty(csv); // must have at least the header row
|
||||
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.Single(lines); // only header, no data rows
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_NodeValues_AppearInOutput()
|
||||
{
|
||||
var svc = new StorageCsvExportService();
|
||||
var nodes = new List<StorageNode>
|
||||
{
|
||||
new() { Name = "Reports", Library = "Reports", SiteTitle = "ProjectSite",
|
||||
TotalSizeBytes = 2048, FileStreamSizeBytes = 1024, TotalFileCount = 10 }
|
||||
};
|
||||
var csv = svc.BuildCsv(nodes);
|
||||
Assert.Contains("Reports", csv);
|
||||
Assert.Contains("ProjectSite", csv);
|
||||
Assert.Contains("10", csv);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox.Tests/Services/Export/StorageHtmlExportServiceTests.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services.Export;
|
||||
using Xunit;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services.Export;
|
||||
|
||||
public class StorageHtmlExportServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildHtml_WithNodes_ContainsToggleJs()
|
||||
{
|
||||
var svc = new StorageHtmlExportService();
|
||||
var nodes = new List<StorageNode>
|
||||
{
|
||||
new() { Name = "Shared Documents", Library = "Shared Documents", SiteTitle = "Site1",
|
||||
TotalSizeBytes = 5000, FileStreamSizeBytes = 4000, TotalFileCount = 20,
|
||||
Children = new List<StorageNode>
|
||||
{
|
||||
new() { Name = "Archive", Library = "Shared Documents", SiteTitle = "Site1",
|
||||
TotalSizeBytes = 1000, FileStreamSizeBytes = 800, TotalFileCount = 5 }
|
||||
} }
|
||||
};
|
||||
var html = svc.BuildHtml(nodes);
|
||||
Assert.Contains("toggle(", html);
|
||||
Assert.Contains("<!DOCTYPE html>", html);
|
||||
Assert.Contains("Shared Documents", html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithEmptyList_ReturnsValidHtml()
|
||||
{
|
||||
var svc = new StorageHtmlExportService();
|
||||
var html = svc.BuildHtml(new List<StorageNode>());
|
||||
Assert.Contains("<!DOCTYPE html>", html);
|
||||
Assert.Contains("<html", html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithMultipleLibraries_EachLibraryAppearsInOutput()
|
||||
{
|
||||
var svc = new StorageHtmlExportService();
|
||||
var nodes = new List<StorageNode>
|
||||
{
|
||||
new() { Name = "Documents", Library = "Documents", SiteTitle = "Site1", TotalSizeBytes = 1000 },
|
||||
new() { Name = "Images", Library = "Images", SiteTitle = "Site1", TotalSizeBytes = 2000 }
|
||||
};
|
||||
var html = svc.BuildHtml(nodes);
|
||||
Assert.Contains("Documents", html);
|
||||
Assert.Contains("Images", html);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox.Tests/Services/Export/SearchExportServiceTests.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services.Export;
|
||||
using Xunit;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services.Export;
|
||||
|
||||
public class SearchExportServiceTests
|
||||
{
|
||||
private static SearchResult MakeSample() => new()
|
||||
{
|
||||
Title = "Q1 Budget.xlsx",
|
||||
Path = "https://contoso.sharepoint.com/sites/Finance/Shared Documents/Q1 Budget.xlsx",
|
||||
FileExtension = "xlsx",
|
||||
Created = new DateTime(2024, 1, 10),
|
||||
LastModified = new DateTime(2024, 3, 20),
|
||||
Author = "Alice Smith",
|
||||
ModifiedBy = "Bob Jones",
|
||||
SizeBytes = 48_000
|
||||
};
|
||||
|
||||
// ── CSV tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_WithKnownResults_ContainsExpectedHeader()
|
||||
{
|
||||
var svc = new SearchCsvExportService();
|
||||
var csv = svc.BuildCsv(new List<SearchResult> { MakeSample() });
|
||||
Assert.Contains("File Name", csv);
|
||||
Assert.Contains("Extension", csv);
|
||||
Assert.Contains("Created", csv);
|
||||
Assert.Contains("Created By", csv);
|
||||
Assert.Contains("Modified By", csv);
|
||||
Assert.Contains("Size", csv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_WithEmptyList_ReturnsHeaderOnly()
|
||||
{
|
||||
var svc = new SearchCsvExportService();
|
||||
var csv = svc.BuildCsv(new List<SearchResult>());
|
||||
Assert.NotEmpty(csv);
|
||||
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.Single(lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_ResultValues_AppearInOutput()
|
||||
{
|
||||
var svc = new SearchCsvExportService();
|
||||
var csv = svc.BuildCsv(new List<SearchResult> { MakeSample() });
|
||||
Assert.Contains("Alice Smith", csv);
|
||||
Assert.Contains("xlsx", csv);
|
||||
}
|
||||
|
||||
// ── HTML tests ─────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithResults_ContainsSortableColumnScript()
|
||||
{
|
||||
var svc = new SearchHtmlExportService();
|
||||
var html = svc.BuildHtml(new List<SearchResult> { MakeSample() });
|
||||
Assert.Contains("<!DOCTYPE html>", html);
|
||||
Assert.Contains("sort", html); // sortable columns JS
|
||||
Assert.Contains("Q1 Budget.xlsx", html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithResults_ContainsFilterInput()
|
||||
{
|
||||
var svc = new SearchHtmlExportService();
|
||||
var html = svc.BuildHtml(new List<SearchResult> { MakeSample() });
|
||||
Assert.Contains("filter", html); // filter input element
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithEmptyList_ReturnsValidHtml()
|
||||
{
|
||||
var svc = new SearchHtmlExportService();
|
||||
var html = svc.BuildHtml(new List<SearchResult>());
|
||||
Assert.Contains("<!DOCTYPE html>", html);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// SharepointToolbox.Tests/Services/Export/DuplicatesHtmlExportServiceTests.cs
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services.Export;
|
||||
using Xunit;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services.Export;
|
||||
|
||||
public class DuplicatesHtmlExportServiceTests
|
||||
{
|
||||
private static DuplicateGroup MakeGroup(string name, int count) => new()
|
||||
{
|
||||
GroupKey = $"{name}|1024",
|
||||
Name = name,
|
||||
Items = Enumerable.Range(1, count).Select(i => new DuplicateItem
|
||||
{
|
||||
Name = name,
|
||||
Path = $"https://contoso.sharepoint.com/sites/Site{i}/{name}",
|
||||
Library = "Shared Documents",
|
||||
SizeBytes = 1024
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithGroups_ContainsGroupCards()
|
||||
{
|
||||
var svc = new DuplicatesHtmlExportService();
|
||||
var groups = new List<DuplicateGroup> { MakeGroup("report.docx", 3) };
|
||||
var html = svc.BuildHtml(groups);
|
||||
Assert.Contains("<!DOCTYPE html>", html);
|
||||
Assert.Contains("report.docx", html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithMultipleGroups_AllGroupNamesPresent()
|
||||
{
|
||||
var svc = new DuplicatesHtmlExportService();
|
||||
var groups = new List<DuplicateGroup>
|
||||
{
|
||||
MakeGroup("budget.xlsx", 2),
|
||||
MakeGroup("photo.jpg", 4)
|
||||
};
|
||||
var html = svc.BuildHtml(groups);
|
||||
Assert.Contains("budget.xlsx", html);
|
||||
Assert.Contains("photo.jpg", html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithEmptyList_ReturnsValidHtml()
|
||||
{
|
||||
var svc = new DuplicatesHtmlExportService();
|
||||
var html = svc.BuildHtml(new List<DuplicateGroup>());
|
||||
Assert.Contains("<!DOCTYPE html>", html);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~DuplicatesServiceTests" -x
|
||||
```
|
||||
|
||||
Expected: 5 real tests pass (MakeKey logic tests), CSOM stubs skip
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx
|
||||
dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~DuplicatesServiceTests|FullyQualifiedName~StorageServiceTests" -x
|
||||
```
|
||||
|
||||
Expected: 0 build errors; 7 DuplicatesServiceTests+StorageServiceTests pass or skip with no CS errors
|
||||
|
||||
> **Note on unfiltered test run at Wave 0:** Running `dotnet test` without a filter at this stage will show approximately 15 failing tests across `StorageCsvExportServiceTests`, `StorageHtmlExportServiceTests`, `SearchExportServiceTests`, and `DuplicatesHtmlExportServiceTests`. This is expected — all 5 export service stubs return `string.Empty` until Plans 03-03 and 03-05 implement the real logic. Do not treat these failures as a blocker for Wave 0 completion.
|
||||
|
||||
## Commit Message
|
||||
feat(03-01): create Phase 3 models, interfaces, export stubs, and test scaffolds
|
||||
|
||||
## Output
|
||||
|
||||
After completion, create `.planning/phases/03-storage/03-01-SUMMARY.md`
|
||||
Reference in New Issue
Block a user