diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index d04dd81..ed9df0d 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -114,9 +114,6 @@ 5F9D7B0D27AF6F7500A8AB22 /* CryptoTokenKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 5F9D7B0E27AF6FCA00A8AB22 /* CryptoTokenKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 5F9D7B0F27AF6FD200A8AB22 /* CryptoTokenKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - 8A4716692F5EF56900C7A64D /* AppKeychainTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */; }; - 8A4716712F5EF7A900C7A64D /* PersistenceControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */; }; - 8AD8EBF32F5E2723007475AB /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 8AD8EBF22F5E268D007475AB /* Fixtures */; }; 9A1D1CE526E5D1CE0052028E /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1D1CE426E5D1CE0052028E /* OneTimePassword */; }; 9A1D1CE726E5D2230052028E /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1D1CE626E5D2230052028E /* OneTimePassword */; }; 9A1F47FA26E5CF4B000C0E01 /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1F47F926E5CF4B000C0E01 /* OneTimePassword */; }; @@ -198,7 +195,7 @@ DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; }; DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; }; DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */; }; - DC6474532D20DD0C004B4BBC /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474522D20DD0C004B4BBC /* PersistenceController.swift */; }; + DC6474532D20DD0C004B4BBC /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */; }; DC64745C2D29BE9B004B4BBC /* PasswordEntityTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */; }; DC64745D2D29BEA9004B4BBC /* CoreDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */; }; DC64745F2D45B240004B4BBC /* GitRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC64745E2D45B23A004B4BBC /* GitRepository.swift */; }; @@ -425,9 +422,6 @@ 30F6C1B327664C7200BE5AB2 /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SVProgressHUD.xcframework; path = Carthage/Build/SVProgressHUD.xcframework; sourceTree = ""; }; 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoTokenKit.framework; path = System/Library/Frameworks/CryptoTokenKit.framework; sourceTree = SDKROOT; }; - 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKeychainTest.swift; sourceTree = ""; }; - 8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceControllerTest.swift; sourceTree = ""; }; - 8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = ""; }; 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; 9A1EF0B524C50EE00074FEAC /* passBetaExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaExtension.entitlements; sourceTree = ""; }; @@ -504,7 +498,7 @@ DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = ""; }; DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = ""; }; DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = ""; }; - DC6474522D20DD0C004B4BBC /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; + DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTestCase.swift; sourceTree = ""; }; DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEntityTest.swift; sourceTree = ""; }; DC64745E2D45B23A004B4BBC /* GitRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitRepository.swift; sourceTree = ""; }; @@ -630,7 +624,6 @@ 301F6464216164670071A4CE /* Helpers */ = { isa = PBXGroup; children = ( - 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */, 3032328922C9FBA2009EBD9C /* KeyFileManagerTest.swift */, ); path = Helpers; @@ -768,14 +761,6 @@ path = Crypto; sourceTree = ""; }; - 8A4716702F5EF7A900C7A64D /* Controllers */ = { - isa = PBXGroup; - children = ( - 8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */, - ); - path = Controllers; - sourceTree = ""; - }; 9A58664F25AADB66006719C2 /* Services */ = { isa = PBXGroup; children = ( @@ -894,12 +879,10 @@ A26075861EEC6F34005DB03E /* passKitTests */ = { isa = PBXGroup; children = ( - 8A4716702F5EF7A900C7A64D /* Controllers */, DC64745A2D29BD43004B4BBC /* CoreData */, 30A86F93230F235800F821A4 /* Crypto */, 30BAC8C322E3BA4300438475 /* Testbase */, 30697C5521F63F870064FCAC /* Extensions */, - 8AD8EBF22F5E268D007475AB /* Fixtures */, 301F6464216164670071A4CE /* Helpers */, 30C015A7214ED378005BB6DF /* Models */, 30C015A6214ED32A005BB6DF /* Parser */, @@ -930,7 +913,7 @@ children = ( 30697C3121F63C8B0064FCAC /* PasscodeLockPresenter.swift */, 30697C3221F63C8B0064FCAC /* PasscodeLockViewController.swift */, - DC6474522D20DD0C004B4BBC /* PersistenceController.swift */, + DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */, ); path = Controllers; sourceTree = ""; @@ -1444,7 +1427,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8AD8EBF32F5E2723007475AB /* Fixtures in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1622,7 +1604,7 @@ 3087574F2343E42A00B971A2 /* Colors.swift in Sources */, 30697C2C21F63C5A0064FCAC /* FileManagerExtension.swift in Sources */, 30697C3321F63C8B0064FCAC /* PasscodeLockPresenter.swift in Sources */, - DC6474532D20DD0C004B4BBC /* PersistenceController.swift in Sources */, + DC6474532D20DD0C004B4BBC /* CoreDataStack.swift in Sources */, 30697C3D21F63C990064FCAC /* UIViewExtension.swift in Sources */, 30697C3A21F63C990064FCAC /* UIViewControllerExtension.swift in Sources */, 30697C2E21F63C5A0064FCAC /* Utils.swift in Sources */, @@ -1641,7 +1623,6 @@ 30A86F95230F237000F821A4 /* CryptoFrameworkTest.swift in Sources */, 30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */, 30DAFD4C240985E3002456E7 /* Array+SlicesTest.swift in Sources */, - 8A4716712F5EF7A900C7A64D /* PersistenceControllerTest.swift in Sources */, 301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */, 9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */, 30BAC8CB22E3BB6C00438475 /* DictBasedKeychain.swift in Sources */, @@ -1649,7 +1630,6 @@ A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */, 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */, A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */, - 8A4716692F5EF56900C7A64D /* AppKeychainTest.swift in Sources */, 30695E2524FAEF2600C9D46E /* GitCredentialTest.swift in Sources */, 30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */, 30B04860209A5141001013CA /* PasswordTest.swift in Sources */, diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 10d8b30..5f26010 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -73,7 +73,6 @@ "KeyImportError." = "Cannot import the key."; "FileNotFoundError." = "File '%@' cannot be read."; "PasswordDuplicatedError." = "Cannot add the password; password is duplicated."; -"CannotDeleteDirectoryError." = "Cannot delete directories; delete passwords instead."; "GitResetError." = "Cannot identify the latest synced commit."; "GitCreateSignatureError." = "Cannot create a valid author/committer signature."; "GitPushNotSuccessfulError." = "Pushing local changes was not successful. Make sure there are no uncommitted changes on the remote repository."; diff --git a/passKit/Controllers/PersistenceController.swift b/passKit/Controllers/CoreDataStack.swift similarity index 90% rename from passKit/Controllers/PersistenceController.swift rename to passKit/Controllers/CoreDataStack.swift index 5d05001..0452259 100644 --- a/passKit/Controllers/PersistenceController.swift +++ b/passKit/Controllers/CoreDataStack.swift @@ -1,5 +1,5 @@ // -// PersistenceController.swift +// CoreDataStack.swift // passKit // // Created by Mingshen Sun on 12/28/24. @@ -18,19 +18,19 @@ public class PersistenceController { let container: NSPersistentContainer - init(storeURL: URL? = nil) { + init(isUnitTest: Bool = false) { self.container = NSPersistentContainer(name: Self.modelName, managedObjectModel: .sharedModel) let description = container.persistentStoreDescriptions.first description?.shouldMigrateStoreAutomatically = false description?.shouldInferMappingModelAutomatically = false - description?.url = storeURL ?? URL(fileURLWithPath: Globals.dbPath) + if isUnitTest { + description?.url = URL(fileURLWithPath: "/dev/null") + } else { + description?.url = URL(fileURLWithPath: Globals.dbPath) + } setup() } - static func forUnitTests() -> PersistenceController { - PersistenceController(storeURL: URL(fileURLWithPath: "/dev/null")) - } - func setup() { container.loadPersistentStores { _, error in if error != nil { diff --git a/passKit/Crypto/PGPAgent.swift b/passKit/Crypto/PGPAgent.swift index f193515..66fbbed 100644 --- a/passKit/Crypto/PGPAgent.swift +++ b/passKit/Crypto/PGPAgent.swift @@ -34,10 +34,6 @@ public class PGPAgent { pgpInterface = nil } - public func isInitialized() -> Bool { - pgpInterface != nil - } - public func getKeyID() throws -> [String] { try checkAndInit() return pgpInterface?.keyID ?? [] diff --git a/passKit/Helpers/AppError.swift b/passKit/Helpers/AppError.swift index 8bcc84b..8e0aa21 100644 --- a/passKit/Helpers/AppError.swift +++ b/passKit/Helpers/AppError.swift @@ -15,7 +15,6 @@ public enum AppError: Error, Equatable { case keyImport case readingFile(fileName: String) case passwordDuplicated - case cannotDeleteDirectory case gitReset case gitCommit case gitCreateSignature diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 78c92c4..495dce2 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -273,10 +273,6 @@ public class PasswordStore { } public func delete(passwordEntity: PasswordEntity) throws { - if passwordEntity.isDir { - throw AppError.cannotDeleteDirectory - } - let deletedFileURL = passwordEntity.fileURL(in: storeURL) let deletedFilePath = passwordEntity.path try gitRm(path: passwordEntity.path) @@ -324,11 +320,11 @@ public class PasswordStore { saveUpdatedContext() } - private func saveUpdatedContext() { + public func saveUpdatedContext() { PersistenceController.shared.save() } - private func deleteCoreData() { + public func deleteCoreData() { PasswordEntity.deleteAll(in: context) PersistenceController.shared.save() } diff --git a/passKitTests/Controllers/PersistenceControllerTest.swift b/passKitTests/Controllers/PersistenceControllerTest.swift deleted file mode 100644 index 4ae642f..0000000 --- a/passKitTests/Controllers/PersistenceControllerTest.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// PersistenceControllerTest.swift -// passKitTests -// -// Created by Lysann Tranvouez on 9/3/26. -// Copyright © 2026 Bob Sun. All rights reserved. -// - -import CoreData -import XCTest - -@testable import passKit - -final class PersistenceControllerTest: XCTestCase { - func testModelLoads() { - let controller = PersistenceController.forUnitTests() - let context = controller.viewContext() - - let entityNames = context.persistentStoreCoordinator!.managedObjectModel.entities.map(\.name) - XCTAssertEqual(entityNames, ["PasswordEntity"]) - } - - func testInsertAndFetch() { - let controller = PersistenceController.forUnitTests() - let context = controller.viewContext() - XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 0) - - PasswordEntity.insert(name: "test", path: "test.gpg", isDir: false, into: context) - try? context.save() - - XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 1) - } - - func testReinitializePersistentStoreClearsData() { - let controller = PersistenceController.forUnitTests() - let context = controller.viewContext() - - PasswordEntity.insert(name: "test1", path: "test1.gpg", isDir: false, into: context) - PasswordEntity.insert(name: "test2", path: "test2.gpg", isDir: false, into: context) - try? context.save() - XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 2) - - controller.reinitializePersistentStore() - - // After reinitialize, old data should be gone - // (reinitializePersistentStore calls initPasswordEntityCoreData with the default repo URL, - // which won't exist in tests, so the result should be an empty store) - let remaining = PasswordEntity.fetchAll(in: context) - XCTAssertEqual(remaining.count, 0) - } - - func testMultipleControllersAreIndependent() { - let controller1 = PersistenceController.forUnitTests() - let controller2 = PersistenceController.forUnitTests() - - let context1 = controller1.viewContext() - let context2 = controller2.viewContext() - - PasswordEntity.insert(name: "only-in-1", path: "only-in-1.gpg", isDir: false, into: context1) - try? context1.save() - - XCTAssertEqual(PasswordEntity.fetchAll(in: context1).count, 1) - XCTAssertEqual(PasswordEntity.fetchAll(in: context2).count, 0) - } - - func testSaveAndLoadFromFile() throws { - let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: tempDir) } - let storeURL = tempDir.appendingPathComponent("test.sqlite") - - // Write - let controller1 = PersistenceController(storeURL: storeURL) - let context1 = controller1.viewContext() - PasswordEntity.insert(name: "saved", path: "saved.gpg", isDir: false, into: context1) - PasswordEntity.insert(name: "dir", path: "dir", isDir: true, into: context1) - controller1.save() - - // Load in a fresh controller from the same file - let controller2 = PersistenceController(storeURL: storeURL) - let context2 = controller2.viewContext() - let allEntities = PasswordEntity.fetchAll(in: context2) - - XCTAssertEqual(allEntities.count, 2) - XCTAssertNotNil(allEntities.first { $0.name == "saved" && !$0.isDir }) - XCTAssertNotNil(allEntities.first { $0.name == "dir" && $0.isDir }) - } - - func testSaveError() throws { - // NOTE: save() calls fatalError on Core Data save failures, so error propagation - // cannot be tested without refactoring save() to throw... - } -} diff --git a/passKitTests/CoreData/CoreDataTestCase.swift b/passKitTests/CoreData/CoreDataTestCase.swift index b11ddd1..fa356bf 100644 --- a/passKitTests/CoreData/CoreDataTestCase.swift +++ b/passKitTests/CoreData/CoreDataTestCase.swift @@ -20,7 +20,7 @@ class CoreDataTestCase: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - controller = PersistenceController.forUnitTests() + controller = PersistenceController(isUnitTest: true) } override func tearDown() { diff --git a/passKitTests/CoreData/PasswordEntityTest.swift b/passKitTests/CoreData/PasswordEntityTest.swift index ad70f73..6362e2a 100644 --- a/passKitTests/CoreData/PasswordEntityTest.swift +++ b/passKitTests/CoreData/PasswordEntityTest.swift @@ -85,99 +85,4 @@ final class PasswordEntityTest: CoreDataTestCase { XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 0) } - - // MARK: - initPasswordEntityCoreData tests - - func testInitPasswordEntityCoreDataBuildsTree() throws { - let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: rootDir) } - - // Create directory structure: - // email/ - // work.gpg - // personal.gpg - // social/ - // mastodon.gpg - // toplevel.gpg - // notes.txt (non-.gpg file) - let emailDir = rootDir.appendingPathComponent("email") - let socialDir = rootDir.appendingPathComponent("social") - try FileManager.default.createDirectory(at: emailDir, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: socialDir, withIntermediateDirectories: true) - try Data("test1".utf8).write(to: emailDir.appendingPathComponent("work.gpg")) - try Data("test2".utf8).write(to: emailDir.appendingPathComponent("personal.gpg")) - try Data("test3".utf8).write(to: socialDir.appendingPathComponent("mastodon.gpg")) - try Data("test4".utf8).write(to: rootDir.appendingPathComponent("toplevel.gpg")) - try Data("test5".utf8).write(to: rootDir.appendingPathComponent("notes.txt")) - - let context = controller.viewContext() - PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) - - // Verify total counts - let allEntities = PasswordEntity.fetchAll(in: context) - let files = allEntities.filter { !$0.isDir } - let dirs = allEntities.filter(\.isDir) - XCTAssertEqual(files.count, 5) // 4 .gpg + 1 .txt - XCTAssertEqual(dirs.count, 2) // email, social - - // Verify .gpg extension is stripped - let workEntity = allEntities.first { $0.path == "email/work.gpg" } - XCTAssertNotNil(workEntity) - XCTAssertEqual(workEntity!.name, "work") - - // Verify non-.gpg file keeps its extension - let notesEntity = allEntities.first { $0.path == "notes.txt" } - XCTAssertNotNil(notesEntity) - XCTAssertEqual(notesEntity!.name, "notes.txt") - - // Verify parent-child relationships - let emailEntity = allEntities.first { $0.path == "email" && $0.isDir } - XCTAssertNotNil(emailEntity) - XCTAssertEqual(emailEntity!.children.count, 2) - - // Verify top-level files have no parent (root was deleted) - let toplevelEntity = allEntities.first { $0.path == "toplevel.gpg" } - XCTAssertNotNil(toplevelEntity) - XCTAssertEqual(toplevelEntity!.name, "toplevel") - XCTAssertNil(toplevelEntity!.parent) - } - - func testInitPasswordEntityCoreDataSkipsHiddenFiles() throws { - let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: rootDir) } - - try Data("test".utf8).write(to: rootDir.appendingPathComponent("visible.gpg")) - try Data("test".utf8).write(to: rootDir.appendingPathComponent(".hidden.gpg")) - try Data("test".utf8).write(to: rootDir.appendingPathComponent(".gpg-id")) - try FileManager.default.createDirectory(at: rootDir.appendingPathComponent(".git"), withIntermediateDirectories: true) - try Data("test".utf8).write(to: rootDir.appendingPathComponent(".git/config")) - - let context = controller.viewContext() - PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) - - let allEntities = PasswordEntity.fetchAll(in: context) - XCTAssertEqual(allEntities.count, 1) - XCTAssertEqual(allEntities.first!.name, "visible") - } - - func testInitPasswordEntityCoreDataHandlesEmptyDirectory() throws { - let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: rootDir) } - - try FileManager.default.createDirectory(at: rootDir.appendingPathComponent("emptydir"), withIntermediateDirectories: true) - - let context = controller.viewContext() - PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) - - let allEntities = PasswordEntity.fetchAll(in: context) - let dirs = allEntities.filter(\.isDir) - let files = allEntities.filter { !$0.isDir } - XCTAssertEqual(dirs.count, 1) - XCTAssertEqual(dirs.first!.name, "emptydir") - XCTAssertEqual(dirs.first!.children.count, 0) - XCTAssertEqual(files.count, 0) - } } diff --git a/passKitTests/Fixtures/password-store-empty.git/HEAD b/passKitTests/Fixtures/password-store-empty.git/HEAD deleted file mode 100644 index b870d82..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/main diff --git a/passKitTests/Fixtures/password-store-empty.git/config b/passKitTests/Fixtures/password-store-empty.git/config deleted file mode 100644 index e6da231..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/config +++ /dev/null @@ -1,6 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true - ignorecase = true - precomposeunicode = true diff --git a/passKitTests/Fixtures/password-store-empty.git/description b/passKitTests/Fixtures/password-store-empty.git/description deleted file mode 100644 index 498b267..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/applypatch-msg.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/applypatch-msg.sample deleted file mode 100755 index a5d7b84..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/applypatch-msg.sample +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message taken by -# applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. The hook is -# allowed to edit the commit message file. -# -# To enable this hook, rename this file to "applypatch-msg". - -. git-sh-setup -commitmsg="$(git rev-parse --git-path hooks/commit-msg)" -test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} -: diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/commit-msg.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/commit-msg.sample deleted file mode 100755 index b58d118..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/commit-msg.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message. -# Called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero -# status after issuing an appropriate message if it wants to stop the -# commit. The hook is allowed to edit the commit message file. -# -# To enable this hook, rename this file to "commit-msg". - -# Uncomment the below to add a Signed-off-by line to the message. -# Doing this in a hook is a bad idea in general, but the prepare-commit-msg -# hook is more suited to it. -# -# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" - -# This example catches duplicate Signed-off-by lines. - -test "" = "$(grep '^Signed-off-by: ' "$1" | - sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { - echo >&2 Duplicate Signed-off-by lines. - exit 1 -} diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/fsmonitor-watchman.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/fsmonitor-watchman.sample deleted file mode 100755 index 23e856f..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/fsmonitor-watchman.sample +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use IPC::Open2; - -# An example hook script to integrate Watchman -# (https://facebook.github.io/watchman/) with git to speed up detecting -# new and modified files. -# -# The hook is passed a version (currently 2) and last update token -# formatted as a string and outputs to stdout a new update token and -# all files that have been modified since the update token. Paths must -# be relative to the root of the working tree and separated by a single NUL. -# -# To enable this hook, rename this file to "query-watchman" and set -# 'git config core.fsmonitor .git/hooks/query-watchman' -# -my ($version, $last_update_token) = @ARGV; - -# Uncomment for debugging -# print STDERR "$0 $version $last_update_token\n"; - -# Check the hook interface version -if ($version ne 2) { - die "Unsupported query-fsmonitor hook version '$version'.\n" . - "Falling back to scanning...\n"; -} - -my $git_work_tree = get_working_dir(); - -my $retry = 1; - -my $json_pkg; -eval { - require JSON::XS; - $json_pkg = "JSON::XS"; - 1; -} or do { - require JSON::PP; - $json_pkg = "JSON::PP"; -}; - -launch_watchman(); - -sub launch_watchman { - my $o = watchman_query(); - if (is_work_tree_watched($o)) { - output_result($o->{clock}, @{$o->{files}}); - } -} - -sub output_result { - my ($clockid, @files) = @_; - - # Uncomment for debugging watchman output - # open (my $fh, ">", ".git/watchman-output.out"); - # binmode $fh, ":utf8"; - # print $fh "$clockid\n@files\n"; - # close $fh; - - binmode STDOUT, ":utf8"; - print $clockid; - print "\0"; - local $, = "\0"; - print @files; -} - -sub watchman_clock { - my $response = qx/watchman clock "$git_work_tree"/; - die "Failed to get clock id on '$git_work_tree'.\n" . - "Falling back to scanning...\n" if $? != 0; - - return $json_pkg->new->utf8->decode($response); -} - -sub watchman_query { - my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') - or die "open2() failed: $!\n" . - "Falling back to scanning...\n"; - - # In the query expression below we're asking for names of files that - # changed since $last_update_token but not from the .git folder. - # - # To accomplish this, we're using the "since" generator to use the - # recency index to select candidate nodes and "fields" to limit the - # output to file names only. Then we're using the "expression" term to - # further constrain the results. - my $last_update_line = ""; - if (substr($last_update_token, 0, 1) eq "c") { - $last_update_token = "\"$last_update_token\""; - $last_update_line = qq[\n"since": $last_update_token,]; - } - my $query = <<" END"; - ["query", "$git_work_tree", {$last_update_line - "fields": ["name"], - "expression": ["not", ["dirname", ".git"]] - }] - END - - # Uncomment for debugging the watchman query - # open (my $fh, ">", ".git/watchman-query.json"); - # print $fh $query; - # close $fh; - - print CHLD_IN $query; - close CHLD_IN; - my $response = do {local $/; }; - - # Uncomment for debugging the watch response - # open ($fh, ">", ".git/watchman-response.json"); - # print $fh $response; - # close $fh; - - die "Watchman: command returned no output.\n" . - "Falling back to scanning...\n" if $response eq ""; - die "Watchman: command returned invalid output: $response\n" . - "Falling back to scanning...\n" unless $response =~ /^\{/; - - return $json_pkg->new->utf8->decode($response); -} - -sub is_work_tree_watched { - my ($output) = @_; - my $error = $output->{error}; - if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { - $retry--; - my $response = qx/watchman watch "$git_work_tree"/; - die "Failed to make watchman watch '$git_work_tree'.\n" . - "Falling back to scanning...\n" if $? != 0; - $output = $json_pkg->new->utf8->decode($response); - $error = $output->{error}; - die "Watchman: $error.\n" . - "Falling back to scanning...\n" if $error; - - # Uncomment for debugging watchman output - # open (my $fh, ">", ".git/watchman-output.out"); - # close $fh; - - # Watchman will always return all files on the first query so - # return the fast "everything is dirty" flag to git and do the - # Watchman query just to get it over with now so we won't pay - # the cost in git to look up each individual file. - my $o = watchman_clock(); - $error = $output->{error}; - - die "Watchman: $error.\n" . - "Falling back to scanning...\n" if $error; - - output_result($o->{clock}, ("/")); - $last_update_token = $o->{clock}; - - eval { launch_watchman() }; - return 0; - } - - die "Watchman: $error.\n" . - "Falling back to scanning...\n" if $error; - - return 1; -} - -sub get_working_dir { - my $working_dir; - if ($^O =~ 'msys' || $^O =~ 'cygwin') { - $working_dir = Win32::GetCwd(); - $working_dir =~ tr/\\/\//; - } else { - require Cwd; - $working_dir = Cwd::cwd(); - } - - return $working_dir; -} diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/post-update.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/post-update.sample deleted file mode 100755 index ec17ec1..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/post-update.sample +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare a packed repository for use over -# dumb transports. -# -# To enable this hook, rename this file to "post-update". - -exec git update-server-info diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-applypatch.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-applypatch.sample deleted file mode 100755 index 4142082..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-applypatch.sample +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed -# by applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-applypatch". - -. git-sh-setup -precommit="$(git rev-parse --git-path hooks/pre-commit)" -test -x "$precommit" && exec "$precommit" ${1+"$@"} -: diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-commit.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-commit.sample deleted file mode 100755 index e144712..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-commit.sample +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -if git rev-parse --verify HEAD >/dev/null 2>&1 -then - against=HEAD -else - # Initial commit: diff against an empty tree object - against=$(git hash-object -t tree /dev/null) -fi - -# If you want to allow non-ASCII filenames set this variable to true. -allownonascii=$(git config --type=bool hooks.allownonascii) - -# Redirect output to stderr. -exec 1>&2 - -# Cross platform projects tend to avoid non-ASCII filenames; prevent -# them from being added to the repository. We exploit the fact that the -# printable range starts at the space character and ends with tilde. -if [ "$allownonascii" != "true" ] && - # Note that the use of brackets around a tr range is ok here, (it's - # even required, for portability to Solaris 10's /usr/bin/tr), since - # the square bracket bytes happen to fall in the designated range. - test $(git diff --cached --name-only --diff-filter=A -z $against | - LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 -then - cat <<\EOF -Error: Attempt to add a non-ASCII file name. - -This can cause problems if you want to work with people on other platforms. - -To be portable it is advisable to rename the file. - -If you know what you are doing you can disable this check using: - - git config hooks.allownonascii true -EOF - exit 1 -fi - -# If there are whitespace errors, print the offending file names and fail. -exec git diff-index --check --cached $against -- diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-merge-commit.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-merge-commit.sample deleted file mode 100755 index 399eab1..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-merge-commit.sample +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git merge" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message to -# stderr if it wants to stop the merge commit. -# -# To enable this hook, rename this file to "pre-merge-commit". - -. git-sh-setup -test -x "$GIT_DIR/hooks/pre-commit" && - exec "$GIT_DIR/hooks/pre-commit" -: diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-push.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-push.sample deleted file mode 100755 index 4ce688d..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-push.sample +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -# An example hook script to verify what is about to be pushed. Called by "git -# push" after it has checked the remote status, but before anything has been -# pushed. If this script exits with a non-zero status nothing will be pushed. -# -# This hook is called with the following parameters: -# -# $1 -- Name of the remote to which the push is being done -# $2 -- URL to which the push is being done -# -# If pushing without using a named remote those arguments will be equal. -# -# Information about the commits which are being pushed is supplied as lines to -# the standard input in the form: -# -# -# -# This sample shows how to prevent push of commits where the log message starts -# with "WIP" (work in progress). - -remote="$1" -url="$2" - -zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" - exit 1 - fi - fi -done - -exit 0 diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-rebase.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-rebase.sample deleted file mode 100755 index 6cbef5c..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-rebase.sample +++ /dev/null @@ -1,169 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2006, 2008 Junio C Hamano -# -# The "pre-rebase" hook is run just before "git rebase" starts doing -# its job, and can prevent the command from running by exiting with -# non-zero status. -# -# The hook is called with the following parameters: -# -# $1 -- the upstream the series was forked from. -# $2 -- the branch being rebased (or empty when rebasing the current branch). -# -# This sample shows how to prevent topic branches that are already -# merged to 'next' branch from getting rebased, because allowing it -# would result in rebasing already published history. - -publish=next -basebranch="$1" -if test "$#" = 2 -then - topic="refs/heads/$2" -else - topic=`git symbolic-ref HEAD` || - exit 0 ;# we do not interrupt rebasing detached HEAD -fi - -case "$topic" in -refs/heads/??/*) - ;; -*) - exit 0 ;# we do not interrupt others. - ;; -esac - -# Now we are dealing with a topic branch being rebased -# on top of master. Is it OK to rebase it? - -# Does the topic really exist? -git show-ref -q "$topic" || { - echo >&2 "No such branch $topic" - exit 1 -} - -# Is topic fully merged to master? -not_in_master=`git rev-list --pretty=oneline ^master "$topic"` -if test -z "$not_in_master" -then - echo >&2 "$topic is fully merged to master; better remove it." - exit 1 ;# we could allow it, but there is no point. -fi - -# Is topic ever merged to next? If so you should not be rebasing it. -only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` -only_next_2=`git rev-list ^master ${publish} | sort` -if test "$only_next_1" = "$only_next_2" -then - not_in_topic=`git rev-list "^$topic" master` - if test -z "$not_in_topic" - then - echo >&2 "$topic is already up to date with master" - exit 1 ;# we could allow it, but there is no point. - else - exit 0 - fi -else - not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` - /usr/bin/perl -e ' - my $topic = $ARGV[0]; - my $msg = "* $topic has commits already merged to public branch:\n"; - my (%not_in_next) = map { - /^([0-9a-f]+) /; - ($1 => 1); - } split(/\n/, $ARGV[1]); - for my $elem (map { - /^([0-9a-f]+) (.*)$/; - [$1 => $2]; - } split(/\n/, $ARGV[2])) { - if (!exists $not_in_next{$elem->[0]}) { - if ($msg) { - print STDERR $msg; - undef $msg; - } - print STDERR " $elem->[1]\n"; - } - } - ' "$topic" "$not_in_next" "$not_in_master" - exit 1 -fi - -<<\DOC_END - -This sample hook safeguards topic branches that have been -published from being rewound. - -The workflow assumed here is: - - * Once a topic branch forks from "master", "master" is never - merged into it again (either directly or indirectly). - - * Once a topic branch is fully cooked and merged into "master", - it is deleted. If you need to build on top of it to correct - earlier mistakes, a new topic branch is created by forking at - the tip of the "master". This is not strictly necessary, but - it makes it easier to keep your history simple. - - * Whenever you need to test or publish your changes to topic - branches, merge them into "next" branch. - -The script, being an example, hardcodes the publish branch name -to be "next", but it is trivial to make it configurable via -$GIT_DIR/config mechanism. - -With this workflow, you would want to know: - -(1) ... if a topic branch has ever been merged to "next". Young - topic branches can have stupid mistakes you would rather - clean up before publishing, and things that have not been - merged into other branches can be easily rebased without - affecting other people. But once it is published, you would - not want to rewind it. - -(2) ... if a topic branch has been fully merged to "master". - Then you can delete it. More importantly, you should not - build on top of it -- other people may already want to - change things related to the topic as patches against your - "master", so if you need further changes, it is better to - fork the topic (perhaps with the same name) afresh from the - tip of "master". - -Let's look at this example: - - o---o---o---o---o---o---o---o---o---o "next" - / / / / - / a---a---b A / / - / / / / - / / c---c---c---c B / - / / / \ / - / / / b---b C \ / - / / / / \ / - ---o---o---o---o---o---o---o---o---o---o---o "master" - - -A, B and C are topic branches. - - * A has one fix since it was merged up to "next". - - * B has finished. It has been fully merged up to "master" and "next", - and is ready to be deleted. - - * C has not merged to "next" at all. - -We would want to allow C to be rebased, refuse A, and encourage -B to be deleted. - -To compute (1): - - git rev-list ^master ^topic next - git rev-list ^master next - - if these match, topic has not merged in next at all. - -To compute (2): - - git rev-list master..topic - - if this is empty, it is fully merged to "master". - -DOC_END diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-receive.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-receive.sample deleted file mode 100755 index a1fd29e..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/pre-receive.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to make use of push options. -# The example simply echoes all push options that start with 'echoback=' -# and rejects all pushes when the "reject" push option is used. -# -# To enable this hook, rename this file to "pre-receive". - -if test -n "$GIT_PUSH_OPTION_COUNT" -then - i=0 - while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" - do - eval "value=\$GIT_PUSH_OPTION_$i" - case "$value" in - echoback=*) - echo "echo from the pre-receive-hook: ${value#*=}" >&2 - ;; - reject) - exit 1 - esac - i=$((i + 1)) - done -fi diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/prepare-commit-msg.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/prepare-commit-msg.sample deleted file mode 100755 index 10fa14c..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/prepare-commit-msg.sample +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare the commit log message. -# Called by "git commit" with the name of the file that has the -# commit message, followed by the description of the commit -# message's source. The hook's purpose is to edit the commit -# message file. If the hook fails with a non-zero status, -# the commit is aborted. -# -# To enable this hook, rename this file to "prepare-commit-msg". - -# This hook includes three examples. The first one removes the -# "# Please enter the commit message..." help message. -# -# The second includes the output of "git diff --name-status -r" -# into the message, just before the "git status" output. It is -# commented because it doesn't cope with --amend or with squashed -# commits. -# -# The third example adds a Signed-off-by line to the message, that can -# still be edited. This is rarely a good idea. - -COMMIT_MSG_FILE=$1 -COMMIT_SOURCE=$2 -SHA1=$3 - -/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" - -# case "$COMMIT_SOURCE,$SHA1" in -# ,|template,) -# /usr/bin/perl -i.bak -pe ' -# print "\n" . `git diff --cached --name-status -r` -# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; -# *) ;; -# esac - -# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" -# if test -z "$COMMIT_SOURCE" -# then -# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" -# fi diff --git a/passKitTests/Fixtures/password-store-empty.git/hooks/push-to-checkout.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/push-to-checkout.sample deleted file mode 100755 index af5a0c0..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/hooks/push-to-checkout.sample +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/sh - -# An example hook script to update a checked-out tree on a git push. -# -# This hook is invoked by git-receive-pack(1) when it reacts to git -# push and updates reference(s) in its repository, and when the push -# tries to update the branch that is currently checked out and the -# receive.denyCurrentBranch configuration variable is set to -# updateInstead. -# -# By default, such a push is refused if the working tree and the index -# of the remote repository has any difference from the currently -# checked out commit; when both the working tree and the index match -# the current commit, they are updated to match the newly pushed tip -# of the branch. This hook is to be used to override the default -# behaviour; however the code below reimplements the default behaviour -# as a starting point for convenient modification. -# -# The hook receives the commit with which the tip of the current -# branch is going to be updated: -commit=$1 - -# It can exit with a non-zero status to refuse the push (when it does -# so, it must not modify the index or the working tree). -die () { - echo >&2 "$*" - exit 1 -} - -# Or it can make any necessary changes to the working tree and to the -# index to bring them to the desired state when the tip of the current -# branch is updated to the new commit, and exit with a zero status. -# -# For example, the hook can simply run git read-tree -u -m HEAD "$1" -# in order to emulate git fetch that is run in the reverse direction -# with git push, as the two-tree form of git read-tree -u -m is -# essentially the same as git switch or git checkout that switches -# branches while keeping the local changes in the working tree that do -# not interfere with the difference between the branches. - -# The below is a more-or-less exact translation to shell of the C code -# for the default behaviour for git's push-to-checkout hook defined in -# the push_to_deploy() function in builtin/receive-pack.c. -# -# Note that the hook will be executed from the repository directory, -# not from the working tree, so if you want to perform operations on -# the working tree, you will have to adapt your code accordingly, e.g. -# by adding "cd .." or using relative paths. - -if ! git update-index -q --ignore-submodules --refresh -then - die "Up-to-date check failed" -fi - -if ! git diff-files --quiet --ignore-submodules -- -then - die "Working directory has unstaged changes" -fi - -# This is a rough translation of: -# -# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX -if git cat-file -e HEAD 2>/dev/null -then - head=HEAD -else - head=$(git hash-object -t tree --stdin &2 - echo " (if you want, you could supply GIT_DIR then run" >&2 - echo " $0 )" >&2 - exit 1 -fi - -if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then - echo "usage: $0 " >&2 - exit 1 -fi - -# --- Config -allowunannotated=$(git config --type=bool hooks.allowunannotated) -allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) -denycreatebranch=$(git config --type=bool hooks.denycreatebranch) -allowdeletetag=$(git config --type=bool hooks.allowdeletetag) -allowmodifytag=$(git config --type=bool hooks.allowmodifytag) - -# check for no description -projectdesc=$(sed -e '1q' "$GIT_DIR/description") -case "$projectdesc" in -"Unnamed repository"* | "") - echo "*** Project description file hasn't been set" >&2 - exit 1 - ;; -esac - -# --- Check types -# if $newrev is 0000...0000, it's a commit to delete a ref. -zero=$(git hash-object --stdin &2 - echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 - exit 1 - fi - ;; - refs/tags/*,delete) - # delete tag - if [ "$allowdeletetag" != "true" ]; then - echo "*** Deleting a tag is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/tags/*,tag) - # annotated tag - if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 - then - echo "*** Tag '$refname' already exists." >&2 - echo "*** Modifying a tag is not allowed in this repository." >&2 - exit 1 - fi - ;; - refs/heads/*,commit) - # branch - if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then - echo "*** Creating a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/heads/*,delete) - # delete branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/remotes/*,commit) - # tracking branch - ;; - refs/remotes/*,delete) - # delete tracking branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a tracking branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - *) - # Anything else (is there anything else?) - echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 - exit 1 - ;; -esac - -# --- Finished -exit 0 diff --git a/passKitTests/Fixtures/password-store-empty.git/info/exclude b/passKitTests/Fixtures/password-store-empty.git/info/exclude deleted file mode 100644 index a5196d1..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 deleted file mode 100644 index adf6411..0000000 Binary files a/passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 and /dev/null differ diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/4e/b23a2d659dcaa6fbc01ada57aed6d1fbeb0520 b/passKitTests/Fixtures/password-store-empty.git/objects/4e/b23a2d659dcaa6fbc01ada57aed6d1fbeb0520 deleted file mode 100644 index c1ae0b0..0000000 Binary files a/passKitTests/Fixtures/password-store-empty.git/objects/4e/b23a2d659dcaa6fbc01ada57aed6d1fbeb0520 and /dev/null differ diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/50/96ac11d1376ea9b22ddedac1130f45ec618d11 b/passKitTests/Fixtures/password-store-empty.git/objects/50/96ac11d1376ea9b22ddedac1130f45ec618d11 deleted file mode 100644 index 5f9c0af..0000000 Binary files a/passKitTests/Fixtures/password-store-empty.git/objects/50/96ac11d1376ea9b22ddedac1130f45ec618d11 and /dev/null differ diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/ae/a863facd4acba3b4862e3f42847da1000e486a b/passKitTests/Fixtures/password-store-empty.git/objects/ae/a863facd4acba3b4862e3f42847da1000e486a deleted file mode 100644 index 7dbbc42..0000000 Binary files a/passKitTests/Fixtures/password-store-empty.git/objects/ae/a863facd4acba3b4862e3f42847da1000e486a and /dev/null differ diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd b/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd deleted file mode 100644 index 3e561dd..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd +++ /dev/null @@ -1,3 +0,0 @@ -xM -0@a9 \zi:@BzzK}eN*3Sv48J=Eo؎D҈k"EMnKNRy,_pr_9p;qETھ -&rɒi@ \ No newline at end of file diff --git a/passKitTests/Fixtures/password-store-empty.git/packed-refs b/passKitTests/Fixtures/password-store-empty.git/packed-refs deleted file mode 100644 index 432aeb4..0000000 --- a/passKitTests/Fixtures/password-store-empty.git/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -f095bb4897e4cd58faadfe4d4f678fb697be3ffd refs/heads/main diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/FETCH_HEAD b/passKitTests/Fixtures/password-store-with-gpgid.git/FETCH_HEAD deleted file mode 100644 index ef06926..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/FETCH_HEAD +++ /dev/null @@ -1 +0,0 @@ -925eb0f6b19282b5f10dfe008e0062b4be6dd41a not-for-merge branch 'master' of https://github.com/mssun/passforios-password-store diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/HEAD b/passKitTests/Fixtures/password-store-with-gpgid.git/HEAD deleted file mode 100644 index cb089cd..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/config b/passKitTests/Fixtures/password-store-with-gpgid.git/config deleted file mode 100644 index 876f087..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/config +++ /dev/null @@ -1,9 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true - ignorecase = true - precomposeunicode = true -[remote "origin"] - url = https://github.com/mssun/passforios-password-store.git - fetch = +refs/heads/*:refs/remotes/origin/* diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/description b/passKitTests/Fixtures/password-store-with-gpgid.git/description deleted file mode 100644 index 5536ff7..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/description +++ /dev/null @@ -1 +0,0 @@ -Example password store repository for passforios tests with .gpg-id files. diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/applypatch-msg.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/applypatch-msg.sample deleted file mode 100755 index a5d7b84..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/applypatch-msg.sample +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message taken by -# applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. The hook is -# allowed to edit the commit message file. -# -# To enable this hook, rename this file to "applypatch-msg". - -. git-sh-setup -commitmsg="$(git rev-parse --git-path hooks/commit-msg)" -test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} -: diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/commit-msg.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/commit-msg.sample deleted file mode 100755 index b58d118..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/commit-msg.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message. -# Called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero -# status after issuing an appropriate message if it wants to stop the -# commit. The hook is allowed to edit the commit message file. -# -# To enable this hook, rename this file to "commit-msg". - -# Uncomment the below to add a Signed-off-by line to the message. -# Doing this in a hook is a bad idea in general, but the prepare-commit-msg -# hook is more suited to it. -# -# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" - -# This example catches duplicate Signed-off-by lines. - -test "" = "$(grep '^Signed-off-by: ' "$1" | - sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { - echo >&2 Duplicate Signed-off-by lines. - exit 1 -} diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/fsmonitor-watchman.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/fsmonitor-watchman.sample deleted file mode 100755 index 23e856f..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/fsmonitor-watchman.sample +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use IPC::Open2; - -# An example hook script to integrate Watchman -# (https://facebook.github.io/watchman/) with git to speed up detecting -# new and modified files. -# -# The hook is passed a version (currently 2) and last update token -# formatted as a string and outputs to stdout a new update token and -# all files that have been modified since the update token. Paths must -# be relative to the root of the working tree and separated by a single NUL. -# -# To enable this hook, rename this file to "query-watchman" and set -# 'git config core.fsmonitor .git/hooks/query-watchman' -# -my ($version, $last_update_token) = @ARGV; - -# Uncomment for debugging -# print STDERR "$0 $version $last_update_token\n"; - -# Check the hook interface version -if ($version ne 2) { - die "Unsupported query-fsmonitor hook version '$version'.\n" . - "Falling back to scanning...\n"; -} - -my $git_work_tree = get_working_dir(); - -my $retry = 1; - -my $json_pkg; -eval { - require JSON::XS; - $json_pkg = "JSON::XS"; - 1; -} or do { - require JSON::PP; - $json_pkg = "JSON::PP"; -}; - -launch_watchman(); - -sub launch_watchman { - my $o = watchman_query(); - if (is_work_tree_watched($o)) { - output_result($o->{clock}, @{$o->{files}}); - } -} - -sub output_result { - my ($clockid, @files) = @_; - - # Uncomment for debugging watchman output - # open (my $fh, ">", ".git/watchman-output.out"); - # binmode $fh, ":utf8"; - # print $fh "$clockid\n@files\n"; - # close $fh; - - binmode STDOUT, ":utf8"; - print $clockid; - print "\0"; - local $, = "\0"; - print @files; -} - -sub watchman_clock { - my $response = qx/watchman clock "$git_work_tree"/; - die "Failed to get clock id on '$git_work_tree'.\n" . - "Falling back to scanning...\n" if $? != 0; - - return $json_pkg->new->utf8->decode($response); -} - -sub watchman_query { - my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') - or die "open2() failed: $!\n" . - "Falling back to scanning...\n"; - - # In the query expression below we're asking for names of files that - # changed since $last_update_token but not from the .git folder. - # - # To accomplish this, we're using the "since" generator to use the - # recency index to select candidate nodes and "fields" to limit the - # output to file names only. Then we're using the "expression" term to - # further constrain the results. - my $last_update_line = ""; - if (substr($last_update_token, 0, 1) eq "c") { - $last_update_token = "\"$last_update_token\""; - $last_update_line = qq[\n"since": $last_update_token,]; - } - my $query = <<" END"; - ["query", "$git_work_tree", {$last_update_line - "fields": ["name"], - "expression": ["not", ["dirname", ".git"]] - }] - END - - # Uncomment for debugging the watchman query - # open (my $fh, ">", ".git/watchman-query.json"); - # print $fh $query; - # close $fh; - - print CHLD_IN $query; - close CHLD_IN; - my $response = do {local $/; }; - - # Uncomment for debugging the watch response - # open ($fh, ">", ".git/watchman-response.json"); - # print $fh $response; - # close $fh; - - die "Watchman: command returned no output.\n" . - "Falling back to scanning...\n" if $response eq ""; - die "Watchman: command returned invalid output: $response\n" . - "Falling back to scanning...\n" unless $response =~ /^\{/; - - return $json_pkg->new->utf8->decode($response); -} - -sub is_work_tree_watched { - my ($output) = @_; - my $error = $output->{error}; - if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { - $retry--; - my $response = qx/watchman watch "$git_work_tree"/; - die "Failed to make watchman watch '$git_work_tree'.\n" . - "Falling back to scanning...\n" if $? != 0; - $output = $json_pkg->new->utf8->decode($response); - $error = $output->{error}; - die "Watchman: $error.\n" . - "Falling back to scanning...\n" if $error; - - # Uncomment for debugging watchman output - # open (my $fh, ">", ".git/watchman-output.out"); - # close $fh; - - # Watchman will always return all files on the first query so - # return the fast "everything is dirty" flag to git and do the - # Watchman query just to get it over with now so we won't pay - # the cost in git to look up each individual file. - my $o = watchman_clock(); - $error = $output->{error}; - - die "Watchman: $error.\n" . - "Falling back to scanning...\n" if $error; - - output_result($o->{clock}, ("/")); - $last_update_token = $o->{clock}; - - eval { launch_watchman() }; - return 0; - } - - die "Watchman: $error.\n" . - "Falling back to scanning...\n" if $error; - - return 1; -} - -sub get_working_dir { - my $working_dir; - if ($^O =~ 'msys' || $^O =~ 'cygwin') { - $working_dir = Win32::GetCwd(); - $working_dir =~ tr/\\/\//; - } else { - require Cwd; - $working_dir = Cwd::cwd(); - } - - return $working_dir; -} diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/post-update.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/post-update.sample deleted file mode 100755 index ec17ec1..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/post-update.sample +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare a packed repository for use over -# dumb transports. -# -# To enable this hook, rename this file to "post-update". - -exec git update-server-info diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-applypatch.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-applypatch.sample deleted file mode 100755 index 4142082..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-applypatch.sample +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed -# by applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-applypatch". - -. git-sh-setup -precommit="$(git rev-parse --git-path hooks/pre-commit)" -test -x "$precommit" && exec "$precommit" ${1+"$@"} -: diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-commit.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-commit.sample deleted file mode 100755 index e144712..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-commit.sample +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -if git rev-parse --verify HEAD >/dev/null 2>&1 -then - against=HEAD -else - # Initial commit: diff against an empty tree object - against=$(git hash-object -t tree /dev/null) -fi - -# If you want to allow non-ASCII filenames set this variable to true. -allownonascii=$(git config --type=bool hooks.allownonascii) - -# Redirect output to stderr. -exec 1>&2 - -# Cross platform projects tend to avoid non-ASCII filenames; prevent -# them from being added to the repository. We exploit the fact that the -# printable range starts at the space character and ends with tilde. -if [ "$allownonascii" != "true" ] && - # Note that the use of brackets around a tr range is ok here, (it's - # even required, for portability to Solaris 10's /usr/bin/tr), since - # the square bracket bytes happen to fall in the designated range. - test $(git diff --cached --name-only --diff-filter=A -z $against | - LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 -then - cat <<\EOF -Error: Attempt to add a non-ASCII file name. - -This can cause problems if you want to work with people on other platforms. - -To be portable it is advisable to rename the file. - -If you know what you are doing you can disable this check using: - - git config hooks.allownonascii true -EOF - exit 1 -fi - -# If there are whitespace errors, print the offending file names and fail. -exec git diff-index --check --cached $against -- diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-merge-commit.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-merge-commit.sample deleted file mode 100755 index 399eab1..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-merge-commit.sample +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git merge" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message to -# stderr if it wants to stop the merge commit. -# -# To enable this hook, rename this file to "pre-merge-commit". - -. git-sh-setup -test -x "$GIT_DIR/hooks/pre-commit" && - exec "$GIT_DIR/hooks/pre-commit" -: diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-push.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-push.sample deleted file mode 100755 index 4ce688d..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-push.sample +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -# An example hook script to verify what is about to be pushed. Called by "git -# push" after it has checked the remote status, but before anything has been -# pushed. If this script exits with a non-zero status nothing will be pushed. -# -# This hook is called with the following parameters: -# -# $1 -- Name of the remote to which the push is being done -# $2 -- URL to which the push is being done -# -# If pushing without using a named remote those arguments will be equal. -# -# Information about the commits which are being pushed is supplied as lines to -# the standard input in the form: -# -# -# -# This sample shows how to prevent push of commits where the log message starts -# with "WIP" (work in progress). - -remote="$1" -url="$2" - -zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" - exit 1 - fi - fi -done - -exit 0 diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-rebase.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-rebase.sample deleted file mode 100755 index 6cbef5c..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-rebase.sample +++ /dev/null @@ -1,169 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2006, 2008 Junio C Hamano -# -# The "pre-rebase" hook is run just before "git rebase" starts doing -# its job, and can prevent the command from running by exiting with -# non-zero status. -# -# The hook is called with the following parameters: -# -# $1 -- the upstream the series was forked from. -# $2 -- the branch being rebased (or empty when rebasing the current branch). -# -# This sample shows how to prevent topic branches that are already -# merged to 'next' branch from getting rebased, because allowing it -# would result in rebasing already published history. - -publish=next -basebranch="$1" -if test "$#" = 2 -then - topic="refs/heads/$2" -else - topic=`git symbolic-ref HEAD` || - exit 0 ;# we do not interrupt rebasing detached HEAD -fi - -case "$topic" in -refs/heads/??/*) - ;; -*) - exit 0 ;# we do not interrupt others. - ;; -esac - -# Now we are dealing with a topic branch being rebased -# on top of master. Is it OK to rebase it? - -# Does the topic really exist? -git show-ref -q "$topic" || { - echo >&2 "No such branch $topic" - exit 1 -} - -# Is topic fully merged to master? -not_in_master=`git rev-list --pretty=oneline ^master "$topic"` -if test -z "$not_in_master" -then - echo >&2 "$topic is fully merged to master; better remove it." - exit 1 ;# we could allow it, but there is no point. -fi - -# Is topic ever merged to next? If so you should not be rebasing it. -only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` -only_next_2=`git rev-list ^master ${publish} | sort` -if test "$only_next_1" = "$only_next_2" -then - not_in_topic=`git rev-list "^$topic" master` - if test -z "$not_in_topic" - then - echo >&2 "$topic is already up to date with master" - exit 1 ;# we could allow it, but there is no point. - else - exit 0 - fi -else - not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` - /usr/bin/perl -e ' - my $topic = $ARGV[0]; - my $msg = "* $topic has commits already merged to public branch:\n"; - my (%not_in_next) = map { - /^([0-9a-f]+) /; - ($1 => 1); - } split(/\n/, $ARGV[1]); - for my $elem (map { - /^([0-9a-f]+) (.*)$/; - [$1 => $2]; - } split(/\n/, $ARGV[2])) { - if (!exists $not_in_next{$elem->[0]}) { - if ($msg) { - print STDERR $msg; - undef $msg; - } - print STDERR " $elem->[1]\n"; - } - } - ' "$topic" "$not_in_next" "$not_in_master" - exit 1 -fi - -<<\DOC_END - -This sample hook safeguards topic branches that have been -published from being rewound. - -The workflow assumed here is: - - * Once a topic branch forks from "master", "master" is never - merged into it again (either directly or indirectly). - - * Once a topic branch is fully cooked and merged into "master", - it is deleted. If you need to build on top of it to correct - earlier mistakes, a new topic branch is created by forking at - the tip of the "master". This is not strictly necessary, but - it makes it easier to keep your history simple. - - * Whenever you need to test or publish your changes to topic - branches, merge them into "next" branch. - -The script, being an example, hardcodes the publish branch name -to be "next", but it is trivial to make it configurable via -$GIT_DIR/config mechanism. - -With this workflow, you would want to know: - -(1) ... if a topic branch has ever been merged to "next". Young - topic branches can have stupid mistakes you would rather - clean up before publishing, and things that have not been - merged into other branches can be easily rebased without - affecting other people. But once it is published, you would - not want to rewind it. - -(2) ... if a topic branch has been fully merged to "master". - Then you can delete it. More importantly, you should not - build on top of it -- other people may already want to - change things related to the topic as patches against your - "master", so if you need further changes, it is better to - fork the topic (perhaps with the same name) afresh from the - tip of "master". - -Let's look at this example: - - o---o---o---o---o---o---o---o---o---o "next" - / / / / - / a---a---b A / / - / / / / - / / c---c---c---c B / - / / / \ / - / / / b---b C \ / - / / / / \ / - ---o---o---o---o---o---o---o---o---o---o---o "master" - - -A, B and C are topic branches. - - * A has one fix since it was merged up to "next". - - * B has finished. It has been fully merged up to "master" and "next", - and is ready to be deleted. - - * C has not merged to "next" at all. - -We would want to allow C to be rebased, refuse A, and encourage -B to be deleted. - -To compute (1): - - git rev-list ^master ^topic next - git rev-list ^master next - - if these match, topic has not merged in next at all. - -To compute (2): - - git rev-list master..topic - - if this is empty, it is fully merged to "master". - -DOC_END diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-receive.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-receive.sample deleted file mode 100755 index a1fd29e..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-receive.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to make use of push options. -# The example simply echoes all push options that start with 'echoback=' -# and rejects all pushes when the "reject" push option is used. -# -# To enable this hook, rename this file to "pre-receive". - -if test -n "$GIT_PUSH_OPTION_COUNT" -then - i=0 - while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" - do - eval "value=\$GIT_PUSH_OPTION_$i" - case "$value" in - echoback=*) - echo "echo from the pre-receive-hook: ${value#*=}" >&2 - ;; - reject) - exit 1 - esac - i=$((i + 1)) - done -fi diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/prepare-commit-msg.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/prepare-commit-msg.sample deleted file mode 100755 index 10fa14c..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/prepare-commit-msg.sample +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare the commit log message. -# Called by "git commit" with the name of the file that has the -# commit message, followed by the description of the commit -# message's source. The hook's purpose is to edit the commit -# message file. If the hook fails with a non-zero status, -# the commit is aborted. -# -# To enable this hook, rename this file to "prepare-commit-msg". - -# This hook includes three examples. The first one removes the -# "# Please enter the commit message..." help message. -# -# The second includes the output of "git diff --name-status -r" -# into the message, just before the "git status" output. It is -# commented because it doesn't cope with --amend or with squashed -# commits. -# -# The third example adds a Signed-off-by line to the message, that can -# still be edited. This is rarely a good idea. - -COMMIT_MSG_FILE=$1 -COMMIT_SOURCE=$2 -SHA1=$3 - -/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" - -# case "$COMMIT_SOURCE,$SHA1" in -# ,|template,) -# /usr/bin/perl -i.bak -pe ' -# print "\n" . `git diff --cached --name-status -r` -# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; -# *) ;; -# esac - -# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" -# if test -z "$COMMIT_SOURCE" -# then -# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" -# fi diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/push-to-checkout.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/push-to-checkout.sample deleted file mode 100755 index af5a0c0..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/push-to-checkout.sample +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/sh - -# An example hook script to update a checked-out tree on a git push. -# -# This hook is invoked by git-receive-pack(1) when it reacts to git -# push and updates reference(s) in its repository, and when the push -# tries to update the branch that is currently checked out and the -# receive.denyCurrentBranch configuration variable is set to -# updateInstead. -# -# By default, such a push is refused if the working tree and the index -# of the remote repository has any difference from the currently -# checked out commit; when both the working tree and the index match -# the current commit, they are updated to match the newly pushed tip -# of the branch. This hook is to be used to override the default -# behaviour; however the code below reimplements the default behaviour -# as a starting point for convenient modification. -# -# The hook receives the commit with which the tip of the current -# branch is going to be updated: -commit=$1 - -# It can exit with a non-zero status to refuse the push (when it does -# so, it must not modify the index or the working tree). -die () { - echo >&2 "$*" - exit 1 -} - -# Or it can make any necessary changes to the working tree and to the -# index to bring them to the desired state when the tip of the current -# branch is updated to the new commit, and exit with a zero status. -# -# For example, the hook can simply run git read-tree -u -m HEAD "$1" -# in order to emulate git fetch that is run in the reverse direction -# with git push, as the two-tree form of git read-tree -u -m is -# essentially the same as git switch or git checkout that switches -# branches while keeping the local changes in the working tree that do -# not interfere with the difference between the branches. - -# The below is a more-or-less exact translation to shell of the C code -# for the default behaviour for git's push-to-checkout hook defined in -# the push_to_deploy() function in builtin/receive-pack.c. -# -# Note that the hook will be executed from the repository directory, -# not from the working tree, so if you want to perform operations on -# the working tree, you will have to adapt your code accordingly, e.g. -# by adding "cd .." or using relative paths. - -if ! git update-index -q --ignore-submodules --refresh -then - die "Up-to-date check failed" -fi - -if ! git diff-files --quiet --ignore-submodules -- -then - die "Working directory has unstaged changes" -fi - -# This is a rough translation of: -# -# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX -if git cat-file -e HEAD 2>/dev/null -then - head=HEAD -else - head=$(git hash-object -t tree --stdin &2 - echo " (if you want, you could supply GIT_DIR then run" >&2 - echo " $0 )" >&2 - exit 1 -fi - -if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then - echo "usage: $0 " >&2 - exit 1 -fi - -# --- Config -allowunannotated=$(git config --type=bool hooks.allowunannotated) -allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) -denycreatebranch=$(git config --type=bool hooks.denycreatebranch) -allowdeletetag=$(git config --type=bool hooks.allowdeletetag) -allowmodifytag=$(git config --type=bool hooks.allowmodifytag) - -# check for no description -projectdesc=$(sed -e '1q' "$GIT_DIR/description") -case "$projectdesc" in -"Unnamed repository"* | "") - echo "*** Project description file hasn't been set" >&2 - exit 1 - ;; -esac - -# --- Check types -# if $newrev is 0000...0000, it's a commit to delete a ref. -zero=$(git hash-object --stdin &2 - echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 - exit 1 - fi - ;; - refs/tags/*,delete) - # delete tag - if [ "$allowdeletetag" != "true" ]; then - echo "*** Deleting a tag is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/tags/*,tag) - # annotated tag - if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 - then - echo "*** Tag '$refname' already exists." >&2 - echo "*** Modifying a tag is not allowed in this repository." >&2 - exit 1 - fi - ;; - refs/heads/*,commit) - # branch - if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then - echo "*** Creating a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/heads/*,delete) - # delete branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/remotes/*,commit) - # tracking branch - ;; - refs/remotes/*,delete) - # delete tracking branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a tracking branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - *) - # Anything else (is there anything else?) - echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 - exit 1 - ;; -esac - -# --- Finished -exit 0 diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude b/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude deleted file mode 100644 index a5196d1..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx b/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx deleted file mode 100644 index 7efd056..0000000 Binary files a/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx and /dev/null differ diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack b/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack deleted file mode 100644 index 038de79..0000000 Binary files a/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack and /dev/null differ diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/packed-refs b/passKitTests/Fixtures/password-store-with-gpgid.git/packed-refs deleted file mode 100644 index 5b72267..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -925eb0f6b19282b5f10dfe008e0062b4be6dd41a refs/heads/master diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/refs/remotes/origin/master b/passKitTests/Fixtures/password-store-with-gpgid.git/refs/remotes/origin/master deleted file mode 100644 index 7d10008..0000000 --- a/passKitTests/Fixtures/password-store-with-gpgid.git/refs/remotes/origin/master +++ /dev/null @@ -1 +0,0 @@ -925eb0f6b19282b5f10dfe008e0062b4be6dd41a diff --git a/passKitTests/Helpers/AppKeychainTest.swift b/passKitTests/Helpers/AppKeychainTest.swift deleted file mode 100644 index 9b420fc..0000000 --- a/passKitTests/Helpers/AppKeychainTest.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// AppKeychainTest.swift -// passKitTests -// -// Created by Lysann Tranvouez on 9/3/26. -// Copyright © 2026 Bob Sun. All rights reserved. -// - -import XCTest - -@testable import passKit - -final class AppKeychainTest: XCTestCase { - private let keychain = AppKeychain.shared - private let testPrefix = "test.AppKeychainTest." - - override func tearDown() { - super.tearDown() - keychain.removeAllContent(withPrefix: testPrefix) - } - - private func key(_ name: String) -> String { - "\(testPrefix)\(name)" - } - - // MARK: - Basic round-trip - - func testAddAndGet() { - keychain.add(string: "hello", for: key("addGet")) - - XCTAssertEqual(keychain.get(for: key("addGet")), "hello") - } - - func testGetMissingKeyReturnsNil() { - XCTAssertNil(keychain.get(for: key("nonexistent"))) - } - - func testOverwriteValue() { - keychain.add(string: "first", for: key("overwrite")) - keychain.add(string: "second", for: key("overwrite")) - - XCTAssertEqual(keychain.get(for: key("overwrite")), "second") - } - - func testAddNilRemovesValue() { - keychain.add(string: "value", for: key("addNil")) - keychain.add(string: nil, for: key("addNil")) - - XCTAssertNil(keychain.get(for: key("addNil"))) - XCTAssertFalse(keychain.contains(key: key("addNil"))) - } - - // MARK: - contains - - func testContainsReturnsTrueForExistingKey() { - keychain.add(string: "value", for: key("exists")) - - XCTAssertTrue(keychain.contains(key: key("exists"))) - } - - func testContainsReturnsFalseForMissingKey() { - XCTAssertFalse(keychain.contains(key: key("missing"))) - } - - // MARK: - removeContent - - func testRemoveContent() { - keychain.add(string: "value", for: key("remove")) - keychain.removeContent(for: key("remove")) - - XCTAssertNil(keychain.get(for: key("remove"))) - XCTAssertFalse(keychain.contains(key: key("remove"))) - } - - func testRemoveContentForMissingKeyDoesNotThrow() { - keychain.removeContent(for: key("neverExisted")) - // No assertion needed — just verifying it doesn't crash - } - - // MARK: - removeAllContent(withPrefix:) - - func testRemoveAllContentWithPrefix() { - keychain.add(string: "1", for: key("prefixA.one")) - keychain.add(string: "2", for: key("prefixA.two")) - keychain.add(string: "3", for: key("prefixB.one")) - - keychain.removeAllContent(withPrefix: key("prefixA")) - - XCTAssertNil(keychain.get(for: key("prefixA.one"))) - XCTAssertNil(keychain.get(for: key("prefixA.two"))) - XCTAssertEqual(keychain.get(for: key("prefixB.one")), "3") - } - - func testRemoveAllContentWithPrefixNoMatches() { - keychain.add(string: "value", for: key("survivor")) - - keychain.removeAllContent(withPrefix: key("noMatch")) - - XCTAssertEqual(keychain.get(for: key("survivor")), "value") - } -} diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index de3f7b3..b65bf95 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,331 +13,47 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { - private let localRepoURL: URL = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") - - private var passwordStore: PasswordStore! = nil - - override func setUp() { - passwordStore = PasswordStore(url: localRepoURL) - } - - override func tearDown() { - passwordStore.erase() - passwordStore = nil - - Defaults.removeAll() - } - - func testInitPasswordEntityCoreData() throws { - try cloneRepository(.withGPGID) - - XCTAssertEqual(passwordStore.numberOfPasswords, 4) - XCTAssertEqual(passwordStore.numberOfCommits, 16) - XCTAssertEqual(passwordStore.numberOfLocalCommits, 0) - - let entity = passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg") - XCTAssertEqual(entity!.path, "personal/github.com.gpg") - XCTAssertEqual(entity!.name, "github.com") - XCTAssertTrue(entity!.isSynced) - XCTAssertEqual(entity!.parent!.name, "personal") - - XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "family/amazon.com.gpg")) - XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "work/github.com.gpg")) - XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "shared/github.com.gpg")) - - let dirEntity = passwordStore.fetchPasswordEntity(with: "shared") - XCTAssertNotNil(dirEntity) - XCTAssertTrue(dirEntity!.isDir) - XCTAssertEqual(dirEntity!.name, "shared") - XCTAssertEqual(dirEntity!.children.count, 1) - } - - func testEraseStoreData() throws { - try cloneRepository(.withGPGID) - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.path)) - XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0) - XCTAssertNotNil(passwordStore.gitRepository) - - expectation(forNotification: .passwordStoreUpdated, object: nil) - expectation(forNotification: .passwordStoreErased, object: nil) - passwordStore.eraseStoreData() - - XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.path)) - XCTAssertEqual(passwordStore.numberOfPasswords, 0) - XCTAssertNil(passwordStore.gitRepository) - waitForExpectations(timeout: 1, handler: nil) - } - - func testErase() throws { - try cloneRepository(.withGPGID) - try importSinglePGPKey() - Defaults.gitSignatureName = "Test User" - PasscodeLock.shared.save(passcode: "1234") - - XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0) - XCTAssertTrue(AppKeychain.shared.contains(key: PGPKey.PUBLIC.getKeychainKey())) - XCTAssertEqual(Defaults.gitSignatureName, "Test User") - XCTAssertTrue(PasscodeLock.shared.hasPasscode) - XCTAssertTrue(PGPAgent.shared.isInitialized()) - - expectation(forNotification: .passwordStoreUpdated, object: nil) - expectation(forNotification: .passwordStoreErased, object: nil) - passwordStore.erase() - - XCTAssertEqual(passwordStore.numberOfPasswords, 0) - XCTAssertFalse(AppKeychain.shared.contains(key: PGPKey.PUBLIC.getKeychainKey())) - XCTAssertFalse(Defaults.hasKey(\.gitSignatureName)) - XCTAssertFalse(PasscodeLock.shared.hasPasscode) - XCTAssertFalse(PGPAgent.shared.isInitialized()) - waitForExpectations(timeout: 1, handler: nil) - } - - func testFetchPasswordEntityCoreDataByParent() throws { - try cloneRepository(.withGPGID) - - let rootChildren = passwordStore.fetchPasswordEntityCoreData(parent: nil) - XCTAssertGreaterThan(rootChildren.count, 0) - rootChildren.forEach { entity in - XCTAssertTrue(entity.isDir) - } - - let personalDir = passwordStore.fetchPasswordEntity(with: "personal") - let personalChildren = passwordStore.fetchPasswordEntityCoreData(parent: personalDir) - XCTAssertEqual(personalChildren.count, 1) - XCTAssertEqual(personalChildren.first?.name, "github.com") - } - - func testFetchPasswordEntityCoreDataWithDir() throws { - try cloneRepository(.withGPGID) - - let allPasswords = passwordStore.fetchPasswordEntityCoreData(withDir: false) - XCTAssertEqual(allPasswords.count, 4) - allPasswords.forEach { entity in - XCTAssertFalse(entity.isDir) - } - } - - func testAddPassword() throws { - try cloneRepository(.empty) - try importSinglePGPKey() - let numCommitsBefore = passwordStore.numberOfCommits! - let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - - let password1 = Password(name: "test1", path: "test1.gpg", plainText: "foobar") - let password2 = Password(name: "test2", path: "test2.gpg", plainText: "hello world") - let password3 = Password(name: "test3", path: "folder/test3.gpg", plainText: "lorem ipsum") - let password4 = Password(name: "test4", path: "test4.gpg", plainText: "you are valuable and you matter") - - for password in [password1, password2, password3, password4] { - expectation(forNotification: .passwordStoreUpdated, object: nil) - - let savedEntity = try passwordStore.add(password: password) - - XCTAssertEqual(savedEntity!.name, password.name) - waitForExpectations(timeout: 1, handler: nil) - } - - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("test1.gpg").path)) - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("test2.gpg").path)) - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("folder").path)) - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("folder/test3.gpg").path)) - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("test4.gpg").path)) - - XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore + 4) - XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore + 4) - } - - func testAddAndDecryptRoundTrip() throws { - try cloneRepository(.empty) - try importSinglePGPKey() - - let password = Password(name: "test", path: "test.gpg", plainText: "foobar") - let savedEntity = try passwordStore.add(password: password) - - let decryptedPassword = try passwordStore.decrypt(passwordEntity: savedEntity!, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - XCTAssertEqual(decryptedPassword.plainText, "foobar") - } - - func testDeletePassword() throws { - try cloneRepository(.withGPGID) - let numCommitsBefore = passwordStore.numberOfCommits! - let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - - expectation(forNotification: .passwordStoreUpdated, object: nil) - - let entity = passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg") - try passwordStore.delete(passwordEntity: entity!) - - XCTAssertNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")) - XCTAssertNil(passwordStore.fetchPasswordEntity(with: "personal")) - XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal").path)) - XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore + 1) - XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore + 1) - waitForExpectations(timeout: 1, handler: nil) - } - - func testDeleteDirectoryFails() throws { - try cloneRepository(.withGPGID) - let numCommitsBefore = passwordStore.numberOfCommits! - let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - - expectation(forNotification: .passwordStoreUpdated, object: nil).isInverted = true - - let entity = passwordStore.fetchPasswordEntity(with: "personal") - XCTAssertThrowsError(try passwordStore.delete(passwordEntity: entity!)) { error in - XCTAssertTrue(error is AppError, "Unexpected error type: \(type(of: error))") - XCTAssertEqual(error as? AppError, .cannotDeleteDirectory) - } - - XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")) - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal/github.com.gpg").path)) - XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore) - XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore) - waitForExpectations(timeout: 0.1, handler: nil) - } - - func testEditPasswordValue() throws { - try cloneRepository(.withGPGID) - try importSinglePGPKey() - let numCommitsBefore = passwordStore.numberOfCommits! - let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - let entity = passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")! - - expectation(forNotification: .passwordStoreUpdated, object: nil) - - let editedPassword = Password(name: entity.name, path: entity.path, plainText: "editedpassword") - editedPassword.changed = PasswordChange.content.rawValue - let editedEntity = try passwordStore.edit(passwordEntity: entity, password: editedPassword) - - XCTAssertNotNil(editedEntity) - XCTAssertEqual(editedEntity!.name, "github.com") - XCTAssertFalse(editedEntity!.isSynced) - XCTAssertEqual(try decrypt(path: "personal/github.com.gpg").plainText, "editedpassword") - XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore + 1) - XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore + 1) - waitForExpectations(timeout: 1, handler: nil) - } - - func testMovePassword() throws { - try cloneRepository(.withGPGID) - try importSinglePGPKey() - let numCommitsBefore = passwordStore.numberOfCommits! - let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - let entity = passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")! - - expectation(forNotification: .passwordStoreUpdated, object: nil) - - let editedPassword = Password(name: "new name", path: "new name.gpg", plainText: "passwordforpersonal\n") - editedPassword.changed = PasswordChange.path.rawValue - let editedEntity = try passwordStore.edit(passwordEntity: entity, password: editedPassword) - - XCTAssertEqual(editedEntity!.name, "new name") - XCTAssertFalse(editedEntity!.isSynced) - XCTAssertEqual(try decrypt(path: "new name.gpg").plainText, "passwordforpersonal\n") - XCTAssertNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")) - XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore + 1) - XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore + 1) - waitForExpectations(timeout: 1, handler: nil) - } - - func testReset() throws { - try cloneRepository(.withGPGID) - try importSinglePGPKey() - let numCommitsBefore = passwordStore.numberOfCommits! - let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - - _ = try passwordStore.add(password: Password(name: "test", path: "test.gpg", plainText: "foobar")) - try passwordStore.delete(passwordEntity: passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")!) - - expectation(forNotification: .passwordStoreUpdated, object: nil) - let numDroppedCommits = try passwordStore.reset() - - XCTAssertEqual(numDroppedCommits, 2) - XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("test.gpg").path)) - XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal/github.com.gpg").path)) - XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore) - XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore) - waitForExpectations(timeout: 1, handler: nil) - } - - // MARK: - .gpg-id support + private let remoteRepoURL = URL(string: "https://github.com/mssun/passforios-password-store.git")! func testCloneAndDecryptMultiKeys() throws { - try cloneRepository(.withGPGID) - try importMultiplePGPKeys() + let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") Defaults.isEnableGPGIDOn = true + let passwordStore = PasswordStore(url: url) + try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") + expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: url.path) }, evaluatedWith: nil) + waitForExpectations(timeout: 3, handler: nil) [ ("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"), ("personal/github.com", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"), ].forEach { path, id in - let keyID = findGPGID(from: localRepoURL.appendingPathComponent(path)) + let keyID = findGPGID(from: url.appendingPathComponent(path)) XCTAssertEqual(keyID, id) } - let personal = try decrypt(path: "personal/github.com.gpg") + let keychain = AppKeychain.shared + try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.publicKeys) + try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.privateKeys) + try PGPAgent.shared.initKeys() + + let personal = try decrypt(passwordStore: passwordStore, path: "personal/github.com.gpg", passphrase: "passforios") XCTAssertEqual(personal.plainText, "passwordforpersonal\n") - let work = try decrypt(path: "work/github.com.gpg") + let work = try decrypt(passwordStore: passwordStore, path: "work/github.com.gpg", passphrase: "passforios") XCTAssertEqual(work.plainText, "passwordforwork\n") let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword") let testPasswordEntity = try passwordStore.add(password: testPassword)! let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) XCTAssertEqual(testPasswordPlain.plainText, "testpassword") + + passwordStore.erase() + Defaults.isEnableGPGIDOn = false } - // MARK: - Helpers - - private enum RemoteRepo { - case empty - case withGPGID - - var url: URL { - switch self { - case .empty: - Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-empty.git") - case .withGPGID: - Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-with-gpgid.git") - } - } - - var branchName: String { - switch self { - case .empty: - "main" - case .withGPGID: - "master" - } - } - } - - private func cloneRepository(_ remote: RemoteRepo) throws { - expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil) - expectation(forNotification: .passwordStoreUpdated, object: nil) - - try passwordStore.cloneRepository(remoteRepoURL: remote.url, branchName: remote.branchName) - - waitForExpectations(timeout: 3, handler: nil) - } - - private func importSinglePGPKey() throws { - let keychain = AppKeychain.shared - try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA4096.publicKey) - try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA4096.privateKey) - try PGPAgent.shared.initKeys() - } - - private func importMultiplePGPKeys() throws { - let keychain = AppKeychain.shared - try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.publicKeys) - try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.privateKeys) - try PGPAgent.shared.initKeys() - } - - private func decrypt(path: String, keyID: String? = nil) throws -> Password { + private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { let entity = passwordStore.fetchPasswordEntity(with: path)! - return try passwordStore.decrypt(passwordEntity: entity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) } } diff --git a/plans/01-improve-test-coverage-plan.md b/plans/01-improve-test-coverage-plan.md new file mode 100644 index 0000000..d9f8efe --- /dev/null +++ b/plans/01-improve-test-coverage-plan.md @@ -0,0 +1,98 @@ +# Improve Test Coverage Plan + +## Motivation + +The passKit codebase has ~100 test methods but critical components that will be heavily refactored (for multi-store support and other changes) have little or no test coverage. Adding regression tests now prevents silent breakage during future work. + +This is standalone — it should be done before any other refactoring. + +--- + +## Current Test Coverage + +### Well-tested areas +- Password parsing (`Password`, `Parser`, `AdditionField`, OTP, `TokenBuilder`) — ~40 tests +- `PGPAgent` — 8 tests covering multiple key types, error cases, passphrase handling +- `PasswordGenerator` — 8 tests +- `GitRepository` — 8 tests (uses real temp git repos on disk) +- `GitCredential` — 6 tests (SSH test is skipped/"failed in CI") +- `PasswordEntity` Core Data operations — 6 tests (uses in-memory store via `CoreDataTestCase`) +- `KeyFileManager` — 7 tests +- `QRKeyScanner` — 6 tests +- String/Array extensions — 6 tests + +### Critical gaps (zero tests) + +| Component | Notes | +|-----------|-------| +| **`PasswordStore`** (36 methods) | Only 1 integration test that clones from GitHub. No unit tests for pull, push, add, delete, edit, decrypt, encrypt, reset, erase, eraseStoreData, deleteCoreData, fetchPasswordEntityCoreData, initPasswordEntityCoreData. | +| **`AppKeychain`** | Zero tests. Only exercised indirectly via `DictBasedKeychain` mock. | +| **`PersistenceController` / Core Data stack** | Only the `isUnitTest: true` path is exercised. No tests for `reinitializePersistentStore`, `deletePersistentStore`, error recovery. | +| **Services** (`PasswordDecryptor`, `PasswordEncryptor`, `PasswordManager`, `PasswordNavigationDataSource`) | Zero tests. Core business logic that ties `PasswordStore` + `PGPAgent` together. | +| **All view controllers (28+)** | Zero tests. No UI test target exists. | +| **AutoFill / Share / Shortcuts extensions** | Zero tests. No test targets for extensions. | +| **`PasscodeLock`** | Zero tests. Security-critical. | + +### Test infrastructure that already exists +- `CoreDataTestCase` — base class with in-memory `PersistenceController` (reusable) +- `DictBasedKeychain` — in-memory `KeyStore` mock (reusable) +- `TestPGPKeys` — PGP key fixtures for RSA2048, RSA4096, ED25519, NISTP384, multi-key sets + +--- + +## Implementation + +### 1. `PasswordStore` unit tests (highest priority) + +The single existing test (`testCloneAndDecryptMultiKeys`) depends on network access. Add offline unit tests using a local git repo fixture: + +- **Setup/teardown**: Create a temp directory, `git init`, add `.gpg-id` + encrypted `.gpg` files, so tests don't need network. +- **Test `initPasswordEntityCoreData`**: Clone a local fixture repo → verify correct `PasswordEntity` tree in Core Data (names, paths, directories, parent-child relationships). +- **Test `deleteCoreData`**: Populate, then delete, verify empty. +- **Test `eraseStoreData`**: Verify repo directory deleted, Core Data cleared, git handle nil'd. +- **Test `erase`**: Verify full cleanup (keychain, defaults, passcode, PGP state). +- **Test `fetchPasswordEntityCoreData`**: Verify fetch with parent filter, withDir filter. +- **Test encrypt → save → decrypt round-trip**: Using `DictBasedKeychain` + test PGP keys + local repo. +- **Test `add` / `delete` / `edit`**: Verify filesystem + Core Data + git commit. +- **Test `reset`**: Verify Core Data rebuilt to match filesystem after git reset. + +### 2. `PasswordEntity` relationship tests + +Extend `PasswordEntityTest` (already uses `CoreDataTestCase`): + +- **Test `initPasswordEntityCoreData` BFS walk**: Create a temp directory tree with `.gpg` files, call the static method, verify entity tree matches filesystem. +- **Test that `.gpg` extension is stripped** from names but non-`.gpg` files keep their names. +- **Test hidden files are skipped**. +- **Test empty directories**. + +### 3. `AppKeychain` tests + +Basic tests against the real Keychain API (or a test wrapper): + +- **Test `add` / `get` / `removeContent`** round-trip. +- **Test `removeAllContent`**. +- **Test `contains`**. +- **Test `removeAllContent(withPrefix:)`** — this method already exists and will be useful for per-store cleanup. + +### 4. `PersistenceController` tests + +- **Test `reinitializePersistentStore`** — verify existing data is gone after reinit. +- **Test model loading** — verify the `.momd` loads correctly. + +### 5. Test infrastructure: local git repo fixture builder + +A helper that creates a temp git repo with configurable `.gpg-id`, encrypted `.gpg` files, and directory structure. Replaces the current network-dependent clone in `PasswordStoreTest`. + +--- + +## Implementation Order + +All steps are independent and can be done in parallel: + +| Step | Description | +|------|-------------| +| 1 | `PasswordStore` unit tests (offline, local git fixture) | +| 2 | `PasswordEntity` BFS walk + relationship tests | +| 3 | `AppKeychain` tests | +| 4 | `PersistenceController` tests | +| 5 | Local git repo fixture builder (prerequisite for step 1) |