feat(04-01): add Phase 4 models, interfaces, BulkOperationRunner, and test scaffolds
- Install CsvHelper 33.1.0 and Microsoft.Graph 5.74.0 (main + test projects) - Add 14 core model/enum files (BulkOperationResult, BulkMemberRow, BulkSiteRow, TransferJob, FolderStructureRow, SiteTemplate, SiteTemplateOptions, TemplateLibraryInfo, TemplateFolderInfo, TemplatePermissionGroup, ConflictPolicy, TransferMode, CsvValidationRow) - Add 6 service interfaces (IFileTransferService, IBulkMemberService, IBulkSiteService, ITemplateService, IFolderStructureService, ICsvValidationService) - Add BulkOperationRunner with continue-on-error and cancellation support - Add BulkResultCsvExportService stub (compile-ready) - Add test scaffolds: BulkOperationRunnerTests (5 passing), BulkResultCsvExportServiceTests (2 passing), CsvValidationServiceTests (6 skipped), TemplateRepositoryTests (4 skipped)
This commit is contained in:
105
SharepointToolbox.Tests/Services/BulkOperationRunnerTests.cs
Normal file
105
SharepointToolbox.Tests/Services/BulkOperationRunnerTests.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
public class BulkOperationRunnerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunAsync_AllSucceed_ReturnsAllSuccess()
|
||||
{
|
||||
var items = new List<string> { "a", "b", "c" };
|
||||
var progress = new Progress<OperationProgress>();
|
||||
|
||||
var summary = await BulkOperationRunner.RunAsync(
|
||||
items,
|
||||
(item, idx, ct) => Task.CompletedTask,
|
||||
progress,
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(3, summary.TotalCount);
|
||||
Assert.Equal(3, summary.SuccessCount);
|
||||
Assert.Equal(0, summary.FailedCount);
|
||||
Assert.False(summary.HasFailures);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_SomeItemsFail_ContinuesAndReportsPerItem()
|
||||
{
|
||||
var items = new List<string> { "ok1", "fail", "ok2" };
|
||||
var progress = new Progress<OperationProgress>();
|
||||
|
||||
var summary = await BulkOperationRunner.RunAsync(
|
||||
items,
|
||||
(item, idx, ct) =>
|
||||
{
|
||||
if (item == "fail") throw new InvalidOperationException("Test error");
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
progress,
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(3, summary.TotalCount);
|
||||
Assert.Equal(2, summary.SuccessCount);
|
||||
Assert.Equal(1, summary.FailedCount);
|
||||
Assert.True(summary.HasFailures);
|
||||
Assert.Contains(summary.FailedItems, r => r.ErrorMessage == "Test error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Cancelled_ThrowsOperationCanceled()
|
||||
{
|
||||
var items = new List<string> { "a", "b", "c" };
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
var progress = new Progress<OperationProgress>();
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
BulkOperationRunner.RunAsync(
|
||||
items,
|
||||
(item, idx, ct) => Task.CompletedTask,
|
||||
progress,
|
||||
cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_CancelledMidOperation_StopsProcessing()
|
||||
{
|
||||
var items = new List<string> { "a", "b", "c", "d" };
|
||||
var cts = new CancellationTokenSource();
|
||||
var processedCount = 0;
|
||||
var progress = new Progress<OperationProgress>();
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
BulkOperationRunner.RunAsync(
|
||||
items,
|
||||
async (item, idx, ct) =>
|
||||
{
|
||||
Interlocked.Increment(ref processedCount);
|
||||
if (idx == 1) cts.Cancel(); // cancel after second item
|
||||
await Task.CompletedTask;
|
||||
},
|
||||
progress,
|
||||
cts.Token));
|
||||
|
||||
Assert.True(processedCount <= 3); // should not process all 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReportsProgress()
|
||||
{
|
||||
var items = new List<string> { "a", "b" };
|
||||
var progressReports = new List<OperationProgress>();
|
||||
var progress = new Progress<OperationProgress>(p => progressReports.Add(p));
|
||||
|
||||
await BulkOperationRunner.RunAsync(
|
||||
items,
|
||||
(item, idx, ct) => Task.CompletedTask,
|
||||
progress,
|
||||
CancellationToken.None);
|
||||
|
||||
// Progress is async, give it a moment to flush
|
||||
await Task.Delay(100);
|
||||
Assert.True(progressReports.Count >= 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services.Export;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
public class BulkResultCsvExportServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildFailedItemsCsv_WithFailedItems_IncludesErrorColumn()
|
||||
{
|
||||
var service = new BulkResultCsvExportService();
|
||||
var items = new List<BulkItemResult<BulkMemberRow>>
|
||||
{
|
||||
BulkItemResult<BulkMemberRow>.Failed(
|
||||
new BulkMemberRow { Email = "bad@test.com", GroupName = "G1", GroupUrl = "http://test", Role = "Member" },
|
||||
"User not found"),
|
||||
};
|
||||
|
||||
var csv = service.BuildFailedItemsCsv(items);
|
||||
|
||||
Assert.Contains("Error", csv);
|
||||
Assert.Contains("Timestamp", csv);
|
||||
Assert.Contains("bad@test.com", csv);
|
||||
Assert.Contains("User not found", csv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFailedItemsCsv_SuccessItems_Excluded()
|
||||
{
|
||||
var service = new BulkResultCsvExportService();
|
||||
var items = new List<BulkItemResult<BulkMemberRow>>
|
||||
{
|
||||
BulkItemResult<BulkMemberRow>.Success(
|
||||
new BulkMemberRow { Email = "ok@test.com", GroupName = "G1", GroupUrl = "http://test", Role = "Member" }),
|
||||
BulkItemResult<BulkMemberRow>.Failed(
|
||||
new BulkMemberRow { Email = "bad@test.com", GroupName = "G1", GroupUrl = "http://test", Role = "Member" },
|
||||
"Error"),
|
||||
};
|
||||
|
||||
var csv = service.BuildFailedItemsCsv(items);
|
||||
|
||||
Assert.DoesNotContain("ok@test.com", csv);
|
||||
Assert.Contains("bad@test.com", csv);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
public class CsvValidationServiceTests
|
||||
{
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void ParseAndValidateMembers_ValidCsv_ReturnsValidRows()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void ParseAndValidateMembers_InvalidEmail_ReturnsErrors()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void ParseAndValidateSites_TeamWithoutOwner_ReturnsError()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void ParseAndValidateFolders_ValidCsv_ReturnsValidRows()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void ParseAndValidate_BomDetection_WorksWithAndWithoutBom()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void ParseAndValidate_SemicolonDelimiter_DetectedAutomatically()
|
||||
{
|
||||
}
|
||||
}
|
||||
26
SharepointToolbox.Tests/Services/TemplateRepositoryTests.cs
Normal file
26
SharepointToolbox.Tests/Services/TemplateRepositoryTests.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
public class TemplateRepositoryTests
|
||||
{
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void SaveAndLoad_RoundTrips_Correctly()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void GetAll_ReturnsAllSavedTemplates()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void Delete_RemovesTemplate()
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Implemented in Plan 04-02")]
|
||||
public void Rename_UpdatesTemplateName()
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user