pashage

Yet Another Opinionated Re-engineering of the Unix Password Store
git clone https://git.instinctive.eu/pashage.git
Log | Files | Refs | README | LICENSE

pashage_extra_spec.sh (57457B)


      1 # pashage - age-backed POSIX password manager
      2 # Copyright (C) 2024-2025  Natasha Kerensikova
      3 #
      4 # This program is free software; you can redistribute it and/or
      5 # modify it under the terms of the GNU General Public License
      6 # as published by the Free Software Foundation; either version 2
      7 # of the License, or (at your option) any later version.
      8 #
      9 # This program is distributed in the hope that it will be useful,
     10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12 # GNU General Public License for more details.
     13 #
     14 # You should have received a copy of the GNU General Public License
     15 # along with this program; if not, write to the Free Software
     16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
     17 
     18 # This test file exercises the whole software through command functions,
     19 # using minimal mocking, limited to cryptography (to make it more robust
     20 # and easier to debug), like the `pass_usage.sh` suite.
     21 # It complements `pass_usage.sh` with pashage-specific behavior, aiming for
     22 # maximal coverage of normal code paths.
     23 
     24 Describe 'Integrated Command Functions'
     25   Include src/pashage.sh
     26   if [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
     27     Set 'errexit:on' 'nounset:on'
     28   else
     29     Set 'errexit:on' 'nounset:on' 'pipefail:on'
     30   fi
     31 
     32   GITLOG="${SHELLSPEC_WORKDIR}/git-log.txt"
     33 
     34   AGE='mock-age'
     35   IDENTITIES_FILE="${SHELLSPEC_WORKDIR}/age-identities"
     36   PREFIX="${SHELLSPEC_WORKDIR}/store"
     37 
     38   CHARACTER_SET='[:punct:][:alnum:]'
     39   CHARACTER_SET_NO_SYMBOLS='[:alnum:]'
     40   CLIP_TIME=45
     41   GENERATED_LENGTH=25
     42   X_SELECTION=clipboard
     43 
     44   TREE__='   '
     45   TREE_I='|  '
     46   TREE_T='|- '
     47   TREE_L='`- '
     48 
     49   BOLD_TEXT='(B)'
     50   NORMAL_TEXT='(N)'
     51   RED_TEXT='(R)'
     52   BLUE_TEXT='(B)'
     53   UNDERLINE_TEXT='(U)'
     54   NO_UNDERLINE_TEXT='(!U)'
     55 
     56   git_log() {
     57     @git -C "${PREFIX}" status --porcelain >&2
     58     @git -C "${PREFIX}" log --format='%s' --stat >|"${GITLOG}"
     59   }
     60 
     61   setup_log() { %text
     62     #|Initial setup
     63     #|
     64     #| extra/subdir/file.age  | 2 ++
     65     #| fluff/.age-recipients  | 2 ++
     66     #| fluff/one.age          | 3 +++
     67     #| fluff/three.age        | 5 +++++
     68     #| fluff/two.age          | 4 ++++
     69     #| old.gpg                | 3 +++
     70     #| shared/.age-recipients | 2 ++
     71     #| stale.age              | 3 +++
     72     #| subdir/file.age        | 2 ++
     73     #| y.txt                  | 3 +++
     74     #| 10 files changed, 29 insertions(+)
     75   }
     76 
     77   setup_log_bin() { %text
     78     #|Initial setup
     79     #|
     80     #| extra/subdir/file.age  | Bin 0 -> 33 bytes
     81     #| fluff/.age-recipients  |   2 ++
     82     #| fluff/one.age          | Bin 0 -> 55 bytes
     83     #| fluff/three.age        | Bin 0 -> 110 bytes
     84     #| fluff/two.age          | Bin 0 -> 90 bytes
     85     #| old.gpg                |   3 +++
     86     #| shared/.age-recipients |   2 ++
     87     #| stale.age              | Bin 0 -> 55 bytes
     88     #| subdir/file.age        | Bin 0 -> 33 bytes
     89     #| y.txt                  |   3 +++
     90     #| 10 files changed, 10 insertions(+)
     91   }
     92 
     93   expected_log() { setup_log; } # Default log to override as needed
     94 
     95   check_git_log() {
     96     git_log && expected_log | diff -u "${GITLOG}" - >&2
     97   }
     98 
     99   setup_id() {
    100     @mkdir -p "${PREFIX}/$1"
    101     @cat >"${PREFIX}/$1/.age-recipients"
    102   }
    103 
    104   setup_secret() {
    105     [ "$1" = "${1%/*}" ] || @mkdir -p "${PREFIX}/${1%/*}"
    106     @sed 's/^/age/' >"${PREFIX}/$1.age"
    107   }
    108 
    109   setup() {
    110     @git init -q -b main "${PREFIX}"
    111     @git -C "${PREFIX}" config --local user.name 'Test User'
    112     @git -C "${PREFIX}" config --local user.email 'test@example.com'
    113     %putsn 'myself' >"${IDENTITIES_FILE}"
    114     %text | setup_secret 'subdir/file'
    115     #|Recipient:myself
    116     #|:p4ssw0rd
    117     %text | setup_secret 'extra/subdir/file'
    118     #|Recipient:myself
    119     #|:Pa55worD
    120     %text | setup_id 'shared'
    121     #|myself
    122     #|friend
    123     %text | setup_id 'fluff'
    124     #|master
    125     #|myself
    126     %text | setup_secret 'fluff/one'
    127     #|Recipient:master
    128     #|Recipient:myself
    129     #|:1-password
    130     %text | setup_secret 'fluff/two'
    131     #|Recipient:master
    132     #|Recipient:myself
    133     #|:2-password
    134     #|:URL: https://example.com/login
    135     %text | setup_secret 'fluff/three'
    136     #|Recipient:master
    137     #|Recipient:myself
    138     #|:3-password
    139     #|:Username: 3Jane
    140     #|:URL: https://example.com/login
    141     %text | setup_secret 'stale'
    142     #|Recipient:master
    143     #|Recipient:myself
    144     #|:0-password
    145     %text >"${PREFIX}/old.gpg"
    146     #|gpgRecipient:myOldSelf
    147     #|gpg:very-old-password
    148     #|gpg:Username: previous-life
    149     %text >"${PREFIX}/y.txt"
    150     #|# Title
    151     #|Line of text
    152     #|End of note
    153     @git -C "${PREFIX}" add .
    154     @git -C "${PREFIX}" commit -m 'Initial setup' >/dev/null
    155 
    156     # Check setup_log consistency
    157     git_log
    158     setup_log | @diff -u - "${GITLOG}"
    159   }
    160 
    161   cleanup() {
    162     @rm -rf "${PREFIX}"
    163     @rm -f "${IDENTITIES_FILE}"
    164     @rm -rf "${SHELLSPEC_WORKDIR}/clone"
    165     @rm -rf "${SHELLSPEC_WORKDIR}/secure"
    166   }
    167 
    168   BeforeEach setup
    169   AfterEach cleanup
    170 
    171   basename() { @basename "$@"; }
    172   cat()      { @cat      "$@"; }
    173   cp()       { @cp       "$@"; }
    174   dd()       { @dd       "$@"; }
    175   diff()     { @diff     "$@"; }
    176   dirname()  { @dirname  "$@"; }
    177   git()      { @git      "$@"; }
    178   mkdir()    { @mkdir    "$@"; }
    179   mktemp()   { @mktemp   "$@"; }
    180   mv()       { @mv       "$@"; }
    181   rm()       { @rm       "$@"; }
    182   tr()       { @tr       "$@"; }
    183 
    184   platform_tmpdir() {
    185     SECURE_TMPDIR="${SHELLSPEC_WORKDIR}/secure"
    186     @mkdir -p "${SECURE_TMPDIR}"
    187   }
    188 
    189 # Describe 'cmd_copy' is not needed (covered by 'cmd_copy_move')
    190 
    191   Describe 'cmd_copy_move'
    192     It 'processes several files and directories into a directory'
    193       When call cmd_move extra stale subdir
    194       The status should be success
    195       The error should be blank
    196       The output should be blank
    197       expected_log() { %text
    198         #|Move stale.age to subdir/stale.age
    199         #|
    200         #| stale.age => subdir/stale.age | 0
    201         #| 1 file changed, 0 insertions(+), 0 deletions(-)
    202         #|Move extra/ to subdir/extra/
    203         #|
    204         #| {extra => subdir/extra}/subdir/file.age | 0
    205         #| 1 file changed, 0 insertions(+), 0 deletions(-)
    206         setup_log
    207       }
    208       The result of function check_git_log should be successful
    209     End
    210 
    211     It 'processes unencrypted files'
    212       When run cmd_move y.txt shared/yy
    213       The status should be success
    214       The error should be blank
    215       The output should be blank
    216       expected_log() { %text
    217         #|Move y.txt to shared/yy
    218         #|
    219         #| y.txt => shared/yy | 0
    220         #| 1 file changed, 0 insertions(+), 0 deletions(-)
    221         setup_log
    222       }
    223       The result of function check_git_log should be successful
    224     End
    225 
    226     It 'does not overwrite a file without confirmation'
    227       Data 'n'
    228       When call cmd_copy subdir/file stale
    229       The status should be success
    230       The error should be blank
    231       The output should equal 'stale.age already exists. Overwrite? [y/n]'
    232       The result of function check_git_log should be successful
    233     End
    234 
    235     It 'overwrites a file after confirmation'
    236       Data 'y'
    237       When call cmd_copy subdir/file stale
    238       The status should be success
    239       The error should be blank
    240       The output should equal 'stale.age already exists. Overwrite? [y/n]'
    241       expected_log() { %text
    242         #|Copy subdir/file.age to stale.age
    243         #|
    244         #| stale.age | 3 +--
    245         #| 1 file changed, 1 insertion(+), 2 deletions(-)
    246         setup_log
    247       }
    248       The result of function check_git_log should be successful
    249     End
    250 
    251     It 'does not re-encrypt by default when recipients do not change'
    252       When call cmd_move stale renamed
    253       The status should be success
    254       The error should be blank
    255       The output should be blank
    256       expected_log() { %text
    257         #|Move stale.age to renamed.age
    258         #|
    259         #| stale.age => renamed.age | 0
    260         #| 1 file changed, 0 insertions(+), 0 deletions(-)
    261         setup_log
    262       }
    263       The result of function check_git_log should be successful
    264     End
    265 
    266     It 're-encrypts by default when recipients change'
    267       When call cmd_move stale shared
    268       The status should be success
    269       The error should be blank
    270       The output should be blank
    271       expected_file() { %text
    272         #|ageRecipient:myself
    273         #|ageRecipient:friend
    274         #|age:0-password
    275       }
    276       The contents of file "${PREFIX}/shared/stale.age" should \
    277         equal "$(expected_file)"
    278       expected_log() { %text
    279         #|Move stale.age to shared/stale.age
    280         #|
    281         #| stale.age => shared/stale.age | 2 +-
    282         #| 1 file changed, 1 insertion(+), 1 deletion(-)
    283         setup_log
    284       }
    285       The result of function check_git_log should be successful
    286     End
    287 
    288     It 'always re-encrypts when forced'
    289       When call cmd_move --reencrypt stale renamed
    290       The status should be success
    291       The error should be blank
    292       The output should be blank
    293       expected_log() { %text
    294         #|Move stale.age to renamed.age
    295         #|
    296         #| stale.age => renamed.age | 1 -
    297         #| 1 file changed, 1 deletion(-)
    298         setup_log
    299       }
    300       The result of function check_git_log should be successful
    301     End
    302 
    303     It 'never re-encrypts when forced'
    304       When call cmd_move --keep stale shared
    305       The status should be success
    306       The error should be blank
    307       The output should be blank
    308       expected_log() { %text
    309         #|Move stale.age to shared/stale.age
    310         #|
    311         #| stale.age => shared/stale.age | 0
    312         #| 1 file changed, 0 insertions(+), 0 deletions(-)
    313         setup_log
    314       }
    315       The result of function check_git_log should be successful
    316     End
    317 
    318     It 'interactively re-encrypts when asked'
    319       Data
    320         #|n
    321         #|y
    322       End
    323       When call cmd_move --interactive stale extra/subdir/file shared
    324       The status should be success
    325       The error should be blank
    326       The output should equal 'Reencrypt stale into shared/stale? [y/n]Reencrypt extra/subdir/file into shared/file? [y/n]'
    327       expected_file() { %text
    328         #|ageRecipient:myself
    329         #|ageRecipient:friend
    330         #|age:Pa55worD
    331       }
    332       The contents of file "${PREFIX}/shared/file.age" should \
    333         equal "$(expected_file)"
    334       expected_log() { %text
    335         #|Move extra/subdir/file.age to shared/file.age
    336         #|
    337         #| {extra/subdir => shared}/file.age | 1 +
    338         #| 1 file changed, 1 insertion(+)
    339         #|Move stale.age to shared/stale.age
    340         #|
    341         #| stale.age => shared/stale.age | 0
    342         #| 1 file changed, 0 insertions(+), 0 deletions(-)
    343         setup_log
    344       }
    345       The result of function check_git_log should be successful
    346     End
    347 
    348     It 'aborts on decryption failure even without pipefail'
    349       if ! [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
    350         Set 'pipefail:off'
    351       fi
    352       AGE=false
    353       When run cmd_move --reencrypt stale renamed
    354       The status should equal 1
    355       The error should equal \
    356         "Fatal(1): false -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age"
    357       The output should be blank
    358       The result of function check_git_log should be successful
    359     End
    360 
    361     It 'displays usage when called with incompatible reencryption arguments'
    362       PROGRAM=prg
    363       COMMAND=copy
    364       When run cmd_copy_move -eik stale shared/
    365       The status should equal 1
    366       The output should be blank
    367       expected_err() { %text
    368         #|Usage: prg copy [--reencrypt,-e | --interactive,-i | --keep,-k ]
    369         #|                [--force,-f] old-path new-path
    370       }
    371       The error should equal "$(expected_err)"
    372       The result of function check_git_log should be successful
    373     End
    374 
    375     It 'displays copy usage with `c*` commands'
    376       PROGRAM=prg
    377       COMMAND=curious
    378       When run cmd_copy_move single
    379       The status should equal 1
    380       The output should be blank
    381       expected_err() { %text
    382         #|Usage: prg copy [--reencrypt,-e | --interactive,-i | --keep,-k ]
    383         #|                [--force,-f] old-path new-path
    384       }
    385       The error should equal "$(expected_err)"
    386       The result of function check_git_log should be successful
    387     End
    388 
    389     It 'displays move usage with `m*` commands'
    390       PROGRAM=prg
    391       COMMAND=memory
    392       When run cmd_copy_move single
    393       The status should equal 1
    394       The output should be blank
    395       expected_err() { %text
    396         #|Usage: prg move [--reencrypt,-e | --interactive,-i | --keep,-k ]
    397         #|                [--force,-f] old-path new-path
    398       }
    399       The error should equal "$(expected_err)"
    400       The result of function check_git_log should be successful
    401     End
    402 
    403     It 'displays both usages when in doubt'
    404       PROGRAM=prg
    405       COMMAND=bad
    406       When run cmd_copy_move single
    407       The status should equal 1
    408       The output should be blank
    409       expected_err() { %text
    410         #|Usage: prg copy [--reencrypt,-e | --interactive,-i | --keep,-k ]
    411         #|                [--force,-f] old-path new-path
    412         #|       prg move [--reencrypt,-e | --interactive,-i | --keep,-k ]
    413         #|                [--force,-f] old-path new-path
    414       }
    415       The error should equal "$(expected_err)"
    416       The result of function check_git_log should be successful
    417     End
    418   End
    419 
    420   Describe 'cmd_delete'
    421     It 'deletes multiple files at once, prompting before each one'
    422       Data
    423         #|y
    424         #|n
    425         #|y
    426       End
    427       When call cmd_delete stale subdir/file fluff/two
    428       The status should be success
    429       The output should equal 'Are you sure you would like to delete stale? [y/n]Are you sure you would like to delete subdir/file? [y/n]Are you sure you would like to delete fluff/two? [y/n]'
    430       The error should be blank
    431       The file "${PREFIX}/fluff/two.age" should not be exist
    432       The file "${PREFIX}/stale.age" should not be exist
    433       The file "${PREFIX}/subdir/file.age" should be exist
    434       expected_log() { %text
    435         #|Remove fluff/two from store.
    436         #|
    437         #| fluff/two.age | 4 ----
    438         #| 1 file changed, 4 deletions(-)
    439         #|Remove stale from store.
    440         #|
    441         #| stale.age | 3 ---
    442         #| 1 file changed, 3 deletions(-)
    443         setup_log
    444       }
    445       The result of function check_git_log should be successful
    446     End
    447   End
    448 
    449   Describe 'cmd_edit'
    450     It 'uses EDITOR in a dumb terminal'
    451       unset EDIT_CMD
    452       EDITOR=false
    453       TERM=dumb
    454       VISUAL=true
    455       When run cmd_edit stale
    456       The status should equal 1
    457       The output should be blank
    458       The error should equal 'Editor "false" exited with code 1'
    459       expected_file() { %text:expand
    460         #|ageRecipient:master
    461         #|ageRecipient:myself
    462         #|age:0-password
    463       }
    464       The contents of file "${PREFIX}/stale.age" should \
    465         equal "$(expected_file)"
    466       The result of function check_git_log should be successful
    467     End
    468 
    469     It 'uses EDITOR when VISUAL is not set'
    470       unset EDIT_CMD
    471       EDITOR=false
    472       TERM=not-dumb
    473       unset VISUAL
    474       When run cmd_edit stale
    475       The status should equal 1
    476       The output should be blank
    477       The error should equal 'Editor "false" exited with code 1'
    478       expected_file() { %text:expand
    479         #|ageRecipient:master
    480         #|ageRecipient:myself
    481         #|age:0-password
    482       }
    483       The contents of file "${PREFIX}/stale.age" should \
    484         equal "$(expected_file)"
    485       The result of function check_git_log should be successful
    486     End
    487 
    488     It 'uses VISUAL in a non-dumb terminal'
    489       unset EDIT_CMD
    490       EDITOR=true
    491       TERM=not-dumb
    492       VISUAL=false
    493       When run cmd_edit stale
    494       The status should equal 1
    495       The output should be blank
    496       The error should equal 'Editor "false" exited with code 1'
    497       expected_file() { %text:expand
    498         #|ageRecipient:master
    499         #|ageRecipient:myself
    500         #|age:0-password
    501       }
    502       The contents of file "${PREFIX}/stale.age" should \
    503         equal "$(expected_file)"
    504       The result of function check_git_log should be successful
    505     End
    506 
    507     It 'falls back on vi without EDITOR nor visual'
    508       unset EDIT_CMD
    509       unset EDITOR
    510       unset VISUAL
    511       When run cmd_edit subdir/new
    512       The status should equal 127
    513       The output should be blank
    514       The line 1 of error should include 'not found'
    515       The line 2 of error should equal 'Editor "vi" exited with code 127'
    516       The file "${PREFIX}/subdir/new.age" should not be exist
    517       The file "${PREFIX}/subdir/new.gpg" should not be exist
    518       The result of function check_git_log should be successful
    519     End
    520 
    521     It 'reports unchanged file'
    522       EDIT_CMD=true
    523       When call cmd_edit stale
    524       The status should be success
    525       The output should equal 'Password for stale unchanged.'
    526       The error should be blank
    527       expected_file() { %text:expand
    528         #|ageRecipient:master
    529         #|ageRecipient:myself
    530         #|age:0-password
    531       }
    532       The contents of file "${PREFIX}/stale.age" should \
    533         equal "$(expected_file)"
    534       The result of function check_git_log should be successful
    535     End
    536 
    537     It 'allows lack of file creation without error'
    538       EDIT_CMD=true
    539       When run cmd_edit subdir/new
    540       The status should be success
    541       The output should equal 'New password for subdir/new not saved.'
    542       The error should be blank
    543       The file "${PREFIX}/subdir/new.age" should not be exist
    544       The file "${PREFIX}/subdir/new.gpg" should not be exist
    545       The result of function check_git_log should be successful
    546     End
    547 
    548     It 'reports editor failure'
    549       ret42() { return 42; }
    550       EDIT_CMD=ret42
    551       When run cmd_edit subdir/new
    552       The status should equal 42
    553       The output should be blank
    554       The error should equal 'Editor "ret42" exited with code 42'
    555       The file "${PREFIX}/subdir/new.age" should not be exist
    556       The file "${PREFIX}/subdir/new.gpg" should not be exist
    557       The result of function check_git_log should be successful
    558     End
    559 
    560     It 'aborts on decryption failure even without pipefail'
    561       if ! [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
    562         Set 'pipefail:off'
    563       fi
    564       AGE=false
    565       tail() { @tail "$@"; }
    566       When run cmd_edit stale
    567       The status should equal 1
    568       The error should equal \
    569         "Fatal(1): false -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age"
    570       The output should be blank
    571       The result of function check_git_log should be successful
    572     End
    573   End
    574 
    575   Describe 'cmd_find'
    576     grep() { @grep "$@"; }
    577 
    578     It 'interprets the pattern as a regular expression'
    579       expected_output() { %text
    580         #|Search pattern: ^o
    581         #||- (B)fluff(N)
    582         #||  `- one
    583         #|`- (R)old(N)
    584       }
    585       When call cmd_find '^o'
    586       The status should be success
    587       The output should equal "$(expected_output)"
    588       The error should be blank
    589       The result of function check_git_log should be successful
    590     End
    591 
    592     It 'forwards flags to grep'
    593       expected_output() { %text
    594         #|Search pattern: -E -i F|I
    595         #||- (B)extra(N)
    596         #||  `- (B)subdir(N)
    597         #||     `- file
    598         #|`- (B)subdir(N)
    599         #|   `- file
    600       }
    601       When call cmd_find -E -i 'F|I'
    602       The status should be success
    603       The output should equal "$(expected_output)"
    604       The error should be blank
    605       The result of function check_git_log should be successful
    606     End
    607 
    608     It 'can output a raw list of secrets'
    609       expected_output() { %text
    610         #|extra/subdir/file
    611         #|subdir/file
    612       }
    613       When call cmd_find -r -E -i 'F|I'
    614       The status should be success
    615       The output should equal "$(expected_output)"
    616       The error should be blank
    617       The result of function check_git_log should be successful
    618     End
    619   End
    620 
    621   Describe 'cmd_generate'
    622     It 'uses the character set given explicitly instead of environment'
    623       CHARACTER_SET='[0-9]'
    624       CHARACTER_SET_NO_SYMBOLS='[0-9]'
    625       When call cmd_generate new 5 '[:upper:]'
    626       The status should be success
    627       The error should be blank
    628       The lines of output should equal 2
    629       The line 1 of output should \
    630         equal '(B)The generated password for (U)new(!U) is:(N)'
    631       The line 2 of output should match pattern '[A-Z][A-Z][A-Z][A-Z][A-Z]'
    632       expected_log() { %text
    633         #|Add generated password for new.
    634         #|
    635         #| new.age | 2 ++
    636         #| 1 file changed, 2 insertions(+)
    637         setup_log
    638       }
    639       The result of function check_git_log should be successful
    640     End
    641 
    642     It 'overwrites after asking for confirmation'
    643       Data 'y'
    644       When call cmd_generate subdir/file 10
    645       The status should be success
    646       The output should start with 'An entry already exists for subdir/file. Overwrite it? [y/n](B)The generated password for (U)subdir/file(!U) is:(N)'
    647       The error should be blank
    648       expected_log() { %text
    649         #|Add generated password for subdir/file.
    650         #|
    651         #| subdir/file.age | 2 +-
    652         #| 1 file changed, 1 insertion(+), 1 deletion(-)
    653         setup_log
    654       }
    655       The result of function check_git_log should be successful
    656     End
    657 
    658     It 'does nothing without confirmation'
    659       Data 'n'
    660       When call cmd_generate subdir/file 10
    661       The status should be success
    662       The output should equal \
    663         'An entry already exists for subdir/file. Overwrite it? [y/n]'
    664       The error should be blank
    665       The result of function check_git_log should be successful
    666     End
    667 
    668     It 'cannot overwrite a directory'
    669       run_test() {
    670         mkdir -p "${PREFIX}/new-secret.age" && \
    671         cmd_generate -f new-secret 10
    672       }
    673       When run run_test
    674       The status should equal 1
    675       The output should be blank
    676       The error should equal 'Cannot replace directory new-secret.age'
    677       The result of function check_git_log should be successful
    678     End
    679 
    680     It 'aborts on decryption failure even without pipefail'
    681       if ! [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
    682         Set 'pipefail:off'
    683       fi
    684       AGE=false
    685       tail() { @tail "$@"; }
    686       When run cmd_generate --in-place stale
    687       The status should equal 1
    688       The error should equal \
    689         "Fatal(1): false -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age"
    690       The output should equal 'Decrypting previous secret for stale'
    691       The result of function check_git_log should be successful
    692     End
    693 
    694     It 'saves after showing and getting confirmation'
    695       Data 'y'
    696       When call cmd_generate --try new
    697       The status should be success
    698       The error should be blank
    699       The file "${PREFIX}/new.age" should be exist
    700       expected_out() {
    701         %putsn '(B)The generated password for (U)new(!U) is:(N)'
    702         @sed '$s/^age://p;d' "${PREFIX}/new.age"
    703         %putsn 'Save generated password for new? [y/n]'
    704       }
    705       The output should equal "$(expected_out)"
    706       expected_log() { %text
    707         #|Add generated password for new.
    708         #|
    709         #| new.age | 2 ++
    710         #| 1 file changed, 2 insertions(+)
    711         setup_log
    712       }
    713       The result of function check_git_log should be successful
    714     End
    715 
    716     It 'does not save after showing and getting cancellation'
    717       Data 'n'
    718       When call cmd_generate --try new 5 '[:lower:]'
    719       The status should be success
    720       The error should be blank
    721       The lines of output should equal 3
    722       The line 1 of output should \
    723         equal '(B)The generated password for (U)new(!U) is:(N)'
    724       The line 2 of output should match pattern '[a-z][a-z][a-z][a-z][a-z]'
    725       The line 3 of output should \
    726         equal 'Save generated password for new? [y/n]'
    727       The result of function check_git_log should be successful
    728     End
    729 
    730     It 'accepts an extra line after the generated secret'
    731       Data 'extra comment line'
    732       When call cmd_generate --multiline new 15
    733       The status should be success
    734       The error should be blank
    735       The lines of output should equal 3
    736       The line 1 of output should \
    737         equal 'Enter extra secrets then Ctrl+D when finished:'
    738       The line 2 of output should \
    739         equal '(B)The generated password for (U)new(!U) is:(N)'
    740       The line 3 of output should match pattern '???????????????'
    741       The lines of contents of file "${PREFIX}/new.age" should equal 3
    742       The line 3 of contents of file "${PREFIX}/new.age" should \
    743         equal 'age:extra comment line'
    744       expected_log() { %text
    745         #|Add generated password for new.
    746         #|
    747         #| new.age | 3 +++
    748         #| 1 file changed, 3 insertions(+)
    749         setup_log
    750       }
    751       The result of function check_git_log should be successful
    752     End
    753 
    754     It 'accepts extra lines after the generated secret when overwriting'
    755       Data
    756         #|Extra: line
    757         #|Extra: end of input
    758       End
    759       When call cmd_generate --multiline --force fluff/three 5
    760       The status should be success
    761       The error should be blank
    762       The lines of output should equal 3
    763       The line 1 of output should \
    764         equal 'Enter extra secrets then Ctrl+D when finished:'
    765       The line 2 of output should \
    766         equal '(B)The generated password for (U)fluff/three(!U) is:(N)'
    767       The line 3 of output should match pattern '?????'
    768       The lines of contents of file "${PREFIX}/fluff/three.age" should equal 5
    769       The line 4 of contents of file "${PREFIX}/fluff/three.age" should \
    770         equal 'age:Extra: line'
    771       The line 5 of contents of file "${PREFIX}/fluff/three.age" should \
    772         equal 'age:Extra: end of input'
    773       expected_log() { %text
    774         #|Add generated password for fluff/three.
    775         #|
    776         #| fluff/three.age | 6 +++---
    777         #| 1 file changed, 3 insertions(+), 3 deletions(-)
    778         setup_log
    779       }
    780       The result of function check_git_log should be successful
    781     End
    782 
    783     It 'accepts extra lines after the generated secret after in-place data'
    784       Data
    785         #|Extra: line
    786         #|Extra: end of input
    787       End
    788       When call cmd_generate --multiline --in-place fluff/three 5
    789       The status should be success
    790       The error should be blank
    791       The lines of output should equal 4
    792       The line 1 of output should \
    793         equal 'Decrypting previous secret for fluff/three'
    794       The line 2 of output should \
    795         equal 'Enter extra secrets then Ctrl+D when finished:'
    796       The line 3 of output should \
    797         equal '(B)The generated password for (U)fluff/three(!U) is:(N)'
    798       The line 4 of output should match pattern '?????'
    799       The lines of contents of file "${PREFIX}/fluff/three.age" should equal 7
    800       The line 4 of contents of file "${PREFIX}/fluff/three.age" should \
    801         equal 'age:Username: 3Jane'
    802       The line 5 of contents of file "${PREFIX}/fluff/three.age" should \
    803         equal 'age:URL: https://example.com/login'
    804       The line 6 of contents of file "${PREFIX}/fluff/three.age" should \
    805         equal 'age:Extra: line'
    806       The line 7 of contents of file "${PREFIX}/fluff/three.age" should \
    807         equal 'age:Extra: end of input'
    808       expected_log() { %text
    809         #|Replace generated password for fluff/three.
    810         #|
    811         #| fluff/three.age | 4 +++-
    812         #| 1 file changed, 3 insertions(+), 1 deletion(-)
    813         setup_log
    814       }
    815       The result of function check_git_log should be successful
    816     End
    817   End
    818 
    819   Describe 'cmd_git'
    820     It 'initializes a clone like a new repository'
    821       SOURCE="${PREFIX}"
    822       PREFIX="${SHELLSPEC_WORKDIR}/clone"
    823       expected_err() { %text:expand
    824         #|Cloning into '${PREFIX}'...
    825         #|done.
    826       }
    827       When call cmd_git clone "${SOURCE}"
    828       The status should be success
    829       The output should be blank
    830       The error should equal "$(expected_err)"
    831       The file "${PREFIX}/.gitattributes" should be exist
    832       The contents of file "${PREFIX}/.gitattributes" should equal \
    833         '*.age diff=age'
    834       expected_log() { %text
    835         #|Configure git repository for age file diff.
    836         #|
    837         #| .gitattributes | 1 +
    838         #| 1 file changed, 1 insertion(+)
    839         setup_log_bin
    840       }
    841       The result of function check_git_log should be successful
    842       PREFIX="${SOURCE}"
    843     End
    844   End
    845 
    846   Describe 'cmd_grep'
    847     It 'aborts on decryption failure even without pipefail'
    848       if ! [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
    849         Set 'pipefail:off'
    850       fi
    851       grep() { @grep "$@"; }
    852       AGE=false
    853       When run cmd_grep foo
    854       The status should equal 1
    855       The error should equal \
    856         "Fatal(1): false -d -i ${IDENTITIES_FILE} -- file.age"
    857       The output should be blank
    858       The result of function check_git_log should be successful
    859     End
    860   End
    861 
    862   Describe 'cmd_gitconfig'
    863     grep() { @grep "$@"; }
    864 
    865     It 'creates a new .gitattributes and configures diff'
    866       When call cmd_gitconfig
    867       The status should be success
    868       The output should be blank
    869       The error should be blank
    870       The file "${PREFIX}/.gitattributes" should be exist
    871       The contents of file "${PREFIX}/.gitattributes" should equal \
    872         '*.age diff=age'
    873       expected_log() { %text
    874         #|Configure git repository for age file diff.
    875         #|
    876         #| .gitattributes | 1 +
    877         #| 1 file changed, 1 insertion(+)
    878         setup_log_bin
    879       }
    880       The result of function check_git_log should be successful
    881     End
    882 
    883     It 'expands an existing .gitattributes'
    884       run_test() {
    885         %putsn '# Existing but empty' >"${PREFIX}/.gitattributes"
    886         @git -C "${PREFIX}" add .gitattributes >/dev/null
    887         @git -C "${PREFIX}" commit -m 'Test case setup' >/dev/null
    888         cmd_gitconfig
    889       }
    890       When call run_test
    891       The status should be success
    892       The output should be blank
    893       The error should be blank
    894       expected_file() { %text
    895         #|# Existing but empty
    896         #|*.age diff=age
    897       }
    898       The file "${PREFIX}/.gitattributes" should be exist
    899       The contents of file "${PREFIX}/.gitattributes" should \
    900         equal "$(expected_file)"
    901       expected_log() { %text
    902         #|Configure git repository for age file diff.
    903         #|
    904         #| .gitattributes | 1 +
    905         #| 1 file changed, 1 insertion(+)
    906         #|Test case setup
    907         #|
    908         #| .gitattributes | 1 +
    909         #| 1 file changed, 1 insertion(+)
    910         setup_log_bin
    911       }
    912       The result of function check_git_log should be successful
    913     End
    914 
    915     It 'is idempotent'
    916       run_test() {
    917         cmd_gitconfig && cmd_gitconfig
    918       }
    919       When call run_test
    920       The status should be success
    921       The output should be blank
    922       The error should be blank
    923       The file "${PREFIX}/.gitattributes" should be exist
    924       The contents of file "${PREFIX}/.gitattributes" should equal \
    925         '*.age diff=age'
    926       expected_log() { %text
    927         #|Configure git repository for age file diff.
    928         #|
    929         #| .gitattributes | 1 +
    930         #| 1 file changed, 1 insertion(+)
    931         setup_log_bin
    932       }
    933       The result of function check_git_log should be successful
    934     End
    935   End
    936 
    937   Describe 'cmd_help'
    938     It 'displays a help text with pashage-specific supported commands'
    939       PROGRAM=prg
    940       When call cmd_help
    941       The status should be success
    942       The output should include ' prg copy '
    943       The output should include ' prg delete '
    944       The output should include ' prg gitconfig'
    945       The output should include ' prg move '
    946       The output should include ' prg random '
    947       The output should include ' prg reencrypt '
    948     End
    949   End
    950 
    951   Describe 'cmd_init'
    952     It 're-encrypts the whole store using a recipient ids named like a flag'
    953       When call cmd_init -- -p 'new-id'
    954       The status should be success
    955       The output should equal 'Password store recipients set at store root'
    956       The error should be blank
    957       expected_file() { %text
    958         #|-p
    959         #|new-id
    960       }
    961       The contents of file "${PREFIX}/.age-recipients" should \
    962         equal "$(expected_file)"
    963       expected_log() { %text
    964         #|Set age recipients at store root
    965         #|
    966         #| .age-recipients       | 2 ++
    967         #| extra/subdir/file.age | 3 ++-
    968         #| stale.age             | 4 ++--
    969         #| subdir/file.age       | 3 ++-
    970         #| 4 files changed, 8 insertions(+), 4 deletions(-)
    971         setup_log
    972       }
    973       The result of function check_git_log should be successful
    974     End
    975 
    976     It 'does not re-encrypt with `keep` flag'
    977       When call cmd_init -k 'new-id'
    978       The status should be success
    979       The output should equal 'Password store recipients set at store root'
    980       The error should be blank
    981       The contents of file "${PREFIX}/.age-recipients" should equal 'new-id'
    982       expected_log() { %text
    983         #|Set age recipients at store root
    984         #|
    985         #| .age-recipients | 1 +
    986         #| 1 file changed, 1 insertion(+)
    987         setup_log
    988       }
    989       The result of function check_git_log should be successful
    990     End
    991 
    992     It 'asks before re-encrypting each file with `interactive` flag'
    993       Data
    994         #|n
    995         #|y
    996         #|n
    997       End
    998       When call cmd_init -i 'new-id'
    999       The status should be success
   1000       The output should equal 'Re-encrypt extra/subdir/file? [y/n]Re-encrypt stale? [y/n]Re-encrypt subdir/file? [y/n]Password store recipients set at store root'
   1001       The error should be blank
   1002       The contents of file "${PREFIX}/.age-recipients" should equal 'new-id'
   1003       expected_log() { %text
   1004         #|Set age recipients at store root
   1005         #|
   1006         #| .age-recipients | 1 +
   1007         #| stale.age       | 3 +--
   1008         #| 2 files changed, 2 insertions(+), 2 deletions(-)
   1009         setup_log
   1010       }
   1011       The result of function check_git_log should be successful
   1012     End
   1013 
   1014     usage_text() { %text
   1015       #|Usage: prg init [--interactive,-i | --keep,-k ]
   1016       #|                [--path=subfolder,-p subfolder] age-recipient ...
   1017     }
   1018 
   1019     It 'displays usage when using incompatible options (`-i` then `-k`)'
   1020       PROGRAM=prg
   1021       When run cmd_init --interactive --keep 'new-id'
   1022       The status should equal 1
   1023       The output should be blank
   1024       The error should equal "$(usage_text)"
   1025       The result of function check_git_log should be successful
   1026     End
   1027 
   1028     It 'displays usage when using incompatible options (`-k` then `-i`)'
   1029       PROGRAM=prg
   1030       When run cmd_init -ki 'new-id'
   1031       The status should equal 1
   1032       The output should be blank
   1033       The error should equal "$(usage_text)"
   1034       The result of function check_git_log should be successful
   1035     End
   1036   End
   1037 
   1038   Describe 'cmd_insert'
   1039     It 'inserts an entry encrypted using an explicit recipient file'
   1040       PASHAGE_RECIPIENTS_FILE="${PREFIX}/fluff/.age-recipients"
   1041       PASSAGE_RECIPIENTS_FILE="${PREFIX}/shared/.age-recipients"
   1042       PASHAGE_RECIPIENTS='shadowed'
   1043       PASSAGE_RECIPIENTS='shadowed'
   1044       Data 'pass'
   1045       When call cmd_insert -e shared/new-file
   1046       The status should be success
   1047       The output should include 'shared/new-file'
   1048       expected_file() { %text:expand
   1049         #|ageRecipient:master
   1050         #|ageRecipient:myself
   1051         #|age:pass
   1052       }
   1053       The contents of file "${PREFIX}/shared/new-file.age" should \
   1054         equal "$(expected_file)"
   1055       expected_log() { %text
   1056         #|Add given password for shared/new-file to store.
   1057         #|
   1058         #| shared/new-file.age | 3 +++
   1059         #| 1 file changed, 3 insertions(+)
   1060         setup_log
   1061       }
   1062       The result of function check_git_log should be successful
   1063     End
   1064 
   1065     It 'inserts an entry encrypted using explicit recipients'
   1066       PASHAGE_RECIPIENTS='force-1 force-2'
   1067       PASSAGE_RECIPIENTS='shadowed'
   1068       Data 'pass'
   1069       When call cmd_insert -e shared/new-file
   1070       The status should be success
   1071       The output should include 'shared/new-file'
   1072       expected_file() { %text:expand
   1073         #|ageRecipient:force-1
   1074         #|ageRecipient:force-2
   1075         #|age:pass
   1076       }
   1077       The contents of file "${PREFIX}/shared/new-file.age" should \
   1078         equal "$(expected_file)"
   1079       expected_log() { %text
   1080         #|Add given password for shared/new-file to store.
   1081         #|
   1082         #| shared/new-file.age | 3 +++
   1083         #| 1 file changed, 3 insertions(+)
   1084         setup_log
   1085       }
   1086       The result of function check_git_log should be successful
   1087     End
   1088 
   1089     It 'inserts several new single-line entries'
   1090       stty() { false; }
   1091       Data
   1092         #|password-1
   1093         #|n
   1094         #|password-2
   1095         #|password-3
   1096       End
   1097       When call cmd_insert -e newdir/pass-1 subdir/file newdir/pass-2
   1098       The status should be success
   1099       The error should be blank
   1100       The output should equal 'Enter password for newdir/pass-1: An entry already exists for subdir/file. Overwrite it? [y/n]Enter password for newdir/pass-2: '
   1101       The contents of file "${PREFIX}/newdir/pass-1.age" \
   1102         should include "age:password-1"
   1103       The contents of file "${PREFIX}/newdir/pass-2.age" \
   1104         should include "age:password-2"
   1105       expected_log() { %text
   1106         #|Add given password for newdir/pass-2 to store.
   1107         #|
   1108         #| newdir/pass-2.age | 2 ++
   1109         #| 1 file changed, 2 insertions(+)
   1110         #|Add given password for newdir/pass-1 to store.
   1111         #|
   1112         #| newdir/pass-1.age | 2 ++
   1113         #| 1 file changed, 2 insertions(+)
   1114         setup_log
   1115       }
   1116       The result of function check_git_log should be successful
   1117     End
   1118 
   1119     It 'inserts several new multi-line entries'
   1120       stty() { false; }
   1121       Data
   1122         #|password-1
   1123         #| extra spaced line
   1124         #|
   1125         #|y
   1126         #|password-2
   1127         #|	extra tabbed line
   1128         #|
   1129         #|password-3
   1130       End
   1131       When call cmd_insert -m newdir/pass-1 subdir/file newdir/pass-2
   1132       The status should be success
   1133       The error should be blank
   1134       expected_out() { %text
   1135         #|Enter contents of newdir/pass-1 and
   1136         #|press Ctrl+D or enter an empty line when finished:
   1137         #|An entry already exists for subdir/file. Overwrite it? [y/n]Enter contents of subdir/file and
   1138         #|press Ctrl+D or enter an empty line when finished:
   1139         #|Enter contents of newdir/pass-2 and
   1140         #|press Ctrl+D or enter an empty line when finished:
   1141       }
   1142       The output should equal "$(expected_out)"
   1143       expected_file_1() { %text
   1144         #|ageRecipient:myself
   1145         #|age:password-1
   1146         #|age: extra spaced line
   1147       }
   1148       expected_file_2() { %text
   1149         #|ageRecipient:myself
   1150         #|age:password-2
   1151         #|age:	extra tabbed line
   1152       }
   1153       expected_file_3() { %text
   1154         #|ageRecipient:myself
   1155         #|age:password-3
   1156       }
   1157       The contents of file "${PREFIX}/newdir/pass-1.age" \
   1158         should equal "$(expected_file_1)"
   1159       The contents of file "${PREFIX}/subdir/file.age" \
   1160         should equal "$(expected_file_2)"
   1161       The contents of file "${PREFIX}/newdir/pass-2.age" \
   1162         should equal "$(expected_file_3)"
   1163       expected_log() { %text
   1164         #|Add given password for newdir/pass-2 to store.
   1165         #|
   1166         #| newdir/pass-2.age | 2 ++
   1167         #| 1 file changed, 2 insertions(+)
   1168         #|Add given password for subdir/file to store.
   1169         #|
   1170         #| subdir/file.age | 3 ++-
   1171         #| 1 file changed, 2 insertions(+), 1 deletion(-)
   1172         #|Add given password for newdir/pass-1 to store.
   1173         #|
   1174         #| newdir/pass-1.age | 3 +++
   1175         #| 1 file changed, 3 insertions(+)
   1176         setup_log
   1177       }
   1178       The result of function check_git_log should be successful
   1179     End
   1180 
   1181     It 'inserts a new single-line entry on the second try'
   1182       stty() { :; }
   1183       Data
   1184         #|first try
   1185         #|First Try
   1186         #|pass-word
   1187         #|pass-word
   1188       End
   1189       When call cmd_insert newdir/newpass
   1190       The status should be success
   1191       The error should be blank
   1192       expected_out() { %text | @sed 's/\$$//'
   1193         #|Enter password for newdir/newpass:  $
   1194         #|Retype password for newdir/newpass: $
   1195         #|Passwords don't match$
   1196         #|Enter password for newdir/newpass:  $
   1197         #|Retype password for newdir/newpass: $
   1198       }
   1199       The output should equal "$(expected_out)"
   1200       The contents of file "${PREFIX}/newdir/newpass.age" \
   1201         should include "age:pass-word"
   1202       expected_log() { %text
   1203         #|Add given password for newdir/newpass to store.
   1204         #|
   1205         #| newdir/newpass.age | 2 ++
   1206         #| 1 file changed, 2 insertions(+)
   1207         setup_log
   1208       }
   1209       The result of function check_git_log should be successful
   1210     End
   1211 
   1212     It 'overwrites an entry after confirmation'
   1213       Data
   1214         #|y
   1215         #|pass-word
   1216       End
   1217       When call cmd_insert -e subdir/file
   1218       The status should be success
   1219       The error should be blank
   1220       The output should equal 'An entry already exists for subdir/file. Overwrite it? [y/n]Enter password for subdir/file: '
   1221       expected_file() { %text
   1222         #|ageRecipient:myself
   1223         #|age:pass-word
   1224       }
   1225       The contents of file "${PREFIX}/subdir/file.age" \
   1226         should equal "$(expected_file)"
   1227       expected_log() { %text
   1228         #|Add given password for subdir/file to store.
   1229         #|
   1230         #| subdir/file.age | 2 +-
   1231         #| 1 file changed, 1 insertion(+), 1 deletion(-)
   1232         setup_log
   1233       }
   1234       The result of function check_git_log should be successful
   1235     End
   1236 
   1237     It 'does not overwrite an entry without confirmation'
   1238       Data
   1239         #|n
   1240         #|pass-word
   1241       End
   1242       When call cmd_insert -e subdir/file
   1243       The status should be success
   1244       The error should be blank
   1245       The output should equal \
   1246         'An entry already exists for subdir/file. Overwrite it? [y/n]'
   1247       The result of function check_git_log should be successful
   1248     End
   1249   End
   1250 
   1251   Describe 'cmd_list_or_show'
   1252     It 'displays the whole store as a raw list'
   1253       When call cmd_list_or_show --raw
   1254       The status should be success
   1255       The error should be blank
   1256       expected_out() { %text
   1257         #|extra/subdir/file
   1258         #|fluff/one
   1259         #|fluff/three
   1260         #|fluff/two
   1261         #|old
   1262         #|stale
   1263         #|subdir/file
   1264       }
   1265       The output should equal "$(expected_out)"
   1266     End
   1267 
   1268     It 'displays a subdirectory as a raw list'
   1269       When call cmd_list_or_show -r fluff
   1270       The status should be success
   1271       The error should be blank
   1272       expected_out() { %text
   1273         #|fluff/one
   1274         #|fluff/three
   1275         #|fluff/two
   1276       }
   1277       The output should equal "$(expected_out)"
   1278     End
   1279 
   1280     It 'decrypts a GPG secret in the store using GPG'
   1281       GPG=mock-gpg
   1282       gpg() { false; }
   1283       gpg2() { false; }
   1284       When call cmd_list_or_show old
   1285       The status should be success
   1286       The error should be blank
   1287       expected_out() { %text
   1288         #|very-old-password
   1289         #|Username: previous-life
   1290       }
   1291       The output should equal "$(expected_out)"
   1292     End
   1293 
   1294     It 'decrypts a GPG secret in the store using gpg2'
   1295       unset GPG
   1296       gpg() { false; }
   1297       gpg2() {
   1298         [ $# -eq 9 ] && [ "$6" = '--batch' ] && [ "$7" = '--use-agent' ] \
   1299          && mock-gpg "$1" "$2" "$3" "$4" "$5" "$8" "$9"
   1300       }
   1301       When call cmd_list_or_show old
   1302       The status should be success
   1303       The error should be blank
   1304       expected_out() { %text
   1305         #|very-old-password
   1306         #|Username: previous-life
   1307       }
   1308       The output should equal "$(expected_out)"
   1309     End
   1310 
   1311     It 'decrypts a GPG secret in the store using gpg'
   1312       unset GPG
   1313       gpg() { mock-gpg "$@"; }
   1314       When call cmd_list_or_show old
   1315       The status should be success
   1316       The error should be blank
   1317       expected_out() { %text
   1318         #|very-old-password
   1319         #|Username: previous-life
   1320       }
   1321       The output should equal "$(expected_out)"
   1322     End
   1323 
   1324     It 'fails to decrypt a GPG secret without gpg'
   1325       unset GPG
   1326       When run cmd_list_or_show old
   1327       The status should equal 1
   1328       The error should equal 'GPG does not seem available'
   1329       The output should be blank
   1330     End
   1331 
   1332     It 'displays both list and show usage on parse error with ambiguity'
   1333       PROGRAM=prg
   1334       COMMAND=both
   1335       When run cmd_list_or_show -x
   1336       The status should equal 1
   1337       The output should be blank
   1338       expected_err() { %text
   1339         #|Usage: prg [list] [--raw,-r] [subfolder]
   1340         #|       prg [show] [--clip[=line-number],-c[line-number] |
   1341         #|                   --qrcode[=line-number],-q[line-number]] pass-name
   1342       }
   1343       The error should equal "$(expected_err)"
   1344     End
   1345 
   1346     It 'displays list usage on parse error with list command'
   1347       PROGRAM=prg
   1348       COMMAND=list
   1349       When run cmd_list_or_show -x
   1350       The status should equal 1
   1351       The output should be blank
   1352       expected_err() { %text
   1353         #|Usage: prg [list] [--raw,-r] [subfolder]
   1354       }
   1355       The error should equal "$(expected_err)"
   1356     End
   1357 
   1358     It 'displays show usage on parse error with show command'
   1359       PROGRAM=prg
   1360       COMMAND=show
   1361       When run cmd_list_or_show -x
   1362       The status should equal 1
   1363       The output should be blank
   1364       expected_err() { %text
   1365         #|Usage: prg [show] [--clip[=line-number],-c[line-number] |
   1366         #|                   --qrcode[=line-number],-q[line-number]] pass-name
   1367       }
   1368       The error should equal "$(expected_err)"
   1369     End
   1370 
   1371     It 'aborts on age decryption failure even without pipefail'
   1372       if ! [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
   1373         Set 'pipefail:off'
   1374       fi
   1375       AGE=false
   1376       When run cmd_list_or_show stale
   1377       The status should equal 1
   1378       The error should equal \
   1379         "Fatal(1): false -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age"
   1380       The output should be blank
   1381       The result of function check_git_log should be successful
   1382     End
   1383 
   1384     It 'aborts on gpg decryption failure even without pipefail'
   1385       if ! [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
   1386         Set 'pipefail:off'
   1387       fi
   1388       GPG=false
   1389       When run cmd_list_or_show old
   1390       The status should equal 1
   1391       The error should equal "Fatal(1): false -d --quiet --yes --compress-algo=none --no-encrypt-to -- ${PREFIX}/old.gpg"
   1392       The output should be blank
   1393       The result of function check_git_log should be successful
   1394     End
   1395   End
   1396 
   1397 # Describe 'cmd_move' is not needed (covered by 'cmd_copy_move')
   1398 
   1399   Describe 'cmd_random'
   1400     It 'generates random characters'
   1401       When call cmd_random 2 '[:digit:]'
   1402       The status should be success
   1403       The error should be blank
   1404       The output should match pattern '[0-9][0-9]'
   1405     End
   1406 
   1407     It 'defaults to using CHARACTER_SET'
   1408       PREV_CHARACTER_SET="${CHARACTER_SET}"
   1409       CHARACTER_SET='[:lower:]'
   1410       When call cmd_random 2
   1411       The status should be success
   1412       The error should be blank
   1413       The output should match pattern '[a-z][a-z]'
   1414       CHARACTER_SET="${PREV_CHARACTER_SET}"
   1415     End
   1416 
   1417     It 'defaults to using both GENERATED_LENGTH and CHARACTER_SET'
   1418       PREV_CHARACTER_SET="${CHARACTER_SET}"
   1419       PREV_GENERATED_LENGTH="${GENERATED_LENGTH}"
   1420       CHARACTER_SET='[:upper:]'
   1421       GENERATED_LENGTH=5
   1422       When call cmd_random
   1423       The status should be success
   1424       The error should be blank
   1425       The output should match pattern '[A-Z][A-Z][A-Z][A-Z][A-Z]'
   1426       CHARACTER_SET="${PREV_CHARACTER_SET}"
   1427       GENERATED_LENGTH="${PREV_GENERATED_LENGTH}"
   1428     End
   1429 
   1430     It 'displays usage when called with too many arguments'
   1431       PROGRAM=prg
   1432       When run cmd_random 2 '[:digit:]' extra
   1433       The status should equal 1
   1434       The output should be blank
   1435       The error should equal 'Usage: prg random [pass-length [character-set]]'
   1436     End
   1437   End
   1438 
   1439   Describe 'cmd_reencrypt'
   1440     usage_text() { %text
   1441       #|Usage: prg reencrypt [--interactive,-i] pass-name|subfolder ...
   1442     }
   1443 
   1444     It 'reencrypts a single file'
   1445       When call cmd_reencrypt stale
   1446       The status should be success
   1447       The error should be blank
   1448       The output should be blank
   1449       expected_file() { %text
   1450         #|ageRecipient:myself
   1451         #|age:0-password
   1452       }
   1453       The contents of file "${PREFIX}/stale.age" \
   1454         should equal "$(expected_file)"
   1455       expected_log() { %text
   1456         #|Re-encrypt stale
   1457         #|
   1458         #| stale.age | 1 -
   1459         #| 1 file changed, 1 deletion(-)
   1460         setup_log
   1461       }
   1462       The result of function check_git_log should be successful
   1463     End
   1464 
   1465     It 'reencrypts a single file interactively'
   1466       Data 'y'
   1467       When call cmd_reencrypt -i stale
   1468       The status should be success
   1469       The error should be blank
   1470       The output should equal 'Re-encrypt stale? [y/n]'
   1471       expected_file() { %text
   1472         #|ageRecipient:myself
   1473         #|age:0-password
   1474       }
   1475       The contents of file "${PREFIX}/stale.age" \
   1476         should equal "$(expected_file)"
   1477       expected_log() { %text
   1478         #|Re-encrypt stale
   1479         #|
   1480         #| stale.age | 1 -
   1481         #| 1 file changed, 1 deletion(-)
   1482         setup_log
   1483       }
   1484       The result of function check_git_log should be successful
   1485     End
   1486 
   1487     It 'does not reencrypt a single file when interactively refused'
   1488       Data 'n'
   1489       When call cmd_reencrypt --interactive stale
   1490       The status should be success
   1491       The error should be blank
   1492       The output should equal 'Re-encrypt stale? [y/n]'
   1493       expected_file() { %text
   1494         #|ageRecipient:master
   1495         #|ageRecipient:myself
   1496         #|age:0-password
   1497       }
   1498       The contents of file "${PREFIX}/stale.age" \
   1499         should equal "$(expected_file)"
   1500       The result of function check_git_log should be successful
   1501     End
   1502 
   1503     It 'reencrypts a directory recursively'
   1504       When call cmd_reencrypt /
   1505       The status should be success
   1506       The error should be blank
   1507       The output should be blank
   1508       expected_file() { %text
   1509         #|ageRecipient:myself
   1510         #|age:0-password
   1511       }
   1512       The contents of file "${PREFIX}/stale.age" \
   1513         should equal "$(expected_file)"
   1514       expected_log() { %text
   1515         #|Re-encrypt /
   1516         #|
   1517         #| stale.age | 1 -
   1518         #| 1 file changed, 1 deletion(-)
   1519         setup_log
   1520       }
   1521       The result of function check_git_log should be successful
   1522     End
   1523 
   1524     It 'reencrypts a directory recursively and interactively'
   1525       Data
   1526         #|n
   1527         #|y
   1528         #|n
   1529       End
   1530       When call cmd_reencrypt -i ''
   1531       The status should be success
   1532       The error should be blank
   1533       The output should equal 'Re-encrypt extra/subdir/file? [y/n]Re-encrypt stale? [y/n]Re-encrypt subdir/file? [y/n]'
   1534       expected_file() { %text
   1535         #|ageRecipient:myself
   1536         #|age:0-password
   1537       }
   1538       The contents of file "${PREFIX}/stale.age" \
   1539         should equal "$(expected_file)"
   1540       expected_log() { %text
   1541         #|Re-encrypt /
   1542         #|
   1543         #| stale.age | 1 -
   1544         #| 1 file changed, 1 deletion(-)
   1545         setup_log
   1546       }
   1547       The result of function check_git_log should be successful
   1548     End
   1549 
   1550     It 'fails to reencrypt a file named like a flag without escape'
   1551       PROGRAM=prg
   1552       When run cmd_reencrypt -g
   1553       The status should equal 1
   1554       The error should equal "$(usage_text)"
   1555       The output should be blank
   1556       The result of function check_git_log should be successful
   1557     End
   1558 
   1559     It 'fails to reencrypt a non-existent direcotry'
   1560       When run cmd_reencrypt -- -y/
   1561       The status should equal 1
   1562       The error should equal 'Error: -y/ is not in the password store.'
   1563       The output should be blank
   1564       The result of function check_git_log should be successful
   1565     End
   1566 
   1567     It 'fails to reencrypt a non-existent file'
   1568       When run cmd_reencrypt -- -y
   1569       The status should equal 1
   1570       The error should equal 'Error: -y is not in the password store.'
   1571       The output should be blank
   1572       The result of function check_git_log should be successful
   1573     End
   1574 
   1575     It 'rejects a path containing ..'
   1576       When run cmd_reencrypt fluff/../stale
   1577       The status should equal 1
   1578       The output should be blank
   1579       The error should include 'sneaky'
   1580       The result of function check_git_log should be successful
   1581     End
   1582 
   1583     It 'aborts on age decryption failure even without pipefail'
   1584       if ! [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
   1585         Set 'pipefail:off'
   1586       fi
   1587       AGE=false
   1588       When run cmd_reencrypt stale
   1589       The status should equal 1
   1590       The error should equal \
   1591         "Fatal(1): false -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age"
   1592       The output should be blank
   1593       The result of function check_git_log should be successful
   1594     End
   1595   End
   1596 
   1597   Describe 'cmd_usage'
   1598     It 'defaults to four-space indentation'
   1599       PROGRAM=prg
   1600       When call cmd_usage
   1601       The status should be success
   1602       The error should be blank
   1603       The output should equal "$(cmd_usage '    ')"
   1604     End
   1605 
   1606     It 'fails with an unknown command'
   1607       PROGRAM=prg
   1608       When run cmd_usage 'Usage: ' bad version
   1609       The status should equal 1
   1610       The output should be blank
   1611       The error should equal 'cmd_usage: unknown command "bad"'
   1612     End
   1613   End
   1614 
   1615 # Describe 'cmd_version' is not needed (fully covered in pass_spec.sh)
   1616 
   1617   Describe 'refuse to operate on dirty checkout:'
   1618     make_dirty() {
   1619       %putsn 'untracked data' >"${PREFIX}/untracked.txt"
   1620     }
   1621     BeforeEach make_dirty
   1622 
   1623     git_log() {
   1624       @rm -f "${PREFIX}/untracked.txt"
   1625       @git -C "${PREFIX}" status --porcelain >&2
   1626       @git -C "${PREFIX}" log --format='%s' --stat >|"${GITLOG}"
   1627     }
   1628 
   1629     # 'copy' relies on 'copy/move'
   1630 
   1631     Example 'copy/move'
   1632       When run cmd_copy_move stale subdir/
   1633       The status should equal 1
   1634       The error should equal 'There are already pending changes.'
   1635       The output should be blank
   1636       The result of function check_git_log should be successful
   1637     End
   1638 
   1639     Example 'delete'
   1640       When run cmd_delete -f stale
   1641       The status should equal 1
   1642       The error should equal 'There are already pending changes.'
   1643       The output should equal 'Removing stale'
   1644       The result of function check_git_log should be successful
   1645     End
   1646 
   1647     Example 'edit'
   1648       VISUAL='false'
   1649       When run cmd_edit subdir/file
   1650       The status should equal 1
   1651       The error should equal 'There are already pending changes.'
   1652       The output should be blank
   1653       The result of function check_git_log should be successful
   1654     End
   1655 
   1656     # 'find' does not change the repository
   1657 
   1658     Example 'generate'
   1659       When run cmd_generate new-pass
   1660       The status should equal 1
   1661       The error should equal 'There are already pending changes.'
   1662       The output should be blank
   1663       The result of function check_git_log should be successful
   1664     End
   1665 
   1666     # 'git' does not change directly the repository
   1667 
   1668     Example 'gitconfig'
   1669       When run cmd_gitconfig
   1670       The status should equal 1
   1671       The error should equal 'There are already pending changes.'
   1672       The output should be blank
   1673       The result of function check_git_log should be successful
   1674     End
   1675 
   1676     # 'grep' does not change the repository
   1677     # 'help' does not change the repository
   1678 
   1679     Example 'init'
   1680       When run cmd_init -p subdir/ new-id
   1681       The status should equal 1
   1682       The error should equal 'There are already pending changes.'
   1683       The output should be blank
   1684       The result of function check_git_log should be successful
   1685     End
   1686 
   1687     Example 'init (deinit)'
   1688       When run cmd_init -p fluff/ ''
   1689       The status should equal 1
   1690       The error should equal 'There are already pending changes.'
   1691       The output should be blank
   1692       The result of function check_git_log should be successful
   1693     End
   1694 
   1695     Example 'insert'
   1696       When run cmd_insert -e fluff/four
   1697       The status should equal 1
   1698       The error should equal 'There are already pending changes.'
   1699       The output should be blank
   1700       The result of function check_git_log should be successful
   1701     End
   1702 
   1703     # 'list_or_show' does not change the repository
   1704     # 'move' relies on 'copy/move'
   1705     # 'random' does not change the repository
   1706 
   1707     Example 'reencrypt'
   1708       When run cmd_reencrypt stale
   1709       The status should equal 1
   1710       The error should equal 'There are already pending changes.'
   1711       The output should be blank
   1712       The result of function check_git_log should be successful
   1713     End
   1714 
   1715     # 'usage' does not change the repository
   1716     # 'version' does not change the repository
   1717   End
   1718 
   1719   Describe 'unreachable defensive code'
   1720     # This sections breaks the end-to-end scheme of this file
   1721     # to reach full coverage, by precisely identifying unreachable lines
   1722     # written for defensive programming against internal inconsistencies.
   1723 
   1724     It 'includes invalid values of DECISION in do_copy_move_file'
   1725       DECISION='invalid'
   1726       When run do_copy_move_file subdir/file.age extra/file.age
   1727       The status should equal 1
   1728       The output should be blank
   1729       The error should equal 'Unexpected DECISION value "invalid"'
   1730     End
   1731 
   1732     It 'includes overwriting a file using do_encrypt'
   1733       OVERWRITE=no
   1734       When run do_encrypt 'y.txt'
   1735       The status should equal 1
   1736       The output should be blank
   1737       The error should equal 'Refusing to overwite y.txt'
   1738     End
   1739 
   1740     It 'includes invalid values of SHOW in do_show'
   1741       SHOW='invalid'
   1742       When run do_show
   1743       The status should equal 1
   1744       The output should be blank
   1745       expected_err() { %text
   1746         #|Usage: prg [show] [--clip[=line-number],-c[line-number] |
   1747         #|                   --qrcode[=line-number],-q[line-number]] pass-name
   1748       }
   1749       The error should equal 'Unexpected SHOW value "invalid"'
   1750     End
   1751 
   1752     It 'includes interactive yesno'
   1753       # Technically not unreachable, but not worse than faking a terminal
   1754       # for each call of `yesno` when the whole test suite is outside
   1755       # of terminal anyway
   1756 
   1757       stty() { true; }
   1758       Data
   1759         #|x
   1760         #|Y
   1761       End
   1762       When call yesno 'Prompt?'
   1763       The status should be success
   1764       The error should be blank
   1765       The output should equal 'Prompt? [y/n]'
   1766       The variable ANSWER should equal 'y'
   1767     End
   1768   End
   1769 End