using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SharepointToolbox.Core.Models; using SharepointToolbox.Services.Export; namespace SharepointToolbox.Tests.Services.Export; /// /// Unit tests for UserAccessCsvExportService (Phase 7 Plan 08). /// Verifies: summary section, column count, RFC 4180 escaping, per-user content. /// public class UserAccessCsvExportServiceTests { // ── Helper factory ──────────────────────────────────────────────────────── private static UserAccessEntry MakeEntry( string userDisplay = "Alice Smith", string userLogin = "alice@contoso.com", string siteUrl = "https://contoso.sharepoint.com", string siteTitle = "Contoso", string objectType = "List", string objectTitle = "Docs", string objectUrl = "https://contoso.sharepoint.com/Docs", string permLevel = "Read", AccessType accessType = AccessType.Direct, string grantedThrough = "Direct Permissions", bool isHighPrivilege = false, bool isExternal = false) => new(userDisplay, userLogin, siteUrl, siteTitle, objectType, objectTitle, objectUrl, permLevel, accessType, grantedThrough, isHighPrivilege, isExternal); private static readonly UserAccessEntry DefaultEntry = MakeEntry(); // ── Test 1: BuildCsv includes summary section ───────────────────────────── [Fact] public void BuildCsv_includes_summary_section() { var svc = new UserAccessCsvExportService(); var csv = svc.BuildCsv("Alice Smith", "alice@contoso.com", new[] { DefaultEntry }); Assert.Contains("User Access Audit Report", csv); Assert.Contains("Alice Smith", csv); Assert.Contains("alice@contoso.com", csv); Assert.Contains("Total Accesses", csv); Assert.Contains("Sites", csv); } // ── Test 2: BuildCsv includes data header line ──────────────────────────── [Fact] public void BuildCsv_includes_data_header() { var svc = new UserAccessCsvExportService(); var csv = svc.BuildCsv("Alice Smith", "alice@contoso.com", new[] { DefaultEntry }); Assert.Contains("Site", csv); Assert.Contains("Object Type", csv); Assert.Contains("Object", csv); Assert.Contains("Permission Level", csv); Assert.Contains("Access Type", csv); Assert.Contains("Granted Through", csv); } // ── Test 3: BuildCsv escapes double quotes (RFC 4180) ───────────────────── [Fact] public void BuildCsv_escapes_quotes() { var entryWithQuotes = MakeEntry(objectTitle: "Document \"Template\" Library"); var svc = new UserAccessCsvExportService(); var csv = svc.BuildCsv("Alice", "alice@contoso.com", new[] { entryWithQuotes }); // RFC 4180: double quotes inside a quoted field are doubled Assert.Contains("\"\"Template\"\"", csv); } // ── Test 4: BuildCsv data rows have correct column count ────────────────── [Fact] public void BuildCsv_correct_column_count() { var svc = new UserAccessCsvExportService(); var csv = svc.BuildCsv("Alice", "alice@contoso.com", new[] { DefaultEntry }); // Find the header row and count its quoted comma-separated fields // Header is: "Site","Object Type","Object","URL","Permission Level","Access Type","Granted Through" // That is 7 fields. var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries); // Find a data row (after the blank line separating summary from data) // Data rows contain the entry content (not the header line itself) // We want to count fields in the header row: var headerLine = lines.FirstOrDefault(l => l.Contains("\"Site\",\"Object Type\"")); Assert.NotNull(headerLine); // Count comma-separated quoted fields: split by "," boundary var fields = CountCsvFields(headerLine!); Assert.Equal(7, fields); } // ── Test 5: WriteSingleFileAsync includes entries for all users ─────────── [Fact] public async Task WriteSingleFileAsync_includes_all_users() { var alice = MakeEntry(userDisplay: "Alice", userLogin: "alice@contoso.com"); var bob = MakeEntry(userDisplay: "Bob", userLogin: "bob@contoso.com"); var svc = new UserAccessCsvExportService(); var tmpFile = Path.GetTempFileName(); try { await svc.WriteSingleFileAsync(new[] { alice, bob }, tmpFile, CancellationToken.None); var content = await File.ReadAllTextAsync(tmpFile); Assert.Contains("alice@contoso.com", content); Assert.Contains("bob@contoso.com", content); Assert.Contains("Users Audited", content); } finally { File.Delete(tmpFile); } } // ── Private helpers ─────────────────────────────────────────────────────── /// /// Counts the number of comma-separated fields in a CSV line by stripping /// surrounding quotes from each field. /// private static int CountCsvFields(string line) { // Simple RFC 4180 field counter — works for well-formed quoted fields int count = 1; bool inQuotes = false; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '"') { if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') i++; // skip escaped quote else inQuotes = !inQuotes; } else if (c == ',' && !inQuotes) { count++; } } return count; } }