Compare commits
17 commits
c8cce5c322
...
f0c21dd880
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c21dd880 | ||
|
|
55b682b4b0 | ||
|
|
b8b7e1f913 | ||
|
|
cde82d956b | ||
|
|
4c21ab99ad | ||
|
|
c6a4f80503 | ||
|
|
e1da1988b4 | ||
|
|
e195280efc | ||
|
|
c3bfa861f4 | ||
|
|
98646242e0 | ||
|
|
12c8c04203 | ||
|
|
98ad323431 | ||
|
|
e5650ec756 | ||
|
|
60999c7eab | ||
|
|
ef188fcfba | ||
|
|
85972a02c3 | ||
|
|
17b6bb8bc2 |
43 changed files with 756 additions and 34 deletions
|
|
@ -114,6 +114,9 @@
|
||||||
5F9D7B0D27AF6F7500A8AB22 /* CryptoTokenKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
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, ); }; };
|
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, ); }; };
|
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 */; };
|
9A1D1CE526E5D1CE0052028E /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1D1CE426E5D1CE0052028E /* OneTimePassword */; };
|
||||||
9A1D1CE726E5D2230052028E /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1D1CE626E5D2230052028E /* OneTimePassword */; };
|
9A1D1CE726E5D2230052028E /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1D1CE626E5D2230052028E /* OneTimePassword */; };
|
||||||
9A1F47FA26E5CF4B000C0E01 /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1F47F926E5CF4B000C0E01 /* OneTimePassword */; };
|
9A1F47FA26E5CF4B000C0E01 /* OneTimePassword in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1F47F926E5CF4B000C0E01 /* OneTimePassword */; };
|
||||||
|
|
@ -195,7 +198,7 @@
|
||||||
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; };
|
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; };
|
||||||
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; };
|
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; };
|
||||||
DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.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 */; };
|
DC64745C2D29BE9B004B4BBC /* PasswordEntityTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */; };
|
||||||
DC64745D2D29BEA9004B4BBC /* CoreDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */; };
|
DC64745D2D29BEA9004B4BBC /* CoreDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */; };
|
||||||
DC64745F2D45B240004B4BBC /* GitRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC64745E2D45B23A004B4BBC /* GitRepository.swift */; };
|
DC64745F2D45B240004B4BBC /* GitRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC64745E2D45B23A004B4BBC /* GitRepository.swift */; };
|
||||||
|
|
@ -422,6 +425,9 @@
|
||||||
30F6C1B327664C7200BE5AB2 /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SVProgressHUD.xcframework; path = Carthage/Build/SVProgressHUD.xcframework; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
9A1EF0B524C50EE00074FEAC /* passBetaExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaExtension.entitlements; sourceTree = "<group>"; };
|
||||||
|
|
@ -498,7 +504,7 @@
|
||||||
DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
DC64745E2D45B23A004B4BBC /* GitRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitRepository.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -624,6 +630,7 @@
|
||||||
301F6464216164670071A4CE /* Helpers */ = {
|
301F6464216164670071A4CE /* Helpers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */,
|
||||||
3032328922C9FBA2009EBD9C /* KeyFileManagerTest.swift */,
|
3032328922C9FBA2009EBD9C /* KeyFileManagerTest.swift */,
|
||||||
);
|
);
|
||||||
path = Helpers;
|
path = Helpers;
|
||||||
|
|
@ -761,6 +768,14 @@
|
||||||
path = Crypto;
|
path = Crypto;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
8A4716702F5EF7A900C7A64D /* Controllers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */,
|
||||||
|
);
|
||||||
|
path = Controllers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9A58664F25AADB66006719C2 /* Services */ = {
|
9A58664F25AADB66006719C2 /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -879,10 +894,12 @@
|
||||||
A26075861EEC6F34005DB03E /* passKitTests */ = {
|
A26075861EEC6F34005DB03E /* passKitTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
8A4716702F5EF7A900C7A64D /* Controllers */,
|
||||||
DC64745A2D29BD43004B4BBC /* CoreData */,
|
DC64745A2D29BD43004B4BBC /* CoreData */,
|
||||||
30A86F93230F235800F821A4 /* Crypto */,
|
30A86F93230F235800F821A4 /* Crypto */,
|
||||||
30BAC8C322E3BA4300438475 /* Testbase */,
|
30BAC8C322E3BA4300438475 /* Testbase */,
|
||||||
30697C5521F63F870064FCAC /* Extensions */,
|
30697C5521F63F870064FCAC /* Extensions */,
|
||||||
|
8AD8EBF22F5E268D007475AB /* Fixtures */,
|
||||||
301F6464216164670071A4CE /* Helpers */,
|
301F6464216164670071A4CE /* Helpers */,
|
||||||
30C015A7214ED378005BB6DF /* Models */,
|
30C015A7214ED378005BB6DF /* Models */,
|
||||||
30C015A6214ED32A005BB6DF /* Parser */,
|
30C015A6214ED32A005BB6DF /* Parser */,
|
||||||
|
|
@ -913,7 +930,7 @@
|
||||||
children = (
|
children = (
|
||||||
30697C3121F63C8B0064FCAC /* PasscodeLockPresenter.swift */,
|
30697C3121F63C8B0064FCAC /* PasscodeLockPresenter.swift */,
|
||||||
30697C3221F63C8B0064FCAC /* PasscodeLockViewController.swift */,
|
30697C3221F63C8B0064FCAC /* PasscodeLockViewController.swift */,
|
||||||
DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */,
|
DC6474522D20DD0C004B4BBC /* PersistenceController.swift */,
|
||||||
);
|
);
|
||||||
path = Controllers;
|
path = Controllers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -1427,6 +1444,7 @@
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
8AD8EBF32F5E2723007475AB /* Fixtures in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -1604,7 +1622,7 @@
|
||||||
3087574F2343E42A00B971A2 /* Colors.swift in Sources */,
|
3087574F2343E42A00B971A2 /* Colors.swift in Sources */,
|
||||||
30697C2C21F63C5A0064FCAC /* FileManagerExtension.swift in Sources */,
|
30697C2C21F63C5A0064FCAC /* FileManagerExtension.swift in Sources */,
|
||||||
30697C3321F63C8B0064FCAC /* PasscodeLockPresenter.swift in Sources */,
|
30697C3321F63C8B0064FCAC /* PasscodeLockPresenter.swift in Sources */,
|
||||||
DC6474532D20DD0C004B4BBC /* CoreDataStack.swift in Sources */,
|
DC6474532D20DD0C004B4BBC /* PersistenceController.swift in Sources */,
|
||||||
30697C3D21F63C990064FCAC /* UIViewExtension.swift in Sources */,
|
30697C3D21F63C990064FCAC /* UIViewExtension.swift in Sources */,
|
||||||
30697C3A21F63C990064FCAC /* UIViewControllerExtension.swift in Sources */,
|
30697C3A21F63C990064FCAC /* UIViewControllerExtension.swift in Sources */,
|
||||||
30697C2E21F63C5A0064FCAC /* Utils.swift in Sources */,
|
30697C2E21F63C5A0064FCAC /* Utils.swift in Sources */,
|
||||||
|
|
@ -1623,6 +1641,7 @@
|
||||||
30A86F95230F237000F821A4 /* CryptoFrameworkTest.swift in Sources */,
|
30A86F95230F237000F821A4 /* CryptoFrameworkTest.swift in Sources */,
|
||||||
30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */,
|
30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */,
|
||||||
30DAFD4C240985E3002456E7 /* Array+SlicesTest.swift in Sources */,
|
30DAFD4C240985E3002456E7 /* Array+SlicesTest.swift in Sources */,
|
||||||
|
8A4716712F5EF7A900C7A64D /* PersistenceControllerTest.swift in Sources */,
|
||||||
301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */,
|
301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */,
|
||||||
9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */,
|
9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */,
|
||||||
30BAC8CB22E3BB6C00438475 /* DictBasedKeychain.swift in Sources */,
|
30BAC8CB22E3BB6C00438475 /* DictBasedKeychain.swift in Sources */,
|
||||||
|
|
@ -1630,6 +1649,7 @@
|
||||||
A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */,
|
A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */,
|
||||||
30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */,
|
30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */,
|
||||||
A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */,
|
A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */,
|
||||||
|
8A4716692F5EF56900C7A64D /* AppKeychainTest.swift in Sources */,
|
||||||
30695E2524FAEF2600C9D46E /* GitCredentialTest.swift in Sources */,
|
30695E2524FAEF2600C9D46E /* GitCredentialTest.swift in Sources */,
|
||||||
30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */,
|
30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */,
|
||||||
30B04860209A5141001013CA /* PasswordTest.swift in Sources */,
|
30B04860209A5141001013CA /* PasswordTest.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@
|
||||||
"KeyImportError." = "Schlüssel kann nicht importiert werden.";
|
"KeyImportError." = "Schlüssel kann nicht importiert werden.";
|
||||||
"FileNotFoundError." = "Die Datei '%@' kann nicht gelesen werden.";
|
"FileNotFoundError." = "Die Datei '%@' kann nicht gelesen werden.";
|
||||||
"PasswordDuplicatedError." = "Passwort kann nicht hinzugefügt werden; es existiert bereits.";
|
"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.";
|
"GitResetError." = "Der zuletzt synchronisierte Commit kann nicht identifiziert werden.";
|
||||||
"GitCreateSignatureError." = "Es konnte keine valide Signatur für den Author/Committer angelegt 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.";
|
"GitPushNotSuccessfulError." = "Die Übertragung der lokalen Änderungen war nicht erfolgreich. Stelle bitte sicher, dass auf dem Remote-Repository alle Änderungen commitet sind.";
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
"KeyImportError." = "Cannot import the key.";
|
"KeyImportError." = "Cannot import the key.";
|
||||||
"FileNotFoundError." = "File '%@' cannot be read.";
|
"FileNotFoundError." = "File '%@' cannot be read.";
|
||||||
"PasswordDuplicatedError." = "Cannot add the password; password is duplicated.";
|
"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.";
|
"GitResetError." = "Cannot identify the latest synced commit.";
|
||||||
"GitCreateSignatureError." = "Cannot create a valid author/committer signature.";
|
"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.";
|
"GitPushNotSuccessfulError." = "Pushing local changes was not successful. Make sure there are no uncommitted changes on the remote repository.";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// CoreDataStack.swift
|
// PersistenceController.swift
|
||||||
// passKit
|
// passKit
|
||||||
//
|
//
|
||||||
// Created by Mingshen Sun on 12/28/24.
|
// Created by Mingshen Sun on 12/28/24.
|
||||||
|
|
@ -18,19 +18,19 @@ public class PersistenceController {
|
||||||
|
|
||||||
let container: NSPersistentContainer
|
let container: NSPersistentContainer
|
||||||
|
|
||||||
init(isUnitTest: Bool = false) {
|
init(storeURL: URL? = nil) {
|
||||||
self.container = NSPersistentContainer(name: Self.modelName, managedObjectModel: .sharedModel)
|
self.container = NSPersistentContainer(name: Self.modelName, managedObjectModel: .sharedModel)
|
||||||
let description = container.persistentStoreDescriptions.first
|
let description = container.persistentStoreDescriptions.first
|
||||||
description?.shouldMigrateStoreAutomatically = false
|
description?.shouldMigrateStoreAutomatically = false
|
||||||
description?.shouldInferMappingModelAutomatically = false
|
description?.shouldInferMappingModelAutomatically = false
|
||||||
if isUnitTest {
|
description?.url = storeURL ?? URL(fileURLWithPath: Globals.dbPath)
|
||||||
description?.url = URL(fileURLWithPath: "/dev/null")
|
|
||||||
} else {
|
|
||||||
description?.url = URL(fileURLWithPath: Globals.dbPath)
|
|
||||||
}
|
|
||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func forUnitTests() -> PersistenceController {
|
||||||
|
PersistenceController(storeURL: URL(fileURLWithPath: "/dev/null"))
|
||||||
|
}
|
||||||
|
|
||||||
func setup() {
|
func setup() {
|
||||||
container.loadPersistentStores { _, error in
|
container.loadPersistentStores { _, error in
|
||||||
if error != nil {
|
if error != nil {
|
||||||
|
|
@ -34,6 +34,10 @@ public class PGPAgent {
|
||||||
pgpInterface = nil
|
pgpInterface = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isInitialized() -> Bool {
|
||||||
|
pgpInterface != nil
|
||||||
|
}
|
||||||
|
|
||||||
public func getKeyID() throws -> [String] {
|
public func getKeyID() throws -> [String] {
|
||||||
try checkAndInit()
|
try checkAndInit()
|
||||||
return pgpInterface?.keyID ?? []
|
return pgpInterface?.keyID ?? []
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public enum AppError: Error, Equatable {
|
||||||
case keyImport
|
case keyImport
|
||||||
case readingFile(fileName: String)
|
case readingFile(fileName: String)
|
||||||
case passwordDuplicated
|
case passwordDuplicated
|
||||||
|
case cannotDeleteNonEmptyDirectory
|
||||||
case gitReset
|
case gitReset
|
||||||
case gitCommit
|
case gitCommit
|
||||||
case gitCreateSignature
|
case gitCreateSignature
|
||||||
|
|
|
||||||
|
|
@ -273,9 +273,15 @@ public class PasswordStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete(passwordEntity: PasswordEntity) throws {
|
public func delete(passwordEntity: PasswordEntity) throws {
|
||||||
|
if !passwordEntity.children.isEmpty {
|
||||||
|
throw AppError.cannotDeleteNonEmptyDirectory
|
||||||
|
}
|
||||||
|
|
||||||
let deletedFileURL = passwordEntity.fileURL(in: storeURL)
|
let deletedFileURL = passwordEntity.fileURL(in: storeURL)
|
||||||
let deletedFilePath = passwordEntity.path
|
let deletedFilePath = passwordEntity.path
|
||||||
try gitRm(path: passwordEntity.path)
|
if !passwordEntity.isDir {
|
||||||
|
try gitRm(path: passwordEntity.path)
|
||||||
|
}
|
||||||
try deletePasswordEntities(passwordEntity: passwordEntity)
|
try deletePasswordEntities(passwordEntity: passwordEntity)
|
||||||
try deleteDirectoryTree(at: deletedFileURL)
|
try deleteDirectoryTree(at: deletedFileURL)
|
||||||
try gitCommit(message: "RemovePassword.".localize(deletedFilePath))
|
try gitCommit(message: "RemovePassword.".localize(deletedFilePath))
|
||||||
|
|
@ -283,6 +289,11 @@ public class PasswordStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func edit(passwordEntity: PasswordEntity, password: Password, keyID: String? = nil) throws -> PasswordEntity? {
|
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
|
var newPasswordEntity: PasswordEntity? = passwordEntity
|
||||||
let url = passwordEntity.fileURL(in: storeURL)
|
let url = passwordEntity.fileURL(in: storeURL)
|
||||||
|
|
||||||
|
|
@ -320,11 +331,11 @@ public class PasswordStore {
|
||||||
saveUpdatedContext()
|
saveUpdatedContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func saveUpdatedContext() {
|
private func saveUpdatedContext() {
|
||||||
PersistenceController.shared.save()
|
PersistenceController.shared.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deleteCoreData() {
|
private func deleteCoreData() {
|
||||||
PasswordEntity.deleteAll(in: context)
|
PasswordEntity.deleteAll(in: context)
|
||||||
PersistenceController.shared.save()
|
PersistenceController.shared.save()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
93
passKitTests/Controllers/PersistenceControllerTest.swift
Normal file
93
passKitTests/Controllers/PersistenceControllerTest.swift
Normal file
|
|
@ -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 {
|
||||||
|
// NOTE: save() calls fatalError on Core Data save failures, so error propagation
|
||||||
|
// cannot be tested without refactoring save() to throw...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,7 @@ class CoreDataTestCase: XCTestCase {
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
try super.setUpWithError()
|
try super.setUpWithError()
|
||||||
|
|
||||||
controller = PersistenceController(isUnitTest: true)
|
controller = PersistenceController.forUnitTests()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
|
|
|
||||||
|
|
@ -85,4 +85,99 @@ final class PasswordEntityTest: CoreDataTestCase {
|
||||||
|
|
||||||
XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 0)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
passKitTests/Fixtures/password-store-empty-dirs.git/HEAD
Normal file
1
passKitTests/Fixtures/password-store-empty-dirs.git/HEAD
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/main
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# 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]
|
||||||
|
# *~
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
fbcbb5819e1c864ef33cfffa179a71387a5d90d0 refs/heads/main
|
||||||
1
passKitTests/Fixtures/password-store-empty.git/HEAD
Normal file
1
passKitTests/Fixtures/password-store-empty.git/HEAD
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/main
|
||||||
6
passKitTests/Fixtures/password-store-empty.git/config
Normal file
6
passKitTests/Fixtures/password-store-empty.git/config
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# 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]
|
||||||
|
# *~
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,3 @@
|
||||||
|
x•ÍM
|
||||||
|
Â0@a×9Åì™ü<E284A2>¤ â\z<>i:Å@›BœzzKÕ¸}‹ï¥ež³€îôN*3Sôv¤48J=ÙÞEoØŽÎDÒˆÈk"EMnK…ËóN¥ÀµRy,<2C>_pœ¶r–_9p;<3B>Á¢‹ˆìqETÚ¾Âÿ
|
||||||
|
&ú¯ rÉ’i‚¥ÞÃ@í
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
f095bb4897e4cd58faadfe4d4f678fb697be3ffd refs/heads/main
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
925eb0f6b19282b5f10dfe008e0062b4be6dd41a not-for-merge branch 'master' of https://github.com/mssun/passforios-password-store
|
||||||
1
passKitTests/Fixtures/password-store-with-gpgid.git/HEAD
Normal file
1
passKitTests/Fixtures/password-store-with-gpgid.git/HEAD
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/master
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[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/*
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Example password store repository for passforios tests with .gpg-id files.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# 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]
|
||||||
|
# *~
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
925eb0f6b19282b5f10dfe008e0062b4be6dd41a refs/heads/master
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
925eb0f6b19282b5f10dfe008e0062b4be6dd41a
|
||||||
101
passKitTests/Helpers/AppKeychainTest.swift
Normal file
101
passKitTests/Helpers/AppKeychainTest.swift
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
//
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,47 +13,386 @@ import XCTest
|
||||||
@testable import passKit
|
@testable import passKit
|
||||||
|
|
||||||
final class PasswordStoreTest: XCTestCase {
|
final class PasswordStoreTest: XCTestCase {
|
||||||
private let remoteRepoURL = URL(string: "https://github.com/mssun/passforios-password-store.git")!
|
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 testDeletePasswordKeepsFileSystemFolderIfNotEmpty() throws {
|
||||||
|
try cloneRepository(.withGPGID)
|
||||||
|
|
||||||
|
// /work contains .gpg-id in addition to a password file
|
||||||
|
let entity = passwordStore.fetchPasswordEntity(with: "work/github.com.gpg")
|
||||||
|
try passwordStore.delete(passwordEntity: entity!)
|
||||||
|
|
||||||
|
XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("work/github.com.gpg").path))
|
||||||
|
XCTAssertNil(passwordStore.fetchPasswordEntity(with: "work/github.com.gpg"))
|
||||||
|
XCTAssertNil(passwordStore.fetchPasswordEntity(with: "work"))
|
||||||
|
XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("work/.gpg-id").path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteEmptyDirectory() throws {
|
||||||
|
try cloneRepository(.emptyDirs)
|
||||||
|
let numCommitsBefore = passwordStore.numberOfCommits!
|
||||||
|
let numLocalCommitsBefore = passwordStore.numberOfLocalCommits
|
||||||
|
|
||||||
|
expectation(forNotification: .passwordStoreUpdated, object: nil)
|
||||||
|
|
||||||
|
// Note: the directory isn't truely empty since Git doesn't track empty directories,
|
||||||
|
// but it should be treated as empty by the app since it contains only hidden files
|
||||||
|
let entityToDelete = passwordStore.fetchPasswordEntity(with: "empty-dir")
|
||||||
|
XCTAssertNotNil(entityToDelete)
|
||||||
|
try passwordStore.delete(passwordEntity: entityToDelete!)
|
||||||
|
|
||||||
|
XCTAssertNil(passwordStore.fetchPasswordEntity(with: "empty-dir"))
|
||||||
|
XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("empty-dir/.gitkeep").path))
|
||||||
|
XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore + 1)
|
||||||
|
XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore + 1)
|
||||||
|
waitForExpectations(timeout: 1, handler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteNonEmptyDirectoryFails() 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, .cannotDeleteNonEmptyDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 testEditDirectoryFails() throws {
|
||||||
|
try cloneRepository(.withGPGID)
|
||||||
|
try importSinglePGPKey()
|
||||||
|
let numCommitsBefore = passwordStore.numberOfCommits!
|
||||||
|
|
||||||
|
let directoryEntity = passwordStore.fetchPasswordEntity(with: "personal")!
|
||||||
|
let editedPassword = Password(name: "new name", path: "new name", plainText: "")
|
||||||
|
editedPassword.changed = PasswordChange.path.rawValue
|
||||||
|
XCTAssertThrowsError(try passwordStore.edit(passwordEntity: directoryEntity, password: editedPassword)) { error in
|
||||||
|
XCTAssertTrue(error is AppError, "Unexpected error type: \(type(of: error))")
|
||||||
|
XCTAssertEqual(error as? AppError, .other(message: "Cannot edit a directory"))
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "personal"))
|
||||||
|
XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
func testCloneAndDecryptMultiKeys() throws {
|
func testCloneAndDecryptMultiKeys() throws {
|
||||||
let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/")
|
try cloneRepository(.withGPGID)
|
||||||
|
try importMultiplePGPKeys()
|
||||||
|
|
||||||
Defaults.isEnableGPGIDOn = true
|
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"),
|
("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"),
|
||||||
("personal/github.com", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"),
|
("personal/github.com", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"),
|
||||||
].forEach { path, id in
|
].forEach { path, id in
|
||||||
let keyID = findGPGID(from: url.appendingPathComponent(path))
|
let keyID = findGPGID(from: localRepoURL.appendingPathComponent(path))
|
||||||
XCTAssertEqual(keyID, id)
|
XCTAssertEqual(keyID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
let keychain = AppKeychain.shared
|
let personal = try decrypt(path: "personal/github.com.gpg")
|
||||||
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")
|
XCTAssertEqual(personal.plainText, "passwordforpersonal\n")
|
||||||
|
|
||||||
let work = try decrypt(passwordStore: passwordStore, path: "work/github.com.gpg", passphrase: "passforios")
|
let work = try decrypt(path: "work/github.com.gpg")
|
||||||
XCTAssertEqual(work.plainText, "passwordforwork\n")
|
XCTAssertEqual(work.plainText, "passwordforwork\n")
|
||||||
|
|
||||||
let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword")
|
let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword")
|
||||||
let testPasswordEntity = try passwordStore.add(password: testPassword)!
|
let testPasswordEntity = try passwordStore.add(password: testPassword)!
|
||||||
let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
||||||
XCTAssertEqual(testPasswordPlain.plainText, "testpassword")
|
XCTAssertEqual(testPasswordPlain.plainText, "testpassword")
|
||||||
|
|
||||||
passwordStore.erase()
|
|
||||||
Defaults.isEnableGPGIDOn = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password {
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private enum RemoteRepo {
|
||||||
|
case empty
|
||||||
|
case emptyDirs
|
||||||
|
case withGPGID
|
||||||
|
|
||||||
|
var url: URL {
|
||||||
|
switch self {
|
||||||
|
case .empty:
|
||||||
|
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-empty.git")
|
||||||
|
case .emptyDirs:
|
||||||
|
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-empty-dirs.git")
|
||||||
|
case .withGPGID:
|
||||||
|
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-with-gpgid.git")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var branchName: String {
|
||||||
|
switch self {
|
||||||
|
case .empty:
|
||||||
|
"main"
|
||||||
|
case .emptyDirs:
|
||||||
|
"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 {
|
||||||
let entity = passwordStore.fetchPasswordEntity(with: path)!
|
let entity = passwordStore.fetchPasswordEntity(with: path)!
|
||||||
return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
return try passwordStore.decrypt(passwordEntity: entity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue