// // StorageStatsManagerTests.swift // damusTests // // Created by OpenCode on 2026-02-25. // import XCTest @testable import damus import Kingfisher /// Comprehensive test suite for storage usage calculation logic /// /// Tests cover: /// - StorageStats calculations (total size, percentages) /// - File size calculations with temporary test files /// - Async storage stats calculations /// - Byte formatting utilities /// - Ndb.getStats() database statistics /// - Integration between components /// - Thread safety and error handling final class StorageStatsManagerTests: XCTestCase { var tempDirectory: URL! var mockNostrDBPath: String! var mockSnapshotPath: String! override func setUp() { super.setUp() // Create temporary directory for test files tempDirectory = FileManager.default.temporaryDirectory .appendingPathComponent("StorageStatsManagerTests-\(UUID().uuidString)") try? FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true) // Create mock database directories let nostrDBDir = tempDirectory.appendingPathComponent("nostrdb") let snapshotDir = tempDirectory.appendingPathComponent("snapshot") try? FileManager.default.createDirectory(at: nostrDBDir, withIntermediateDirectories: true) try? FileManager.default.createDirectory(at: snapshotDir, withIntermediateDirectories: true) mockNostrDBPath = nostrDBDir.path mockSnapshotPath = snapshotDir.path } override func tearDown() { // Clean up temporary files if let tempDirectory = tempDirectory { try? FileManager.default.removeItem(at: tempDirectory) } tempDirectory = nil mockNostrDBPath = nil mockSnapshotPath = nil super.tearDown() } // MARK: - Helper Methods /// Create a temporary file with specified size /// - Parameters: /// - path: Full path for the file /// - size: Size in bytes private func createTestFile(at path: String, size: UInt64) throws { let data = Data(repeating: 0, count: Int(size)) try data.write(to: URL(fileURLWithPath: path)) } /// Get file size using FileManager (reference implementation) private func getActualFileSize(at path: String) -> UInt64? { guard FileManager.default.fileExists(atPath: path) else { return nil } do { let attributes = try FileManager.default.attributesOfItem(atPath: path) return attributes[.size] as? UInt64 } catch { return nil } } // MARK: - 1. StorageStats Structure Tests /// Test that totalSize correctly sums all storage components func testTotalSizeCalculation() { let stats = StorageStats( nostrdbDetails: nil, nostrdbSize: 1000, snapshotSize: 500, imageCacheSize: 250 ) XCTAssertEqual(stats.totalSize, 1750, "Total size should sum all components") } /// Test percentage calculation accuracy func testPercentageCalculation() { let stats = StorageStats( nostrdbDetails: nil, nostrdbSize: 600, snapshotSize: 300, imageCacheSize: 100 ) // Total = 1000, so 600 should be 60% let nostrdbPercentage = stats.percentage(for: 600) XCTAssertEqual(nostrdbPercentage, 60.0, accuracy: 0.01, "NostrDB should be 60% of total") let snapshotPercentage = stats.percentage(for: 300) XCTAssertEqual(snapshotPercentage, 30.0, accuracy: 0.01, "Snapshot should be 30% of total") let cachePercentage = stats.percentage(for: 100) XCTAssertEqual(cachePercentage, 10.0, accuracy: 0.01, "Cache should be 10% of total") } /// Test percentage calculation when total is zero (edge case) func testPercentageWithZeroTotal() { let stats = StorageStats( nostrdbDetails: nil, nostrdbSize: 0, snapshotSize: 0, imageCacheSize: 0 ) let percentage = stats.percentage(for: 100) XCTAssertEqual(percentage, 0.0, "Percentage should be 0 when total is 0") } /// Test that StorageStats conforms to Hashable properly func testStorageStatsHashableConformance() { let stats1 = StorageStats( nostrdbDetails: nil, nostrdbSize: 1000, snapshotSize: 500, imageCacheSize: 250 ) let stats2 = StorageStats( nostrdbDetails: nil, nostrdbSize: 1000, snapshotSize: 500, imageCacheSize: 250 ) let stats3 = StorageStats( nostrdbDetails: nil, nostrdbSize: 2000, snapshotSize: 500, imageCacheSize: 250 ) // Equal stats should be equal and have same hash XCTAssertEqual(stats1, stats2, "Identical stats should be equal") XCTAssertEqual(stats1.hashValue, stats2.hashValue, "Equal stats should have same hash") // Different stats should not be equal XCTAssertNotEqual(stats1, stats3, "Different stats should not be equal") // Should work in Set let set: Set = [stats1, stats2, stats3] XCTAssertEqual(set.count, 2, "Set should contain 2 unique stats") } // MARK: - 2. File Size Calculation Tests /// Test file size calculation with an existing file func testGetFileSizeWithExistingFile() throws { let testFilePath = tempDirectory.appendingPathComponent("test-file.dat").path let expectedSize: UInt64 = 1024 * 1024 // 1 MB // Create test file with known size try createTestFile(at: testFilePath, size: expectedSize) // Verify file was created correctly let actualSize = getActualFileSize(at: testFilePath) XCTAssertNotNil(actualSize, "Test file should exist") XCTAssertEqual(actualSize, expectedSize, "Test file should have expected size") } /// Test file size calculation when file doesn't exist (should return 0) func testGetFileSizeWithNonexistentFile() { let nonexistentPath = tempDirectory.appendingPathComponent("nonexistent.dat").path // Verify file doesn't exist XCTAssertFalse(FileManager.default.fileExists(atPath: nonexistentPath), "File should not exist") let size = getActualFileSize(at: nonexistentPath) XCTAssertNil(size, "Size should be nil for nonexistent file") } /// Test NostrDB file size calculation with valid path func testGetNostrDBSizeWithValidPath() throws { let dbFilePath = "\(mockNostrDBPath!)/\(Ndb.main_db_file_name)" let expectedSize: UInt64 = 5 * 1024 * 1024 // 5 MB // Create mock database file try createTestFile(at: dbFilePath, size: expectedSize) // Verify file size can be retrieved let actualSize = getActualFileSize(at: dbFilePath) XCTAssertNotNil(actualSize, "DB file should exist") XCTAssertEqual(actualSize, expectedSize, "DB file should have expected size") } /// Test snapshot database file size calculation with valid path func testGetSnapshotDBSizeWithValidPath() throws { let dbFilePath = "\(mockSnapshotPath!)/\(Ndb.main_db_file_name)" let expectedSize: UInt64 = 2 * 1024 * 1024 // 2 MB // Create mock snapshot database file try createTestFile(at: dbFilePath, size: expectedSize) // Verify file size can be retrieved let actualSize = getActualFileSize(at: dbFilePath) XCTAssertNotNil(actualSize, "Snapshot DB file should exist") XCTAssertEqual(actualSize, expectedSize, "Snapshot DB file should have expected size") } // MARK: - 3. Byte Formatting Tests /// Test formatting of zero bytes func testFormatBytesZero() { let formatted = StorageStatsManager.formatBytes(0) // ByteCountFormatter may format as "Zero bytes", "0 bytes", "0 KB", etc. // We just verify it's a valid non-empty string XCTAssertFalse(formatted.isEmpty, "Formatted string should not be empty") // Most common formats include "0" or "Zero" let containsZero = formatted.contains("0") || formatted.uppercased().contains("ZERO") XCTAssertTrue(containsZero, "Zero bytes should contain '0' or 'Zero', got: \(formatted)") } /// Test formatting of small byte values (< 1 KB) func testFormatBytesSmall() { let formatted = StorageStatsManager.formatBytes(512) XCTAssertFalse(formatted.isEmpty, "Formatted string should not be empty") // Should contain a numeric value XCTAssertTrue(formatted.contains("512") || formatted.contains("0.5"), "Should contain size value") } /// Test formatting of kilobyte values func testFormatBytesKilobytes() { let oneKB: UInt64 = 1024 let formatted = StorageStatsManager.formatBytes(oneKB * 5) // 5 KB XCTAssertFalse(formatted.isEmpty, "Formatted string should not be empty") // Should mention KB or kilobytes XCTAssertTrue(formatted.uppercased().contains("KB") || formatted.uppercased().contains("K"), "Should indicate kilobytes: \(formatted)") } /// Test formatting of megabyte values func testFormatBytesMegabytes() { let oneMB: UInt64 = 1024 * 1024 let formatted = StorageStatsManager.formatBytes(oneMB * 10) // 10 MB XCTAssertFalse(formatted.isEmpty, "Formatted string should not be empty") // Should mention MB or megabytes XCTAssertTrue(formatted.uppercased().contains("MB") || formatted.uppercased().contains("M"), "Should indicate megabytes: \(formatted)") } /// Test formatting of gigabyte values func testFormatBytesGigabytes() { let oneGB: UInt64 = 1024 * 1024 * 1024 let formatted = StorageStatsManager.formatBytes(oneGB * 2) // 2 GB XCTAssertFalse(formatted.isEmpty, "Formatted string should not be empty") // Should mention GB or gigabytes XCTAssertTrue(formatted.uppercased().contains("GB") || formatted.uppercased().contains("G"), "Should indicate gigabytes: \(formatted)") } /// Test formatting of very large values func testFormatBytesLarge() { let oneTB: UInt64 = 1024 * 1024 * 1024 * 1024 let formatted = StorageStatsManager.formatBytes(oneTB) XCTAssertFalse(formatted.isEmpty, "Formatted string should not be empty") // Should handle terabyte values gracefully XCTAssertTrue(formatted.uppercased().contains("TB") || formatted.uppercased().contains("T") || formatted.uppercased().contains("GB") || formatted.uppercased().contains("G"), "Should format large values: \(formatted)") } // MARK: - 4. Async Storage Stats Calculation Tests /// Test storage stats calculation without Ndb instance func testCalculateStorageStatsWithoutNdb() async throws { // Note: This test verifies the calculation succeeds and returns valid stats // We don't check exact values since they depend on actual system state let stats = try await StorageStatsManager.shared.calculateStorageStats(ndb: nil) // Verify stats structure is valid XCTAssertNotNil(stats, "Stats should not be nil") XCTAssertNil(stats.nostrdbDetails, "Details should be nil when no Ndb provided") // All sizes should be non-negative XCTAssertGreaterThanOrEqual(stats.nostrdbSize, 0, "NostrDB size should be non-negative") XCTAssertGreaterThanOrEqual(stats.snapshotSize, 0, "Snapshot size should be non-negative") XCTAssertGreaterThanOrEqual(stats.imageCacheSize, 0, "Image cache size should be non-negative") // Total should equal sum let expectedTotal = stats.nostrdbSize + stats.snapshotSize + stats.imageCacheSize XCTAssertEqual(stats.totalSize, expectedTotal, "Total should equal sum of components") } // MARK: - 5. NdbDatabaseStats Tests /// Test NdbDatabaseStats total size calculation func testNdbDatabaseStatsCalculations() { let dbStats = NdbDatabaseStats( database: .note, keySize: 1000, valueSize: 5000 ) XCTAssertEqual(dbStats.totalSize, 6000, "Total should be key + value size") XCTAssertEqual(dbStats.database, .note, "Database type should be preserved") XCTAssertEqual(dbStats.keySize, 1000, "Key size should be preserved") XCTAssertEqual(dbStats.valueSize, 5000, "Value size should be preserved") } /// Test NdbStats total size calculation func testNdbStatsTotalCalculation() { let stats = NdbStats(databaseStats: [ NdbDatabaseStats(database: .note, keySize: 1000, valueSize: 5000), NdbDatabaseStats(database: .profile, keySize: 500, valueSize: 2000), NdbDatabaseStats(database: .noteId, keySize: 200, valueSize: 800) ]) // Total should be sum of all database totals // (1000+5000) + (500+2000) + (200+800) = 9500 XCTAssertEqual(stats.totalSize, 9500, "Total should sum all database sizes") } /// Test NdbStats with empty database list func testNdbStatsEmpty() { let stats = NdbStats(databaseStats: []) XCTAssertEqual(stats.totalSize, 0, "Empty stats should have zero total") XCTAssertTrue(stats.databaseStats.isEmpty, "Database stats should be empty") } /// Test NdbDatabaseStats hashable conformance func testNdbDatabaseStatsHashableConformance() { let stats1 = NdbDatabaseStats(database: .note, keySize: 1000, valueSize: 5000) let stats2 = NdbDatabaseStats(database: .note, keySize: 1000, valueSize: 5000) let stats3 = NdbDatabaseStats(database: .profile, keySize: 1000, valueSize: 5000) XCTAssertEqual(stats1, stats2, "Identical stats should be equal") XCTAssertNotEqual(stats1, stats3, "Different database type should not be equal") // Should work in Set let set: Set = [stats1, stats2, stats3] XCTAssertEqual(set.count, 2, "Set should contain 2 unique stats") } /// Test NdbStats hashable conformance func testNdbStatsHashableConformance() { let dbStats1 = NdbDatabaseStats(database: .note, keySize: 1000, valueSize: 5000) let dbStats2 = NdbDatabaseStats(database: .profile, keySize: 500, valueSize: 2000) let stats1 = NdbStats(databaseStats: [dbStats1, dbStats2]) let stats2 = NdbStats(databaseStats: [dbStats1, dbStats2]) let stats3 = NdbStats(databaseStats: [dbStats1]) XCTAssertEqual(stats1, stats2, "Identical stats should be equal") XCTAssertNotEqual(stats1, stats3, "Different database count should not be equal") // Should work in Set let set: Set = [stats1, stats2, stats3] XCTAssertEqual(set.count, 2, "Set should contain 2 unique stats") } // MARK: - 6. NdbDatabase Enum Tests /// Test NdbDatabase display names func testNdbDatabaseDisplayNames() { // Display names include the C enum names in parentheses XCTAssertEqual(NdbDatabase.note.displayName, "Notes (NDB_DB_NOTE)", "Note database display name") XCTAssertEqual(NdbDatabase.profile.displayName, "Profiles (NDB_DB_PROFILE)", "Profile database display name") XCTAssertEqual(NdbDatabase.noteBlocks.displayName, "Note Blocks", "Note blocks display name") XCTAssertEqual(NdbDatabase.noteId.displayName, "Note ID Index", "Note ID index display name") XCTAssertEqual(NdbDatabase.meta.displayName, "Metadata (NDB_DB_META)", "Metadata display name") XCTAssertEqual(NdbDatabase.other.displayName, "Other Data", "Other data display name") } /// Test NdbDatabase icons func testNdbDatabaseIcons() { // Verify each database has an icon (non-empty string) XCTAssertFalse(NdbDatabase.note.icon.isEmpty, "Note should have icon") XCTAssertFalse(NdbDatabase.profile.icon.isEmpty, "Profile should have icon") XCTAssertFalse(NdbDatabase.noteBlocks.icon.isEmpty, "Note blocks should have icon") XCTAssertFalse(NdbDatabase.other.icon.isEmpty, "Other should have icon") } /// Test NdbDatabase colors func testNdbDatabaseColors() { // Verify each database has a color assigned // We can't easily compare Color values, but we can verify they return Color instances _ = NdbDatabase.note.color _ = NdbDatabase.profile.color _ = NdbDatabase.noteBlocks.color _ = NdbDatabase.other.color // If we get here without crashes, colors are working XCTAssertTrue(true, "All database colors should be accessible") } /// Test NdbDatabase initialization from index func testNdbDatabaseFromIndex() { // Test valid indices let db0 = NdbDatabase(fromIndex: 0) XCTAssertNotEqual(db0, .other, "Index 0 should map to a valid database") let db1 = NdbDatabase(fromIndex: 1) XCTAssertNotEqual(db1, .other, "Index 1 should map to a valid database") // Test invalid index (should default to .other) let dbInvalid = NdbDatabase(fromIndex: 9999) XCTAssertEqual(dbInvalid, .other, "Invalid index should default to .other") } // MARK: - 7. Integration Tests /// Test complete storage stats flow with real-ish data func testStorageStatsIntegrationFlow() async throws { // This test verifies the entire flow works end-to-end // We use actual calculation but don't assert specific values let stats = try await StorageStatsManager.shared.calculateStorageStats(ndb: nil) // Verify structure XCTAssertNotNil(stats, "Stats should be calculated") // Verify all components are accessible let _ = stats.nostrdbSize let _ = stats.snapshotSize let _ = stats.imageCacheSize let _ = stats.totalSize // Verify percentage calculation works if stats.totalSize > 0 { let percentage = stats.percentage(for: stats.nostrdbSize) XCTAssertGreaterThanOrEqual(percentage, 0.0, "Percentage should be non-negative") XCTAssertLessThanOrEqual(percentage, 100.0, "Percentage should not exceed 100%") } // Verify formatting works let formatted = StorageStatsManager.formatBytes(stats.totalSize) XCTAssertFalse(formatted.isEmpty, "Formatted size should not be empty") } /// Test concurrent stats calculations (thread safety) func testConcurrentStatsCalculations() async throws { let iterations = 5 // Launch multiple concurrent calculations try await withThrowingTaskGroup(of: StorageStats.self) { group in for _ in 0..