improve directory deletion/editing handling
This commit is contained in:
parent
b8b7e1f913
commit
55b682b4b0
17 changed files with 86 additions and 7 deletions
|
|
@ -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,7 +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.";
|
||||||
"CannotDeleteDirectoryError." = "Cannot delete directories; delete passwords instead.";
|
"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.";
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public enum AppError: Error, Equatable {
|
||||||
case keyImport
|
case keyImport
|
||||||
case readingFile(fileName: String)
|
case readingFile(fileName: String)
|
||||||
case passwordDuplicated
|
case passwordDuplicated
|
||||||
case cannotDeleteDirectory
|
case cannotDeleteNonEmptyDirectory
|
||||||
case gitReset
|
case gitReset
|
||||||
case gitCommit
|
case gitCommit
|
||||||
case gitCreateSignature
|
case gitCreateSignature
|
||||||
|
|
|
||||||
|
|
@ -273,13 +273,15 @@ public class PasswordStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete(passwordEntity: PasswordEntity) throws {
|
public func delete(passwordEntity: PasswordEntity) throws {
|
||||||
if passwordEntity.isDir {
|
if !passwordEntity.children.isEmpty {
|
||||||
throw AppError.cannotDeleteDirectory
|
throw AppError.cannotDeleteNonEmptyDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
let deletedFileURL = passwordEntity.fileURL(in: storeURL)
|
let deletedFileURL = passwordEntity.fileURL(in: storeURL)
|
||||||
let deletedFilePath = passwordEntity.path
|
let deletedFilePath = passwordEntity.path
|
||||||
|
if !passwordEntity.isDir {
|
||||||
try gitRm(path: passwordEntity.path)
|
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))
|
||||||
|
|
@ -287,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)
|
||||||
|
|
||||||
|
|
|
||||||
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
|
||||||
|
|
@ -176,7 +176,40 @@ final class PasswordStoreTest: XCTestCase {
|
||||||
waitForExpectations(timeout: 1, handler: nil)
|
waitForExpectations(timeout: 1, handler: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDeleteDirectoryFails() throws {
|
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)
|
try cloneRepository(.withGPGID)
|
||||||
let numCommitsBefore = passwordStore.numberOfCommits!
|
let numCommitsBefore = passwordStore.numberOfCommits!
|
||||||
let numLocalCommitsBefore = passwordStore.numberOfLocalCommits
|
let numLocalCommitsBefore = passwordStore.numberOfLocalCommits
|
||||||
|
|
@ -186,7 +219,7 @@ final class PasswordStoreTest: XCTestCase {
|
||||||
let entity = passwordStore.fetchPasswordEntity(with: "personal")
|
let entity = passwordStore.fetchPasswordEntity(with: "personal")
|
||||||
XCTAssertThrowsError(try passwordStore.delete(passwordEntity: entity!)) { error in
|
XCTAssertThrowsError(try passwordStore.delete(passwordEntity: entity!)) { error in
|
||||||
XCTAssertTrue(error is AppError, "Unexpected error type: \(type(of: error))")
|
XCTAssertTrue(error is AppError, "Unexpected error type: \(type(of: error))")
|
||||||
XCTAssertEqual(error as? AppError, .cannotDeleteDirectory)
|
XCTAssertEqual(error as? AppError, .cannotDeleteNonEmptyDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg"))
|
XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg"))
|
||||||
|
|
@ -240,6 +273,23 @@ final class PasswordStoreTest: XCTestCase {
|
||||||
waitForExpectations(timeout: 1, handler: nil)
|
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 {
|
func testReset() throws {
|
||||||
try cloneRepository(.withGPGID)
|
try cloneRepository(.withGPGID)
|
||||||
try importSinglePGPKey()
|
try importSinglePGPKey()
|
||||||
|
|
@ -292,12 +342,15 @@ final class PasswordStoreTest: XCTestCase {
|
||||||
|
|
||||||
private enum RemoteRepo {
|
private enum RemoteRepo {
|
||||||
case empty
|
case empty
|
||||||
|
case emptyDirs
|
||||||
case withGPGID
|
case withGPGID
|
||||||
|
|
||||||
var url: URL {
|
var url: URL {
|
||||||
switch self {
|
switch self {
|
||||||
case .empty:
|
case .empty:
|
||||||
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-empty.git")
|
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:
|
case .withGPGID:
|
||||||
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-with-gpgid.git")
|
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-with-gpgid.git")
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +360,8 @@ final class PasswordStoreTest: XCTestCase {
|
||||||
switch self {
|
switch self {
|
||||||
case .empty:
|
case .empty:
|
||||||
"main"
|
"main"
|
||||||
|
case .emptyDirs:
|
||||||
|
"main"
|
||||||
case .withGPGID:
|
case .withGPGID:
|
||||||
"master"
|
"master"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue