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 (60560B)


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