import { describe, test, afterEach } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, rmSync, writeFileSync, existsSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { ensureDirectories, saveBatch, loadAllBatches, loadAllPosts, listExports, saveWeeklyReport, loadWeeklyReport, loadAllWeeklyReports, } from "../src/utils/storage.js"; import type { AnalyticsBatch, PostAnalytics, WeeklyReport } from "../src/models/types.js"; // Helper function to create test post data function createTestPost(overrides?: Partial): PostAnalytics { return { id: overrides?.id || "test-post-1", title: overrides?.title || "Test post content", publishedDate: overrides?.publishedDate || "2026-01-15", metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 8.5, ...(overrides?.metrics || {}), }, importedAt: overrides?.importedAt || "2026-01-20T10:00:00Z", exportSource: overrides?.exportSource || "test-export.csv", }; } // Helper function to create test batch data function createTestBatch(overrides?: Partial): AnalyticsBatch { const posts = overrides?.posts || [createTestPost()]; return { batchId: overrides?.batchId || "12345678-1234-1234-1234-123456789abc", importedAt: overrides?.importedAt || "2026-01-20T10:00:00Z", exportFilename: overrides?.exportFilename || "test-export.csv", dateRange: overrides?.dateRange || { from: "2026-01-15", to: "2026-01-20" }, postCount: posts.length, posts, }; } // Helper function to create test weekly report function createTestWeeklyReport(overrides?: Partial): WeeklyReport { return { week: overrides?.week || "2026-W03", generatedAt: overrides?.generatedAt || "2026-01-20T10:00:00Z", summary: { totalPosts: 5, totalImpressions: 5000, totalReactions: 250, totalComments: 50, totalShares: 25, totalClicks: 100, avgEngagementRate: 8.5, avgImpressionsPerPost: 1000, ...(overrides?.summary || {}), }, topPerformers: overrides?.topPerformers || [], underperformers: overrides?.underperformers || [], trends: { impressionsTrend: "up", engagementTrend: "stable", comparedTo: "2026-W02", percentChange: { impressions: 10, engagement: 2, }, ...(overrides?.trends || {}), }, alerts: overrides?.alerts || [], }; } describe("storage", () => { let tempDir: string; // Create temp directory before each test function setupTempDir(): string { return mkdtempSync(join(tmpdir(), "analytics-test-")); } // Clean up temp directory after each test afterEach(() => { if (tempDir && existsSync(tempDir)) { rmSync(tempDir, { recursive: true, force: true }); } }); describe("ensureDirectories", () => { test("should create directories", () => { tempDir = setupTempDir(); ensureDirectories(tempDir); assert.ok(existsSync(join(tempDir, "exports"))); assert.ok(existsSync(join(tempDir, "posts"))); assert.ok(existsSync(join(tempDir, "weekly-reports"))); }); test("should not fail if directories already exist", () => { tempDir = setupTempDir(); ensureDirectories(tempDir); // Call again - should not throw ensureDirectories(tempDir); assert.ok(existsSync(join(tempDir, "exports"))); assert.ok(existsSync(join(tempDir, "posts"))); assert.ok(existsSync(join(tempDir, "weekly-reports"))); }); }); describe("saveBatch", () => { test("should write JSON file", () => { tempDir = setupTempDir(); const batch = createTestBatch(); const filename = saveBatch(tempDir, batch); assert.ok(filename.startsWith("2026-01-15-")); assert.ok(filename.endsWith(".json")); const filepath = join(tempDir, "posts", filename); assert.ok(existsSync(filepath)); // Verify content const loadedBatches = loadAllBatches(tempDir); assert.equal(loadedBatches.length, 1); assert.equal(loadedBatches[0].batchId, batch.batchId); }); test("should create directories if they don't exist", () => { tempDir = setupTempDir(); const batch = createTestBatch(); saveBatch(tempDir, batch); assert.ok(existsSync(join(tempDir, "posts"))); }); test("should use short batch ID in filename", () => { tempDir = setupTempDir(); const batch = createTestBatch({ batchId: "abcdef12-3456-7890-abcd-ef1234567890", dateRange: { from: "2026-01-15", to: "2026-01-20" }, }); const filename = saveBatch(tempDir, batch); assert.ok(filename.startsWith("2026-01-15-abcdef12")); }); }); describe("loadAllBatches", () => { test("should load saved batches", () => { tempDir = setupTempDir(); const batch1 = createTestBatch({ batchId: "batch1", importedAt: "2026-01-20T10:00:00Z", }); const batch2 = createTestBatch({ batchId: "batch2", importedAt: "2026-01-21T10:00:00Z", dateRange: { from: "2026-01-21", to: "2026-01-21" }, }); saveBatch(tempDir, batch1); saveBatch(tempDir, batch2); const batches = loadAllBatches(tempDir); assert.equal(batches.length, 2); assert.equal(batches[0].batchId, "batch1"); assert.equal(batches[1].batchId, "batch2"); }); test("should return empty array if posts directory doesn't exist", () => { tempDir = setupTempDir(); const batches = loadAllBatches(tempDir); assert.deepEqual(batches, []); }); test("should sort by importedAt timestamp", () => { tempDir = setupTempDir(); const batch1 = createTestBatch({ batchId: "batch1", importedAt: "2026-01-22T10:00:00Z", dateRange: { from: "2026-01-22", to: "2026-01-22" }, }); const batch2 = createTestBatch({ batchId: "batch2", importedAt: "2026-01-20T10:00:00Z", }); const batch3 = createTestBatch({ batchId: "batch3", importedAt: "2026-01-21T10:00:00Z", dateRange: { from: "2026-01-21", to: "2026-01-21" }, }); saveBatch(tempDir, batch1); saveBatch(tempDir, batch2); saveBatch(tempDir, batch3); const batches = loadAllBatches(tempDir); assert.equal(batches.length, 3); assert.equal(batches[0].batchId, "batch2"); // Earliest assert.equal(batches[1].batchId, "batch3"); assert.equal(batches[2].batchId, "batch1"); // Latest }); }); describe("loadAllPosts", () => { test("should deduplicate by post ID", () => { tempDir = setupTempDir(); const post1 = createTestPost({ id: "post1", title: "Old version", publishedDate: "2026-01-15", }); const post1Updated = createTestPost({ id: "post1", title: "New version", publishedDate: "2026-01-15", }); const post2 = createTestPost({ id: "post2", publishedDate: "2026-01-16", }); const batch1 = createTestBatch({ batchId: "batch1", importedAt: "2026-01-20T10:00:00Z", posts: [post1, post2], }); const batch2 = createTestBatch({ batchId: "batch2", importedAt: "2026-01-21T10:00:00Z", dateRange: { from: "2026-01-21", to: "2026-01-21" }, posts: [post1Updated], // Later import of post1 }); saveBatch(tempDir, batch1); saveBatch(tempDir, batch2); const posts = loadAllPosts(tempDir); assert.equal(posts.length, 2); // Should have the updated version of post1 const foundPost1 = posts.find(p => p.id === "post1"); assert.equal(foundPost1?.title, "New version"); }); test("should sort by publishedDate descending", () => { tempDir = setupTempDir(); const post1 = createTestPost({ id: "post1", publishedDate: "2026-01-15", }); const post2 = createTestPost({ id: "post2", publishedDate: "2026-01-17", }); const post3 = createTestPost({ id: "post3", publishedDate: "2026-01-16", }); const batch = createTestBatch({ posts: [post1, post2, post3], }); saveBatch(tempDir, batch); const posts = loadAllPosts(tempDir); assert.equal(posts.length, 3); assert.equal(posts[0].id, "post2"); // 2026-01-17 assert.equal(posts[1].id, "post3"); // 2026-01-16 assert.equal(posts[2].id, "post1"); // 2026-01-15 }); test("should return empty array if no batches exist", () => { tempDir = setupTempDir(); const posts = loadAllPosts(tempDir); assert.deepEqual(posts, []); }); }); describe("listExports", () => { test("should list CSV files", () => { tempDir = setupTempDir(); ensureDirectories(tempDir); const exportsDir = join(tempDir, "exports"); writeFileSync(join(exportsDir, "export1.csv"), "data"); writeFileSync(join(exportsDir, "export2.csv"), "data"); writeFileSync(join(exportsDir, "other.txt"), "data"); // Non-CSV const exports = listExports(tempDir); assert.equal(exports.length, 2); assert.ok(exports.includes("export1.csv")); assert.ok(exports.includes("export2.csv")); }); test("should return empty for missing directory", () => { tempDir = setupTempDir(); const exports = listExports(tempDir); assert.deepEqual(exports, []); }); test("should return sorted list", () => { tempDir = setupTempDir(); ensureDirectories(tempDir); const exportsDir = join(tempDir, "exports"); writeFileSync(join(exportsDir, "c.csv"), "data"); writeFileSync(join(exportsDir, "a.csv"), "data"); writeFileSync(join(exportsDir, "b.csv"), "data"); const exports = listExports(tempDir); assert.deepEqual(exports, ["a.csv", "b.csv", "c.csv"]); }); }); describe("saveWeeklyReport", () => { test("should write report JSON", () => { tempDir = setupTempDir(); const report = createTestWeeklyReport({ week: "2026-W03" }); const filename = saveWeeklyReport(tempDir, report); assert.equal(filename, "2026-W03.json"); const filepath = join(tempDir, "weekly-reports", filename); assert.ok(existsSync(filepath)); // Verify content const loaded = loadWeeklyReport(tempDir, "2026-W03"); assert.ok(loaded); assert.equal(loaded.week, "2026-W03"); assert.equal(loaded.summary.totalPosts, 5); }); test("should create directories if they don't exist", () => { tempDir = setupTempDir(); const report = createTestWeeklyReport(); saveWeeklyReport(tempDir, report); assert.ok(existsSync(join(tempDir, "weekly-reports"))); }); }); describe("loadWeeklyReport", () => { test("should return null for missing report", () => { tempDir = setupTempDir(); const report = loadWeeklyReport(tempDir, "2026-W99"); assert.equal(report, null); }); test("should load existing report", () => { tempDir = setupTempDir(); const report = createTestWeeklyReport({ week: "2026-W03" }); saveWeeklyReport(tempDir, report); const loaded = loadWeeklyReport(tempDir, "2026-W03"); assert.ok(loaded); assert.equal(loaded.week, "2026-W03"); assert.equal(loaded.summary.totalPosts, 5); }); }); describe("loadAllWeeklyReports", () => { test("should load all reports sorted", () => { tempDir = setupTempDir(); const report1 = createTestWeeklyReport({ week: "2026-W03" }); const report2 = createTestWeeklyReport({ week: "2026-W01" }); const report3 = createTestWeeklyReport({ week: "2026-W05" }); saveWeeklyReport(tempDir, report1); saveWeeklyReport(tempDir, report2); saveWeeklyReport(tempDir, report3); const reports = loadAllWeeklyReports(tempDir); assert.equal(reports.length, 3); assert.equal(reports[0].week, "2026-W05"); // Newest first assert.equal(reports[1].week, "2026-W03"); assert.equal(reports[2].week, "2026-W01"); }); test("should return empty array if directory doesn't exist", () => { tempDir = setupTempDir(); const reports = loadAllWeeklyReports(tempDir); assert.deepEqual(reports, []); }); test("should ignore non-JSON files", () => { tempDir = setupTempDir(); ensureDirectories(tempDir); const reportsDir = join(tempDir, "weekly-reports"); const report = createTestWeeklyReport({ week: "2026-W03" }); saveWeeklyReport(tempDir, report); writeFileSync(join(reportsDir, "readme.txt"), "data"); const reports = loadAllWeeklyReports(tempDir); assert.equal(reports.length, 1); assert.equal(reports[0].week, "2026-W03"); }); }); });