From 2726ee79a58247b0bdc9a579af2e82a8fda657ad Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Sun, 8 Mar 2026 22:05:58 +0100 Subject: [PATCH 01/31] fix test cleanup --- passKitTests/Models/PasswordStoreTest.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index b65bf95..c6550e8 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -19,7 +19,13 @@ final class PasswordStoreTest: XCTestCase { let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") Defaults.isEnableGPGIDOn = true + defer { + Defaults.isEnableGPGIDOn = false + } let passwordStore = PasswordStore(url: url) + defer { + passwordStore.erase() + } try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: url.path) }, evaluatedWith: nil) waitForExpectations(timeout: 3, handler: nil) @@ -47,9 +53,6 @@ final class PasswordStoreTest: XCTestCase { let testPasswordEntity = try passwordStore.add(password: testPassword)! let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) XCTAssertEqual(testPasswordPlain.plainText, "testpassword") - - passwordStore.erase() - Defaults.isEnableGPGIDOn = false } private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { From 4ada2ce69c7da80d93549e813d85b043e42a93a7 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Sun, 8 Mar 2026 22:56:21 +0100 Subject: [PATCH 02/31] include repo as text fixture, no need to clone from actual github --- pass.xcodeproj/project.pbxproj | 4 + .../Fixtures/password-store.git/FETCH_HEAD | 1 + passKitTests/Fixtures/password-store.git/HEAD | 1 + .../Fixtures/password-store.git/config | 9 + .../Fixtures/password-store.git/description | 1 + .../hooks/applypatch-msg.sample | 15 ++ .../hooks/commit-msg.sample | 24 +++ .../hooks/fsmonitor-watchman.sample | 174 ++++++++++++++++++ .../hooks/post-update.sample | 8 + .../hooks/pre-applypatch.sample | 14 ++ .../hooks/pre-commit.sample | 49 +++++ .../hooks/pre-merge-commit.sample | 13 ++ .../password-store.git/hooks/pre-push.sample | 53 ++++++ .../hooks/pre-rebase.sample | 169 +++++++++++++++++ .../hooks/pre-receive.sample | 24 +++ .../hooks/prepare-commit-msg.sample | 42 +++++ .../hooks/push-to-checkout.sample | 78 ++++++++ .../password-store.git/hooks/update.sample | 128 +++++++++++++ .../Fixtures/password-store.git/info/exclude | 6 + ...8dbb253e7642cc425de97363624aab04882615.idx | Bin 0 -> 2724 bytes ...dbb253e7642cc425de97363624aab04882615.pack | Bin 0 -> 15331 bytes .../Fixtures/password-store.git/packed-refs | 2 + .../refs/remotes/origin/master | 1 + passKitTests/Models/PasswordStoreTest.swift | 3 +- 24 files changed, 817 insertions(+), 2 deletions(-) create mode 100644 passKitTests/Fixtures/password-store.git/FETCH_HEAD create mode 100644 passKitTests/Fixtures/password-store.git/HEAD create mode 100644 passKitTests/Fixtures/password-store.git/config create mode 100644 passKitTests/Fixtures/password-store.git/description create mode 100755 passKitTests/Fixtures/password-store.git/hooks/applypatch-msg.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/commit-msg.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/fsmonitor-watchman.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/post-update.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/pre-applypatch.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/pre-commit.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/pre-merge-commit.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/pre-push.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/pre-rebase.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/pre-receive.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/prepare-commit-msg.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/push-to-checkout.sample create mode 100755 passKitTests/Fixtures/password-store.git/hooks/update.sample create mode 100644 passKitTests/Fixtures/password-store.git/info/exclude create mode 100644 passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx create mode 100644 passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack create mode 100644 passKitTests/Fixtures/password-store.git/packed-refs create mode 100644 passKitTests/Fixtures/password-store.git/refs/remotes/origin/master diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index ed9df0d..e5522cb 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ 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, ); }; }; + 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 */; }; @@ -422,6 +423,7 @@ 30F6C1B327664C7200BE5AB2 /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SVProgressHUD.xcframework; path = Carthage/Build/SVProgressHUD.xcframework; sourceTree = ""; }; 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoTokenKit.framework; path = System/Library/Frameworks/CryptoTokenKit.framework; sourceTree = SDKROOT; }; + 8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = ""; }; 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; 9A1EF0B524C50EE00074FEAC /* passBetaExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaExtension.entitlements; sourceTree = ""; }; @@ -883,6 +885,7 @@ 30A86F93230F235800F821A4 /* Crypto */, 30BAC8C322E3BA4300438475 /* Testbase */, 30697C5521F63F870064FCAC /* Extensions */, + 8AD8EBF22F5E268D007475AB /* Fixtures */, 301F6464216164670071A4CE /* Helpers */, 30C015A7214ED378005BB6DF /* Models */, 30C015A6214ED32A005BB6DF /* Parser */, @@ -1427,6 +1430,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8AD8EBF32F5E2723007475AB /* Fixtures in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/passKitTests/Fixtures/password-store.git/FETCH_HEAD b/passKitTests/Fixtures/password-store.git/FETCH_HEAD new file mode 100644 index 0000000..ef06926 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/FETCH_HEAD @@ -0,0 +1 @@ +925eb0f6b19282b5f10dfe008e0062b4be6dd41a not-for-merge branch 'master' of https://github.com/mssun/passforios-password-store diff --git a/passKitTests/Fixtures/password-store.git/HEAD b/passKitTests/Fixtures/password-store.git/HEAD new file mode 100644 index 0000000..cb089cd --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/passKitTests/Fixtures/password-store.git/config b/passKitTests/Fixtures/password-store.git/config new file mode 100644 index 0000000..876f087 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/config @@ -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/* diff --git a/passKitTests/Fixtures/password-store.git/description b/passKitTests/Fixtures/password-store.git/description new file mode 100644 index 0000000..9258d15 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/description @@ -0,0 +1 @@ +Example password store repository for passforios tests. diff --git a/passKitTests/Fixtures/password-store.git/hooks/applypatch-msg.sample b/passKitTests/Fixtures/password-store.git/hooks/applypatch-msg.sample new file mode 100755 index 0000000..a5d7b84 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/passKitTests/Fixtures/password-store.git/hooks/commit-msg.sample b/passKitTests/Fixtures/password-store.git/hooks/commit-msg.sample new file mode 100755 index 0000000..b58d118 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/passKitTests/Fixtures/password-store.git/hooks/fsmonitor-watchman.sample b/passKitTests/Fixtures/password-store.git/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/passKitTests/Fixtures/password-store.git/hooks/post-update.sample b/passKitTests/Fixtures/password-store.git/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-applypatch.sample b/passKitTests/Fixtures/password-store.git/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-commit.sample b/passKitTests/Fixtures/password-store.git/hooks/pre-commit.sample new file mode 100755 index 0000000..e144712 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-merge-commit.sample b/passKitTests/Fixtures/password-store.git/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-push.sample b/passKitTests/Fixtures/password-store.git/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-rebase.sample b/passKitTests/Fixtures/password-store.git/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-receive.sample b/passKitTests/Fixtures/password-store.git/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/passKitTests/Fixtures/password-store.git/hooks/prepare-commit-msg.sample b/passKitTests/Fixtures/password-store.git/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/passKitTests/Fixtures/password-store.git/hooks/push-to-checkout.sample b/passKitTests/Fixtures/password-store.git/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/passKitTests/Fixtures/password-store.git/info/exclude b/passKitTests/Fixtures/password-store.git/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/info/exclude @@ -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] +# *~ diff --git a/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx b/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx new file mode 100644 index 0000000000000000000000000000000000000000..7efd056d551ebf17f65647ce79e30d8731c56274 GIT binary patch literal 2724 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 AY5)KL literal 0 HcmV?d00001 diff --git a/passKitTests/Fixtures/password-store.git/packed-refs b/passKitTests/Fixtures/password-store.git/packed-refs new file mode 100644 index 0000000..5b72267 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled sorted +925eb0f6b19282b5f10dfe008e0062b4be6dd41a refs/heads/master diff --git a/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master b/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master new file mode 100644 index 0000000..7d10008 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master @@ -0,0 +1 @@ +925eb0f6b19282b5f10dfe008e0062b4be6dd41a diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index c6550e8..3f1363d 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,9 +13,8 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { - private let remoteRepoURL = URL(string: "https://github.com/mssun/passforios-password-store.git")! - func testCloneAndDecryptMultiKeys() throws { + let remoteRepoURL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") Defaults.isEnableGPGIDOn = true From 1a52952ff84886cb3b99f1a072e8d35f5133fee1 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 00:17:31 +0100 Subject: [PATCH 03/31] basic core data tests upon clone --- passKitTests/Models/PasswordStoreTest.swift | 56 +++++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 3f1363d..40357fa 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,27 +13,55 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { + private let remoteRepoURL: URL = Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/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 + } + + func testInitPasswordEntityCoreData() throws { + try cloneRepository() + + XCTAssertEqual(passwordStore.numberOfPasswords, 4) + + 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 testCloneAndDecryptMultiKeys() throws { - let remoteRepoURL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") - let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") + try cloneRepository() Defaults.isEnableGPGIDOn = true defer { Defaults.isEnableGPGIDOn = false } - let passwordStore = PasswordStore(url: url) - defer { - passwordStore.erase() - } - try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") - expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: url.path) }, evaluatedWith: nil) - waitForExpectations(timeout: 3, handler: nil) [ ("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"), ("personal/github.com", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"), ].forEach { path, id in - let keyID = findGPGID(from: url.appendingPathComponent(path)) + let keyID = findGPGID(from: localRepoURL.appendingPathComponent(path)) XCTAssertEqual(keyID, id) } @@ -54,7 +82,13 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(testPasswordPlain.plainText, "testpassword") } - private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { + fileprivate func cloneRepository() throws { + try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") + expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil) + waitForExpectations(timeout: 3, handler: nil) + } + + fileprivate func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { let entity = passwordStore.fetchPasswordEntity(with: path)! return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) } From d4fa6846b3a0d319ac82e5ea36796d911f172275 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 00:34:34 +0100 Subject: [PATCH 04/31] more tests: entity fetching + erase --- passKit/Crypto/PGPAgent.swift | 4 + passKit/Models/PasswordStore.swift | 2 +- passKitTests/Models/PasswordStoreTest.swift | 84 ++++++++++++++++++--- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/passKit/Crypto/PGPAgent.swift b/passKit/Crypto/PGPAgent.swift index 66fbbed..f193515 100644 --- a/passKit/Crypto/PGPAgent.swift +++ b/passKit/Crypto/PGPAgent.swift @@ -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 ?? [] diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 495dce2..37f57f8 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -324,7 +324,7 @@ public class PasswordStore { PersistenceController.shared.save() } - public func deleteCoreData() { + private func deleteCoreData() { PasswordEntity.deleteAll(in: context) PersistenceController.shared.save() } diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 40357fa..5e12555 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,7 +13,7 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { - private let remoteRepoURL: URL = Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store.git") + private lazy var remoteRepoURL: URL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") private let localRepoURL: URL = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") private var passwordStore: PasswordStore! = nil @@ -49,8 +49,68 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(dirEntity!.children.count, 1) } + func testEraseStoreData() throws { + try cloneRepository() + XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.path)) + XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0) + XCTAssertNotNil(passwordStore.gitRepository) + + passwordStore.eraseStoreData() + + XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.path)) + XCTAssertEqual(passwordStore.numberOfPasswords, 0) + XCTAssertNil(passwordStore.gitRepository) + } + + func testErase() throws { + try cloneRepository() + try importPGPKeys() + 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()) + + 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()) + } + + func testFetchPasswordEntityCoreDataByParent() throws { + try cloneRepository() + + 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() + + let allPasswords = passwordStore.fetchPasswordEntityCoreData(withDir: false) + XCTAssertEqual(allPasswords.count, 4) + allPasswords.forEach { entity in + XCTAssertFalse(entity.isDir) + } + } + func testCloneAndDecryptMultiKeys() throws { try cloneRepository() + try importPGPKeys() Defaults.isEnableGPGIDOn = true defer { @@ -65,15 +125,10 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(keyID, id) } - let keychain = AppKeychain.shared - try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.publicKeys) - try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.privateKeys) - try PGPAgent.shared.initKeys() - - let personal = try decrypt(passwordStore: passwordStore, path: "personal/github.com.gpg", passphrase: "passforios") + let personal = try decrypt(path: "personal/github.com.gpg") 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") let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword") @@ -82,13 +137,22 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(testPasswordPlain.plainText, "testpassword") } - fileprivate func cloneRepository() throws { + // MARK: - Helpers + + private func cloneRepository() throws { try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil) waitForExpectations(timeout: 3, handler: nil) } - fileprivate func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { + private func importPGPKeys() 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) throws -> Password { let entity = passwordStore.fetchPasswordEntity(with: path)! return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) } From beff8290244862098d136beb3000967be6b682b7 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 10:57:39 +0100 Subject: [PATCH 05/31] add encrypt-save-decrypt roundtrip test --- .../Fixtures/password-store-empty.git/HEAD | 1 + .../Fixtures/password-store-empty.git/config | 6 + .../password-store-empty.git/description | 1 + .../hooks/applypatch-msg.sample | 0 .../hooks/commit-msg.sample | 0 .../hooks/fsmonitor-watchman.sample | 0 .../hooks/post-update.sample | 0 .../hooks/pre-applypatch.sample | 0 .../hooks/pre-commit.sample | 0 .../hooks/pre-merge-commit.sample | 0 .../hooks/pre-push.sample | 0 .../hooks/pre-rebase.sample | 0 .../hooks/pre-receive.sample | 0 .../hooks/prepare-commit-msg.sample | 0 .../hooks/push-to-checkout.sample | 0 .../hooks/update.sample | 0 .../info/exclude | 0 .../4b/825dc642cb6eb9a060e54bf8d69288fbee4904 | Bin 0 -> 15 bytes .../4e/b23a2d659dcaa6fbc01ada57aed6d1fbeb0520 | Bin 0 -> 139 bytes .../50/96ac11d1376ea9b22ddedac1130f45ec618d11 | Bin 0 -> 57 bytes .../ae/a863facd4acba3b4862e3f42847da1000e486a | Bin 0 -> 52 bytes .../f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd | 3 + .../password-store-empty.git/packed-refs | 2 + .../FETCH_HEAD | 0 .../HEAD | 0 .../config | 0 .../password-store-with-gpgid.git/description | 1 + .../hooks/applypatch-msg.sample | 15 ++ .../hooks/commit-msg.sample | 24 +++ .../hooks/fsmonitor-watchman.sample | 174 ++++++++++++++++++ .../hooks/post-update.sample | 8 + .../hooks/pre-applypatch.sample | 14 ++ .../hooks/pre-commit.sample | 49 +++++ .../hooks/pre-merge-commit.sample | 13 ++ .../hooks/pre-push.sample | 53 ++++++ .../hooks/pre-rebase.sample | 169 +++++++++++++++++ .../hooks/pre-receive.sample | 24 +++ .../hooks/prepare-commit-msg.sample | 42 +++++ .../hooks/push-to-checkout.sample | 78 ++++++++ .../hooks/update.sample | 128 +++++++++++++ .../info/exclude | 6 + ...8dbb253e7642cc425de97363624aab04882615.idx | Bin ...dbb253e7642cc425de97363624aab04882615.pack | Bin .../packed-refs | 0 .../refs/remotes/origin/master | 0 .../Fixtures/password-store.git/description | 1 - passKitTests/Models/PasswordStoreTest.swift | 72 ++++++-- 47 files changed, 866 insertions(+), 18 deletions(-) create mode 100644 passKitTests/Fixtures/password-store-empty.git/HEAD create mode 100644 passKitTests/Fixtures/password-store-empty.git/config create mode 100644 passKitTests/Fixtures/password-store-empty.git/description rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/applypatch-msg.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/commit-msg.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/fsmonitor-watchman.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/post-update.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/pre-applypatch.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/pre-commit.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/pre-merge-commit.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/pre-push.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/pre-rebase.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/pre-receive.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/prepare-commit-msg.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/push-to-checkout.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/hooks/update.sample (100%) rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/info/exclude (100%) create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/4e/b23a2d659dcaa6fbc01ada57aed6d1fbeb0520 create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/50/96ac11d1376ea9b22ddedac1130f45ec618d11 create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/ae/a863facd4acba3b4862e3f42847da1000e486a create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd create mode 100644 passKitTests/Fixtures/password-store-empty.git/packed-refs rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/FETCH_HEAD (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/HEAD (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/config (100%) create mode 100644 passKitTests/Fixtures/password-store-with-gpgid.git/description create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/applypatch-msg.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/commit-msg.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/fsmonitor-watchman.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/post-update.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-applypatch.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-commit.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-merge-commit.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-push.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-rebase.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-receive.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/prepare-commit-msg.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/push-to-checkout.sample create mode 100755 passKitTests/Fixtures/password-store-with-gpgid.git/hooks/update.sample create mode 100644 passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/packed-refs (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/refs/remotes/origin/master (100%) delete mode 100644 passKitTests/Fixtures/password-store.git/description diff --git a/passKitTests/Fixtures/password-store-empty.git/HEAD b/passKitTests/Fixtures/password-store-empty.git/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/passKitTests/Fixtures/password-store-empty.git/config b/passKitTests/Fixtures/password-store-empty.git/config new file mode 100644 index 0000000..e6da231 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + ignorecase = true + precomposeunicode = true diff --git a/passKitTests/Fixtures/password-store-empty.git/description b/passKitTests/Fixtures/password-store-empty.git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/passKitTests/Fixtures/password-store.git/hooks/applypatch-msg.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/applypatch-msg.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/applypatch-msg.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/applypatch-msg.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/commit-msg.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/commit-msg.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/commit-msg.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/commit-msg.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/fsmonitor-watchman.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/fsmonitor-watchman.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/fsmonitor-watchman.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/fsmonitor-watchman.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/post-update.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/post-update.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/post-update.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/post-update.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-applypatch.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-applypatch.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/pre-applypatch.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/pre-applypatch.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-commit.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-commit.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/pre-commit.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/pre-commit.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-merge-commit.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-merge-commit.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/pre-merge-commit.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/pre-merge-commit.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-push.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-push.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/pre-push.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/pre-push.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-rebase.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-rebase.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/pre-rebase.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/pre-rebase.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/pre-receive.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/pre-receive.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/pre-receive.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/pre-receive.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/prepare-commit-msg.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/prepare-commit-msg.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/prepare-commit-msg.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/prepare-commit-msg.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/push-to-checkout.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/push-to-checkout.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/push-to-checkout.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/push-to-checkout.sample diff --git a/passKitTests/Fixtures/password-store.git/hooks/update.sample b/passKitTests/Fixtures/password-store-empty.git/hooks/update.sample similarity index 100% rename from passKitTests/Fixtures/password-store.git/hooks/update.sample rename to passKitTests/Fixtures/password-store-empty.git/hooks/update.sample diff --git a/passKitTests/Fixtures/password-store.git/info/exclude b/passKitTests/Fixtures/password-store-empty.git/info/exclude similarity index 100% rename from passKitTests/Fixtures/password-store.git/info/exclude rename to passKitTests/Fixtures/password-store-empty.git/info/exclude diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 new file mode 100644 index 0000000000000000000000000000000000000000..adf64119a33d7621aeeaa505d30adb58afaa5559 GIT binary patch literal 15 Wcmbxed=mpo3N25RFzb7z)m#LF#lE?O>d;b__(?p%%XJz0Po5p1u^g&Az;9qc@zU twOsgD4%E&d`~Ha9B0AWkL=KJLS9Ud!DSwv+~->H?%nz I)N=Tv0PoloKmY&$ literal 0 HcmV?d00001 diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd b/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd new file mode 100644 index 0000000..3e561dd --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd @@ -0,0 +1,3 @@ +xM +0@a9 \zi:@BzzK}eN*3Sv48J=Eo؎D҈k"EMnKNRy,_pr_9p;qETھ +&rɒi@ \ No newline at end of file diff --git a/passKitTests/Fixtures/password-store-empty.git/packed-refs b/passKitTests/Fixtures/password-store-empty.git/packed-refs new file mode 100644 index 0000000..432aeb4 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled sorted +f095bb4897e4cd58faadfe4d4f678fb697be3ffd refs/heads/main diff --git a/passKitTests/Fixtures/password-store.git/FETCH_HEAD b/passKitTests/Fixtures/password-store-with-gpgid.git/FETCH_HEAD similarity index 100% rename from passKitTests/Fixtures/password-store.git/FETCH_HEAD rename to passKitTests/Fixtures/password-store-with-gpgid.git/FETCH_HEAD diff --git a/passKitTests/Fixtures/password-store.git/HEAD b/passKitTests/Fixtures/password-store-with-gpgid.git/HEAD similarity index 100% rename from passKitTests/Fixtures/password-store.git/HEAD rename to passKitTests/Fixtures/password-store-with-gpgid.git/HEAD diff --git a/passKitTests/Fixtures/password-store.git/config b/passKitTests/Fixtures/password-store-with-gpgid.git/config similarity index 100% rename from passKitTests/Fixtures/password-store.git/config rename to passKitTests/Fixtures/password-store-with-gpgid.git/config diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/description b/passKitTests/Fixtures/password-store-with-gpgid.git/description new file mode 100644 index 0000000..5536ff7 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/description @@ -0,0 +1 @@ +Example password store repository for passforios tests with .gpg-id files. diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/applypatch-msg.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/applypatch-msg.sample new file mode 100755 index 0000000..a5d7b84 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/commit-msg.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/commit-msg.sample new file mode 100755 index 0000000..b58d118 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/fsmonitor-watchman.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/post-update.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-applypatch.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-commit.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-commit.sample new file mode 100755 index 0000000..e144712 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-merge-commit.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-push.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-rebase.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-receive.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/prepare-commit-msg.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/push-to-checkout.sample b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude b/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude @@ -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] +# *~ diff --git a/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx b/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx similarity index 100% rename from passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx rename to passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx diff --git a/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack b/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack similarity index 100% rename from passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack rename to passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack diff --git a/passKitTests/Fixtures/password-store.git/packed-refs b/passKitTests/Fixtures/password-store-with-gpgid.git/packed-refs similarity index 100% rename from passKitTests/Fixtures/password-store.git/packed-refs rename to passKitTests/Fixtures/password-store-with-gpgid.git/packed-refs diff --git a/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master b/passKitTests/Fixtures/password-store-with-gpgid.git/refs/remotes/origin/master similarity index 100% rename from passKitTests/Fixtures/password-store.git/refs/remotes/origin/master rename to passKitTests/Fixtures/password-store-with-gpgid.git/refs/remotes/origin/master diff --git a/passKitTests/Fixtures/password-store.git/description b/passKitTests/Fixtures/password-store.git/description deleted file mode 100644 index 9258d15..0000000 --- a/passKitTests/Fixtures/password-store.git/description +++ /dev/null @@ -1 +0,0 @@ -Example password store repository for passforios tests. diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 5e12555..205ab13 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,7 +13,6 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { - private lazy var remoteRepoURL: URL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") private let localRepoURL: URL = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") private var passwordStore: PasswordStore! = nil @@ -25,10 +24,12 @@ final class PasswordStoreTest: XCTestCase { override func tearDown() { passwordStore.erase() passwordStore = nil + + Defaults.removeAll() } func testInitPasswordEntityCoreData() throws { - try cloneRepository() + try cloneRepository(.withGPGID) XCTAssertEqual(passwordStore.numberOfPasswords, 4) @@ -50,7 +51,7 @@ final class PasswordStoreTest: XCTestCase { } func testEraseStoreData() throws { - try cloneRepository() + try cloneRepository(.withGPGID) XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.path)) XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0) XCTAssertNotNil(passwordStore.gitRepository) @@ -63,8 +64,8 @@ final class PasswordStoreTest: XCTestCase { } func testErase() throws { - try cloneRepository() - try importPGPKeys() + try cloneRepository(.withGPGID) + try importSinglePGPKey() Defaults.gitSignatureName = "Test User" PasscodeLock.shared.save(passcode: "1234") @@ -84,7 +85,7 @@ final class PasswordStoreTest: XCTestCase { } func testFetchPasswordEntityCoreDataByParent() throws { - try cloneRepository() + try cloneRepository(.withGPGID) let rootChildren = passwordStore.fetchPasswordEntityCoreData(parent: nil) XCTAssertGreaterThan(rootChildren.count, 0) @@ -99,7 +100,7 @@ final class PasswordStoreTest: XCTestCase { } func testFetchPasswordEntityCoreDataWithDir() throws { - try cloneRepository() + try cloneRepository(.withGPGID) let allPasswords = passwordStore.fetchPasswordEntityCoreData(withDir: false) XCTAssertEqual(allPasswords.count, 4) @@ -108,14 +109,21 @@ final class PasswordStoreTest: XCTestCase { } } + func testEncryptSaveDecryptMultiline() throws { + try cloneRepository(.empty) + try importSinglePGPKey() + + let password = Password(name: "test", path: "test.gpg", plainText: "foobar\nwith\nmultiple\nlines") + _ = try passwordStore.add(password: password) + let decryptedPassword = try decrypt(path: "test.gpg") + XCTAssertEqual(decryptedPassword.plainText, "foobar\nwith\nmultiple\nlines") + } + func testCloneAndDecryptMultiKeys() throws { - try cloneRepository() - try importPGPKeys() + try cloneRepository(.withGPGID) + try importMultiplePGPKeys() Defaults.isEnableGPGIDOn = true - defer { - Defaults.isEnableGPGIDOn = false - } [ ("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"), @@ -139,21 +147,51 @@ final class PasswordStoreTest: XCTestCase { // MARK: - Helpers - private func cloneRepository() throws { - try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") + private enum RemoteRepo { + case empty + case withGPGID + + var url: URL { + switch self { + case .empty: + Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-empty.git") + case .withGPGID: + Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-with-gpgid.git") + } + } + + var branchName: String { + switch self { + case .empty: + "main" + case .withGPGID: + "master" + } + } + } + + private func cloneRepository(_ remote: RemoteRepo) throws { + try passwordStore.cloneRepository(remoteRepoURL: remote.url, branchName: remote.branchName) expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil) waitForExpectations(timeout: 3, handler: nil) } - private func importPGPKeys() throws { + 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) throws -> Password { + private func decrypt(path: String, keyID: String? = nil) throws -> Password { let entity = passwordStore.fetchPasswordEntity(with: path)! - return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + return try passwordStore.decrypt(passwordEntity: entity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) } } From 31dd3c5378af3455a2b99cd9983e185f1b6123fe Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 11:45:04 +0100 Subject: [PATCH 06/31] check notification center notifications --- passKitTests/Models/PasswordStoreTest.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 205ab13..60ddf48 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -56,11 +56,14 @@ final class PasswordStoreTest: XCTestCase { 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 { @@ -75,6 +78,8 @@ final class PasswordStoreTest: XCTestCase { XCTAssertTrue(PasscodeLock.shared.hasPasscode) XCTAssertTrue(PGPAgent.shared.isInitialized()) + expectation(forNotification: .passwordStoreUpdated, object: nil) + expectation(forNotification: .passwordStoreErased, object: nil) passwordStore.erase() XCTAssertEqual(passwordStore.numberOfPasswords, 0) @@ -82,6 +87,7 @@ final class PasswordStoreTest: XCTestCase { XCTAssertFalse(Defaults.hasKey(\.gitSignatureName)) XCTAssertFalse(PasscodeLock.shared.hasPasscode) XCTAssertFalse(PGPAgent.shared.isInitialized()) + waitForExpectations(timeout: 1, handler: nil) } func testFetchPasswordEntityCoreDataByParent() throws { @@ -171,8 +177,11 @@ final class PasswordStoreTest: XCTestCase { } private func cloneRepository(_ remote: RemoteRepo) throws { - try passwordStore.cloneRepository(remoteRepoURL: remote.url, branchName: remote.branchName) 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) } From 4405b7fe3d94fbe81cce66176031cf222af370e3 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 11:46:26 +0100 Subject: [PATCH 07/31] test add, edit, delete --- passKit/Models/PasswordStore.swift | 2 +- passKitTests/Models/PasswordStoreTest.swift | 69 +++++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 37f57f8..22c90d9 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -320,7 +320,7 @@ public class PasswordStore { saveUpdatedContext() } - public func saveUpdatedContext() { + private func saveUpdatedContext() { PersistenceController.shared.save() } diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 60ddf48..883471c 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -115,16 +115,75 @@ final class PasswordStoreTest: XCTestCase { } } - func testEncryptSaveDecryptMultiline() throws { + func testAddPassword() throws { try cloneRepository(.empty) try importSinglePGPKey() - let password = Password(name: "test", path: "test.gpg", plainText: "foobar\nwith\nmultiple\nlines") - _ = try passwordStore.add(password: password) - let decryptedPassword = try decrypt(path: "test.gpg") - XCTAssertEqual(decryptedPassword.plainText, "foobar\nwith\nmultiple\nlines") + 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: "test3.gpg", plainText: "lorem ipsum") + let password4 = Password(name: "test4", path: "test4.gpg", plainText: "you are valuable and you matter") + + [password1, password2, password3, password4].forEach { password in + expectation(forNotification: .passwordStoreUpdated, object: nil) + + let savedEntity = try? passwordStore.add(password: password) + + XCTAssertEqual(savedEntity!.name, password.name) + waitForExpectations(timeout: 1, handler: nil) + } } + func testDeletePassword() throws { + try cloneRepository(.withGPGID) + + 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")) + waitForExpectations(timeout: 1, handler: nil) + } + + func testEditPasswordValue() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() + 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") + waitForExpectations(timeout: 1, handler: nil) + } + + func testMovePassword() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() + 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")) + waitForExpectations(timeout: 1, handler: nil) + } + + // MARK: - .gpg-id support + func testCloneAndDecryptMultiKeys() throws { try cloneRepository(.withGPGID) try importMultiplePGPKeys() From 2c8d27df693e6fd98a0f876fe37fd14340243694 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 12:11:37 +0100 Subject: [PATCH 08/31] fix deleting directory this used to corrupt the local state (password entities remained in DB but files/dirs were removed from git and disk) --- pass/en.lproj/Localizable.strings | 1 + passKit/Helpers/AppError.swift | 1 + passKit/Models/PasswordStore.swift | 4 ++++ passKitTests/Models/PasswordStoreTest.swift | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+) diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 5f26010..10d8b30 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -73,6 +73,7 @@ "KeyImportError." = "Cannot import the key."; "FileNotFoundError." = "File '%@' cannot be read."; "PasswordDuplicatedError." = "Cannot add the password; password is duplicated."; +"CannotDeleteDirectoryError." = "Cannot delete directories; delete passwords instead."; "GitResetError." = "Cannot identify the latest synced commit."; "GitCreateSignatureError." = "Cannot create a valid author/committer signature."; "GitPushNotSuccessfulError." = "Pushing local changes was not successful. Make sure there are no uncommitted changes on the remote repository."; diff --git a/passKit/Helpers/AppError.swift b/passKit/Helpers/AppError.swift index 8e0aa21..8bcc84b 100644 --- a/passKit/Helpers/AppError.swift +++ b/passKit/Helpers/AppError.swift @@ -15,6 +15,7 @@ public enum AppError: Error, Equatable { case keyImport case readingFile(fileName: String) case passwordDuplicated + case cannotDeleteDirectory case gitReset case gitCommit case gitCreateSignature diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 22c90d9..78c92c4 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -273,6 +273,10 @@ public class PasswordStore { } public func delete(passwordEntity: PasswordEntity) throws { + if passwordEntity.isDir { + throw AppError.cannotDeleteDirectory + } + let deletedFileURL = passwordEntity.fileURL(in: storeURL) let deletedFilePath = passwordEntity.path try gitRm(path: passwordEntity.path) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 883471c..8b4d670 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -143,9 +143,27 @@ final class PasswordStoreTest: XCTestCase { 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)) waitForExpectations(timeout: 1, handler: nil) } + func testDeleteDirectoryFails() throws { + try cloneRepository(.withGPGID) + + expectation(forNotification: .passwordStoreUpdated, object: nil).isInverted = true + + let entity = passwordStore.fetchPasswordEntity(with: "personal") + XCTAssertThrowsError(try passwordStore.delete(passwordEntity: entity!)) { error in + XCTAssertTrue(error is AppError, "Unexpected error type: \(type(of: error))") + XCTAssertEqual(error as? AppError, .cannotDeleteDirectory) + } + + XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")) + XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal/github.com.gpg").path)) + waitForExpectations(timeout: 0.1, handler: nil) + } + func testEditPasswordValue() throws { try cloneRepository(.withGPGID) try importSinglePGPKey() From be4dbf81cd425c7e6d148583f7887ad15b4b485a Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 12:49:33 +0100 Subject: [PATCH 09/31] check file system and commits upon changes to store --- passKitTests/Models/PasswordStoreTest.swift | 31 ++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 8b4d670..94dd8eb 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -32,6 +32,8 @@ final class PasswordStoreTest: XCTestCase { 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") @@ -118,10 +120,12 @@ final class PasswordStoreTest: XCTestCase { 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: "test3.gpg", plainText: "lorem ipsum") + 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") [password1, password2, password3, password4].forEach { password in @@ -132,10 +136,21 @@ final class PasswordStoreTest: XCTestCase { 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 testDeletePassword() throws { try cloneRepository(.withGPGID) + let numCommitsBefore = passwordStore.numberOfCommits! + let numLocalCommitsBefore = passwordStore.numberOfLocalCommits expectation(forNotification: .passwordStoreUpdated, object: nil) @@ -145,11 +160,15 @@ final class PasswordStoreTest: XCTestCase { XCTAssertNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")) XCTAssertNil(passwordStore.fetchPasswordEntity(with: "personal")) XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal").path)) + XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore + 1) + XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore + 1) waitForExpectations(timeout: 1, handler: nil) } func testDeleteDirectoryFails() throws { try cloneRepository(.withGPGID) + let numCommitsBefore = passwordStore.numberOfCommits! + let numLocalCommitsBefore = passwordStore.numberOfLocalCommits expectation(forNotification: .passwordStoreUpdated, object: nil).isInverted = true @@ -161,12 +180,16 @@ final class PasswordStoreTest: XCTestCase { 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) @@ -179,12 +202,16 @@ final class PasswordStoreTest: XCTestCase { 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) @@ -197,6 +224,8 @@ final class PasswordStoreTest: XCTestCase { 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) } From d4c61fbb4d37bb9932a86d09a1c78af04804cc7b Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 12:55:16 +0100 Subject: [PATCH 10/31] test resetting local changes --- passKitTests/Models/PasswordStoreTest.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 94dd8eb..812a96b 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -229,6 +229,26 @@ final class PasswordStoreTest: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } + func testReset() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() + let numCommitsBefore = passwordStore.numberOfCommits! + let numLocalCommitsBefore = passwordStore.numberOfLocalCommits + + _ = try? passwordStore.add(password: Password(name: "test", path: "test.gpg", plainText: "foobar")) + try passwordStore.delete(passwordEntity: passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")!) + + expectation(forNotification: .passwordStoreUpdated, object: nil) + let numDroppedCommits = try passwordStore.reset() + + XCTAssertEqual(numDroppedCommits, 2) + XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("test.gpg").path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal/github.com.gpg").path)) + XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore) + XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore) + waitForExpectations(timeout: 1, handler: nil) + } + // MARK: - .gpg-id support func testCloneAndDecryptMultiKeys() throws { From f21431a2434ba43197146cdac82b29b1e36fcb6c Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:15:03 +0100 Subject: [PATCH 11/31] add save and decrypt round trip --- passKitTests/Models/PasswordStoreTest.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 812a96b..de3f7b3 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -128,10 +128,10 @@ final class PasswordStoreTest: XCTestCase { 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") - [password1, password2, password3, password4].forEach { password in + for password in [password1, password2, password3, password4] { expectation(forNotification: .passwordStoreUpdated, object: nil) - let savedEntity = try? passwordStore.add(password: password) + let savedEntity = try passwordStore.add(password: password) XCTAssertEqual(savedEntity!.name, password.name) waitForExpectations(timeout: 1, handler: nil) @@ -147,6 +147,17 @@ final class PasswordStoreTest: XCTestCase { 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! @@ -235,7 +246,7 @@ final class PasswordStoreTest: XCTestCase { let numCommitsBefore = passwordStore.numberOfCommits! let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - _ = try? passwordStore.add(password: Password(name: "test", path: "test.gpg", plainText: "foobar")) + _ = 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) From 7a65a27d34312056c2a674989246e49fe1abb9db Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:30:22 +0100 Subject: [PATCH 12/31] add initPasswordEntityCoreData tests --- .../CoreData/PasswordEntityTest.swift | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/passKitTests/CoreData/PasswordEntityTest.swift b/passKitTests/CoreData/PasswordEntityTest.swift index 6362e2a..ad70f73 100644 --- a/passKitTests/CoreData/PasswordEntityTest.swift +++ b/passKitTests/CoreData/PasswordEntityTest.swift @@ -85,4 +85,99 @@ final class PasswordEntityTest: CoreDataTestCase { XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 0) } + + // MARK: - initPasswordEntityCoreData tests + + func testInitPasswordEntityCoreDataBuildsTree() throws { + let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: rootDir) } + + // Create directory structure: + // email/ + // work.gpg + // personal.gpg + // social/ + // mastodon.gpg + // toplevel.gpg + // notes.txt (non-.gpg file) + let emailDir = rootDir.appendingPathComponent("email") + let socialDir = rootDir.appendingPathComponent("social") + try FileManager.default.createDirectory(at: emailDir, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: socialDir, withIntermediateDirectories: true) + try Data("test1".utf8).write(to: emailDir.appendingPathComponent("work.gpg")) + try Data("test2".utf8).write(to: emailDir.appendingPathComponent("personal.gpg")) + try Data("test3".utf8).write(to: socialDir.appendingPathComponent("mastodon.gpg")) + try Data("test4".utf8).write(to: rootDir.appendingPathComponent("toplevel.gpg")) + try Data("test5".utf8).write(to: rootDir.appendingPathComponent("notes.txt")) + + let context = controller.viewContext() + PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) + + // Verify total counts + let allEntities = PasswordEntity.fetchAll(in: context) + let files = allEntities.filter { !$0.isDir } + let dirs = allEntities.filter(\.isDir) + XCTAssertEqual(files.count, 5) // 4 .gpg + 1 .txt + XCTAssertEqual(dirs.count, 2) // email, social + + // Verify .gpg extension is stripped + let workEntity = allEntities.first { $0.path == "email/work.gpg" } + XCTAssertNotNil(workEntity) + XCTAssertEqual(workEntity!.name, "work") + + // Verify non-.gpg file keeps its extension + let notesEntity = allEntities.first { $0.path == "notes.txt" } + XCTAssertNotNil(notesEntity) + XCTAssertEqual(notesEntity!.name, "notes.txt") + + // Verify parent-child relationships + let emailEntity = allEntities.first { $0.path == "email" && $0.isDir } + XCTAssertNotNil(emailEntity) + XCTAssertEqual(emailEntity!.children.count, 2) + + // Verify top-level files have no parent (root was deleted) + let toplevelEntity = allEntities.first { $0.path == "toplevel.gpg" } + XCTAssertNotNil(toplevelEntity) + XCTAssertEqual(toplevelEntity!.name, "toplevel") + XCTAssertNil(toplevelEntity!.parent) + } + + func testInitPasswordEntityCoreDataSkipsHiddenFiles() throws { + let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: rootDir) } + + try Data("test".utf8).write(to: rootDir.appendingPathComponent("visible.gpg")) + try Data("test".utf8).write(to: rootDir.appendingPathComponent(".hidden.gpg")) + try Data("test".utf8).write(to: rootDir.appendingPathComponent(".gpg-id")) + try FileManager.default.createDirectory(at: rootDir.appendingPathComponent(".git"), withIntermediateDirectories: true) + try Data("test".utf8).write(to: rootDir.appendingPathComponent(".git/config")) + + let context = controller.viewContext() + PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) + + let allEntities = PasswordEntity.fetchAll(in: context) + XCTAssertEqual(allEntities.count, 1) + XCTAssertEqual(allEntities.first!.name, "visible") + } + + func testInitPasswordEntityCoreDataHandlesEmptyDirectory() throws { + let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: rootDir) } + + try FileManager.default.createDirectory(at: rootDir.appendingPathComponent("emptydir"), withIntermediateDirectories: true) + + let context = controller.viewContext() + PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) + + let allEntities = PasswordEntity.fetchAll(in: context) + let dirs = allEntities.filter(\.isDir) + let files = allEntities.filter { !$0.isDir } + XCTAssertEqual(dirs.count, 1) + XCTAssertEqual(dirs.first!.name, "emptydir") + XCTAssertEqual(dirs.first!.children.count, 0) + XCTAssertEqual(files.count, 0) + } } From b9851eb93838419a70419d1af1dac1552ea5cb15 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:36:19 +0100 Subject: [PATCH 13/31] add tests for AppKeychain --- pass.xcodeproj/project.pbxproj | 4 + passKitTests/Helpers/AppKeychainTest.swift | 101 +++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 passKitTests/Helpers/AppKeychainTest.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index e5522cb..4029c79 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ 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 */; }; 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 */; }; @@ -423,6 +424,7 @@ 30F6C1B327664C7200BE5AB2 /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SVProgressHUD.xcframework; path = Carthage/Build/SVProgressHUD.xcframework; sourceTree = ""; }; 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoTokenKit.framework; path = System/Library/Frameworks/CryptoTokenKit.framework; sourceTree = SDKROOT; }; + 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKeychainTest.swift; sourceTree = ""; }; 8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = ""; }; 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; @@ -626,6 +628,7 @@ 301F6464216164670071A4CE /* Helpers */ = { isa = PBXGroup; children = ( + 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */, 3032328922C9FBA2009EBD9C /* KeyFileManagerTest.swift */, ); path = Helpers; @@ -1634,6 +1637,7 @@ A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */, 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */, A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */, + 8A4716692F5EF56900C7A64D /* AppKeychainTest.swift in Sources */, 30695E2524FAEF2600C9D46E /* GitCredentialTest.swift in Sources */, 30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */, 30B04860209A5141001013CA /* PasswordTest.swift in Sources */, diff --git a/passKitTests/Helpers/AppKeychainTest.swift b/passKitTests/Helpers/AppKeychainTest.swift new file mode 100644 index 0000000..9b420fc --- /dev/null +++ b/passKitTests/Helpers/AppKeychainTest.swift @@ -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") + } +} From 5a51425d01e30c5aea09a311b93c60bfe79ae306 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:41:51 +0100 Subject: [PATCH 14/31] rename file to match contained class --- pass.xcodeproj/project.pbxproj | 8 ++++---- .../{CoreDataStack.swift => PersistenceController.swift} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename passKit/Controllers/{CoreDataStack.swift => PersistenceController.swift} (98%) diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 4029c79..33d5fd0 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -197,7 +197,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 */; }; @@ -502,7 +502,7 @@ DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = ""; }; DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = ""; }; DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = ""; }; - DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; + DC6474522D20DD0C004B4BBC /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTestCase.swift; sourceTree = ""; }; DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEntityTest.swift; sourceTree = ""; }; DC64745E2D45B23A004B4BBC /* GitRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitRepository.swift; sourceTree = ""; }; @@ -919,7 +919,7 @@ children = ( 30697C3121F63C8B0064FCAC /* PasscodeLockPresenter.swift */, 30697C3221F63C8B0064FCAC /* PasscodeLockViewController.swift */, - DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */, + DC6474522D20DD0C004B4BBC /* PersistenceController.swift */, ); path = Controllers; sourceTree = ""; @@ -1611,7 +1611,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 */, diff --git a/passKit/Controllers/CoreDataStack.swift b/passKit/Controllers/PersistenceController.swift similarity index 98% rename from passKit/Controllers/CoreDataStack.swift rename to passKit/Controllers/PersistenceController.swift index 0452259..67ad4fe 100644 --- a/passKit/Controllers/CoreDataStack.swift +++ b/passKit/Controllers/PersistenceController.swift @@ -1,5 +1,5 @@ // -// CoreDataStack.swift +// PersistenceController.swift // passKit // // Created by Mingshen Sun on 12/28/24. From c8cce5c3225073071b9d9d516d528af2062a6ea1 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 14:04:12 +0100 Subject: [PATCH 15/31] PersistenceController tests --- pass.xcodeproj/project.pbxproj | 12 +++ .../Controllers/PersistenceController.swift | 12 +-- .../PersistenceControllerTest.swift | 93 +++++++++++++++++++ passKitTests/CoreData/CoreDataTestCase.swift | 2 +- 4 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 passKitTests/Controllers/PersistenceControllerTest.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 33d5fd0..d04dd81 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 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 */; }; @@ -425,6 +426,7 @@ 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoTokenKit.framework; path = System/Library/Frameworks/CryptoTokenKit.framework; sourceTree = SDKROOT; }; 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKeychainTest.swift; sourceTree = ""; }; + 8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceControllerTest.swift; sourceTree = ""; }; 8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = ""; }; 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; @@ -766,6 +768,14 @@ path = Crypto; sourceTree = ""; }; + 8A4716702F5EF7A900C7A64D /* Controllers */ = { + isa = PBXGroup; + children = ( + 8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */, + ); + path = Controllers; + sourceTree = ""; + }; 9A58664F25AADB66006719C2 /* Services */ = { isa = PBXGroup; children = ( @@ -884,6 +894,7 @@ A26075861EEC6F34005DB03E /* passKitTests */ = { isa = PBXGroup; children = ( + 8A4716702F5EF7A900C7A64D /* Controllers */, DC64745A2D29BD43004B4BBC /* CoreData */, 30A86F93230F235800F821A4 /* Crypto */, 30BAC8C322E3BA4300438475 /* Testbase */, @@ -1630,6 +1641,7 @@ 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 */, diff --git a/passKit/Controllers/PersistenceController.swift b/passKit/Controllers/PersistenceController.swift index 67ad4fe..5d05001 100644 --- a/passKit/Controllers/PersistenceController.swift +++ b/passKit/Controllers/PersistenceController.swift @@ -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 { diff --git a/passKitTests/Controllers/PersistenceControllerTest.swift b/passKitTests/Controllers/PersistenceControllerTest.swift new file mode 100644 index 0000000..4ae642f --- /dev/null +++ b/passKitTests/Controllers/PersistenceControllerTest.swift @@ -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... + } +} diff --git a/passKitTests/CoreData/CoreDataTestCase.swift b/passKitTests/CoreData/CoreDataTestCase.swift index fa356bf..b11ddd1 100644 --- a/passKitTests/CoreData/CoreDataTestCase.swift +++ b/passKitTests/CoreData/CoreDataTestCase.swift @@ -20,7 +20,7 @@ class CoreDataTestCase: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - controller = PersistenceController(isUnitTest: true) + controller = PersistenceController.forUnitTests() } override func tearDown() { From 17b6bb8bc235d94d7a8f902e666d1ec53d66597c Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Sun, 8 Mar 2026 22:05:58 +0100 Subject: [PATCH 16/31] fix test cleanup --- passKitTests/Models/PasswordStoreTest.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index b65bf95..c6550e8 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -19,7 +19,13 @@ final class PasswordStoreTest: XCTestCase { let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") Defaults.isEnableGPGIDOn = true + defer { + Defaults.isEnableGPGIDOn = false + } let passwordStore = PasswordStore(url: url) + defer { + passwordStore.erase() + } try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: url.path) }, evaluatedWith: nil) waitForExpectations(timeout: 3, handler: nil) @@ -47,9 +53,6 @@ final class PasswordStoreTest: XCTestCase { let testPasswordEntity = try passwordStore.add(password: testPassword)! let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) XCTAssertEqual(testPasswordPlain.plainText, "testpassword") - - passwordStore.erase() - Defaults.isEnableGPGIDOn = false } private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { From 85972a02c350e7c13f9cf46c8c86963ff5537abd Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Sun, 8 Mar 2026 22:56:21 +0100 Subject: [PATCH 17/31] include repo as text fixture, no need to clone from actual github --- pass.xcodeproj/project.pbxproj | 4 ++++ .../Fixtures/password-store.git/FETCH_HEAD | 1 + passKitTests/Fixtures/password-store.git/HEAD | 1 + passKitTests/Fixtures/password-store.git/config | 9 +++++++++ .../Fixtures/password-store.git/description | 1 + .../Fixtures/password-store.git/info/exclude | 6 ++++++ ...6a8dbb253e7642cc425de97363624aab04882615.idx | Bin 0 -> 2724 bytes ...a8dbb253e7642cc425de97363624aab04882615.pack | Bin 0 -> 15331 bytes .../Fixtures/password-store.git/packed-refs | 2 ++ .../refs/remotes/origin/master | 1 + passKitTests/Models/PasswordStoreTest.swift | 3 +-- 11 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 passKitTests/Fixtures/password-store.git/FETCH_HEAD create mode 100644 passKitTests/Fixtures/password-store.git/HEAD create mode 100644 passKitTests/Fixtures/password-store.git/config create mode 100644 passKitTests/Fixtures/password-store.git/description create mode 100644 passKitTests/Fixtures/password-store.git/info/exclude create mode 100644 passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx create mode 100644 passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack create mode 100644 passKitTests/Fixtures/password-store.git/packed-refs create mode 100644 passKitTests/Fixtures/password-store.git/refs/remotes/origin/master diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 807205a..e3e63e8 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ 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, ); }; }; + 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 */; }; @@ -422,6 +423,7 @@ 30F6C1B327664C7200BE5AB2 /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SVProgressHUD.xcframework; path = Carthage/Build/SVProgressHUD.xcframework; sourceTree = ""; }; 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoTokenKit.framework; path = System/Library/Frameworks/CryptoTokenKit.framework; sourceTree = SDKROOT; }; + 8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = ""; }; 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; 9A1EF0B524C50EE00074FEAC /* passBetaExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaExtension.entitlements; sourceTree = ""; }; @@ -883,6 +885,7 @@ 30A86F93230F235800F821A4 /* Crypto */, 30BAC8C322E3BA4300438475 /* Testbase */, 30697C5521F63F870064FCAC /* Extensions */, + 8AD8EBF22F5E268D007475AB /* Fixtures */, 301F6464216164670071A4CE /* Helpers */, 30C015A7214ED378005BB6DF /* Models */, 30C015A6214ED32A005BB6DF /* Parser */, @@ -1436,6 +1439,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8AD8EBF32F5E2723007475AB /* Fixtures in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/passKitTests/Fixtures/password-store.git/FETCH_HEAD b/passKitTests/Fixtures/password-store.git/FETCH_HEAD new file mode 100644 index 0000000..ef06926 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/FETCH_HEAD @@ -0,0 +1 @@ +925eb0f6b19282b5f10dfe008e0062b4be6dd41a not-for-merge branch 'master' of https://github.com/mssun/passforios-password-store diff --git a/passKitTests/Fixtures/password-store.git/HEAD b/passKitTests/Fixtures/password-store.git/HEAD new file mode 100644 index 0000000..cb089cd --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/passKitTests/Fixtures/password-store.git/config b/passKitTests/Fixtures/password-store.git/config new file mode 100644 index 0000000..876f087 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/config @@ -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/* diff --git a/passKitTests/Fixtures/password-store.git/description b/passKitTests/Fixtures/password-store.git/description new file mode 100644 index 0000000..9258d15 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/description @@ -0,0 +1 @@ +Example password store repository for passforios tests. diff --git a/passKitTests/Fixtures/password-store.git/info/exclude b/passKitTests/Fixtures/password-store.git/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/info/exclude @@ -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] +# *~ diff --git a/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx b/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx new file mode 100644 index 0000000000000000000000000000000000000000..7efd056d551ebf17f65647ce79e30d8731c56274 GIT binary patch literal 2724 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 AY5)KL literal 0 HcmV?d00001 diff --git a/passKitTests/Fixtures/password-store.git/packed-refs b/passKitTests/Fixtures/password-store.git/packed-refs new file mode 100644 index 0000000..5b72267 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled sorted +925eb0f6b19282b5f10dfe008e0062b4be6dd41a refs/heads/master diff --git a/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master b/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master new file mode 100644 index 0000000..7d10008 --- /dev/null +++ b/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master @@ -0,0 +1 @@ +925eb0f6b19282b5f10dfe008e0062b4be6dd41a diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index c6550e8..3f1363d 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,9 +13,8 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { - private let remoteRepoURL = URL(string: "https://github.com/mssun/passforios-password-store.git")! - func testCloneAndDecryptMultiKeys() throws { + let remoteRepoURL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") Defaults.isEnableGPGIDOn = true From ef188fcfba7604a52847d266e423b74347d1c0d1 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 00:17:31 +0100 Subject: [PATCH 18/31] basic core data tests upon clone --- passKitTests/Models/PasswordStoreTest.swift | 56 +++++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 3f1363d..40357fa 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,27 +13,55 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { + private let remoteRepoURL: URL = Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/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 + } + + func testInitPasswordEntityCoreData() throws { + try cloneRepository() + + XCTAssertEqual(passwordStore.numberOfPasswords, 4) + + 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 testCloneAndDecryptMultiKeys() throws { - let remoteRepoURL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") - let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") + try cloneRepository() Defaults.isEnableGPGIDOn = true defer { Defaults.isEnableGPGIDOn = false } - let passwordStore = PasswordStore(url: url) - defer { - passwordStore.erase() - } - try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") - expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: url.path) }, evaluatedWith: nil) - waitForExpectations(timeout: 3, handler: nil) [ ("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"), ("personal/github.com", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"), ].forEach { path, id in - let keyID = findGPGID(from: url.appendingPathComponent(path)) + let keyID = findGPGID(from: localRepoURL.appendingPathComponent(path)) XCTAssertEqual(keyID, id) } @@ -54,7 +82,13 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(testPasswordPlain.plainText, "testpassword") } - private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { + fileprivate func cloneRepository() throws { + try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") + expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil) + waitForExpectations(timeout: 3, handler: nil) + } + + fileprivate func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { let entity = passwordStore.fetchPasswordEntity(with: path)! return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) } From 60999c7eabe4548f73c6277c0fedcb845880ae85 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 00:34:34 +0100 Subject: [PATCH 19/31] more tests: entity fetching + erase --- passKit/Crypto/PGPAgent.swift | 4 + passKit/Models/PasswordStore.swift | 2 +- passKitTests/Models/PasswordStoreTest.swift | 84 ++++++++++++++++++--- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/passKit/Crypto/PGPAgent.swift b/passKit/Crypto/PGPAgent.swift index 66fbbed..f193515 100644 --- a/passKit/Crypto/PGPAgent.swift +++ b/passKit/Crypto/PGPAgent.swift @@ -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 ?? [] diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 495dce2..37f57f8 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -324,7 +324,7 @@ public class PasswordStore { PersistenceController.shared.save() } - public func deleteCoreData() { + private func deleteCoreData() { PasswordEntity.deleteAll(in: context) PersistenceController.shared.save() } diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 40357fa..5e12555 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,7 +13,7 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { - private let remoteRepoURL: URL = Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store.git") + private lazy var remoteRepoURL: URL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") private let localRepoURL: URL = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") private var passwordStore: PasswordStore! = nil @@ -49,8 +49,68 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(dirEntity!.children.count, 1) } + func testEraseStoreData() throws { + try cloneRepository() + XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.path)) + XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0) + XCTAssertNotNil(passwordStore.gitRepository) + + passwordStore.eraseStoreData() + + XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.path)) + XCTAssertEqual(passwordStore.numberOfPasswords, 0) + XCTAssertNil(passwordStore.gitRepository) + } + + func testErase() throws { + try cloneRepository() + try importPGPKeys() + 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()) + + 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()) + } + + func testFetchPasswordEntityCoreDataByParent() throws { + try cloneRepository() + + 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() + + let allPasswords = passwordStore.fetchPasswordEntityCoreData(withDir: false) + XCTAssertEqual(allPasswords.count, 4) + allPasswords.forEach { entity in + XCTAssertFalse(entity.isDir) + } + } + func testCloneAndDecryptMultiKeys() throws { try cloneRepository() + try importPGPKeys() Defaults.isEnableGPGIDOn = true defer { @@ -65,15 +125,10 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(keyID, id) } - let keychain = AppKeychain.shared - try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.publicKeys) - try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.privateKeys) - try PGPAgent.shared.initKeys() - - let personal = try decrypt(passwordStore: passwordStore, path: "personal/github.com.gpg", passphrase: "passforios") + let personal = try decrypt(path: "personal/github.com.gpg") 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") let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword") @@ -82,13 +137,22 @@ final class PasswordStoreTest: XCTestCase { XCTAssertEqual(testPasswordPlain.plainText, "testpassword") } - fileprivate func cloneRepository() throws { + // MARK: - Helpers + + private func cloneRepository() throws { try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil) waitForExpectations(timeout: 3, handler: nil) } - fileprivate func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { + private func importPGPKeys() 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) throws -> Password { let entity = passwordStore.fetchPasswordEntity(with: path)! return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) } From e5650ec75698f931f8e7ed9b1d3aef192edc9be7 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 10:57:39 +0100 Subject: [PATCH 20/31] add encrypt-save-decrypt roundtrip test --- .../Fixtures/password-store-empty.git/HEAD | 1 + .../Fixtures/password-store-empty.git/config | 6 ++ .../password-store-empty.git/description | 1 + .../info/exclude | 0 .../4b/825dc642cb6eb9a060e54bf8d69288fbee4904 | Bin 0 -> 15 bytes .../4e/b23a2d659dcaa6fbc01ada57aed6d1fbeb0520 | Bin 0 -> 139 bytes .../50/96ac11d1376ea9b22ddedac1130f45ec618d11 | Bin 0 -> 57 bytes .../ae/a863facd4acba3b4862e3f42847da1000e486a | Bin 0 -> 52 bytes .../f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd | 3 + .../password-store-empty.git/packed-refs | 2 + .../FETCH_HEAD | 0 .../HEAD | 0 .../config | 0 .../password-store-with-gpgid.git/description | 1 + .../info/exclude | 6 ++ ...8dbb253e7642cc425de97363624aab04882615.idx | Bin ...dbb253e7642cc425de97363624aab04882615.pack | Bin .../packed-refs | 0 .../refs/remotes/origin/master | 0 .../Fixtures/password-store.git/description | 1 - passKitTests/Models/PasswordStoreTest.swift | 72 +++++++++++++----- 21 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 passKitTests/Fixtures/password-store-empty.git/HEAD create mode 100644 passKitTests/Fixtures/password-store-empty.git/config create mode 100644 passKitTests/Fixtures/password-store-empty.git/description rename passKitTests/Fixtures/{password-store.git => password-store-empty.git}/info/exclude (100%) create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/4e/b23a2d659dcaa6fbc01ada57aed6d1fbeb0520 create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/50/96ac11d1376ea9b22ddedac1130f45ec618d11 create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/ae/a863facd4acba3b4862e3f42847da1000e486a create mode 100644 passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd create mode 100644 passKitTests/Fixtures/password-store-empty.git/packed-refs rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/FETCH_HEAD (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/HEAD (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/config (100%) create mode 100644 passKitTests/Fixtures/password-store-with-gpgid.git/description create mode 100644 passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/packed-refs (100%) rename passKitTests/Fixtures/{password-store.git => password-store-with-gpgid.git}/refs/remotes/origin/master (100%) delete mode 100644 passKitTests/Fixtures/password-store.git/description diff --git a/passKitTests/Fixtures/password-store-empty.git/HEAD b/passKitTests/Fixtures/password-store-empty.git/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/passKitTests/Fixtures/password-store-empty.git/config b/passKitTests/Fixtures/password-store-empty.git/config new file mode 100644 index 0000000..e6da231 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + ignorecase = true + precomposeunicode = true diff --git a/passKitTests/Fixtures/password-store-empty.git/description b/passKitTests/Fixtures/password-store-empty.git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/passKitTests/Fixtures/password-store.git/info/exclude b/passKitTests/Fixtures/password-store-empty.git/info/exclude similarity index 100% rename from passKitTests/Fixtures/password-store.git/info/exclude rename to passKitTests/Fixtures/password-store-empty.git/info/exclude diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/passKitTests/Fixtures/password-store-empty.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 new file mode 100644 index 0000000000000000000000000000000000000000..adf64119a33d7621aeeaa505d30adb58afaa5559 GIT binary patch literal 15 Wcmbxed=mpo3N25RFzb7z)m#LF#lE?O>d;b__(?p%%XJz0Po5p1u^g&Az;9qc@zU twOsgD4%E&d`~Ha9B0AWkL=KJLS9Ud!DSwv+~->H?%nz I)N=Tv0PoloKmY&$ literal 0 HcmV?d00001 diff --git a/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd b/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd new file mode 100644 index 0000000..3e561dd --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/objects/f0/95bb4897e4cd58faadfe4d4f678fb697be3ffd @@ -0,0 +1,3 @@ +xM +0@a9 \zi:@BzzK}eN*3Sv48J=Eo؎D҈k"EMnKNRy,_pr_9p;qETھ +&rɒi@ \ No newline at end of file diff --git a/passKitTests/Fixtures/password-store-empty.git/packed-refs b/passKitTests/Fixtures/password-store-empty.git/packed-refs new file mode 100644 index 0000000..432aeb4 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty.git/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled sorted +f095bb4897e4cd58faadfe4d4f678fb697be3ffd refs/heads/main diff --git a/passKitTests/Fixtures/password-store.git/FETCH_HEAD b/passKitTests/Fixtures/password-store-with-gpgid.git/FETCH_HEAD similarity index 100% rename from passKitTests/Fixtures/password-store.git/FETCH_HEAD rename to passKitTests/Fixtures/password-store-with-gpgid.git/FETCH_HEAD diff --git a/passKitTests/Fixtures/password-store.git/HEAD b/passKitTests/Fixtures/password-store-with-gpgid.git/HEAD similarity index 100% rename from passKitTests/Fixtures/password-store.git/HEAD rename to passKitTests/Fixtures/password-store-with-gpgid.git/HEAD diff --git a/passKitTests/Fixtures/password-store.git/config b/passKitTests/Fixtures/password-store-with-gpgid.git/config similarity index 100% rename from passKitTests/Fixtures/password-store.git/config rename to passKitTests/Fixtures/password-store-with-gpgid.git/config diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/description b/passKitTests/Fixtures/password-store-with-gpgid.git/description new file mode 100644 index 0000000..5536ff7 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/description @@ -0,0 +1 @@ +Example password store repository for passforios tests with .gpg-id files. diff --git a/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude b/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/passKitTests/Fixtures/password-store-with-gpgid.git/info/exclude @@ -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] +# *~ diff --git a/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx b/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx similarity index 100% rename from passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx rename to passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.idx diff --git a/passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack b/passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack similarity index 100% rename from passKitTests/Fixtures/password-store.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack rename to passKitTests/Fixtures/password-store-with-gpgid.git/objects/pack/pack-6a8dbb253e7642cc425de97363624aab04882615.pack diff --git a/passKitTests/Fixtures/password-store.git/packed-refs b/passKitTests/Fixtures/password-store-with-gpgid.git/packed-refs similarity index 100% rename from passKitTests/Fixtures/password-store.git/packed-refs rename to passKitTests/Fixtures/password-store-with-gpgid.git/packed-refs diff --git a/passKitTests/Fixtures/password-store.git/refs/remotes/origin/master b/passKitTests/Fixtures/password-store-with-gpgid.git/refs/remotes/origin/master similarity index 100% rename from passKitTests/Fixtures/password-store.git/refs/remotes/origin/master rename to passKitTests/Fixtures/password-store-with-gpgid.git/refs/remotes/origin/master diff --git a/passKitTests/Fixtures/password-store.git/description b/passKitTests/Fixtures/password-store.git/description deleted file mode 100644 index 9258d15..0000000 --- a/passKitTests/Fixtures/password-store.git/description +++ /dev/null @@ -1 +0,0 @@ -Example password store repository for passforios tests. diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 5e12555..205ab13 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -13,7 +13,6 @@ import XCTest @testable import passKit final class PasswordStoreTest: XCTestCase { - private lazy var remoteRepoURL: URL = Bundle(for: type(of: self)).resourceURL!.appendingPathComponent("Fixtures/password-store.git") private let localRepoURL: URL = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") private var passwordStore: PasswordStore! = nil @@ -25,10 +24,12 @@ final class PasswordStoreTest: XCTestCase { override func tearDown() { passwordStore.erase() passwordStore = nil + + Defaults.removeAll() } func testInitPasswordEntityCoreData() throws { - try cloneRepository() + try cloneRepository(.withGPGID) XCTAssertEqual(passwordStore.numberOfPasswords, 4) @@ -50,7 +51,7 @@ final class PasswordStoreTest: XCTestCase { } func testEraseStoreData() throws { - try cloneRepository() + try cloneRepository(.withGPGID) XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.path)) XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0) XCTAssertNotNil(passwordStore.gitRepository) @@ -63,8 +64,8 @@ final class PasswordStoreTest: XCTestCase { } func testErase() throws { - try cloneRepository() - try importPGPKeys() + try cloneRepository(.withGPGID) + try importSinglePGPKey() Defaults.gitSignatureName = "Test User" PasscodeLock.shared.save(passcode: "1234") @@ -84,7 +85,7 @@ final class PasswordStoreTest: XCTestCase { } func testFetchPasswordEntityCoreDataByParent() throws { - try cloneRepository() + try cloneRepository(.withGPGID) let rootChildren = passwordStore.fetchPasswordEntityCoreData(parent: nil) XCTAssertGreaterThan(rootChildren.count, 0) @@ -99,7 +100,7 @@ final class PasswordStoreTest: XCTestCase { } func testFetchPasswordEntityCoreDataWithDir() throws { - try cloneRepository() + try cloneRepository(.withGPGID) let allPasswords = passwordStore.fetchPasswordEntityCoreData(withDir: false) XCTAssertEqual(allPasswords.count, 4) @@ -108,14 +109,21 @@ final class PasswordStoreTest: XCTestCase { } } + func testEncryptSaveDecryptMultiline() throws { + try cloneRepository(.empty) + try importSinglePGPKey() + + let password = Password(name: "test", path: "test.gpg", plainText: "foobar\nwith\nmultiple\nlines") + _ = try passwordStore.add(password: password) + let decryptedPassword = try decrypt(path: "test.gpg") + XCTAssertEqual(decryptedPassword.plainText, "foobar\nwith\nmultiple\nlines") + } + func testCloneAndDecryptMultiKeys() throws { - try cloneRepository() - try importPGPKeys() + try cloneRepository(.withGPGID) + try importMultiplePGPKeys() Defaults.isEnableGPGIDOn = true - defer { - Defaults.isEnableGPGIDOn = false - } [ ("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"), @@ -139,21 +147,51 @@ final class PasswordStoreTest: XCTestCase { // MARK: - Helpers - private func cloneRepository() throws { - try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") + private enum RemoteRepo { + case empty + case withGPGID + + var url: URL { + switch self { + case .empty: + Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-empty.git") + case .withGPGID: + Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-with-gpgid.git") + } + } + + var branchName: String { + switch self { + case .empty: + "main" + case .withGPGID: + "master" + } + } + } + + private func cloneRepository(_ remote: RemoteRepo) throws { + try passwordStore.cloneRepository(remoteRepoURL: remote.url, branchName: remote.branchName) expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil) waitForExpectations(timeout: 3, handler: nil) } - private func importPGPKeys() throws { + 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) throws -> Password { + private func decrypt(path: String, keyID: String? = nil) throws -> Password { let entity = passwordStore.fetchPasswordEntity(with: path)! - return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + return try passwordStore.decrypt(passwordEntity: entity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) } } From 98ad3234316fb462f636f9f16d6b3a86ff59f049 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 11:45:04 +0100 Subject: [PATCH 21/31] check notification center notifications --- passKitTests/Models/PasswordStoreTest.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 205ab13..60ddf48 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -56,11 +56,14 @@ final class PasswordStoreTest: XCTestCase { 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 { @@ -75,6 +78,8 @@ final class PasswordStoreTest: XCTestCase { XCTAssertTrue(PasscodeLock.shared.hasPasscode) XCTAssertTrue(PGPAgent.shared.isInitialized()) + expectation(forNotification: .passwordStoreUpdated, object: nil) + expectation(forNotification: .passwordStoreErased, object: nil) passwordStore.erase() XCTAssertEqual(passwordStore.numberOfPasswords, 0) @@ -82,6 +87,7 @@ final class PasswordStoreTest: XCTestCase { XCTAssertFalse(Defaults.hasKey(\.gitSignatureName)) XCTAssertFalse(PasscodeLock.shared.hasPasscode) XCTAssertFalse(PGPAgent.shared.isInitialized()) + waitForExpectations(timeout: 1, handler: nil) } func testFetchPasswordEntityCoreDataByParent() throws { @@ -171,8 +177,11 @@ final class PasswordStoreTest: XCTestCase { } private func cloneRepository(_ remote: RemoteRepo) throws { - try passwordStore.cloneRepository(remoteRepoURL: remote.url, branchName: remote.branchName) 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) } From 12c8c042035befa0952e726f11cb62457afb387c Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 11:46:26 +0100 Subject: [PATCH 22/31] test add, edit, delete --- passKit/Models/PasswordStore.swift | 2 +- passKitTests/Models/PasswordStoreTest.swift | 69 +++++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 37f57f8..22c90d9 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -320,7 +320,7 @@ public class PasswordStore { saveUpdatedContext() } - public func saveUpdatedContext() { + private func saveUpdatedContext() { PersistenceController.shared.save() } diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 60ddf48..883471c 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -115,16 +115,75 @@ final class PasswordStoreTest: XCTestCase { } } - func testEncryptSaveDecryptMultiline() throws { + func testAddPassword() throws { try cloneRepository(.empty) try importSinglePGPKey() - let password = Password(name: "test", path: "test.gpg", plainText: "foobar\nwith\nmultiple\nlines") - _ = try passwordStore.add(password: password) - let decryptedPassword = try decrypt(path: "test.gpg") - XCTAssertEqual(decryptedPassword.plainText, "foobar\nwith\nmultiple\nlines") + 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: "test3.gpg", plainText: "lorem ipsum") + let password4 = Password(name: "test4", path: "test4.gpg", plainText: "you are valuable and you matter") + + [password1, password2, password3, password4].forEach { password in + expectation(forNotification: .passwordStoreUpdated, object: nil) + + let savedEntity = try? passwordStore.add(password: password) + + XCTAssertEqual(savedEntity!.name, password.name) + waitForExpectations(timeout: 1, handler: nil) + } } + func testDeletePassword() throws { + try cloneRepository(.withGPGID) + + 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")) + waitForExpectations(timeout: 1, handler: nil) + } + + func testEditPasswordValue() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() + 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") + waitForExpectations(timeout: 1, handler: nil) + } + + func testMovePassword() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() + 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")) + waitForExpectations(timeout: 1, handler: nil) + } + + // MARK: - .gpg-id support + func testCloneAndDecryptMultiKeys() throws { try cloneRepository(.withGPGID) try importMultiplePGPKeys() From 98646242e0fa6c4bacbb4edcc99e273e0120064b Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 12:11:37 +0100 Subject: [PATCH 23/31] fix deleting directory this used to corrupt the local state (password entities remained in DB but files/dirs were removed from git and disk) --- pass/en.lproj/Localizable.strings | 1 + passKit/Helpers/AppError.swift | 1 + passKit/Models/PasswordStore.swift | 4 ++++ passKitTests/Models/PasswordStoreTest.swift | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+) diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 5f26010..10d8b30 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -73,6 +73,7 @@ "KeyImportError." = "Cannot import the key."; "FileNotFoundError." = "File '%@' cannot be read."; "PasswordDuplicatedError." = "Cannot add the password; password is duplicated."; +"CannotDeleteDirectoryError." = "Cannot delete directories; delete passwords instead."; "GitResetError." = "Cannot identify the latest synced commit."; "GitCreateSignatureError." = "Cannot create a valid author/committer signature."; "GitPushNotSuccessfulError." = "Pushing local changes was not successful. Make sure there are no uncommitted changes on the remote repository."; diff --git a/passKit/Helpers/AppError.swift b/passKit/Helpers/AppError.swift index 8e0aa21..8bcc84b 100644 --- a/passKit/Helpers/AppError.swift +++ b/passKit/Helpers/AppError.swift @@ -15,6 +15,7 @@ public enum AppError: Error, Equatable { case keyImport case readingFile(fileName: String) case passwordDuplicated + case cannotDeleteDirectory case gitReset case gitCommit case gitCreateSignature diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 22c90d9..78c92c4 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -273,6 +273,10 @@ public class PasswordStore { } public func delete(passwordEntity: PasswordEntity) throws { + if passwordEntity.isDir { + throw AppError.cannotDeleteDirectory + } + let deletedFileURL = passwordEntity.fileURL(in: storeURL) let deletedFilePath = passwordEntity.path try gitRm(path: passwordEntity.path) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 883471c..8b4d670 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -143,9 +143,27 @@ final class PasswordStoreTest: XCTestCase { 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)) waitForExpectations(timeout: 1, handler: nil) } + func testDeleteDirectoryFails() throws { + try cloneRepository(.withGPGID) + + expectation(forNotification: .passwordStoreUpdated, object: nil).isInverted = true + + let entity = passwordStore.fetchPasswordEntity(with: "personal") + XCTAssertThrowsError(try passwordStore.delete(passwordEntity: entity!)) { error in + XCTAssertTrue(error is AppError, "Unexpected error type: \(type(of: error))") + XCTAssertEqual(error as? AppError, .cannotDeleteDirectory) + } + + XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")) + XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal/github.com.gpg").path)) + waitForExpectations(timeout: 0.1, handler: nil) + } + func testEditPasswordValue() throws { try cloneRepository(.withGPGID) try importSinglePGPKey() From c3bfa861f42b7212ce515bfcac74c1b98958d44d Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 12:49:33 +0100 Subject: [PATCH 24/31] check file system and commits upon changes to store --- passKitTests/Models/PasswordStoreTest.swift | 31 ++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 8b4d670..94dd8eb 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -32,6 +32,8 @@ final class PasswordStoreTest: XCTestCase { 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") @@ -118,10 +120,12 @@ final class PasswordStoreTest: XCTestCase { 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: "test3.gpg", plainText: "lorem ipsum") + 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") [password1, password2, password3, password4].forEach { password in @@ -132,10 +136,21 @@ final class PasswordStoreTest: XCTestCase { 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 testDeletePassword() throws { try cloneRepository(.withGPGID) + let numCommitsBefore = passwordStore.numberOfCommits! + let numLocalCommitsBefore = passwordStore.numberOfLocalCommits expectation(forNotification: .passwordStoreUpdated, object: nil) @@ -145,11 +160,15 @@ final class PasswordStoreTest: XCTestCase { XCTAssertNil(passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")) XCTAssertNil(passwordStore.fetchPasswordEntity(with: "personal")) XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal").path)) + XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore + 1) + XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore + 1) waitForExpectations(timeout: 1, handler: nil) } func testDeleteDirectoryFails() throws { try cloneRepository(.withGPGID) + let numCommitsBefore = passwordStore.numberOfCommits! + let numLocalCommitsBefore = passwordStore.numberOfLocalCommits expectation(forNotification: .passwordStoreUpdated, object: nil).isInverted = true @@ -161,12 +180,16 @@ final class PasswordStoreTest: XCTestCase { 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) @@ -179,12 +202,16 @@ final class PasswordStoreTest: XCTestCase { 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) @@ -197,6 +224,8 @@ final class PasswordStoreTest: XCTestCase { 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) } From e195280efc153ef62496872a05fc11ad94142b49 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 12:55:16 +0100 Subject: [PATCH 25/31] test resetting local changes --- passKitTests/Models/PasswordStoreTest.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 94dd8eb..812a96b 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -229,6 +229,26 @@ final class PasswordStoreTest: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } + func testReset() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() + let numCommitsBefore = passwordStore.numberOfCommits! + let numLocalCommitsBefore = passwordStore.numberOfLocalCommits + + _ = try? passwordStore.add(password: Password(name: "test", path: "test.gpg", plainText: "foobar")) + try passwordStore.delete(passwordEntity: passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")!) + + expectation(forNotification: .passwordStoreUpdated, object: nil) + let numDroppedCommits = try passwordStore.reset() + + XCTAssertEqual(numDroppedCommits, 2) + XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("test.gpg").path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.appendingPathComponent("personal/github.com.gpg").path)) + XCTAssertEqual(passwordStore.numberOfCommits!, numCommitsBefore) + XCTAssertEqual(passwordStore.numberOfLocalCommits, numLocalCommitsBefore) + waitForExpectations(timeout: 1, handler: nil) + } + // MARK: - .gpg-id support func testCloneAndDecryptMultiKeys() throws { From e1da1988b45b4852491c923716ad682d12624913 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:15:03 +0100 Subject: [PATCH 26/31] add save and decrypt round trip --- passKitTests/Models/PasswordStoreTest.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 812a96b..de3f7b3 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -128,10 +128,10 @@ final class PasswordStoreTest: XCTestCase { 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") - [password1, password2, password3, password4].forEach { password in + for password in [password1, password2, password3, password4] { expectation(forNotification: .passwordStoreUpdated, object: nil) - let savedEntity = try? passwordStore.add(password: password) + let savedEntity = try passwordStore.add(password: password) XCTAssertEqual(savedEntity!.name, password.name) waitForExpectations(timeout: 1, handler: nil) @@ -147,6 +147,17 @@ final class PasswordStoreTest: XCTestCase { 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! @@ -235,7 +246,7 @@ final class PasswordStoreTest: XCTestCase { let numCommitsBefore = passwordStore.numberOfCommits! let numLocalCommitsBefore = passwordStore.numberOfLocalCommits - _ = try? passwordStore.add(password: Password(name: "test", path: "test.gpg", plainText: "foobar")) + _ = 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) From c6a4f805038d0c8f805019d84bb4d9cb5057df77 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:30:22 +0100 Subject: [PATCH 27/31] add initPasswordEntityCoreData tests --- .../CoreData/PasswordEntityTest.swift | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/passKitTests/CoreData/PasswordEntityTest.swift b/passKitTests/CoreData/PasswordEntityTest.swift index 6362e2a..ad70f73 100644 --- a/passKitTests/CoreData/PasswordEntityTest.swift +++ b/passKitTests/CoreData/PasswordEntityTest.swift @@ -85,4 +85,99 @@ final class PasswordEntityTest: CoreDataTestCase { XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 0) } + + // MARK: - initPasswordEntityCoreData tests + + func testInitPasswordEntityCoreDataBuildsTree() throws { + let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: rootDir) } + + // Create directory structure: + // email/ + // work.gpg + // personal.gpg + // social/ + // mastodon.gpg + // toplevel.gpg + // notes.txt (non-.gpg file) + let emailDir = rootDir.appendingPathComponent("email") + let socialDir = rootDir.appendingPathComponent("social") + try FileManager.default.createDirectory(at: emailDir, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: socialDir, withIntermediateDirectories: true) + try Data("test1".utf8).write(to: emailDir.appendingPathComponent("work.gpg")) + try Data("test2".utf8).write(to: emailDir.appendingPathComponent("personal.gpg")) + try Data("test3".utf8).write(to: socialDir.appendingPathComponent("mastodon.gpg")) + try Data("test4".utf8).write(to: rootDir.appendingPathComponent("toplevel.gpg")) + try Data("test5".utf8).write(to: rootDir.appendingPathComponent("notes.txt")) + + let context = controller.viewContext() + PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) + + // Verify total counts + let allEntities = PasswordEntity.fetchAll(in: context) + let files = allEntities.filter { !$0.isDir } + let dirs = allEntities.filter(\.isDir) + XCTAssertEqual(files.count, 5) // 4 .gpg + 1 .txt + XCTAssertEqual(dirs.count, 2) // email, social + + // Verify .gpg extension is stripped + let workEntity = allEntities.first { $0.path == "email/work.gpg" } + XCTAssertNotNil(workEntity) + XCTAssertEqual(workEntity!.name, "work") + + // Verify non-.gpg file keeps its extension + let notesEntity = allEntities.first { $0.path == "notes.txt" } + XCTAssertNotNil(notesEntity) + XCTAssertEqual(notesEntity!.name, "notes.txt") + + // Verify parent-child relationships + let emailEntity = allEntities.first { $0.path == "email" && $0.isDir } + XCTAssertNotNil(emailEntity) + XCTAssertEqual(emailEntity!.children.count, 2) + + // Verify top-level files have no parent (root was deleted) + let toplevelEntity = allEntities.first { $0.path == "toplevel.gpg" } + XCTAssertNotNil(toplevelEntity) + XCTAssertEqual(toplevelEntity!.name, "toplevel") + XCTAssertNil(toplevelEntity!.parent) + } + + func testInitPasswordEntityCoreDataSkipsHiddenFiles() throws { + let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: rootDir) } + + try Data("test".utf8).write(to: rootDir.appendingPathComponent("visible.gpg")) + try Data("test".utf8).write(to: rootDir.appendingPathComponent(".hidden.gpg")) + try Data("test".utf8).write(to: rootDir.appendingPathComponent(".gpg-id")) + try FileManager.default.createDirectory(at: rootDir.appendingPathComponent(".git"), withIntermediateDirectories: true) + try Data("test".utf8).write(to: rootDir.appendingPathComponent(".git/config")) + + let context = controller.viewContext() + PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) + + let allEntities = PasswordEntity.fetchAll(in: context) + XCTAssertEqual(allEntities.count, 1) + XCTAssertEqual(allEntities.first!.name, "visible") + } + + func testInitPasswordEntityCoreDataHandlesEmptyDirectory() throws { + let rootDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: rootDir) } + + try FileManager.default.createDirectory(at: rootDir.appendingPathComponent("emptydir"), withIntermediateDirectories: true) + + let context = controller.viewContext() + PasswordEntity.initPasswordEntityCoreData(url: rootDir, in: context) + + let allEntities = PasswordEntity.fetchAll(in: context) + let dirs = allEntities.filter(\.isDir) + let files = allEntities.filter { !$0.isDir } + XCTAssertEqual(dirs.count, 1) + XCTAssertEqual(dirs.first!.name, "emptydir") + XCTAssertEqual(dirs.first!.children.count, 0) + XCTAssertEqual(files.count, 0) + } } From 4c21ab99adc9174f8fa17f80cdce87700b0664e7 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:36:19 +0100 Subject: [PATCH 28/31] add tests for AppKeychain --- pass.xcodeproj/project.pbxproj | 4 + passKitTests/Helpers/AppKeychainTest.swift | 101 +++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 passKitTests/Helpers/AppKeychainTest.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index e3e63e8..3b42e4b 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ 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 */; }; 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 */; }; @@ -423,6 +424,7 @@ 30F6C1B327664C7200BE5AB2 /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SVProgressHUD.xcframework; path = Carthage/Build/SVProgressHUD.xcframework; sourceTree = ""; }; 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoTokenKit.framework; path = System/Library/Frameworks/CryptoTokenKit.framework; sourceTree = SDKROOT; }; + 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKeychainTest.swift; sourceTree = ""; }; 8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = ""; }; 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; @@ -626,6 +628,7 @@ 301F6464216164670071A4CE /* Helpers */ = { isa = PBXGroup; children = ( + 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */, 3032328922C9FBA2009EBD9C /* KeyFileManagerTest.swift */, ); path = Helpers; @@ -1643,6 +1646,7 @@ A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */, 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */, A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */, + 8A4716692F5EF56900C7A64D /* AppKeychainTest.swift in Sources */, 30695E2524FAEF2600C9D46E /* GitCredentialTest.swift in Sources */, 30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */, 30B04860209A5141001013CA /* PasswordTest.swift in Sources */, diff --git a/passKitTests/Helpers/AppKeychainTest.swift b/passKitTests/Helpers/AppKeychainTest.swift new file mode 100644 index 0000000..9b420fc --- /dev/null +++ b/passKitTests/Helpers/AppKeychainTest.swift @@ -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") + } +} From cde82d956b08c3b5303552348f59b3f05678636d Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 13:41:51 +0100 Subject: [PATCH 29/31] rename file to match contained class --- pass.xcodeproj/project.pbxproj | 8 ++++---- .../{CoreDataStack.swift => PersistenceController.swift} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename passKit/Controllers/{CoreDataStack.swift => PersistenceController.swift} (98%) diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 3b42e4b..95c542d 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -197,7 +197,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 */; }; @@ -502,7 +502,7 @@ DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = ""; }; DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = ""; }; DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = ""; }; - DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; + DC6474522D20DD0C004B4BBC /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTestCase.swift; sourceTree = ""; }; DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEntityTest.swift; sourceTree = ""; }; DC64745E2D45B23A004B4BBC /* GitRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitRepository.swift; sourceTree = ""; }; @@ -919,7 +919,7 @@ children = ( 30697C3121F63C8B0064FCAC /* PasscodeLockPresenter.swift */, 30697C3221F63C8B0064FCAC /* PasscodeLockViewController.swift */, - DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */, + DC6474522D20DD0C004B4BBC /* PersistenceController.swift */, ); path = Controllers; sourceTree = ""; @@ -1620,7 +1620,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 */, diff --git a/passKit/Controllers/CoreDataStack.swift b/passKit/Controllers/PersistenceController.swift similarity index 98% rename from passKit/Controllers/CoreDataStack.swift rename to passKit/Controllers/PersistenceController.swift index 0452259..67ad4fe 100644 --- a/passKit/Controllers/CoreDataStack.swift +++ b/passKit/Controllers/PersistenceController.swift @@ -1,5 +1,5 @@ // -// CoreDataStack.swift +// PersistenceController.swift // passKit // // Created by Mingshen Sun on 12/28/24. From b8b7e1f913e79071d7441d187b72c4e45a4d5c57 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 14:04:12 +0100 Subject: [PATCH 30/31] PersistenceController tests --- pass.xcodeproj/project.pbxproj | 12 +++ .../Controllers/PersistenceController.swift | 12 +-- .../PersistenceControllerTest.swift | 93 +++++++++++++++++++ passKitTests/CoreData/CoreDataTestCase.swift | 2 +- 4 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 passKitTests/Controllers/PersistenceControllerTest.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 95c542d..952867f 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 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 */; }; @@ -425,6 +426,7 @@ 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 5F9D7B0C27AF6F7300A8AB22 /* CryptoTokenKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoTokenKit.framework; path = System/Library/Frameworks/CryptoTokenKit.framework; sourceTree = SDKROOT; }; 8A4716682F5EF56900C7A64D /* AppKeychainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKeychainTest.swift; sourceTree = ""; }; + 8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceControllerTest.swift; sourceTree = ""; }; 8AD8EBF22F5E268D007475AB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 9A1EF0B324C50DD80074FEAC /* passBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBeta.entitlements; sourceTree = ""; }; 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; @@ -766,6 +768,14 @@ path = Crypto; sourceTree = ""; }; + 8A4716702F5EF7A900C7A64D /* Controllers */ = { + isa = PBXGroup; + children = ( + 8A47166F2F5EF7A900C7A64D /* PersistenceControllerTest.swift */, + ); + path = Controllers; + sourceTree = ""; + }; 9A58664F25AADB66006719C2 /* Services */ = { isa = PBXGroup; children = ( @@ -884,6 +894,7 @@ A26075861EEC6F34005DB03E /* passKitTests */ = { isa = PBXGroup; children = ( + 8A4716702F5EF7A900C7A64D /* Controllers */, DC64745A2D29BD43004B4BBC /* CoreData */, 30A86F93230F235800F821A4 /* Crypto */, 30BAC8C322E3BA4300438475 /* Testbase */, @@ -1639,6 +1650,7 @@ 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 */, diff --git a/passKit/Controllers/PersistenceController.swift b/passKit/Controllers/PersistenceController.swift index 67ad4fe..5d05001 100644 --- a/passKit/Controllers/PersistenceController.swift +++ b/passKit/Controllers/PersistenceController.swift @@ -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 { diff --git a/passKitTests/Controllers/PersistenceControllerTest.swift b/passKitTests/Controllers/PersistenceControllerTest.swift new file mode 100644 index 0000000..4ae642f --- /dev/null +++ b/passKitTests/Controllers/PersistenceControllerTest.swift @@ -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... + } +} diff --git a/passKitTests/CoreData/CoreDataTestCase.swift b/passKitTests/CoreData/CoreDataTestCase.swift index fa356bf..b11ddd1 100644 --- a/passKitTests/CoreData/CoreDataTestCase.swift +++ b/passKitTests/CoreData/CoreDataTestCase.swift @@ -20,7 +20,7 @@ class CoreDataTestCase: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - controller = PersistenceController(isUnitTest: true) + controller = PersistenceController.forUnitTests() } override func tearDown() { From 55b682b4b0cf7e836be1cd099e4010178440d439 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Mon, 9 Mar 2026 22:16:15 +0100 Subject: [PATCH 31/31] improve directory deletion/editing handling --- pass/de.lproj/Localizable.strings | 1 + pass/en.lproj/Localizable.strings | 2 +- passKit/Helpers/AppError.swift | 2 +- passKit/Models/PasswordStore.swift | 13 +++- .../password-store-empty-dirs.git/HEAD | 1 + .../password-store-empty-dirs.git/config | 6 ++ .../password-store-empty-dirs.git/description | 1 + .../info/exclude | 6 ++ .../44/73d8218dcffe837e18d56d74c240e565461aea | Bin 0 -> 131 bytes .../4c/f9f177c4c015836fca6a31f9c3917e89ae29ec | Bin 0 -> 53 bytes .../50/96ac11d1376ea9b22ddedac1130f45ec618d11 | Bin 0 -> 57 bytes .../ce/013625030ba8dba906f756967f9e9ca394464a | Bin 0 -> 21 bytes .../d5/64d0bc3dd917926892c55e3706cc116d5b165e | Bin 0 -> 53 bytes .../e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 | Bin 0 -> 15 bytes .../fb/cbb5819e1c864ef33cfffa179a71387a5d90d0 | Bin 0 -> 131 bytes .../password-store-empty-dirs.git/packed-refs | 2 + passKitTests/Models/PasswordStoreTest.swift | 59 +++++++++++++++++- 17 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/HEAD create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/config create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/description create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/info/exclude create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/objects/44/73d8218dcffe837e18d56d74c240e565461aea create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/objects/4c/f9f177c4c015836fca6a31f9c3917e89ae29ec create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/objects/50/96ac11d1376ea9b22ddedac1130f45ec618d11 create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/objects/d5/64d0bc3dd917926892c55e3706cc116d5b165e create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/objects/fb/cbb5819e1c864ef33cfffa179a71387a5d90d0 create mode 100644 passKitTests/Fixtures/password-store-empty-dirs.git/packed-refs diff --git a/pass/de.lproj/Localizable.strings b/pass/de.lproj/Localizable.strings index 8177a35..355b200 100644 --- a/pass/de.lproj/Localizable.strings +++ b/pass/de.lproj/Localizable.strings @@ -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."; diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 10d8b30..a4f74e3 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -73,7 +73,7 @@ "KeyImportError." = "Cannot import the key."; "FileNotFoundError." = "File '%@' cannot be read."; "PasswordDuplicatedError." = "Cannot add the password; password is duplicated."; -"CannotDeleteDirectoryError." = "Cannot delete directories; delete passwords instead."; +"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."; diff --git a/passKit/Helpers/AppError.swift b/passKit/Helpers/AppError.swift index 8bcc84b..28ceb73 100644 --- a/passKit/Helpers/AppError.swift +++ b/passKit/Helpers/AppError.swift @@ -15,7 +15,7 @@ public enum AppError: Error, Equatable { case keyImport case readingFile(fileName: String) case passwordDuplicated - case cannotDeleteDirectory + case cannotDeleteNonEmptyDirectory case gitReset case gitCommit case gitCreateSignature diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 78c92c4..b918ab4 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -273,13 +273,15 @@ public class PasswordStore { } public func delete(passwordEntity: PasswordEntity) throws { - if passwordEntity.isDir { - throw AppError.cannotDeleteDirectory + 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)) @@ -287,6 +289,11 @@ public class PasswordStore { } 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) diff --git a/passKitTests/Fixtures/password-store-empty-dirs.git/HEAD b/passKitTests/Fixtures/password-store-empty-dirs.git/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty-dirs.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/passKitTests/Fixtures/password-store-empty-dirs.git/config b/passKitTests/Fixtures/password-store-empty-dirs.git/config new file mode 100644 index 0000000..e6da231 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty-dirs.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + ignorecase = true + precomposeunicode = true diff --git a/passKitTests/Fixtures/password-store-empty-dirs.git/description b/passKitTests/Fixtures/password-store-empty-dirs.git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty-dirs.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/passKitTests/Fixtures/password-store-empty-dirs.git/info/exclude b/passKitTests/Fixtures/password-store-empty-dirs.git/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/passKitTests/Fixtures/password-store-empty-dirs.git/info/exclude @@ -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] +# *~ diff --git a/passKitTests/Fixtures/password-store-empty-dirs.git/objects/44/73d8218dcffe837e18d56d74c240e565461aea b/passKitTests/Fixtures/password-store-empty-dirs.git/objects/44/73d8218dcffe837e18d56d74c240e565461aea new file mode 100644 index 0000000000000000000000000000000000000000..6fa149a030a864de0d0dd70acc3d69ad9e8ae592 GIT binary patch literal 131 zcmV-}0DS*=0V^p=O;s>7G-oh0FfcPQQP4{-NY~9wVF;MEM)0C}-pWn7_ih~&=68LQ z*ehsa00astnMJzgnI##z`6U^tMY?I3IjIajKR=cqIUw4ce=5uH=i!NUo$EB;KvbvZ l7L-)#0`)LlO}Vhg_NMrxj7dl1%-PNe=0=Of0RVWLF+DgWJPiN< literal 0 HcmV?d00001 diff --git a/passKitTests/Fixtures/password-store-empty-dirs.git/objects/4c/f9f177c4c015836fca6a31f9c3917e89ae29ec b/passKitTests/Fixtures/password-store-empty-dirs.git/objects/4c/f9f177c4c015836fca6a31f9c3917e89ae29ec new file mode 100644 index 0000000000000000000000000000000000000000..1c17f204ab847733104e72fef4ac750bd82d3e28 GIT binary patch literal 53 zcmV-50LuS(0V^p=O;s>9V=y!@Ff%bxNXyJg)hnqeVK~QVrpnB{;`U0m?_tyG=gnC> L#mx%x literal 0 HcmV?d00001 diff --git a/passKitTests/Fixtures/password-store-empty-dirs.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 b/passKitTests/Fixtures/password-store-empty-dirs.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 new file mode 100644 index 0000000000000000000000000000000000000000..711223894375fe1186ac5bfffdc48fb1fa1e65cc GIT binary patch literal 15 WcmbD(;O_Aj@1=EF ziU5wdlg13z29x7}F{f$bXaaxG`5bH-tY+t(^+-&Ly4=