Files
2025-12-03 16:48:23 +01:00

1374 lines
61 KiB
Swift

// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: LGPL-3.0-or-later
@preconcurrency import FileProvider
import Foundation
@testable import NextcloudFileProviderKit
import NextcloudFileProviderKitMocks
import RealmSwift
import TestInterface
import XCTest
final class FilesDatabaseManagerTests: NextcloudFileProviderKitTestCase {
static let account = Account(
user: "testUser", id: "testUserId", serverUrl: "https://mock.nc.com", password: "abcd"
)
static let dbManager = FilesDatabaseManager(account: account, databaseDirectory: makeDatabaseDirectory(), fileProviderDomainIdentifier: NSFileProviderDomainIdentifier("test"), log: FileProviderLogMock())
override func setUp() {
super.setUp()
Realm.Configuration.defaultConfiguration.inMemoryIdentifier = name
}
func testFilesDatabaseManagerInitialization() {
XCTAssertNotNil(Self.dbManager, "FilesDatabaseManager should be initialized")
}
func testAnyItemMetadatasForAccount() throws {
// Insert test data
let expected = true
let testAccount = "TestAccount"
let metadata = RealmItemMetadata()
metadata.account = testAccount
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(metadata)
}
// Perform test
let result = Self.dbManager.anyItemMetadatasForAccount(testAccount)
XCTAssertEqual(
result,
expected,
"anyItemMetadatasForAccount should return \(expected) for existing account"
)
}
func testItemMetadataFromOcId() throws {
let ocId = "unique-id-123"
let metadata = RealmItemMetadata()
metadata.ocId = ocId
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(metadata)
}
let fetchedMetadata = Self.dbManager.itemMetadata(ocId: ocId)
XCTAssertNotNil(fetchedMetadata, "Should fetch metadata with the specified ocId")
XCTAssertEqual(
fetchedMetadata?.ocId, ocId, "Fetched metadata ocId should match the requested ocId"
)
}
func testUpdateItemMetadatas() {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
var metadata = SendableItemMetadata(ocId: "test", fileName: "test", account: account)
metadata.downloaded = true
metadata.uploaded = true
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [metadata],
keepExistingDownloadState: true
)
XCTAssertEqual(result.newMetadatas?.isEmpty, false, "Should create new metadatas")
XCTAssertEqual(result.updatedMetadatas?.isEmpty, true, "No metadata should be updated")
// Now test we are receiving the updated basic metadatas correctly.
metadata.etag = "new and shiny"
let result2 = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [metadata],
keepExistingDownloadState: true
)
XCTAssertEqual(result2.newMetadatas?.isEmpty, true, "Should create no new metadatas")
XCTAssertEqual(result2.updatedMetadatas?.isEmpty, false, "Metadata should be updated")
// Also check the download state is correctly kept the same.
// We set it to false here to replicate the lack of a download state when converting from
// the NKFiles received during remote enumeration
metadata.downloaded = false
metadata.etag = "new and shiny, but keeping original download state"
let result3 = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [metadata],
keepExistingDownloadState: true
)
XCTAssertEqual(result3.newMetadatas?.isEmpty, true, "Should create no new metadatas")
XCTAssertEqual(result3.updatedMetadatas?.isEmpty, false, "Metadata should be updated")
XCTAssertEqual(result3.updatedMetadatas?.first?.downloaded, true)
}
func testUpdateRenamesDirectoryAndPropagatesToChildren() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
var rootMetadata = SendableItemMetadata(ocId: "root", fileName: "", account: Self.account)
rootMetadata.directory = true
Self.dbManager.addItemMetadata(rootMetadata)
// Insert original parent directory
var parent = SendableItemMetadata(ocId: "dir1", fileName: "oldDir", account: account)
parent.directory = true
parent.serverUrl = account.davFilesUrl
parent.downloaded = true
parent.uploaded = true
Self.dbManager.addItemMetadata(parent)
// Insert a child item inside that directory
var child = SendableItemMetadata(ocId: "child1", fileName: "file.txt", account: account)
child.serverUrl = account.davFilesUrl + "/oldDir"
child.downloaded = true
Self.dbManager.addItemMetadata(child)
var newContainerFolder = SendableItemMetadata(
ocId: "ncf", fileName: "newContainerFolder", account: account
)
newContainerFolder.serverUrl = account.davFilesUrl
newContainerFolder.downloaded = true
Self.dbManager.addItemMetadata(newContainerFolder)
// Rename the directory
var renamedParent = parent
renamedParent.fileName = "newDir"
renamedParent.serverUrl = account.davFilesUrl + "/" + newContainerFolder.fileName
renamedParent.etag = "etag-changed"
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [rootMetadata, renamedParent],
keepExistingDownloadState: true
)
// Ensure rename took place
XCTAssertEqual(result.newMetadatas?.isEmpty, true)
XCTAssertEqual(result.updatedMetadatas?.isEmpty, false)
XCTAssertNotNil(result.updatedMetadatas?.first(where: { $0.fileName == "newDir" }))
// Ensure the child's serverUrl was updated accordingly
let updatedChild = Self.dbManager.itemMetadata(ocId: "child1")
XCTAssertNotNil(updatedChild)
XCTAssertEqual(updatedChild?.serverUrl, account.davFilesUrl + "/newContainerFolder/newDir")
}
func testTransitItemIsNotUpdated() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
// Simulate existing item in transit
var transit = SendableItemMetadata(ocId: "transit1", fileName: "temp", account: account)
transit.uploaded = true
transit.downloaded = false
transit.status = Status.downloading.rawValue
transit.etag = "old-etag"
Self.dbManager.addItemMetadata(transit)
// Send an updated version of the same item
var incoming = transit
incoming.uploaded = true
incoming.downloaded = false
incoming.status = Status.normal.rawValue
incoming.etag = "new-etag"
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [incoming],
keepExistingDownloadState: true
)
XCTAssertEqual(result.updatedMetadatas?.isEmpty, true, "Transit items should not be updated")
XCTAssertEqual(result.newMetadatas?.isEmpty, true)
XCTAssertEqual(result.deletedMetadatas?.isEmpty, true)
let inDb = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: transit.ocId))
XCTAssertEqual(inDb.etag, transit.etag)
}
func testTransitItemIsDeleted() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
// Simulate existing item in transit
var transit = SendableItemMetadata(ocId: "transit1", fileName: "temp", account: account)
transit.uploaded = true
transit.downloaded = false
transit.status = Status.downloading.rawValue
Self.dbManager.addItemMetadata(transit)
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [],
keepExistingDownloadState: true
)
XCTAssertEqual(result.updatedMetadatas?.isEmpty, true)
XCTAssertEqual(result.newMetadatas?.isEmpty, true)
XCTAssertEqual(result.deletedMetadatas?.isEmpty, false)
XCTAssertEqual(result.deletedMetadatas?.first?.ocId, transit.ocId)
}
func testSetStatusForItemMetadata() throws {
// Create and add a test metadata to the database
let metadata = RealmItemMetadata()
metadata.ocId = "unique-id-123"
metadata.status = Status.normal.rawValue
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(metadata)
}
let expectedStatus = Status.uploadError
let updatedMetadata = Self.dbManager.setStatusForItemMetadata(
SendableItemMetadata(value: metadata), status: expectedStatus
)
XCTAssertEqual(
updatedMetadata?.status,
expectedStatus.rawValue,
"Status should be updated to \(expectedStatus)"
)
}
func testAddItemMetadata() {
let metadata = SendableItemMetadata(
ocId: "unique-id-123",
fileName: "b",
account: .init(user: "t", id: "t", serverUrl: "b", password: "")
)
Self.dbManager.addItemMetadata(metadata)
let fetchedMetadata = Self.dbManager.itemMetadata(ocId: "unique-id-123")
XCTAssertNotNil(fetchedMetadata, "Metadata should be added to the database")
}
func testDeleteItemMetadata() throws {
let ocId = "unique-id-123"
let metadata = RealmItemMetadata()
metadata.ocId = ocId
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(metadata)
}
let result = Self.dbManager.deleteItemMetadata(ocId: ocId)
XCTAssertTrue(result, "deleteItemMetadata should return true on successful deletion")
XCTAssertEqual(
Self.dbManager.itemMetadata(ocId: ocId)?.deleted,
true,
"Metadata should be deleted from the database"
)
}
func testRenameItemMetadata() throws {
let ocId = "unique-id-123"
let newFileName = "newFileName.pdf"
let newServerUrl = "https://new.example.com"
let metadata = RealmItemMetadata()
metadata.ocId = ocId
metadata.fileName = "oldFileName.pdf"
metadata.serverUrl = "https://old.example.com"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(metadata)
}
Self.dbManager.renameItemMetadata(
ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName
)
let updatedMetadata = Self.dbManager.itemMetadata(ocId: ocId)
XCTAssertEqual(updatedMetadata?.fileName, newFileName, "File name should be updated")
XCTAssertEqual(updatedMetadata?.serverUrl, newServerUrl, "Server URL should be updated")
}
func testDeleteItemMetadatasBasedOnUpdate() throws {
// Existing metadata in the database
let existingMetadata1 = RealmItemMetadata()
existingMetadata1.ocId = "id-1"
existingMetadata1.fileName = "Existing.pdf"
existingMetadata1.serverUrl = "https://example.com"
existingMetadata1.account = "TestAccount"
existingMetadata1.downloaded = true
existingMetadata1.uploaded = true
let existingMetadata2 = RealmItemMetadata()
existingMetadata2.ocId = "id-2"
existingMetadata2.fileName = "Existing2.pdf"
existingMetadata2.serverUrl = "https://example.com"
existingMetadata2.account = "TestAccount"
existingMetadata2.downloaded = true
existingMetadata2.uploaded = true
let existingMetadata3 = RealmItemMetadata()
existingMetadata3.ocId = "id-3"
existingMetadata3.fileName = "Existing3.pdf"
existingMetadata3.serverUrl = "https://example.com/folder" // Different child path
existingMetadata3.account = "TestAccount"
existingMetadata3.downloaded = true
existingMetadata3.uploaded = true
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(existingMetadata1)
realm.add(existingMetadata2)
realm.add(existingMetadata3)
}
// Simulate updated metadata that leads to a deletion
let updatedMetadatas = [existingMetadata1, existingMetadata3] // Only include 2 of the 3
_ = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: "TestAccount",
serverUrl: "https://example.com",
updatedMetadatas: updatedMetadatas.map { SendableItemMetadata(value: $0) },
keepExistingDownloadState: true
)
let remainingMetadatas = Self.dbManager.itemMetadatas(
account: "TestAccount", underServerUrl: "https://example.com"
)
XCTAssertEqual(remainingMetadatas.filter(\.deleted).count, 1)
XCTAssertEqual(remainingMetadatas.count(where: { !$0.deleted }), 2)
XCTAssertNotNil(remainingMetadatas.first { $0.ocId == "id-1" })
XCTAssertNotNil(remainingMetadatas.first { $0.ocId == "id-3" })
}
func testProcessItemMetadatasToUpdate_NewAndUpdatedSeparation() throws {
let account = Account(
user: "TestAccount", id: "taid", serverUrl: "https://example.com", password: "pass"
)
let parent = RealmItemMetadata()
parent.ocId = "parent"
parent.fileName = "Parent"
parent.account = "TestAccount"
parent.serverUrl = "https://example.com"
parent.directory = true
parent.downloaded = true
parent.uploaded = true
// Simulate existing metadata in the database
let existingMetadata = RealmItemMetadata()
existingMetadata.ocId = "id-1"
existingMetadata.fileName = "File.pdf"
existingMetadata.account = "TestAccount"
existingMetadata.serverUrl = "https://example.com/Parent"
existingMetadata.downloaded = true
existingMetadata.uploaded = true
// Simulate updated metadata that includes changes and a new entry
var updatedParent = SendableItemMetadata(ocId: "parent", fileName: "Parent", account: account)
updatedParent.directory = true
var updatedMetadata =
SendableItemMetadata(ocId: "id-1", fileName: "UpdatedFile.pdf", account: account)
updatedMetadata.serverUrl = "https://example.com/Parent"
var newMetadata =
SendableItemMetadata(ocId: "id-2", fileName: "NewFile.pdf", account: account)
newMetadata.serverUrl = "https://example.com/Parent"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(parent)
realm.add(existingMetadata)
}
let results = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: "TestAccount",
serverUrl: "https://example.com/Parent",
updatedMetadatas: [updatedParent, updatedMetadata, newMetadata],
keepExistingDownloadState: true
)
XCTAssertEqual(results.newMetadatas?.count, 1, "Should create one new metadata")
XCTAssertEqual(results.updatedMetadatas?.count, 2, "Should update two existing metadata")
XCTAssertEqual(
results.newMetadatas?.first?.ocId, "id-2", "New metadata ocId should be 'id-2'"
)
XCTAssertEqual(
results.updatedMetadatas?.last?.fileName,
"UpdatedFile.pdf",
"Updated metadata should have the new file name"
)
}
func testUnuploadedItemsAreNotDeletedDuringUpdate() throws {
let testAccount = Self.account.ncKitAccount
let testServerUrl = Self.account.davFilesUrl
// 1. Item that exists locally and is marked as uploaded
let uploadedItem = RealmItemMetadata()
uploadedItem.ocId = "ocid-uploaded-123"
uploadedItem.fileName = "SyncedFile.txt"
uploadedItem.account = testAccount
uploadedItem.serverUrl = testServerUrl
uploadedItem.downloaded = true
uploadedItem.uploaded = true // IMPORTANT: Marked as uploaded
// 2. Item that exists locally but is NOT marked as uploaded (e.g., new local file)
let unuploadedItem = RealmItemMetadata()
unuploadedItem.ocId = "ocid-local-456" // May or may not have ocId yet
unuploadedItem.fileName = "NewLocalFile.txt"
unuploadedItem.account = testAccount
unuploadedItem.serverUrl = testServerUrl
unuploadedItem.downloaded = true
unuploadedItem.uploaded = false // IMPORTANT: Not marked as uploaded
unuploadedItem.status = Status.normal.rawValue // Ensure it's not in a transient state if relevant
var rootMetadata = SendableItemMetadata(ocId: "root", fileName: "", account: Self.account)
rootMetadata.directory = true
Self.dbManager.addItemMetadata(rootMetadata)
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(uploadedItem)
realm.add(unuploadedItem)
}
XCTAssertEqual(realm.objects(RealmItemMetadata.self).where {
$0.account == testAccount && $0.serverUrl == testServerUrl
}.count, 3)
// Simulate an update from the server that contains NEITHER of these items.
// This means the server thinks 'SyncedFile.txt' was deleted,
// and it doesn't know about 'NewLocalFile.txt' yet.
let updatedMetadatasFromServer = [rootMetadata]
let results = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: testAccount,
serverUrl: testServerUrl,
updatedMetadatas: updatedMetadatasFromServer,
keepExistingDownloadState: true // Value doesn't strictly matter for this test logic
)
// --- Assertion ---
let remainingMetadatas = realm.objects(RealmItemMetadata.self)
.where { $0.account == testAccount && $0.serverUrl == testServerUrl }
// Check the returned delete list (based on the copy made before deletion)
XCTAssertEqual(results.deletedMetadatas?.count, 1, "Should identify the uploaded item as deleted.")
XCTAssertEqual(results.deletedMetadatas?.first?.ocId, "ocid-uploaded-123", "The correct uploaded item should be marked for deletion.")
XCTAssertTrue(results.deletedMetadatas?.first?.uploaded ?? false, "The item marked for deletion should have uploaded=true")
// Check the actual database state after the write transaction
XCTAssertEqual(remainingMetadatas.filter(\.deleted).count, 1)
XCTAssertEqual(remainingMetadatas.count(where: { !$0.deleted }), 2)
let survivingItem = remainingMetadatas.last
XCTAssertNotNil(survivingItem, "An item should survive.")
XCTAssertEqual(survivingItem?.ocId, "ocid-local-456", "The surviving item should be the unuploaded one.")
XCTAssertEqual(survivingItem?.fileName, "NewLocalFile.txt", "Filename should match the unuploaded item.")
XCTAssertFalse(survivingItem!.uploaded, "The surviving item must be the one marked uploaded = false.")
// Check other return values are empty as expected
XCTAssertTrue(results.newMetadatas?.isEmpty ?? true, "No new items should have been created.")
XCTAssertTrue(results.updatedMetadatas?.isEmpty ?? true, "No items should have been updated.")
}
func testDirectoryMetadataRetrieval() throws {
let account = "TestAccount"
let serverUrl = "https://cloud.example.com/files/documents"
let directoryFileName = "documents"
let metadata = RealmItemMetadata()
metadata.ocId = "dir-1"
metadata.account = account
metadata.serverUrl = "https://cloud.example.com/files"
metadata.fileName = directoryFileName
metadata.directory = true
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(metadata)
}
let retrievedMetadata = Self.dbManager.itemMetadata(
account: account, locatedAtRemoteUrl: serverUrl
)
XCTAssertNotNil(retrievedMetadata, "Should retrieve directory metadata")
XCTAssertEqual(
retrievedMetadata?.fileName, directoryFileName, "Should match the directory file name"
)
}
func testChildItemsForDirectory() throws {
let directoryMetadata = RealmItemMetadata()
directoryMetadata.ocId = "dir-1"
directoryMetadata.account = "TestAccount"
directoryMetadata.serverUrl = "https://cloud.example.com/files"
directoryMetadata.fileName = "documents"
directoryMetadata.directory = true
let childMetadata = RealmItemMetadata()
childMetadata.ocId = "item-1"
childMetadata.account = "TestAccount"
childMetadata.serverUrl = "https://cloud.example.com/files/documents"
childMetadata.fileName = "report.pdf"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(directoryMetadata)
realm.add(childMetadata)
}
let children = Self.dbManager.childItems(
directoryMetadata: SendableItemMetadata(value: directoryMetadata)
)
XCTAssertEqual(children.count, 1, "Should return one child item")
XCTAssertEqual(
children.first?.fileName, "report.pdf", "Should match the child item's file name"
)
}
func testDeleteDirectoryAndSubdirectoriesMetadata() throws {
let directoryMetadata = RealmItemMetadata()
directoryMetadata.ocId = "dir-1"
directoryMetadata.account = "TestAccount"
directoryMetadata.serverUrl = "https://cloud.example.com/files"
directoryMetadata.fileName = "documents"
directoryMetadata.directory = true
let childMetadata = RealmItemMetadata()
childMetadata.ocId = "item-1"
childMetadata.account = "TestAccount"
childMetadata.serverUrl = "https://cloud.example.com/files/documents"
childMetadata.fileName = "report.pdf"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(directoryMetadata)
realm.add(childMetadata)
}
let deletedMetadatas = Self.dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: "dir-1"
)
XCTAssertNotNil(deletedMetadatas, "Should return a list of deleted metadatas")
XCTAssertEqual(deletedMetadatas?.count, 2, "Should delete the directory and its child")
}
func testRenameDirectoryAndPropagateToChildren() throws {
let directoryMetadata = RealmItemMetadata()
directoryMetadata.ocId = "dir-1"
directoryMetadata.account = "TestAccount"
directoryMetadata.serverUrl = "https://cloud.example.com/files"
directoryMetadata.fileName = "documents"
directoryMetadata.directory = true
let childMetadata = RealmItemMetadata()
childMetadata.ocId = "item-1"
childMetadata.account = "TestAccount"
childMetadata.serverUrl = "https://cloud.example.com/files/documents"
childMetadata.fileName = "report.pdf"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(directoryMetadata)
realm.add(childMetadata)
}
let updatedChildren = Self.dbManager.renameDirectoryAndPropagateToChildren(
ocId: "dir-1",
newServerUrl: "https://cloud.example.com/office",
newFileName: "files"
)
XCTAssertNotNil(updatedChildren, "Should return updated children metadatas")
XCTAssertEqual(updatedChildren?.count, 1, "Should include one child")
XCTAssertEqual(
updatedChildren?.first?.serverUrl,
"https://cloud.example.com/office/files",
"Should update serverUrl of child items"
)
}
func testErrorOnDirectoryMetadataNotFound() throws {
let nonExistentServerUrl = "https://cloud.example.com/nonexistent"
let directoryMetadata = Self.dbManager.itemMetadata(
account: "TestAccount", locatedAtRemoteUrl: nonExistentServerUrl
)
XCTAssertNil(directoryMetadata, "Should return nil when directory metadata is not found")
}
func testChildItemsForRootDirectory() throws {
let rootMetadata = SendableItemMetadata(
ocId: NSFileProviderItemIdentifier.rootContainer.rawValue,
fileName: "",
account: Account(
user: "TestAccount",
id: "ta",
serverUrl: "https://cloud.example.com/files",
password: ""
)
) // Do not write, we do not track root container
let childMetadata = RealmItemMetadata()
childMetadata.ocId = "item-1"
childMetadata.account = "TestAccount"
childMetadata.serverUrl = rootMetadata.serverUrl
childMetadata.fileName = "report.pdf"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(childMetadata)
}
let children = Self.dbManager.childItems(directoryMetadata: rootMetadata)
XCTAssertEqual(children.count, 1, "Should return one child item for the root directory")
XCTAssertEqual(
children.first?.fileName,
"report.pdf",
"Should match the child item's file name for root directory"
)
}
func testDeleteNestedDirectoriesAndSubdirectoriesMetadata() throws {
// Create nested directories and their child items
let rootDirectoryMetadata = RealmItemMetadata()
rootDirectoryMetadata.ocId = "dir-1"
rootDirectoryMetadata.account = "TestAccount"
rootDirectoryMetadata.serverUrl = "https://cloud.example.com/files"
rootDirectoryMetadata.fileName = "documents"
rootDirectoryMetadata.directory = true
let nestedDirectoryMetadata = RealmItemMetadata()
nestedDirectoryMetadata.ocId = "dir-2"
nestedDirectoryMetadata.account = "TestAccount"
nestedDirectoryMetadata.serverUrl = "https://cloud.example.com/files/documents"
nestedDirectoryMetadata.fileName = "projects"
nestedDirectoryMetadata.directory = true
let childMetadata = RealmItemMetadata()
childMetadata.ocId = "item-1"
childMetadata.account = "TestAccount"
childMetadata.serverUrl = "https://cloud.example.com/files/documents/projects"
childMetadata.fileName = "report.pdf"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(rootDirectoryMetadata)
realm.add(nestedDirectoryMetadata)
realm.add(childMetadata)
}
let deletedMetadatas = Self.dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: "dir-1"
)
XCTAssertNotNil(deletedMetadatas, "Should return a list of deleted metadatas")
XCTAssertEqual(
deletedMetadatas?.count,
3,
"Should delete the root directory, nested directory, and its child"
)
}
func testRecursiveRenameOfDirectoriesAndChildItems() throws {
// Setup a complex directory structure
let rootDirectoryMetadata = RealmItemMetadata()
rootDirectoryMetadata.ocId = "dir-1"
rootDirectoryMetadata.account = "TestAccount"
rootDirectoryMetadata.serverUrl = "https://cloud.example.com/files"
rootDirectoryMetadata.fileName = "documents"
rootDirectoryMetadata.directory = true
let nestedDirectoryMetadata = RealmItemMetadata()
nestedDirectoryMetadata.ocId = "dir-2"
nestedDirectoryMetadata.account = "TestAccount"
nestedDirectoryMetadata.serverUrl = "https://cloud.example.com/files/documents"
nestedDirectoryMetadata.fileName = "projects"
nestedDirectoryMetadata.directory = true
let childMetadata = RealmItemMetadata()
childMetadata.ocId = "item-1"
childMetadata.account = "TestAccount"
childMetadata.serverUrl = "https://cloud.example.com/files/documents/projects"
childMetadata.fileName = "report.pdf"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(rootDirectoryMetadata)
realm.add(nestedDirectoryMetadata)
realm.add(childMetadata)
}
let updatedChildren = Self.dbManager.renameDirectoryAndPropagateToChildren(
ocId: "dir-1",
newServerUrl: "https://cloud.example.com/storage",
newFileName: "files"
)
XCTAssertNotNil(updatedChildren, "Should return updated children metadatas")
XCTAssertEqual(updatedChildren?.count, 2, "Should include the nested directory and child item")
XCTAssertTrue(
updatedChildren?.contains { $0.serverUrl.contains("/storage/files/") } ?? false,
"Should update serverUrl of all child items to reflect new directory path"
)
}
func testDeletingDirectoryWithNoChildren() throws {
let directoryMetadata = RealmItemMetadata()
directoryMetadata.ocId = "dir-1"
directoryMetadata.account = "TestAccount"
directoryMetadata.serverUrl = "https://cloud.example.com/files"
directoryMetadata.fileName = "empty"
directoryMetadata.directory = true
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(directoryMetadata)
}
let deletedMetadatas = Self.dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: "dir-1"
)
XCTAssertNotNil(
deletedMetadatas,
"Should return a list of deleted metadatas even if the directory has no children"
)
XCTAssertEqual(
deletedMetadatas?.count,
1,
"Should only delete the directory itself as there are no children"
)
}
func testRenamingDirectoryWithComplexNestedStructure() throws {
// Create a complex nested directory structure
let rootDirectoryMetadata = RealmItemMetadata()
rootDirectoryMetadata.ocId = "dir-1"
rootDirectoryMetadata.account = "TestAccount"
rootDirectoryMetadata.serverUrl = "https://cloud.example.com/files"
rootDirectoryMetadata.fileName = "dir-1"
rootDirectoryMetadata.directory = true
let nestedDirectoryMetadata = RealmItemMetadata()
nestedDirectoryMetadata.ocId = "dir-2"
nestedDirectoryMetadata.account = "TestAccount"
nestedDirectoryMetadata.serverUrl = "https://cloud.example.com/files/dir-1"
nestedDirectoryMetadata.fileName = "dir-2"
nestedDirectoryMetadata.directory = true
let deepNestedDirectoryMetadata = RealmItemMetadata()
deepNestedDirectoryMetadata.ocId = "dir-3"
deepNestedDirectoryMetadata.account = "TestAccount"
deepNestedDirectoryMetadata.serverUrl = "https://cloud.example.com/files/dir-1/dir-2"
deepNestedDirectoryMetadata.fileName = "dir-3"
deepNestedDirectoryMetadata.directory = true
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(rootDirectoryMetadata)
realm.add(nestedDirectoryMetadata)
realm.add(deepNestedDirectoryMetadata)
}
let updatedChildren = Self.dbManager.renameDirectoryAndPropagateToChildren(
ocId: "dir-1",
newServerUrl: "https://cloud.example.com/storage",
newFileName: "archives"
)
XCTAssertNotNil(updatedChildren, "Should return updated children metadatas")
XCTAssertEqual(updatedChildren?.count, 2, "Should include both nested directories")
XCTAssertTrue(
updatedChildren?.allSatisfy { $0.serverUrl.contains("/storage/archives") } ?? false,
"All children should have their serverUrl updated correctly"
)
}
func testFindingItemBasedOnRemotePath() throws {
let account = "TestAccount"
let filename = "super duper new file"
let parentUrl = "https://cloud.example.com/files/my great and incredible dir/dir-2"
let fullUrl = parentUrl + "/" + filename
let deepNestedDirectoryMetadata = RealmItemMetadata()
deepNestedDirectoryMetadata.ocId = filename
deepNestedDirectoryMetadata.account = account
deepNestedDirectoryMetadata.serverUrl = parentUrl
deepNestedDirectoryMetadata.fileName = filename
deepNestedDirectoryMetadata.directory = true
let realm = Self.dbManager.ncDatabase()
try realm.write { realm.add(deepNestedDirectoryMetadata) }
XCTAssertNotNil(Self.dbManager.itemMetadata(account: account, locatedAtRemoteUrl: fullUrl))
}
func testKeepDownloadedSetting() throws {
let existingMetadata = RealmItemMetadata()
existingMetadata.ocId = "id-1"
existingMetadata.fileName = "File.pdf"
existingMetadata.account = "TestAccount"
existingMetadata.serverUrl = "https://example.com"
XCTAssertFalse(existingMetadata.keepDownloaded)
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(existingMetadata)
}
let sendable = SendableItemMetadata(value: existingMetadata)
var updatedMetadata =
try XCTUnwrap(Self.dbManager.set(keepDownloaded: true, for: sendable))
XCTAssertTrue(updatedMetadata.keepDownloaded)
updatedMetadata.keepDownloaded = false
let finalMetadata =
try XCTUnwrap(Self.dbManager.set(keepDownloaded: false, for: updatedMetadata))
XCTAssertFalse(finalMetadata.keepDownloaded)
}
func testKeepDownloadedRetainedDuringDepth1ReadUpdate() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
// Create initial metadata with keepDownloaded = true
var initialMetadata = SendableItemMetadata(ocId: "test-keep-downloaded", fileName: "test.txt", account: account)
initialMetadata.downloaded = true
initialMetadata.uploaded = true
initialMetadata.keepDownloaded = true
initialMetadata.etag = "old-etag"
Self.dbManager.addItemMetadata(initialMetadata)
// Verify initial state
let storedMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
XCTAssertTrue(storedMetadata.keepDownloaded, "Initial keepDownloaded should be true")
XCTAssertTrue(storedMetadata.downloaded, "Initial downloaded should be true")
// Update metadata with new etag (simulating server update)
var updatedMetadata = initialMetadata
updatedMetadata.etag = "new-etag"
updatedMetadata.keepDownloaded = false // This would be the case when converting from NKFile
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [updatedMetadata],
keepExistingDownloadState: true
)
XCTAssertEqual(result.newMetadatas?.isEmpty, true, "Should create no new metadatas")
XCTAssertEqual(result.updatedMetadatas?.isEmpty, false, "Should update existing metadata")
// Verify keepDownloaded is retained
let finalMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
XCTAssertTrue(finalMetadata.keepDownloaded, "keepDownloaded should be retained during update")
XCTAssertTrue(finalMetadata.downloaded, "downloaded should be retained during update")
XCTAssertEqual(finalMetadata.etag, "new-etag", "etag should be updated")
// Verify in database
let dbMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
XCTAssertTrue(dbMetadata.keepDownloaded, "keepDownloaded should be retained in database")
}
func testKeepDownloadedRetainedWithKeepExistingDownloadStateFalse() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
// Create initial metadata with keepDownloaded = true
var initialMetadata = SendableItemMetadata(ocId: "test-keep-downloaded-false", fileName: "test.txt", account: account)
initialMetadata.downloaded = true
initialMetadata.uploaded = true
initialMetadata.keepDownloaded = true
initialMetadata.etag = "old-etag"
Self.dbManager.addItemMetadata(initialMetadata)
// Update metadata with new etag
var updatedMetadata = initialMetadata
updatedMetadata.etag = "new-etag"
updatedMetadata.keepDownloaded = false // This would be the case when converting from NKFile
updatedMetadata.downloaded = false // Set to false to test keepDownloaded retention
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [updatedMetadata],
keepExistingDownloadState: false // Even when not keeping download state
)
XCTAssertEqual(result.updatedMetadatas?.isEmpty, false, "Should update existing metadata")
// Verify keepDownloaded is still retained even when keepExistingDownloadState is false
let finalMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
XCTAssertTrue(finalMetadata.keepDownloaded, "keepDownloaded should be retained regardless of keepExistingDownloadState")
XCTAssertEqual(finalMetadata.downloaded, false, "downloaded should not be retained when keepExistingDownloadState is false")
}
func testKeepDownloadedRetainedInReadTargetMetadata() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
// Create existing metadata with keepDownloaded = true
var existingMetadata = SendableItemMetadata(ocId: "read-target-test", fileName: "target.txt", account: account)
existingMetadata.keepDownloaded = true
existingMetadata.downloaded = true
existingMetadata.status = Status.normal.rawValue
Self.dbManager.addItemMetadata(existingMetadata)
// Create new read target metadata (simulating reading from server)
var readTargetMetadata = SendableItemMetadata(ocId: "read-target-test", fileName: "target.txt", account: account)
readTargetMetadata.etag = "new-etag"
readTargetMetadata.keepDownloaded = false // Would be false when created from NKFile
readTargetMetadata.downloaded = false
// This simulates the path in depth1ReadUpdateItemMetadatas where readTargetMetadata
// is processed and existing properties should be retained
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [readTargetMetadata],
keepExistingDownloadState: true
)
let updatedMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
XCTAssertTrue(updatedMetadata.keepDownloaded, "keepDownloaded should be retained in read target metadata")
XCTAssertTrue(updatedMetadata.downloaded, "downloaded should be retained when keepExistingDownloadState is true")
}
func testKeepDownloadedNotSetForNewMetadata() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
// Create completely new metadata (not existing in database)
var newMetadata = SendableItemMetadata(ocId: "new-item", fileName: "new.txt", account: account)
newMetadata.etag = "initial-etag"
newMetadata.keepDownloaded = false // Should remain false for new items
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [newMetadata],
keepExistingDownloadState: true
)
XCTAssertEqual(result.newMetadatas?.isEmpty, false, "Should create new metadata")
XCTAssertEqual(result.updatedMetadatas?.isEmpty, true, "Should not update any metadata")
let createdMetadata = try XCTUnwrap(result.newMetadatas?.first)
XCTAssertFalse(createdMetadata.keepDownloaded, "keepDownloaded should remain false for new items")
// Verify in database
let dbMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "new-item"))
XCTAssertFalse(dbMetadata.keepDownloaded, "keepDownloaded should be false in database for new items")
}
func testKeepDownloadedRetainedWithMultipleItems() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
var parentFolder = SendableItemMetadata(ocId: "pf", fileName: "pf", account: account)
parentFolder.uploaded = true
parentFolder.etag = "old-pf"
// Create multiple items with different keepDownloaded states
var item1 = SendableItemMetadata(ocId: "multi-1", fileName: "file1.txt", account: account)
item1.keepDownloaded = true
item1.downloaded = true
item1.uploaded = true
item1.etag = "old-1"
item1.serverUrl = account.davFilesUrl.appending("/pf")
var item2 = SendableItemMetadata(ocId: "multi-2", fileName: "file2.txt", account: account)
item2.keepDownloaded = false
item2.downloaded = false
item2.uploaded = true
item2.etag = "old-2"
item2.serverUrl = account.davFilesUrl.appending("/pf")
var item3 = SendableItemMetadata(ocId: "multi-3", fileName: "file3.txt", account: account)
item3.keepDownloaded = true
item3.downloaded = false
item3.uploaded = true
item3.etag = "old-3"
item3.serverUrl = account.davFilesUrl.appending("/pf")
Self.dbManager.addItemMetadata(parentFolder)
Self.dbManager.addItemMetadata(item1)
Self.dbManager.addItemMetadata(item2)
Self.dbManager.addItemMetadata(item3)
// Update all items with new etags
var updatedParentFolder = parentFolder
updatedParentFolder.etag = "new-pf"
var updatedItem1 = item1
updatedItem1.etag = "new-1"
updatedItem1.keepDownloaded = false // Would be reset when converting from NKFile
var updatedItem2 = item2
updatedItem2.etag = "new-2"
updatedItem2.keepDownloaded = false
var updatedItem3 = item3
updatedItem3.etag = "new-3"
updatedItem3.keepDownloaded = false
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl.appending("/pf"),
updatedMetadatas: [updatedParentFolder, updatedItem1, updatedItem2, updatedItem3],
keepExistingDownloadState: true
)
XCTAssertEqual(result.updatedMetadatas?.count, 4, "Should update all four items")
// Verify each item's keepDownloaded state is correctly retained
let updatedMetadatas = try XCTUnwrap(result.updatedMetadatas)
let finalItem1 = try XCTUnwrap(updatedMetadatas.first { $0.ocId == "multi-1" })
XCTAssertTrue(finalItem1.keepDownloaded, "Item 1 should retain keepDownloaded = true")
let finalItem2 = try XCTUnwrap(updatedMetadatas.first { $0.ocId == "multi-2" })
XCTAssertFalse(finalItem2.keepDownloaded, "Item 2 should retain keepDownloaded = false")
let finalItem3 = try XCTUnwrap(updatedMetadatas.first { $0.ocId == "multi-3" })
XCTAssertTrue(finalItem3.keepDownloaded, "Item 3 should retain keepDownloaded = true")
}
func testParentItemIdentifierWithRemoteFallback() async throws {
let rootItem = MockRemoteItem.rootItem(account: Self.account)
let remoteFolder = MockRemoteItem(
identifier: "folder",
versionIdentifier: "NEW",
name: "folder",
remotePath: Self.account.davFilesUrl + "/folder",
directory: true,
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
let remoteItem = MockRemoteItem(
identifier: "item",
versionIdentifier: "NEW",
name: "item",
remotePath: Self.account.davFilesUrl + "/folder/item",
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
rootItem.children = [remoteFolder]
remoteFolder.parent = rootItem
remoteFolder.children = [remoteItem]
remoteItem.parent = remoteFolder
let remoteItemMetadata = remoteItem.toItemMetadata(account: Self.account)
Self.dbManager.addItemMetadata(remoteItemMetadata)
XCTAssertNil(Self.dbManager.parentItemIdentifierFromMetadata(remoteItemMetadata))
let remoteInterface = MockRemoteInterface(account: Self.account, rootItem: rootItem)
remoteInterface.injectMock(Self.account)
let retrievedParentIdentifier = await Self.dbManager.parentItemIdentifierWithRemoteFallback(
fromMetadata: remoteItemMetadata,
remoteInterface: remoteInterface,
account: Self.account
)
let unwrappedParentIdentifier = try XCTUnwrap(retrievedParentIdentifier)
XCTAssertEqual(unwrappedParentIdentifier.rawValue, remoteFolder.identifier)
}
func testMaterialisedFiles() async throws {
let itemA = RealmItemMetadata()
let itemB = RealmItemMetadata()
let itemC = RealmItemMetadata()
let folderA = RealmItemMetadata()
let folderB = RealmItemMetadata()
let folderC = RealmItemMetadata()
let notFolderA = RealmItemMetadata()
let notFolderB = RealmItemMetadata()
folderA.directory = true
folderB.directory = true
folderC.directory = true
itemA.ocId = "itemA"
itemB.ocId = "itemB"
itemC.ocId = "itemC"
folderA.ocId = "folderA"
folderB.ocId = "folderB"
folderC.ocId = "folderC"
notFolderA.ocId = "notFolderA"
notFolderB.ocId = "notFolderB"
itemA.account = Self.account.ncKitAccount
itemB.account = Self.account.ncKitAccount
itemC.account = "another account"
folderA.account = Self.account.ncKitAccount
folderB.account = Self.account.ncKitAccount
folderC.account = "another account"
notFolderA.account = Self.account.ncKitAccount
notFolderB.account = "another account"
itemA.downloaded = true
itemB.downloaded = false
itemC.downloaded = true
folderA.visitedDirectory = true
folderB.visitedDirectory = false
folderC.visitedDirectory = true
notFolderA.visitedDirectory = true
notFolderB.visitedDirectory = true
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(itemA)
realm.add(itemB)
realm.add(itemC)
realm.add(folderA)
realm.add(folderB)
realm.add(folderC)
}
// Test with addItemMetadata too
var sItemA = SendableItemMetadata(ocId: "sItemA", fileName: "sItemA", account: Self.account)
sItemA.downloaded = true
var sItemB = SendableItemMetadata(ocId: "sItemB", fileName: "sItemB", account: Self.account)
sItemB.downloaded = false
var sItemC = SendableItemMetadata(ocId: "sItemC", fileName: "sItemC", account: Self.account)
sItemC.downloaded = true
var sDirD = SendableItemMetadata(ocId: "sDirD", fileName: "sDirD", account: Self.account)
sDirD.directory = true
sDirD.visitedDirectory = true
Self.dbManager.addItemMetadata(sItemA)
Self.dbManager.addItemMetadata(sItemB)
Self.dbManager.addItemMetadata(sItemC)
Self.dbManager.addItemMetadata(sDirD)
let materialized =
Self.dbManager.materialisedItemMetadatas(account: Self.account.ncKitAccount)
XCTAssertEqual(materialized.count, 5)
let materialisedOcIds = materialized.map(\.ocId)
XCTAssertTrue(materialisedOcIds.contains(itemA.ocId))
XCTAssertTrue(materialisedOcIds.contains(folderA.ocId))
XCTAssertTrue(materialisedOcIds.contains(sItemA.ocId))
XCTAssertTrue(materialisedOcIds.contains(sItemC.ocId))
XCTAssertTrue(materialisedOcIds.contains(sDirD.ocId))
}
func testKeepDownloadedRetainedDuringUpdate() throws {
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
// Create initial metadata with keepDownloaded = true
var initialMetadata = SendableItemMetadata(ocId: "test-keep-downloaded", fileName: "test.txt", account: account)
initialMetadata.downloaded = true
initialMetadata.uploaded = true
initialMetadata.keepDownloaded = true
initialMetadata.etag = "old-etag"
Self.dbManager.addItemMetadata(initialMetadata)
// Verify initial state
let storedMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
XCTAssertTrue(storedMetadata.keepDownloaded, "Initial keepDownloaded should be true")
XCTAssertTrue(storedMetadata.downloaded, "Initial downloaded should be true")
// Update metadata with new etag (simulating server update)
var updatedMetadata = initialMetadata
updatedMetadata.etag = "new-etag"
updatedMetadata.keepDownloaded = false // This would be the case when converting from NKFile
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
account: account.ncKitAccount,
serverUrl: account.davFilesUrl,
updatedMetadatas: [updatedMetadata],
keepExistingDownloadState: true
)
XCTAssertEqual(result.newMetadatas?.isEmpty, true, "Should create no new metadatas")
XCTAssertEqual(result.updatedMetadatas?.isEmpty, false, "Should update existing metadata")
// Verify keepDownloaded is retained
let finalMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
XCTAssertTrue(finalMetadata.keepDownloaded, "keepDownloaded should be retained during update")
XCTAssertTrue(finalMetadata.downloaded, "downloaded should be retained during update")
XCTAssertEqual(finalMetadata.etag, "new-etag", "etag should be updated")
// Verify in database
let dbMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
XCTAssertTrue(dbMetadata.keepDownloaded, "keepDownloaded should be retained in database")
}
func testPendingWorkingSetChanges() {
// 1. Arrange
let anchorDate = Date().addingTimeInterval(-300) // 5 minutes ago
let oldSyncDate = Date().addingTimeInterval(-600) // 10 minutes ago (before anchor)
let recentSyncDate = Date() // Now (after anchor)
// --- Multi-level file structure to test the full scope ---
// LEVEL 1: Root level materialized folder updated recently
var updatedDir = SendableItemMetadata(ocId: "updatedDir", fileName: "UpdatedDir", account: Self.account)
updatedDir.directory = true
updatedDir.visitedDirectory = true // Materialised
updatedDir.syncTime = recentSyncDate
Self.dbManager.addItemMetadata(updatedDir)
// LEVEL 2: Child of updated folder with OLD sync time - should NOT be included
var childOfUpdatedDirOld = SendableItemMetadata(ocId: "childOfUpdatedOld", fileName: "childOld.txt", account: Self.account)
childOfUpdatedDirOld.serverUrl = Self.account.davFilesUrl + "/UpdatedDir"
childOfUpdatedDirOld.syncTime = oldSyncDate // Old sync time
childOfUpdatedDirOld.downloaded = true // Materialised
Self.dbManager.addItemMetadata(childOfUpdatedDirOld)
// LEVEL 2: Child of updated folder with RECENT sync time - should be included (regardless of materialisation)
var childOfUpdatedDirRecent = SendableItemMetadata(ocId: "childOfUpdatedRecent", fileName: "childRecent.txt", account: Self.account)
childOfUpdatedDirRecent.serverUrl = Self.account.davFilesUrl + "/UpdatedDir"
childOfUpdatedDirRecent.syncTime = recentSyncDate // Recent sync time
childOfUpdatedDirRecent.downloaded = false // NOT materialized - but should still be included
Self.dbManager.addItemMetadata(childOfUpdatedDirRecent)
// LEVEL 2: Child folder of updated folder with recent sync time
var childFolderOfUpdated = SendableItemMetadata(ocId: "childFolderOfUpdated", fileName: "ChildFolder", account: Self.account)
childFolderOfUpdated.directory = true
childFolderOfUpdated.visitedDirectory = false // Not materialised
childFolderOfUpdated.serverUrl = Self.account.davFilesUrl + "/UpdatedDir"
childFolderOfUpdated.syncTime = recentSyncDate
Self.dbManager.addItemMetadata(childFolderOfUpdated)
// LEVEL 3: Grandchild with recent sync time - should not be included
var grandchildOfUpdated = SendableItemMetadata(ocId: "grandchildOfUpdated", fileName: "grandchild.txt", account: Self.account)
grandchildOfUpdated.serverUrl = Self.account.davFilesUrl + "/UpdatedDir/ChildFolder"
grandchildOfUpdated.syncTime = recentSyncDate
grandchildOfUpdated.downloaded = false // Not materialised
Self.dbManager.addItemMetadata(grandchildOfUpdated)
// DELETED STRUCTURE: Root level materialized folder deleted recently
var deletedDir = SendableItemMetadata(ocId: "deletedDir", fileName: "DeletedDir", account: Self.account)
deletedDir.directory = true
deletedDir.visitedDirectory = true // Materialised
deletedDir.deleted = true
deletedDir.syncTime = recentSyncDate
Self.dbManager.addItemMetadata(deletedDir)
// Child of deleted folder with OLD sync time - should NOT be included
var childOfDeletedDirOld = SendableItemMetadata(ocId: "childOfDeletedOld", fileName: "childDelOld.txt", account: Self.account)
childOfDeletedDirOld.serverUrl = Self.account.davFilesUrl + "/DeletedDir"
childOfDeletedDirOld.syncTime = oldSyncDate // Old sync time
childOfDeletedDirOld.downloaded = true
Self.dbManager.addItemMetadata(childOfDeletedDirOld)
// Child of deleted folder with RECENT sync time - should be included in deleted
var childOfDeletedDirRecent = SendableItemMetadata(ocId: "childOfDeletedRecent", fileName: "childDelRecent.txt", account: Self.account)
childOfDeletedDirRecent.serverUrl = Self.account.davFilesUrl + "/DeletedDir"
childOfDeletedDirRecent.syncTime = recentSyncDate
childOfDeletedDirRecent.downloaded = false // Not materialised
Self.dbManager.addItemMetadata(childOfDeletedDirRecent)
// Nested structure under deleted folder
var nestedFolderInDeleted = SendableItemMetadata(ocId: "nestedFolderInDeleted", fileName: "NestedFolder", account: Self.account)
nestedFolderInDeleted.directory = true
nestedFolderInDeleted.visitedDirectory = false
nestedFolderInDeleted.serverUrl = Self.account.davFilesUrl + "/DeletedDir"
nestedFolderInDeleted.syncTime = recentSyncDate
Self.dbManager.addItemMetadata(nestedFolderInDeleted)
// Deep nested item under deleted structure
var deepNestedInDeleted = SendableItemMetadata(ocId: "deepNestedInDeleted", fileName: "deepNested.txt", account: Self.account)
deepNestedInDeleted.serverUrl = Self.account.davFilesUrl + "/DeletedDir/NestedFolder"
deepNestedInDeleted.syncTime = recentSyncDate
deepNestedInDeleted.downloaded = false
Self.dbManager.addItemMetadata(deepNestedInDeleted)
// STANDALONE ITEMS: materialized file synced recently - should be returned
var standaloneUpdatedFile = SendableItemMetadata(ocId: "standaloneUpdated", fileName: "standalone.txt", account: Self.account)
standaloneUpdatedFile.downloaded = true // Materialised
standaloneUpdatedFile.syncTime = recentSyncDate
Self.dbManager.addItemMetadata(standaloneUpdatedFile)
// materialized file synced too long ago - should NOT be returned
var standaloneOldFile = SendableItemMetadata(ocId: "standaloneOld", fileName: "old.txt", account: Self.account)
standaloneOldFile.downloaded = true // Materialised
standaloneOldFile.syncTime = oldSyncDate
Self.dbManager.addItemMetadata(standaloneOldFile)
// Non-materialised item synced recently - should NOT be returned (not in initial materialized set)
var nonMaterialisedFile = SendableItemMetadata(ocId: "nonMaterialised", fileName: "non-mat.txt", account: Self.account)
nonMaterialisedFile.downloaded = false
nonMaterialisedFile.syncTime = recentSyncDate
Self.dbManager.addItemMetadata(nonMaterialisedFile)
// MIXED MATERIALISATION: Another materialized folder to test child inclusion
var anotherMaterialisedDir = SendableItemMetadata(ocId: "anotherMatDir", fileName: "AnotherMatDir", account: Self.account)
anotherMaterialisedDir.directory = true
anotherMaterialisedDir.visitedDirectory = true
anotherMaterialisedDir.syncTime = recentSyncDate
Self.dbManager.addItemMetadata(anotherMaterialisedDir)
// Child with recent sync but NOT materialized - should still be included due to recent sync
var nonMatChildRecent = SendableItemMetadata(ocId: "nonMatChildRecent", fileName: "nonMatChild.txt", account: Self.account)
nonMatChildRecent.serverUrl = Self.account.davFilesUrl + "/AnotherMatDir"
nonMatChildRecent.syncTime = recentSyncDate
nonMatChildRecent.downloaded = false // Not materialised
Self.dbManager.addItemMetadata(nonMatChildRecent)
// 2. Act
let result = Self.dbManager.pendingWorkingSetChanges(
account: Self.account, since: anchorDate
)
// 3. Assert - Updated items
let updatedIds = Set(result.updated.map(\.ocId))
// Should include materialized items with recent sync
XCTAssertTrue(updatedIds.contains("updatedDir"), "Updated materialized directory should be included")
XCTAssertTrue(updatedIds.contains("standaloneUpdated"), "Updated materialized file should be included")
XCTAssertTrue(updatedIds.contains("anotherMatDir"), "Another materialized directory should be included")
// Should include children with recent sync regardless of materialisation
XCTAssertTrue(updatedIds.contains("childOfUpdatedRecent"), "Child with recent sync should be included regardless of materialisation")
XCTAssertTrue(updatedIds.contains("childFolderOfUpdated"), "Child folder with recent sync should be included")
XCTAssertTrue(updatedIds.contains("nonMatChildRecent"), "Non-materialised child with recent sync should be included")
// Should NOT include items with old sync times
XCTAssertFalse(updatedIds.contains("childOfUpdatedOld"), "Child with old sync time should NOT be included")
XCTAssertFalse(updatedIds.contains("standaloneOld"), "Materialised file with old sync should NOT be included")
// Should NOT include non-materialised items not under a recently updated path
XCTAssertFalse(updatedIds.contains("nonMaterialised"), "Standalone non-materialised item should NOT be included")
// 4. Assert - Deleted items
let deletedIds = Set(result.deleted.map(\.ocId))
// Should include the deleted materialized directory
XCTAssertTrue(deletedIds.contains("deletedDir"), "Deleted materialized directory should be included")
// Should include children/descendants with recent sync under deleted paths
XCTAssertTrue(deletedIds.contains("childOfDeletedRecent"), "Child of deleted dir with recent sync should be included")
XCTAssertTrue(deletedIds.contains("nestedFolderInDeleted"), "Nested folder under deleted dir should be included")
XCTAssertTrue(deletedIds.contains("deepNestedInDeleted"), "Deep nested item under deleted structure should be included")
// Should NOT include children with old sync times
XCTAssertFalse(deletedIds.contains("childOfDeletedOld"), "Child of deleted dir with old sync should NOT be included")
// 5. Verify expected counts
let expectedUpdatedCount = 6 // updatedDir, standaloneUpdated, anotherMatDir, childOfUpdatedRecent, childFolderOfUpdated, nonMatChildRecent
let expectedDeletedCount = 4 // deletedDir, childOfDeletedRecent, nestedFolderInDeleted, deepNestedInDeleted
XCTAssertEqual(updatedIds.count, expectedUpdatedCount, "Should have \(expectedUpdatedCount) updated items, got \(updatedIds.count): \(updatedIds)")
XCTAssertEqual(deletedIds.count, expectedDeletedCount, "Should have \(expectedDeletedCount) deleted items, got \(deletedIds.count): \(deletedIds)")
}
}