Files
noo/ios/RunnerTests/RunnerTests.swift
T
Ayushya AmitabhandClaude Sonnet 5.5 22a6602f08 Security locks work independently; other-account avatars flush right in the avatar menu
Login lock, lock account switching and lock hidden files no longer depend on
each other: passGate prompts on its own flag, the two sub-locks can be set
with login lock off, and disabling login lock leaves them alone. Turning any
lock on or off asks for device auth (on also checks the device can). The iOS
Share Extension's unlock rules follow. The avatar dropdown's additional
accounts lose the 44px spacer so their avatars sit at the right edge.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 20:07:28 -04:00

251 lines
11 KiB
Swift

import Flutter
import UIKit
import XCTest
@testable import Runner
class RunnerTests: XCTestCase {
func testFileURLEncodesEachSegment() {
let url = WebDAV.fileURL(
serverUrl: "https://cloud.example.com", username: "alice",
remotePath: "/Tax Forms/2026 #1/a&b.pdf")
XCTAssertEqual(
url?.absoluteString,
"https://cloud.example.com/remote.php/dav/files/alice/Tax%20Forms/2026%20%231/a&b.pdf")
}
func testFileURLKeepsServerSubPathAndDropsTrailingSlash() {
let url = WebDAV.fileURL(
serverUrl: "https://example.com/nextcloud/", username: "bob@home", remotePath: "docs/x.txt")
XCTAssertEqual(
url?.absoluteString,
"https://example.com/nextcloud/remote.php/dav/files/bob@home/docs/x.txt")
}
func testFileURLForRoot() {
XCTAssertEqual(
WebDAV.fileURL(serverUrl: "https://c.io", username: "u", remotePath: "/")?.absoluteString,
"https://c.io/remote.php/dav/files/u")
}
func testJoinUsesOneSlash() {
XCTAssertEqual(WebDAV.join("/", "a.txt"), "/a.txt")
XCTAssertEqual(WebDAV.join("/docs", "a.txt"), "/docs/a.txt")
XCTAssertEqual(WebDAV.join("/docs/", "a.txt"), "/docs/a.txt")
}
func testUniqueURLNumbersCollisions() throws {
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("noo-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
XCTAssertEqual(LocalFiles.uniqueURL(in: dir, name: "a.txt").lastPathComponent, "a.txt")
FileManager.default.createFile(atPath: dir.appendingPathComponent("a.txt").path, contents: nil)
XCTAssertEqual(LocalFiles.uniqueURL(in: dir, name: "a.txt").lastPathComponent, "a (1).txt")
FileManager.default.createFile(atPath: dir.appendingPathComponent("a (1).txt").path, contents: nil)
XCTAssertEqual(LocalFiles.uniqueURL(in: dir, name: "a.txt").lastPathComponent, "a (2).txt")
FileManager.default.createFile(atPath: dir.appendingPathComponent("README").path, contents: nil)
XCTAssertEqual(LocalFiles.uniqueURL(in: dir, name: "README").lastPathComponent, "README (1)")
}
// MARK: - SharedInbox (Share Extension hand-off)
private func makeRoot() throws -> URL {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("noo-inbox-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
addTeardownBlock { try? FileManager.default.removeItem(at: root) }
return root
}
private func makeSharedFile(in root: URL, name: String, contents: String = "hi") throws -> SharedItem {
let batch = try SharedInbox.makeBatchDirectory(in: root)
let file = batch.appendingPathComponent(name)
try contents.write(to: file, atomically: true, encoding: .utf8)
return SharedItem(path: file.path, name: name, mimeType: "text/plain", size: Int64(contents.utf8.count))
}
func testInboxConsumeReturnsAppendedItemsOnceThenNothing() throws {
let root = try makeRoot()
let a = try makeSharedFile(in: root, name: "a.txt")
let b = try makeSharedFile(in: root, name: "b.txt")
try SharedInbox.append([a], in: root)
try SharedInbox.append([b], in: root)
XCTAssertEqual(SharedInbox.consume(in: root), [a, b])
XCTAssertEqual(SharedInbox.consume(in: root), [], "each share is delivered once")
}
func testInboxConsumeDropsFilesThatNoLongerExist() throws {
let root = try makeRoot()
let kept = try makeSharedFile(in: root, name: "kept.txt")
let gone = try makeSharedFile(in: root, name: "gone.txt")
try SharedInbox.append([kept, gone], in: root)
try FileManager.default.removeItem(atPath: gone.path)
XCTAssertEqual(SharedInbox.consume(in: root), [kept])
}
func testInboxConsumeWithNothingPendingIsEmpty() throws {
XCTAssertEqual(SharedInbox.consume(in: try makeRoot()), [])
}
func testInboxAppendOfNothingCreatesNoManifest() throws {
let root = try makeRoot()
try SharedInbox.append([], in: root)
XCTAssertEqual(SharedInbox.consume(in: root), [])
}
// MARK: - DavFolderParser (Share Extension folder picker)
private func multistatus(_ responses: [(href: String, collection: Bool)]) -> Data {
let body = responses.map { r in
"""
<d:response><d:href>\(r.href)</d:href><d:propstat><d:prop>\
<d:resourcetype>\(r.collection ? "<d:collection/>" : "")</d:resourcetype>\
</d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response>
"""
}.joined()
return Data(
"<?xml version=\"1.0\"?><d:multistatus xmlns:d=\"DAV:\">\(body)</d:multistatus>".utf8)
}
func testFoldersListsOnlyChildFoldersSortedWithoutHidden() {
let xml = multistatus([
("/remote.php/dav/files/alice/Docs/", true),
("/remote.php/dav/files/alice/Docs/zeta/", true),
("/remote.php/dav/files/alice/Docs/Tax%20Forms/", true),
("/remote.php/dav/files/alice/Docs/note.txt", false),
("/remote.php/dav/files/alice/Docs/.git/", true),
])
XCTAssertEqual(
DavFolderParser.folders(from: xml, excluding: "/Docs"),
[DavFolder(name: "Tax Forms", path: "/Docs/Tax Forms"), DavFolder(name: "zeta", path: "/Docs/zeta")])
XCTAssertEqual(
DavFolderParser.folders(from: xml, excluding: "/Docs", hidden: .include).map(\.name),
[".git", "Tax Forms", "zeta"])
XCTAssertEqual(
DavFolderParser.folders(from: xml, excluding: "/Docs", hidden: .only).map(\.name),
[".git"])
}
func testHiddenFilterTreatsAnyDotSegmentAsHidden() {
XCTAssertTrue(HiddenFilter.isHidden(path: "/.cache"))
XCTAssertTrue(HiddenFilter.isHidden(path: "/.cache/inside/deeper"), "children of a hidden folder are hidden")
XCTAssertFalse(HiddenFilter.isHidden(path: "/Docs/Tax Forms"))
XCTAssertTrue(HiddenFilter.hide.shows(path: "/Docs"))
XCTAssertFalse(HiddenFilter.hide.shows(path: "/.git"))
XCTAssertTrue(HiddenFilter.only.shows(path: "/.git/hooks"))
XCTAssertFalse(HiddenFilter.only.shows(path: "/Docs"))
XCTAssertTrue(HiddenFilter.include.shows(path: "/Docs"))
XCTAssertEqual(HiddenFilter(raw: "include"), .include)
XCTAssertEqual(HiddenFilter(raw: "nonsense"), .hide, "unknown values fall back to hiding")
}
func testFoldersAtRootAndUnderServerSubPath() {
let xml = multistatus([
("/nextcloud/remote.php/dav/files/bob/", true),
("/nextcloud/remote.php/dav/files/bob/Photos/", true),
])
XCTAssertEqual(
DavFolderParser.folders(from: xml, excluding: "/"),
[DavFolder(name: "Photos", path: "/Photos")])
}
func testRelativePath() {
XCTAssertEqual(DavFolderParser.relativePath(fromHref: "/remote.php/dav/files/a/B%20C/D/"), "/B C/D")
XCTAssertEqual(DavFolderParser.relativePath(fromHref: "/remote.php/dav/files/a/"), "/")
XCTAssertNil(DavFolderParser.relativePath(fromHref: "/somewhere/else"))
}
func testFoldersIgnoresGarbage() {
XCTAssertEqual(DavFolderParser.folders(from: Data("not xml".utf8), excluding: "/"), [])
}
// MARK: - Shared account (Keychain group shared with the extension)
private func account(_ id: String, hidden: String = "hide") -> SharedAccount {
SharedAccount(
id: id, serverUrl: "https://\(id).example.com", username: id,
authHeader: "Basic \(id)", displayName: "\(id)@\(id).example.com", hiddenFilter: hidden)
}
func testSharedAccountsRoundTripThroughTheKeychain() throws {
let accounts = SharedAccounts(
accounts: [account("alice", hidden: "include"), account("bob")], activeId: "bob",
loginLockEnabled: true, lockAccountSwitching: true, lockHiddenFiles: false)
addTeardownBlock { SharedAccountStore.clear() }
SharedAccountStore.clear()
XCTAssertNil(SharedAccountStore.load())
try SharedAccountStore.save(accounts)
XCTAssertEqual(SharedAccountStore.load(), accounts)
let replacement = SharedAccounts(
accounts: [account("carol")], activeId: "carol",
loginLockEnabled: false, lockAccountSwitching: false, lockHiddenFiles: false)
try SharedAccountStore.save(replacement)
XCTAssertEqual(SharedAccountStore.load(), replacement, "saving replaces, never duplicates")
SharedAccountStore.clear()
XCTAssertNil(SharedAccountStore.load())
}
func testSharedAccountsActiveFallsBackToTheFirstAccount() {
var shared = SharedAccounts(
accounts: [account("alice"), account("bob")], activeId: "bob",
loginLockEnabled: false, lockAccountSwitching: false, lockHiddenFiles: false)
XCTAssertEqual(shared.active?.id, "bob")
shared.activeId = "gone"
XCTAssertEqual(shared.active?.id, "alice")
shared.activeId = nil
XCTAssertEqual(shared.active?.id, "alice")
}
func testUnlockRulesAreIndependentOfLoginLock() {
func rules(master: Bool, switching: Bool, hidden: Bool) -> (Bool, Bool) {
let shared = SharedAccounts(
accounts: [account("a")], activeId: "a",
loginLockEnabled: master, lockAccountSwitching: switching, lockHiddenFiles: hidden)
return (shared.needsUnlockToSwitchAccount, shared.needsUnlockForHidden)
}
XCTAssertTrue(rules(master: false, switching: true, hidden: false) == (true, false))
XCTAssertTrue(rules(master: false, switching: false, hidden: true) == (false, true))
XCTAssertTrue(rules(master: true, switching: false, hidden: false) == (false, false))
XCTAssertTrue(rules(master: true, switching: true, hidden: true) == (true, true))
}
// MARK: - TransferBatchStore
func testBatchSummaryFiresOnlyOnTheLastFile() {
let batchId = UUID().uuidString
TransferBatchStore.save(
TransferBatch(kind: .upload, remoteFolder: "/Docs", total: 3, succeeded: 0, failed: 0), id: batchId)
let info = TransferTaskInfo(kind: .upload, batch: batchId, name: "a", remoteFolder: "/Docs", stagedPath: nil)
XCTAssertNil(TransferBatchStore.record(info, success: true))
XCTAssertNil(TransferBatchStore.record(info, success: false))
let finished = TransferBatchStore.record(info, success: true)
XCTAssertEqual(finished?.succeeded, 2)
XCTAssertEqual(finished?.failed, 1)
XCTAssertEqual(finished?.remoteFolder, "/Docs")
}
func testFinishedUploadFoldersAreNotedOnceAndConsumed() {
_ = TransferBatchStore.consumeFinishedUploadFolders() // start clean
TransferBatchStore.noteFinishedUpload(folder: "/Docs")
TransferBatchStore.noteFinishedUpload(folder: "/Docs")
TransferBatchStore.noteFinishedUpload(folder: "/Photos")
XCTAssertEqual(TransferBatchStore.consumeFinishedUploadFolders(), ["/Docs", "/Photos"])
XCTAssertEqual(TransferBatchStore.consumeFinishedUploadFolders(), [])
}
func testNotificationFileCountWording() {
XCTAssertEqual(TransferNotifications.files(1), "1 file")
XCTAssertEqual(TransferNotifications.files(3), "3 files")
}
}