Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions pass.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
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 */; };
Expand Down Expand Up @@ -195,7 +198,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 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */; };
DC6474532D20DD0C004B4BBC /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474522D20DD0C004B4BBC /* PersistenceController.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 */; };
Expand Down Expand Up @@ -422,6 +425,9 @@
30F6C1B327664C7200BE5AB2 /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SVProgressHUD.xcframework; path = Carthage/Build/SVProgressHUD.xcframework; sourceTree = "<group>"; };
30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = "<group>"; };
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 = "<group>"; };
8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceControllerTest.swift; sourceTree = "<group>"; };
8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = "<group>"; };
9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = "<group>"; };
9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = "<group>"; };
9A1EF0B524C50EE00074FEAC /* passBetaExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaExtension.entitlements; sourceTree = "<group>"; };
Expand Down Expand Up @@ -498,7 +504,7 @@
DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = "<group>"; };
DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = "<group>"; };
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = "<group>"; };
DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = "<group>"; };
DC6474522D20DD0C004B4BBC /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTestCase.swift; sourceTree = "<group>"; };
DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEntityTest.swift; sourceTree = "<group>"; };
DC64745E2D45B23A004B4BBC /* GitRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitRepository.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -624,6 +630,7 @@
301F6464216164670071A4CE /* Helpers */ = {
isa = PBXGroup;
children = (
8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */,
3032328922C9FBA2009EBD9C /* KeyFileManagerTest.swift */,
);
path = Helpers;
Expand Down Expand Up @@ -761,6 +768,14 @@
path = Crypto;
sourceTree = "<group>";
};
8A4716702F5EF7A900C7A64D /* Controllers */ = {
isa = PBXGroup;
children = (
8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */,
);
path = Controllers;
sourceTree = "<group>";
};
9A58664F25AADB66006719C2 /* Services */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -879,10 +894,12 @@
A26075861EEC6F34005DB03E /* passKitTests */ = {
isa = PBXGroup;
children = (
8A4716702F5EF7A900C7A64D /* Controllers */,
DC64745A2D29BD43004B4BBC /* CoreData */,
30A86F93230F235800F821A4 /* Crypto */,
30BAC8C322E3BA4300438475 /* Testbase */,
30697C5521F63F870064FCAC /* Extensions */,
8AD8EBF22F5E268D007475AB /* Fixtures */,
301F6464216164670071A4CE /* Helpers */,
30C015A7214ED378005BB6DF /* Models */,
30C015A6214ED32A005BB6DF /* Parser */,
Expand Down Expand Up @@ -913,7 +930,7 @@
children = (
30697C3121F63C8B0064FCAC /* PasscodeLockPresenter.swift */,
30697C3221F63C8B0064FCAC /* PasscodeLockViewController.swift */,
DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */,
DC6474522D20DD0C004B4BBC /* PersistenceController.swift */,
);
path = Controllers;
sourceTree = "<group>";
Expand Down Expand Up @@ -1436,6 +1453,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8AD8EBF32F5E2723007475AB /* Fixtures in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1613,7 +1631,7 @@
3087574F2343E42A00B971A2 /* Colors.swift in Sources */,
30697C2C21F63C5A0064FCAC /* FileManagerExtension.swift in Sources */,
30697C3321F63C8B0064FCAC /* PasscodeLockPresenter.swift in Sources */,
DC6474532D20DD0C004B4BBC /* CoreDataStack.swift in Sources */,
DC6474532D20DD0C004B4BBC /* PersistenceController.swift in Sources */,
30697C3D21F63C990064FCAC /* UIViewExtension.swift in Sources */,
30697C3A21F63C990064FCAC /* UIViewControllerExtension.swift in Sources */,
30697C2E21F63C5A0064FCAC /* Utils.swift in Sources */,
Expand All @@ -1632,13 +1650,15 @@
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 */,
DC6474612D46A8F8004B4BBC /* GitRepositoryTest.swift in Sources */,
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 */,
Expand Down
1 change: 1 addition & 0 deletions pass/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"KeyImportError." = "Schlüssel kann nicht importiert werden.";
"FileNotFoundError." = "Die Datei '%@' kann nicht gelesen werden.";
"PasswordDuplicatedError." = "Passwort kann nicht hinzugefügt werden; es existiert bereits.";
"CannotDeleteNonEmptyDirectoryError." = "Ordner muss erst leer sein um gelöscht werden zu können.";
"GitResetError." = "Der zuletzt synchronisierte Commit kann nicht identifiziert werden.";
"GitCreateSignatureError." = "Es konnte keine valide Signatur für den Author/Committer angelegt werden.";
"GitPushNotSuccessfulError." = "Die Übertragung der lokalen Änderungen war nicht erfolgreich. Stelle bitte sicher, dass auf dem Remote-Repository alle Änderungen commitet sind.";
Expand Down
1 change: 1 addition & 0 deletions pass/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"KeyImportError." = "Cannot import the key.";
"FileNotFoundError." = "File '%@' cannot be read.";
"PasswordDuplicatedError." = "Cannot add the password; password is duplicated.";
"CannotDeleteNonEmptyDirectoryError." = "Delete passwords from the directory before deleting the directory itself.";
"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.";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// CoreDataStack.swift
// PersistenceController.swift
// passKit
//
// Created by Mingshen Sun on 12/28/24.
Expand All @@ -18,19 +18,19 @@ public class PersistenceController {

let container: NSPersistentContainer

init(isUnitTest: Bool = false) {
init(storeURL: URL? = nil) {
self.container = NSPersistentContainer(name: Self.modelName, managedObjectModel: .sharedModel)
let description = container.persistentStoreDescriptions.first
description?.shouldMigrateStoreAutomatically = false
description?.shouldInferMappingModelAutomatically = false
if isUnitTest {
description?.url = URL(fileURLWithPath: "/dev/null")
} else {
description?.url = URL(fileURLWithPath: Globals.dbPath)
}
description?.url = storeURL ?? URL(fileURLWithPath: Globals.dbPath)
setup()
}

static func forUnitTests() -> PersistenceController {
PersistenceController(storeURL: URL(fileURLWithPath: "/dev/null"))
}

func setup() {
container.loadPersistentStores { _, error in
if error != nil {
Expand Down
4 changes: 4 additions & 0 deletions passKit/Crypto/PGPAgent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public class PGPAgent {
pgpInterface = nil
}

public func isInitialized() -> Bool {
pgpInterface != nil
}

public func getKeyID() throws -> [String] {
try checkAndInit()
return pgpInterface?.keyID ?? []
Expand Down
1 change: 1 addition & 0 deletions passKit/Helpers/AppError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum AppError: Error, Equatable {
case keyImport
case readingFile(fileName: String)
case passwordDuplicated
case cannotDeleteNonEmptyDirectory
case gitReset
case gitCommit
case gitCreateSignature
Expand Down
17 changes: 14 additions & 3 deletions passKit/Models/PasswordStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,16 +273,27 @@ public class PasswordStore {
}

public func delete(passwordEntity: PasswordEntity) throws {
if !passwordEntity.children.isEmpty {
throw AppError.cannotDeleteNonEmptyDirectory
}

let deletedFileURL = passwordEntity.fileURL(in: storeURL)
let deletedFilePath = passwordEntity.path
try gitRm(path: passwordEntity.path)
if !passwordEntity.isDir {
try gitRm(path: passwordEntity.path)
}
try deletePasswordEntities(passwordEntity: passwordEntity)
try deleteDirectoryTree(at: deletedFileURL)
try gitCommit(message: "RemovePassword.".localize(deletedFilePath))
notificationCenter.post(name: .passwordStoreUpdated, object: nil)
}

public func edit(passwordEntity: PasswordEntity, password: Password, keyID: String? = nil) throws -> PasswordEntity? {
guard !passwordEntity.isDir else {
// caller should ensure this, so this is not a user-facing error
throw AppError.other(message: "Cannot edit a directory")
}

var newPasswordEntity: PasswordEntity? = passwordEntity
let url = passwordEntity.fileURL(in: storeURL)

Expand Down Expand Up @@ -320,11 +331,11 @@ public class PasswordStore {
saveUpdatedContext()
}

public func saveUpdatedContext() {
private func saveUpdatedContext() {
PersistenceController.shared.save()
}

public func deleteCoreData() {
private func deleteCoreData() {
PasswordEntity.deleteAll(in: context)
PersistenceController.shared.save()
}
Expand Down
93 changes: 93 additions & 0 deletions passKitTests/Controllers/PersistenceControllerTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// 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 {

Check failure on line 89 in passKitTests/Controllers/PersistenceControllerTest.swift

View workflow job for this annotation

GitHub Actions / testing

Empty XCTest Method Violation: Empty XCTest method should be avoided (empty_xctest_method)

Check failure on line 89 in passKitTests/Controllers/PersistenceControllerTest.swift

View workflow job for this annotation

GitHub Actions / testing

Empty XCTest Method Violation: Empty XCTest method should be avoided (empty_xctest_method)
// NOTE: save() calls fatalError on Core Data save failures, so error propagation
// cannot be tested without refactoring save() to throw...
}
}
2 changes: 1 addition & 1 deletion passKitTests/CoreData/CoreDataTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CoreDataTestCase: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()

controller = PersistenceController(isUnitTest: true)
controller = PersistenceController.forUnitTests()
}

override func tearDown() {
Expand Down
Loading
Loading