pashage

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

action_spec.sh (67911B)


      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 fully covers all action functions in isolation,
     19 # with all interactions fully mocked.
     20 
     21 Describe 'Action Functions'
     22   Include src/pashage.sh
     23   if [ "${SHELLSPEC_SHELL_TYPE}" = sh ]; then
     24     Set 'errexit:on' 'nounset:on'
     25   else
     26     Set 'errexit:on' 'nounset:on' 'pipefail:on'
     27   fi
     28 
     29   Describe 'do_copy_move'
     30     DECISION=default
     31     OVERWRITE=yes
     32     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
     33 
     34     ACTION=Move
     35     SCM_ACTION=scm_mv
     36 
     37     do_decrypt() {
     38       mocklog do_decrypt "$@"
     39       %putsn data
     40     }
     41 
     42     do_encrypt() {
     43       @cat >/dev/null
     44       mocklog do_encrypt "$@"
     45     }
     46 
     47     basename() { @basename "$@"; }
     48     cat() { @cat "$@"; }
     49     dirname() { @dirname "$@"; }
     50 
     51     mkdir() { mocklog mkdir "$@"; }
     52     scm_add() { mocklog scm_add "$@"; }
     53     scm_begin() { mocklog scm_begin "$@"; }
     54     scm_commit() { mocklog scm_commit "$@"; }
     55     scm_cp() { mocklog scm_cp "$@"; }
     56     scm_mv() { mocklog scm_mv "$@"; }
     57     scm_rm() { mocklog scm_rm "$@"; }
     58 
     59     setup() {
     60       @mkdir -p "${PREFIX}/sub/bare/sub" "${PREFIX}/subdir/notes.txt"
     61       %putsn 'identity 1' >"${PREFIX}/.age-recipients"
     62       %putsn 'identity 2' >"${PREFIX}/sub/.age-recipients"
     63       %putsn 'identity 2' >"${PREFIX}/subdir/.age-recipients"
     64       %putsn data >"${PREFIX}/sub/secret.age"
     65       %putsn data >"${PREFIX}/sub/bare/deep.age"
     66       %putsn data >"${PREFIX}/sub/bare/sub/deepest.age"
     67       %putsn data >"${PREFIX}/subdir/lower.age"
     68       %putsn data >"${PREFIX}/root.age"
     69       %putsn data >"${PREFIX}/notes.txt"
     70     }
     71 
     72     cleanup() {
     73       @rm -rf "${PREFIX}"
     74     }
     75 
     76     BeforeEach setup
     77     AfterEach cleanup
     78 
     79     It 'renames a file without re-encrypting'
     80       result() {
     81         %text:expand
     82         #|$ mkdir -p -- ${PREFIX}/sub
     83         #|$ scm_begin
     84         #|$ scm_mv sub/secret.age sub/renamed.age
     85         #|$ scm_commit Move sub/secret.age to sub/renamed.age
     86       }
     87       When call do_copy_move sub/secret sub/renamed
     88       The status should be success
     89       The output should be blank
     90       The error should equal "$(result)"
     91     End
     92 
     93     It 're-encrypts when copying to another identity'
     94       ACTION=Copy
     95       SCM_ACTION=scm_cp
     96       result() {
     97         %text:expand
     98         #|$ scm_begin
     99         #|$ do_decrypt ${PREFIX}/root.age
    100         #|$ do_encrypt sub/root.age
    101         #|$ scm_add sub/root.age
    102         #|$ scm_commit Copy root.age to sub/root.age
    103       }
    104       When call do_copy_move root sub/
    105       The status should be success
    106       The output should be blank
    107       The error should equal "$(result)"
    108     End
    109 
    110     It 'accepts explicit .age extensions'
    111       ACTION=Copy
    112       SCM_ACTION=scm_cp
    113       result() {
    114         %text:expand
    115         #|$ mkdir -p -- ${PREFIX}/sub
    116         #|$ scm_begin
    117         #|$ do_decrypt ${PREFIX}/root.age
    118         #|$ do_encrypt sub/moved.age
    119         #|$ scm_add sub/moved.age
    120         #|$ scm_commit Copy root.age to sub/moved.age
    121       }
    122       When call do_copy_move root.age sub/moved.age
    123       The status should be success
    124       The output should be blank
    125       The error should equal "$(result)"
    126     End
    127 
    128     It 'can be prevented from re-encrypting when copying to another identity'
    129       DECISION=keep
    130       ACTION=Copy
    131       SCM_ACTION=scm_cp
    132       result() { %text
    133         #|$ scm_begin
    134         #|$ scm_cp root.age sub/root.age
    135         #|$ scm_commit Copy root.age to sub/root.age
    136       }
    137       When call do_copy_move root sub/
    138       The status should be success
    139       The output should be blank
    140       The error should equal "$(result)"
    141     End
    142 
    143     It 'does not re-encrypt a non-encrypted file'
    144       result() { %text
    145         #|$ scm_begin
    146         #|$ scm_mv notes.txt sub/notes.txt
    147         #|$ scm_commit Move notes.txt to sub/notes.txt
    148       }
    149       When call do_copy_move notes.txt sub/
    150       The status should be success
    151       The output should be blank
    152       The error should equal "$(result)"
    153     End
    154 
    155     It 'does not re-encrypt a non-encrypted file even when forced'
    156       DECISION=force
    157       result() { %text
    158         #|$ scm_begin
    159         #|$ scm_mv notes.txt sub/notes.txt
    160         #|$ scm_commit Move notes.txt to sub/notes.txt
    161       }
    162       When call do_copy_move notes.txt sub/
    163       The status should be success
    164       The output should be blank
    165       The error should equal "$(result)"
    166     End
    167 
    168     It 'moves a file without re-encrypting to another directory'
    169       result() { %text
    170         #|$ scm_begin
    171         #|$ scm_mv sub/secret.age subdir/secret.age
    172         #|$ scm_commit Move sub/secret.age to subdir/secret.age
    173       }
    174       When call do_copy_move sub/secret subdir
    175       The status should be success
    176       The output should be blank
    177       The error should equal "$(result)"
    178     End
    179 
    180     It 'asks confirmation before overwriting a file'
    181       OVERWRITE=no
    182       rm() { mocklog rm "$@"; }
    183       yesno() {
    184         mocklog yesno "$@"
    185         ANSWER=y
    186       }
    187       result() {
    188         %text:expand
    189         #|$ mkdir -p -- ${PREFIX}/sub
    190         #|$ scm_begin
    191         #|$ yesno sub/secret.age already exists. Overwrite?
    192         #|$ rm -f -- ${PREFIX}/sub/secret.age
    193         #|$ do_decrypt ${PREFIX}/root.age
    194         #|$ do_encrypt sub/secret.age
    195         #|$ scm_rm root.age
    196         #|$ scm_add sub/secret.age
    197         #|$ scm_commit Move root.age to sub/secret.age
    198       }
    199       When call do_copy_move root sub/secret
    200       The status should be success
    201       The output should be blank
    202       The error should equal "$(result)"
    203     End
    204 
    205     It 'moves a whole directory with identity'
    206       result() {
    207         %text:expand
    208         #|$ scm_begin
    209         #|$ mkdir -p -- ${PREFIX}/subdir/sub
    210         #|$ scm_mv sub/.age-recipients subdir/sub/.age-recipients
    211         #|$ mkdir -p -- ${PREFIX}/subdir/sub/bare
    212         #|$ scm_mv sub/bare/deep.age subdir/sub/bare/deep.age
    213         #|$ mkdir -p -- ${PREFIX}/subdir/sub/bare/sub
    214         #|$ scm_mv sub/bare/sub/deepest.age subdir/sub/bare/sub/deepest.age
    215         #|$ scm_mv sub/secret.age subdir/sub/secret.age
    216         #|$ scm_commit Move sub/ to subdir/sub/
    217       }
    218       When call do_copy_move sub subdir/
    219       The status should be success
    220       The output should be blank
    221       The error should equal "$(result)"
    222     End
    223 
    224     It 'recursively moves files to a directory with the same identity'
    225       result() {
    226         %text:expand
    227         #|$ scm_begin
    228         #|$ mkdir -p -- ${PREFIX}/subdir/new-bare
    229         #|$ scm_mv sub/bare/deep.age subdir/new-bare/deep.age
    230         #|$ mkdir -p -- ${PREFIX}/subdir/new-bare/sub
    231         #|$ scm_mv sub/bare/sub/deepest.age subdir/new-bare/sub/deepest.age
    232         #|$ scm_commit Move sub/bare/ to subdir/new-bare/
    233       }
    234       When call do_copy_move sub/bare subdir/new-bare
    235       The status should be success
    236       The output should be blank
    237       The error should equal "$(result)"
    238     End
    239 
    240     It 'recursively re-encrypts a directory'
    241       result() {
    242         %text:expand
    243         #|$ scm_begin
    244         #|$ mkdir -p -- ${PREFIX}/new-bare
    245         #|$ do_decrypt ${PREFIX}/sub/bare/deep.age
    246         #|$ do_encrypt new-bare/deep.age
    247         #|$ scm_rm sub/bare/deep.age
    248         #|$ scm_add new-bare/deep.age
    249         #|$ mkdir -p -- ${PREFIX}/new-bare/sub
    250         #|$ do_decrypt ${PREFIX}/sub/bare/sub/deepest.age
    251         #|$ do_encrypt new-bare/sub/deepest.age
    252         #|$ scm_rm sub/bare/sub/deepest.age
    253         #|$ scm_add new-bare/sub/deepest.age
    254         #|$ scm_commit Move sub/bare/ to new-bare/
    255       }
    256       When call do_copy_move sub/bare new-bare
    257       The status should be success
    258       The output should be blank
    259       The error should equal "$(result)"
    260     End
    261 
    262     It 'recursively re-encrypts a directory with the same identity when forced'
    263       DECISION=force
    264       result() {
    265         %text:expand
    266         #|$ scm_begin
    267         #|$ mkdir -p -- ${PREFIX}/subdir/new-bare
    268         #|$ do_decrypt ${PREFIX}/sub/bare/deep.age
    269         #|$ do_encrypt subdir/new-bare/deep.age
    270         #|$ scm_rm sub/bare/deep.age
    271         #|$ scm_add subdir/new-bare/deep.age
    272         #|$ mkdir -p -- ${PREFIX}/subdir/new-bare/sub
    273         #|$ do_decrypt ${PREFIX}/sub/bare/sub/deepest.age
    274         #|$ do_encrypt subdir/new-bare/sub/deepest.age
    275         #|$ scm_rm sub/bare/sub/deepest.age
    276         #|$ scm_add subdir/new-bare/sub/deepest.age
    277         #|$ scm_commit Move sub/bare/ to subdir/new-bare/
    278       }
    279       When call do_copy_move sub/bare subdir/new-bare
    280       The status should be success
    281       The output should be blank
    282       The error should equal "$(result)"
    283     End
    284 
    285     It 'interactively re-enecrypts or copies files from a directory'
    286       DECISION=interactive
    287       ACTION=Copy
    288       SCM_ACTION=scm_cp
    289       YESNO_NEXT=n
    290       yesno() {
    291         mocklog yesno "$@"
    292         ANSWER="${YESNO_NEXT}"
    293         YESNO_NEXT=y
    294       }
    295       result() {
    296         %text:expand
    297         #|$ scm_begin
    298         #|$ mkdir -p -- ${PREFIX}/subdir/new-bare
    299         #|$ yesno Reencrypt sub/bare/deep into subdir/new-bare/deep?
    300         #|$ scm_cp sub/bare/deep.age subdir/new-bare/deep.age
    301         #|$ mkdir -p -- ${PREFIX}/subdir/new-bare/sub
    302         #|$ yesno Reencrypt sub/bare/sub/deepest into subdir/new-bare/sub/deepest?
    303         #|$ do_decrypt ${PREFIX}/sub/bare/sub/deepest.age
    304         #|$ do_encrypt subdir/new-bare/sub/deepest.age
    305         #|$ scm_add subdir/new-bare/sub/deepest.age
    306         #|$ scm_commit Copy sub/bare/ to subdir/new-bare/
    307       }
    308       When call do_copy_move sub/bare subdir/new-bare
    309       The status should be success
    310       The output should be blank
    311       The error should equal "$(result)"
    312     End
    313 
    314     It 'reports a file masqueraded as a directory'
    315       When run do_copy_move root.age/ subdir
    316       The output should be blank
    317       The error should equal 'Error: root.age/ is not in the password store.'
    318       The status should equal 1
    319     End
    320 
    321     It 'reports non-existent source'
    322       When run do_copy_move nonexistent subdir
    323       The output should be blank
    324       The error should equal 'Error: nonexistent is not in the password store.'
    325       The status should equal 1
    326     End
    327 
    328     It 'cannot merge similarly-named directories'
    329       When run do_copy_move sub/bare/sub /
    330       The output should be blank
    331       The error should equal 'Error: / already contains sub'
    332       The status should equal 1
    333     End
    334 
    335     It 'cannot move a directory into a file'
    336       When run do_copy_move sub/ root.age
    337       The output should be blank
    338       The error should equal 'Error: root.age is not a directory'
    339       The status should equal 1
    340     End
    341 
    342     It 'cannot overwrite a directory with a file'
    343       When run do_copy_move notes.txt subdir
    344       The output should be blank
    345       The error should equal 'Error: subdir already contains notes.txt/'
    346       The status should equal 1
    347     End
    348 
    349     # Unreachable branches in do_copy_move_file, defensively implemented
    350     It 'defensively avois re-encrypting'
    351       DECISION=keep
    352       result() {
    353         %text
    354         #|$ scm_mv root.age non-existent
    355       }
    356       When run do_copy_move_file root.age non-existent
    357       The status should be success
    358       The output should be blank
    359       The error should equal "$(result)"
    360     End
    361 
    362     It 'defensively checks internal consistency of DECISION'
    363       DECISION=garbage
    364       When run do_copy_move_file root.age non-existent
    365       The output should be blank
    366       The error should equal 'Unexpected DECISION value "garbage"'
    367       The status should equal 1
    368     End
    369   End
    370 
    371   Specify 'do_decrypt'
    372     AGE=age
    373     age() {
    374       mocklog age "$@"
    375       %= 'cleartext'
    376     }
    377 
    378     IDENTITIES_FILE='/path/to/identity'
    379     When call do_decrypt '/path/to/encrypted/file.age'
    380     The status should be success
    381     The output should equal 'cleartext'
    382     The error should equal \
    383       '$ age -d -i /path/to/identity -- /path/to/encrypted/file.age'
    384   End
    385 
    386   Describe 'do_decrypt_gpg'
    387     It 'uses gpg when agent is not available'
    388       gpg() { mocklog gpg "$@"; }
    389       unset GPG_AGENT_INFO
    390       unset GPG
    391       When call do_decrypt_gpg /path/to/encrypted/file.gpg
    392       The status should be success
    393       The error should equal \
    394         '$ gpg -d --quiet --yes --compress-algo=none --no-encrypt-to -- /path/to/encrypted/file.gpg'
    395     End
    396 
    397     It 'uses gpg when agent is available'
    398       gpg() { mocklog gpg "$@"; }
    399       GPG_AGENT_INFO=agent-info
    400       unset GPG
    401       When call do_decrypt_gpg /path/to/encrypted/file.gpg
    402       The status should be success
    403       The error should equal \
    404         '$ gpg -d --quiet --yes --compress-algo=none --no-encrypt-to --batch --use-agent -- /path/to/encrypted/file.gpg'
    405     End
    406 
    407     It 'uses gpg2'
    408       gpg2() { mocklog gpg2 "$@"; }
    409       unset GPG_AGENT_INFO
    410       unset GPG
    411       When call do_decrypt_gpg /path/to/encrypted/file.gpg
    412       The status should be success
    413       The error should equal \
    414         '$ gpg2 -d --quiet --yes --compress-algo=none --no-encrypt-to --batch --use-agent -- /path/to/encrypted/file.gpg'
    415     End
    416 
    417     It 'uses user-provided command'
    418       user_cmd() { mocklog user_cmd "$@"; }
    419       unset GPG_AGENT_INFO
    420       GPG=user_cmd
    421       When call do_decrypt_gpg /path/to/encrypted/file.gpg
    422       The status should be success
    423       The error should equal \
    424         '$ user_cmd -d --quiet --yes --compress-algo=none --no-encrypt-to -- /path/to/encrypted/file.gpg'
    425     End
    426 
    427     It 'bails out when command cannot be guessed'
    428       unset GPG
    429       When run do_decrypt_gpg /path/to/encrypted/file.gpg
    430       The error should equal 'GPG does not seem available'
    431       The status should equal 1
    432     End
    433   End
    434 
    435   Describe 'do_deinit'
    436     DECISION=default
    437     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
    438 
    439     do_reencrypt_dir() { mocklog do_reencrypt_dir "$@"; }
    440     scm_begin() { mocklog scm_begin "$@"; }
    441     scm_commit() { mocklog scm_commit "$@"; }
    442     scm_rm() { mocklog scm_rm "$@"; }
    443 
    444     setup() {
    445       @mkdir -p "${PREFIX}/empty" "${PREFIX}/sub"
    446       %putsn data > "${PREFIX}/.age-recipients"
    447       %putsn data > "${PREFIX}/sub/.age-recipients"
    448     }
    449 
    450     cleanup() {
    451       @rm -rf "${PREFIX}"
    452     }
    453 
    454     BeforeEach setup
    455     AfterEach cleanup
    456 
    457     It 'de-initializes the whole store'
    458       result() {
    459         %text:expand
    460         #|$ scm_begin
    461         #|$ scm_rm .age-recipients
    462         #|$ do_reencrypt_dir ${PREFIX}/
    463         #|$ scm_commit Deinitialize store root
    464       }
    465       When call do_deinit ''
    466       The status should be success
    467       The output should be blank
    468       The error should equal "$(result)"
    469     End
    470 
    471     It 'de-initializes a subdirectory'
    472       result() {
    473         %text:expand
    474         #|$ scm_begin
    475         #|$ scm_rm sub/.age-recipients
    476         #|$ do_reencrypt_dir ${PREFIX}/sub
    477         #|$ scm_commit Deinitialize sub
    478       }
    479       When call do_deinit sub
    480       The status should be success
    481       The output should be blank
    482       The error should equal "$(result)"
    483     End
    484 
    485     It 'can de-initialize without re-encryption'
    486       DECISION=keep
    487       result() {
    488         %text:expand
    489         #|$ scm_begin
    490         #|$ scm_rm sub/.age-recipients
    491         #|$ scm_commit Deinitialize sub
    492       }
    493       When call do_deinit sub
    494       The status should be success
    495       The output should be blank
    496       The error should equal "$(result)"
    497     End
    498 
    499     It 'reports impossible de-initialization'
    500       When run do_deinit non-existent
    501       The output should be blank
    502       The error should equal 'No existing recipient to remove at non-existent'
    503       The status should equal 1
    504     End
    505   End
    506 
    507   Describe 'do_delete'
    508     DECISION=force
    509     RECURSIVE=yes
    510     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
    511 
    512     dirname() { @dirname "$@"; }
    513     scm_begin() { mocklog scm_begin "$@"; }
    514     scm_commit() { mocklog scm_commit "$@"; }
    515     scm_rm() { mocklog scm_rm "$@"; }
    516 
    517     setup() {
    518       @mkdir -p "${PREFIX}/empty" "${PREFIX}/sub"
    519       %putsn data > "${PREFIX}/non-encrypted"
    520       %putsn data > "${PREFIX}/sub.age"
    521       %putsn data > "${PREFIX}/sub/entry.age"
    522     }
    523 
    524     cleanup() {
    525       @rm -rf "${PREFIX}"
    526     }
    527 
    528     BeforeEach setup
    529     AfterEach cleanup
    530 
    531     It 'deletes a file after confirmation'
    532       DECISION=default
    533       yesno() {
    534         mocklog yesno "$@"
    535         ANSWER=y
    536       }
    537       result() {
    538         %text:expand
    539         #|$ yesno Are you sure you would like to delete sub/entry?
    540         #|$ scm_begin
    541         #|$ scm_rm sub/entry.age
    542         #|$ scm_commit Remove sub/entry from store.
    543       }
    544       When call do_delete sub/entry
    545       The status should be success
    546       The output should be blank
    547       The error should equal "$(result)"
    548     End
    549 
    550     It 'does not delete a file without confirmation'
    551       DECISION=default
    552       yesno() {
    553         mocklog yesno "$@"
    554         ANSWER=n
    555       }
    556       result() {
    557         %text
    558         #|$ yesno Are you sure you would like to delete sub/entry?
    559       }
    560       When call do_delete sub/entry
    561       The status should be success
    562       The output should be blank
    563       The error should equal "$(result)"
    564     End
    565 
    566     It 'deletes a directory'
    567       result() {
    568         %text:expand
    569         #|$ scm_begin
    570         #|$ scm_rm empty/
    571         #|$ scm_commit Remove empty/ from store.
    572       }
    573       When call do_delete empty
    574       The status should be success
    575       The output should equal 'Removing empty/'
    576       The error should equal "$(result)"
    577     End
    578 
    579     It 'deletes a file rather than a directory on ambiguity'
    580       result() {
    581         %text:expand
    582         #|$ scm_begin
    583         #|$ scm_rm sub.age
    584         #|$ scm_commit Remove sub from store.
    585       }
    586       When call do_delete sub
    587       The status should be success
    588       The output should equal 'Removing sub'
    589       The error should equal "$(result)"
    590     End
    591 
    592     It 'deletes a directory when explicitly asked'
    593       result() {
    594         %text:expand
    595         #|$ scm_begin
    596         #|$ scm_rm sub/
    597         #|$ scm_commit Remove sub/ from store.
    598       }
    599       When call do_delete sub/
    600       The status should be success
    601       The output should equal 'Removing sub/'
    602       The error should equal "$(result)"
    603     End
    604 
    605     It 'does not delete an explicit directory without RECURSIVE'
    606       RECURSIVE=no
    607       When run do_delete sub/
    608       The output should be blank
    609       The error should equal 'Error: sub/ is a directory'
    610       The status should equal 1
    611     End
    612 
    613     It 'does not delete an implicit directory without RECURSIVE'
    614       RECURSIVE=no
    615       When run do_delete empty
    616       The output should be blank
    617       The error should equal 'Error: empty/ is a directory'
    618       The status should equal 1
    619     End
    620 
    621     It 'does not delete a non-encrypted file'
    622       When run do_delete non-encrypted
    623       The output should be blank
    624       The error should equal \
    625         'Error: non-encrypted is not in the password store.'
    626       The status should equal 1
    627     End
    628 
    629     It 'does not delete a file presented as a directory'
    630       When run do_delete non-encrypted/
    631       The output should be blank
    632       The error should equal \
    633         'Error: non-encrypted/ is not a directory.'
    634       The status should equal 1
    635     End
    636 
    637     It 'reports a non-existent directory'
    638       When run do_delete non-existent/
    639       The output should be blank
    640       The error should equal \
    641         'Error: non-existent/ is not in the password store.'
    642       The status should equal 1
    643     End
    644   End
    645 
    646   Describe 'do_edit'
    647     SECURE_TMPDIR="${SHELLSPEC_WORKDIR}/secure"
    648     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
    649 
    650     diff(){ @diff "$@"; }
    651 
    652     do_decrypt() {
    653       mocklog do_decrypt "$@"
    654       %= foo
    655     }
    656     old_do_decrypt() {
    657       mocklog do_decrypt "$@"
    658       %text
    659       #|old line 1
    660       #|old line 2
    661     }
    662 
    663     do_encrypt() {
    664       mocklog do_encrypt "$@"
    665       @sed 's/^/> /' >&2
    666     }
    667 
    668     mktemp() {
    669       mocklog mktemp "$@"
    670       %putsn "$2"
    671     }
    672 
    673     rm(){ mocklog rm "$@"; @rm "$@"; }
    674 
    675     setup() {
    676       @mkdir -p "${PREFIX}"
    677       %text > "${PREFIX}/existing.age"
    678       #|encrypted data
    679       @mkdir -p "${SECURE_TMPDIR}"
    680       %text > "${SECURE_TMPDIR}/new-cleartext.txt"
    681       #|new line 1
    682       #|old line 2
    683       #|new line 3
    684     }
    685 
    686     scm_add() { mocklog scm_add "$@"; }
    687     scm_begin() { mocklog scm_begin "$@"; }
    688     scm_commit() { mocklog scm_commit "$@"; }
    689 
    690     cleanup() {
    691       @rm -rf "${PREFIX}" "${SECURE_TMPDIR}"
    692     }
    693 
    694     BeforeEach setup
    695     AfterEach cleanup
    696 
    697     It 'creates a new file'
    698       edit(){ @cat "${SECURE_TMPDIR}/new-cleartext.txt" >|"$1"; }
    699       result() {
    700         %text:expand
    701         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    702         #|$ scm_begin
    703         #|$ do_encrypt sub/new.age
    704         #|> new line 1
    705         #|> old line 2
    706         #|> new line 3
    707         #|$ scm_add sub/new.age
    708         #|$ scm_commit Add password for sub/new using edit.
    709         #|$ rm ${SECURE_TMPDIR}/XXXXXX-sub-new.txt
    710       }
    711       EDIT_CMD=edit
    712       When call do_edit sub/new
    713       The status should be success
    714       The output should be blank
    715       The error should equal "$(result)"
    716     End
    717 
    718     It 'handles NOT creating a new file'
    719       result() {
    720         %text:expand
    721         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    722         #|$ scm_begin
    723       }
    724       EDIT_CMD=true
    725       When call do_edit new
    726       The status should be success
    727       The output should equal 'New password for new not saved.'
    728       The error should equal "$(result)"
    729     End
    730 
    731     It 'updates a file'
    732       edit(){ @cat "${SECURE_TMPDIR}/new-cleartext.txt" >|"$1"; }
    733       cat(){ mocklog cat "$@"; @cat "$@"; }
    734       result() {
    735         %text:expand
    736         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    737         #|$ do_decrypt ${PREFIX}/existing.age
    738         #|$ cat ${SECURE_TMPDIR}/XXXXXX-existing.txt
    739         #|$ scm_begin
    740         #|$ do_encrypt existing.age
    741         #|> new line 1
    742         #|> old line 2
    743         #|> new line 3
    744         #|$ scm_add existing.age
    745         #|$ scm_commit Edit password for existing using edit.
    746         #|$ rm ${SECURE_TMPDIR}/XXXXXX-existing.txt
    747       }
    748       EDIT_CMD=edit
    749       When call do_edit existing
    750       The status should be success
    751       The output should be blank
    752       The error should equal "$(result)"
    753     End
    754 
    755     It 'does not re-encrypt an unchanged file'
    756       cat(){ mocklog cat "$@"; @cat "$@"; }
    757       result() {
    758         %text:expand
    759         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    760         #|$ do_decrypt ${PREFIX}/existing.age
    761         #|$ cat ${SECURE_TMPDIR}/XXXXXX-existing.txt
    762         #|$ scm_begin
    763         #|$ rm ${SECURE_TMPDIR}/XXXXXX-existing.txt
    764       }
    765       EDIT_CMD=true
    766       When call do_edit existing
    767       The status should be success
    768       The output should equal 'Password for existing unchanged.'
    769       The error should equal "$(result)"
    770     End
    771 
    772     It 'uses VISUAL on non-dumb terminal'
    773       edit() { mocklog edit "$@"; }
    774       VISUAL=edit
    775       TERM=non-dumb
    776       EDITOR=false
    777       unset EDIT_CMD
    778       result() {
    779         %text:expand
    780         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    781         #|$ scm_begin
    782         #|$ edit ${SECURE_TMPDIR}/XXXXXX-subdir-new.txt
    783       }
    784       When call do_edit subdir/new
    785       The status should be success
    786       The output should equal 'New password for subdir/new not saved.'
    787       The error should equal "$(result)"
    788     End
    789 
    790     It 'uses EDITOR on dumb terminal'
    791       edit() { mocklog edit "$@"; }
    792       VISUAL=false
    793       TERM=dumb
    794       EDITOR=edit
    795       unset EDIT_CMD
    796       result() {
    797         %text:expand
    798         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    799         #|$ scm_begin
    800         #|$ edit ${SECURE_TMPDIR}/XXXXXX-subdir-new.txt
    801       }
    802       When call do_edit subdir/new
    803       The status should be success
    804       The output should equal 'New password for subdir/new not saved.'
    805       The error should equal "$(result)"
    806     End
    807 
    808     It 'uses EDITOR without terminal'
    809       edit() { mocklog edit "$@"; }
    810       VISUAL=false
    811       EDITOR=edit
    812       unset EDIT_CMD
    813       unset TERM
    814       result() {
    815         %text:expand
    816         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    817         #|$ scm_begin
    818         #|$ edit ${SECURE_TMPDIR}/XXXXXX-subdir-new.txt
    819       }
    820       When call do_edit subdir/new
    821       The status should be success
    822       The output should equal 'New password for subdir/new not saved.'
    823       The error should equal "$(result)"
    824     End
    825 
    826     It 'uses EDITOR on non-dumb terminal without VISUAL'
    827       edit() { mocklog edit "$@"; }
    828       TERM=non-dumb
    829       EDITOR=edit
    830       unset VISUAL
    831       unset EDIT_CMD
    832       result() {
    833         %text:expand
    834         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    835         #|$ scm_begin
    836         #|$ edit ${SECURE_TMPDIR}/XXXXXX-subdir-new.txt
    837       }
    838       When call do_edit subdir/new
    839       The status should be success
    840       The output should equal 'New password for subdir/new not saved.'
    841       The error should equal "$(result)"
    842     End
    843 
    844     It 'falls back on vi without EDITOR nor VISUAL'
    845       vi() { mocklog vi "$@"; }
    846       unset EDITOR
    847       unset VISUAL
    848       unset EDIT_CMD
    849       result() {
    850         %text:expand
    851         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    852         #|$ scm_begin
    853         #|$ vi ${SECURE_TMPDIR}/XXXXXX-subdir-new.txt
    854       }
    855       When call do_edit subdir/new
    856       The status should be success
    857       The output should equal 'New password for subdir/new not saved.'
    858       The error should equal "$(result)"
    859     End
    860 
    861     It 'reports EDIT_CMD exit code'
    862       exit42() { mocklog editor "$@"; return 42; }
    863       EDIT_CMD=exit42
    864       result() {
    865         %text:expand
    866         #|$ mktemp -u ${SECURE_TMPDIR}/XXXXXX
    867         #|$ scm_begin
    868         #|$ editor ${SECURE_TMPDIR}/XXXXXX-subdir-new.txt
    869         #|Editor "exit42" exited with code 42
    870       }
    871       When run do_edit subdir/new
    872       The status should equal 42
    873       The error should equal "$(result)"
    874     End
    875   End
    876 
    877   Describe 'do_encrypt'
    878     unset PASHAGE_RECIPIENTS_FILE
    879     unset PASSAGE_RECIPIENTS_FILE
    880     unset PASHAGE_RECIPIENTS
    881     unset PASSAGE_RECIPIENTS
    882     AGE=age
    883     PREFIX=/prefix
    884     dirname() { @dirname "$@"; }
    885 
    886     age() { mocklog age "$@"; }
    887     mkdir() { mocklog mkdir "$@"; }
    888 
    889     setup() {
    890       %= data >"${SHELLSPEC_WORKDIR}/existing-file"
    891     }
    892     BeforeAll 'setup'
    893 
    894     It 'falls back on identity when there is no recipient'
    895       OVERWRITE=yes
    896       IDENTITIES_FILE='/path/to/identity'
    897       set_LOCAL_RECIPIENT_FILE() {
    898         LOCAL_RECIPIENT_FILE=''
    899       }
    900       result() {
    901         %text
    902         #|$ mkdir -p /prefix/encrypted
    903         #|$ age -e -i /path/to/identity -o /prefix/encrypted/file.age
    904       }
    905       When run do_encrypt 'encrypted/file.age'
    906       The status should be success
    907       The error should equal "$(result)"
    908     End
    909 
    910     It 'overwrites existing file only once'
    911       OVERWRITE=once
    912       PREFIX="${SHELLSPEC_WORKDIR}"
    913       set_LOCAL_RECIPIENT_FILE() {
    914         LOCAL_RECIPIENT_FILE='/path/to/recipients'
    915       }
    916       preserve() { %preserve OVERWRITE; }
    917       AfterRun 'preserve'
    918       result() {
    919         %text:expand
    920         #|$ mkdir -p ${PREFIX}
    921         #|$ age -e -R /path/to/recipients -o ${PREFIX}/existing-file
    922       }
    923       When run do_encrypt 'existing-file'
    924       The status should be success
    925       The error should equal "$(result)"
    926       The variable OVERWRITE should equal no
    927     End
    928 
    929     It 'overwrites existing file when requested'
    930       OVERWRITE=yes
    931       PREFIX="${SHELLSPEC_WORKDIR}"
    932       set_LOCAL_RECIPIENT_FILE() {
    933         LOCAL_RECIPIENT_FILE='/path/to/recipients'
    934       }
    935       preserve() { %preserve OVERWRITE; }
    936       AfterRun 'preserve'
    937       result() {
    938         %text:expand
    939         #|$ mkdir -p ${PREFIX}
    940         #|$ age -e -R /path/to/recipients -o ${PREFIX}/existing-file
    941       }
    942       When run do_encrypt 'existing-file'
    943       The status should be success
    944       The error should equal "$(result)"
    945       The variable OVERWRITE should equal yes
    946     End
    947 
    948     It 'refuses to overwrite an existing file'
    949       OVERWRITE=no
    950       PREFIX="${SHELLSPEC_WORKDIR}"
    951       set_LOCAL_RECIPIENT_FILE() {
    952         LOCAL_RECIPIENT_FILE='/path/to/recipients'
    953       }
    954       When run do_encrypt 'existing-file'
    955       The error should equal 'Refusing to overwite existing-file'
    956       The status should equal 1
    957     End
    958 
    959     It 'uses PASSAGE_RECIPIENTS rather than LOCAL_RECIPIENT_FILE'
    960       PASSAGE_RECIPIENTS='inline-recipient-1 inline-recipient-2'
    961       set_LOCAL_RECIPIENT_FILE() {
    962         LOCAL_RECIPIENT_FILE='shadowed'
    963       }
    964       OVERWRITE=yes
    965       result() {
    966         %text
    967         #|$ mkdir -p /prefix/encrypted
    968         #|$ age -e -r inline-recipient-1 -r inline-recipient-2 -o /prefix/encrypted/file.age
    969       }
    970 
    971       When call do_encrypt 'encrypted/file.age'
    972       The status should be success
    973       The error should equal "$(result)"
    974     End
    975 
    976     It 'uses PASHAGE_RECIPIENTS rather than PASSAGE_RECIPIENTS'
    977       PASHAGE_RECIPIENTS='inline-recipient-1 inline-recipient-2'
    978       PASSAGE_RECIPIENTS='shadowed'
    979       set_LOCAL_RECIPIENT_FILE() {
    980         LOCAL_RECIPIENT_FILE='shadowed'
    981       }
    982       OVERWRITE=yes
    983       result() {
    984         %text
    985         #|$ mkdir -p /prefix/encrypted
    986         #|$ age -e -r inline-recipient-1 -r inline-recipient-2 -o /prefix/encrypted/file.age
    987       }
    988 
    989       When call do_encrypt 'encrypted/file.age'
    990       The status should be success
    991       The error should equal "$(result)"
    992     End
    993 
    994     It 'uses PASSAGE_RECIPIENTS_FILE rather than PASHAGE_RECIPIENTS'
    995       PASSAGE_RECIPIENTS_FILE='/path/to/recipients'
    996       PASHAGE_RECIPIENTS='shadowed'
    997       PASSAGE_RECIPIENTS='shadowed'
    998       set_LOCAL_RECIPIENT_FILE() {
    999         LOCAL_RECIPIENT_FILE='shadowed'
   1000       }
   1001       OVERWRITE=yes
   1002       result() {
   1003         %text
   1004         #|$ mkdir -p /prefix/encrypted
   1005         #|$ age -e -R /path/to/recipients -o /prefix/encrypted/file.age
   1006       }
   1007 
   1008       When call do_encrypt 'encrypted/file.age'
   1009       The status should be success
   1010       The error should equal "$(result)"
   1011     End
   1012 
   1013     It 'uses PASHAGE_RECIPIENTS_FILE rather than PASSAGE_RECIPIENTS_FILE'
   1014       PASHAGE_RECIPIENTS_FILE='/path/to/recipients'
   1015       PASSAGE_RECIPIENTS_FILE='shadowed'
   1016       PASHAGE_RECIPIENTS='shadowed'
   1017       PASSAGE_RECIPIENTS='shadowed'
   1018       set_LOCAL_RECIPIENT_FILE() {
   1019         LOCAL_RECIPIENT_FILE='shadowed'
   1020       }
   1021       OVERWRITE=yes
   1022       result() {
   1023         %text
   1024         #|$ mkdir -p /prefix/encrypted
   1025         #|$ age -e -R /path/to/recipients -o /prefix/encrypted/file.age
   1026       }
   1027 
   1028       When call do_encrypt 'encrypted/file.age'
   1029       The status should be success
   1030       The error should equal "$(result)"
   1031     End
   1032   End
   1033 
   1034   Describe 'do_generate'
   1035     DECISION=default
   1036     MULTILINE=no
   1037     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1038     SHOW=none
   1039 
   1040     dd() { %- 0123456789 ; }
   1041     dirname() { @dirname "$@"; }
   1042     tr() { @tr "$@"; }
   1043 
   1044     do_encrypt() {
   1045       mocklog do_encrypt "$@"
   1046       @sed 's/^/> /' >&2
   1047     }
   1048 
   1049     do_show() {
   1050       mocklog do_show "$@"
   1051       @sed 's/^/> /' >&2
   1052     }
   1053 
   1054     mkdir() { mocklog mkdir "$@"; }
   1055     scm_add() { mocklog scm_add "$@"; }
   1056     scm_begin() { mocklog scm_begin "$@"; }
   1057     scm_commit() { mocklog scm_commit "$@"; }
   1058 
   1059     setup() {
   1060       @mkdir -p "${PREFIX}/suspicious.age"
   1061       %putsn data >"${PREFIX}/existing.age"
   1062     }
   1063 
   1064     cleanup() {
   1065       @rm -rf "${PREFIX}"
   1066     }
   1067 
   1068     BeforeEach setup
   1069     AfterEach cleanup
   1070 
   1071     It 'detects short reads'
   1072       When run do_generate suspicious 12 '[:alnum:]'
   1073       The output should be blank
   1074       The error should equal \
   1075         'Error while generating password: 10/12 bytes read'
   1076       The status should equal 1
   1077     End
   1078 
   1079     It 'aborts when a directory is in the way'
   1080       result(){
   1081         %text:expand
   1082         #|$ scm_begin
   1083         #|$ mkdir -p -- ${PREFIX}
   1084         #|Cannot replace directory suspicious.age
   1085       }
   1086       When run do_generate suspicious 10 '[:alnum:]'
   1087       The output should be blank
   1088       The error should equal "$(result)"
   1089       The status should equal 1
   1090     End
   1091 
   1092     It 'generates a new file'
   1093       result(){
   1094         %text:expand
   1095         #|$ scm_begin
   1096         #|$ mkdir -p -- ${PREFIX}/sub
   1097         #|$ do_encrypt sub/new.age
   1098         #|> 0123456789
   1099         #|$ scm_add ${PREFIX}/sub/new.age
   1100         #|$ scm_commit Add generated password for sub/new.
   1101         #|$ do_show sub/new
   1102         #|> 0123456789
   1103       }
   1104       When call do_generate sub/new 10 '[:alnum:]'
   1105       The status should be success
   1106       The output should be blank
   1107       The error should equal "$(result)"
   1108     End
   1109 
   1110     It 'displays a title before text output'
   1111       SHOW=text
   1112       BOLD_TEXT='(B)'
   1113       NORMAL_TEXT='(N)'
   1114       UNDERLINE_TEXT='(U)'
   1115       NO_UNDERLINE_TEXT='(!U)'
   1116       result(){
   1117         %text:expand
   1118         #|$ scm_begin
   1119         #|$ mkdir -p -- ${PREFIX}/sub
   1120         #|$ do_encrypt sub/new.age
   1121         #|> 0123456789
   1122         #|$ scm_add ${PREFIX}/sub/new.age
   1123         #|$ scm_commit Add generated password for sub/new.
   1124         #|$ do_show sub/new
   1125         #|> 0123456789
   1126       }
   1127       When call do_generate sub/new 10 '[:alnum:]'
   1128       The status should be success
   1129       The output should equal \
   1130         '(B)The generated password for (U)sub/new(!U) is:(N)'
   1131       The error should equal "$(result)"
   1132     End
   1133 
   1134     It 'overwrites an existing file when forced'
   1135       MULTILINE=no
   1136       OVERWRITE=yes
   1137       result(){
   1138         %text:expand
   1139         #|$ scm_begin
   1140         #|$ mkdir -p -- ${PREFIX}
   1141         #|$ do_encrypt existing.age
   1142         #|> 0123456789
   1143         #|$ scm_add ${PREFIX}/existing.age
   1144         #|$ scm_commit Add generated password for existing.
   1145         #|$ do_show existing
   1146         #|> 0123456789
   1147       }
   1148       When call do_generate existing 10 '[:alnum:]'
   1149       The status should be success
   1150       The output should be blank
   1151       The error should equal "$(result)"
   1152     End
   1153 
   1154     It 'overwrites an existing file after confirmation'
   1155       MULTILINE=no
   1156       OVERWRITE=no
   1157       yesno() {
   1158         mocklog yesno "$@";
   1159         ANSWER=y
   1160       }
   1161       result(){
   1162         %text:expand
   1163         #|$ scm_begin
   1164         #|$ mkdir -p -- ${PREFIX}
   1165         #|$ yesno An entry already exists for existing. Overwrite it?
   1166         #|$ do_encrypt existing.age
   1167         #|> 0123456789
   1168         #|$ scm_add ${PREFIX}/existing.age
   1169         #|$ scm_commit Add generated password for existing.
   1170         #|$ do_show existing
   1171         #|> 0123456789
   1172       }
   1173       When call do_generate existing 10 '[:alnum:]'
   1174       The status should be success
   1175       The output should be blank
   1176       The error should equal "$(result)"
   1177       The variable OVERWRITE should equal 'once'
   1178     End
   1179 
   1180     It 'does not overwrite an existing file without confirmation'
   1181       MULTILINE=no
   1182       OVERWRITE=no
   1183       yesno() {
   1184         mocklog yesno "$@";
   1185         ANSWER=n
   1186       }
   1187       result(){
   1188         %text:expand
   1189         #|$ scm_begin
   1190         #|$ mkdir -p -- ${PREFIX}
   1191         #|$ yesno An entry already exists for existing. Overwrite it?
   1192       }
   1193       When call do_generate existing 10 '[:alnum:]'
   1194       The status should be success
   1195       The output should be blank
   1196       The error should equal "$(result)"
   1197     End
   1198 
   1199     It 'updates the first line of an existing file'
   1200       MULTILINE=no
   1201       OVERWRITE=reuse
   1202       do_decrypt() {
   1203         mocklog do_decrypt "$@"
   1204         %text
   1205         #|old password
   1206         #|line 2
   1207         #|line 3
   1208       }
   1209       mv() { mocklog mv "$@"; }
   1210       result(){
   1211         %text:expand
   1212         #|$ scm_begin
   1213         #|$ mkdir -p -- ${PREFIX}
   1214         #|$ do_decrypt ${PREFIX}/existing.age
   1215         #|$ do_encrypt existing.age
   1216         #|> 0123456789
   1217         #|> line 2
   1218         #|> line 3
   1219         #|$ scm_add ${PREFIX}/existing.age
   1220         #|$ scm_commit Replace generated password for existing.
   1221         #|$ do_show existing
   1222         #|> 0123456789
   1223       }
   1224       When call do_generate existing 10 '[:alnum:]'
   1225       The status should be success
   1226       The output should equal 'Decrypting previous secret for existing'
   1227       The error should equal "$(result)"
   1228     End
   1229 
   1230     It 'updates the only line of an existing one-line file'
   1231       MULTILINE=no
   1232       OVERWRITE=reuse
   1233       do_decrypt() {
   1234         mocklog do_decrypt "$@"
   1235         %text
   1236         #|old password
   1237       }
   1238       mv() { mocklog mv "$@"; }
   1239       result(){
   1240         %text:expand
   1241         #|$ scm_begin
   1242         #|$ mkdir -p -- ${PREFIX}
   1243         #|$ do_decrypt ${PREFIX}/existing.age
   1244         #|$ do_encrypt existing.age
   1245         #|> 0123456789
   1246         #|$ scm_add ${PREFIX}/existing.age
   1247         #|$ scm_commit Replace generated password for existing.
   1248         #|$ do_show existing
   1249         #|> 0123456789
   1250       }
   1251       When call do_generate existing 10 '[:alnum:]'
   1252       The status should be success
   1253       The output should equal 'Decrypting previous secret for existing'
   1254       The error should equal "$(result)"
   1255     End
   1256 
   1257     It 'saves the password after showing it and getting confirmation'
   1258       DECISION=interactive
   1259       yesno() {
   1260         mocklog yesno "$@"
   1261         ANSWER=y
   1262       }
   1263       result(){
   1264         %text:expand
   1265         #|$ do_show sub/new
   1266         #|> 0123456789
   1267         #|$ yesno Save generated password for sub/new?
   1268         #|$ scm_begin
   1269         #|$ mkdir -p -- ${PREFIX}/sub
   1270         #|$ do_encrypt sub/new.age
   1271         #|> 0123456789
   1272         #|$ scm_add ${PREFIX}/sub/new.age
   1273         #|$ scm_commit Add generated password for sub/new.
   1274       }
   1275       When call do_generate sub/new 10 '[:alnum:]'
   1276       The status should be success
   1277       The output should be blank
   1278       The error should equal "$(result)"
   1279     End
   1280 
   1281     It 'does not save the password after showing it and getting cancellation'
   1282       DECISION=interactive
   1283       yesno() {
   1284         mocklog yesno "$@"
   1285         ANSWER=n
   1286       }
   1287       result(){
   1288         %text:expand
   1289         #|$ do_show sub/new
   1290         #|> 0123456789
   1291         #|$ yesno Save generated password for sub/new?
   1292       }
   1293       When call do_generate sub/new 10 '[:alnum:]'
   1294       The status should be success
   1295       The output should be blank
   1296       The error should equal "$(result)"
   1297     End
   1298 
   1299     It 'accepts an extra line after the generated secret'
   1300       MULTILINE=yes
   1301       Data 'comment line'
   1302       When call do_generate sub/new 10 '[:alnum:]'
   1303       result(){
   1304         %text:expand
   1305         #|$ scm_begin
   1306         #|$ mkdir -p -- ${PREFIX}/sub
   1307         #|$ do_encrypt sub/new.age
   1308         #|> 0123456789
   1309         #|> comment line
   1310         #|$ scm_add ${PREFIX}/sub/new.age
   1311         #|$ scm_commit Add generated password for sub/new.
   1312         #|$ do_show sub/new
   1313         #|> 0123456789
   1314       }
   1315       The status should be success
   1316       The output should equal 'Enter extra secrets then Ctrl+D when finished:'
   1317       The error should equal "$(result)"
   1318     End
   1319 
   1320     It 'accepts several lines after the generated secret'
   1321       MULTILINE=yes
   1322       OVERWRITE=no
   1323       Data
   1324         #|comment line
   1325         #|end of secret
   1326       End
   1327       yesno() {
   1328         mocklog yesno "$@"
   1329         ANSWER=y
   1330       }
   1331       When call do_generate existing 10 '[:alnum:]'
   1332       result(){
   1333         %text:expand
   1334         #|$ scm_begin
   1335         #|$ mkdir -p -- ${PREFIX}
   1336         #|$ yesno An entry already exists for existing. Overwrite it?
   1337         #|$ do_encrypt existing.age
   1338         #|> 0123456789
   1339         #|> comment line
   1340         #|> end of secret
   1341         #|$ scm_add ${PREFIX}/existing.age
   1342         #|$ scm_commit Add generated password for existing.
   1343         #|$ do_show existing
   1344         #|> 0123456789
   1345       }
   1346       The status should be success
   1347       The output should equal 'Enter extra secrets then Ctrl+D when finished:'
   1348       The error should equal "$(result)"
   1349     End
   1350 
   1351     It 'does not asks for extra lines after refusing to overwrite'
   1352       MULTILINE=yes
   1353       OVERWRITE=no
   1354       Data 'n'
   1355       yesno() {
   1356         mocklog yesno "$@"
   1357         ANSWER=n
   1358       }
   1359       When call do_generate existing 10 '[:alnum:]'
   1360       result(){
   1361         %text:expand
   1362         #|$ scm_begin
   1363         #|$ mkdir -p -- ${PREFIX}
   1364         #|$ yesno An entry already exists for existing. Overwrite it?
   1365       }
   1366       The status should be success
   1367       The output should be blank
   1368       The error should equal "$(result)"
   1369     End
   1370 
   1371     It 'inserts extra lines after the in-place secrets'
   1372       MULTILINE=yes
   1373       OVERWRITE=reuse
   1374       do_decrypt() {
   1375         mocklog do_decrypt "$@"
   1376         %text:expand
   1377         #|old password
   1378         #|old annotation
   1379         #|end of $*
   1380       }
   1381       Data
   1382         #|comment line
   1383         #|end of secret
   1384       End
   1385       yesno() {
   1386         mocklog yesno "$@"
   1387         ANSWER=y
   1388       }
   1389       When call do_generate existing 10 '[:alnum:]'
   1390       o_result(){
   1391         %text
   1392         #|Decrypting previous secret for existing
   1393         #|Enter extra secrets then Ctrl+D when finished:
   1394       }
   1395       result(){
   1396         %text:expand
   1397         #|$ scm_begin
   1398         #|$ mkdir -p -- ${PREFIX}
   1399         #|$ do_decrypt ${PREFIX}/existing.age
   1400         #|$ do_encrypt existing.age
   1401         #|> 0123456789
   1402         #|> old annotation
   1403         #|> end of ${PREFIX}/existing.age
   1404         #|> comment line
   1405         #|> end of secret
   1406         #|$ scm_add ${PREFIX}/existing.age
   1407         #|$ scm_commit Replace generated password for existing.
   1408         #|$ do_show existing
   1409         #|> 0123456789
   1410       }
   1411       The status should be success
   1412       The output should equal "$(o_result)"
   1413       The error should equal "$(result)"
   1414     End
   1415   End
   1416 
   1417   Describe 'do_grep'
   1418     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1419     BLUE_TEXT='(B)'
   1420     BOLD_TEXT='(G)'
   1421     NORMAL_TEXT='(N)'
   1422 
   1423     do_decrypt() { @cat "$1"; }
   1424     grep() { @grep "$1"; }
   1425 
   1426     setup() {
   1427       @mkdir -p "${PREFIX}/empty" "${PREFIX}/subdir"
   1428       %putsn data >"${PREFIX}/non-match.age"
   1429       %text >"${PREFIX}/subdir/match.age"
   1430       #|non-match
   1431       #|other
   1432       #|suffix
   1433     }
   1434 
   1435     cleanup() {
   1436       @rm -rf "${PREFIX}"
   1437     }
   1438 
   1439     BeforeEach setup
   1440     AfterEach cleanup
   1441 
   1442     It 'outputs matching files'
   1443       result(){
   1444         %text
   1445         #|(B)subdir/(G)match(N):
   1446         #|other
   1447       }
   1448       When call do_grep "${PREFIX}" ot
   1449       The status should be success
   1450       The output should equal "$(result)"
   1451       The error should be blank
   1452     End
   1453 
   1454     It 'outputs all the matching lines'
   1455       result(){
   1456         %text
   1457         #|(B)subdir/(G)match(N):
   1458         #|other
   1459         #|suffix
   1460       }
   1461       When call do_grep "${PREFIX}" -vea
   1462       The status should be success
   1463       The output should equal "$(result)"
   1464       The error should be blank
   1465     End
   1466 
   1467     It 'outputs nothing without matches'
   1468       When call do_grep "${PREFIX}" z
   1469       The status should be success
   1470       The output should be blank
   1471       The error should be blank
   1472     End
   1473 
   1474     It 'correctly displays matches in the root'
   1475       result(){
   1476         %text
   1477         #|(B)(G)non-match(N):
   1478         #|data
   1479         #|(B)subdir/(G)match(N):
   1480         #|non-match
   1481       }
   1482       When call do_grep "${PREFIX}" a
   1483       The status should be success
   1484       The output should equal "$(result)"
   1485       The error should be blank
   1486     End
   1487   End
   1488 
   1489   Describe 'do_init'
   1490     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1491     DECISION=default
   1492     OVERWRITE=no
   1493 
   1494     do_reencrypt_dir() { mocklog do_reencrypt_dir "$@"; }
   1495     mkdir() { mocklog mkdir "$@"; @mkdir "$@"; }
   1496     scm_add() { mocklog scm_add "$@"; }
   1497     scm_begin() { mocklog scm_begin "$@"; }
   1498     scm_commit() { mocklog scm_commit "$@"; }
   1499 
   1500     cleanup() {
   1501       @rm -rf "${PREFIX}"
   1502     }
   1503 
   1504     AfterEach cleanup
   1505 
   1506     It 'initializes the store'
   1507       result() {
   1508         %text:expand
   1509         #|$ mkdir -p -- ${PREFIX}
   1510         #|$ scm_begin
   1511         #|$ scm_add .age-recipients
   1512         #|$ do_reencrypt_dir ${PREFIX}
   1513         #|$ scm_commit Set age recipients at store root
   1514       }
   1515       When call do_init '' identity
   1516       The status should be success
   1517       The output should equal 'Password store recipients set at store root'
   1518       The error should equal "$(result)"
   1519       The file "${PREFIX}/.age-recipients" should be exist
   1520       The contents of the file "${PREFIX}/.age-recipients" should equal \
   1521         'identity'
   1522     End
   1523 
   1524     It 'initializes a subdirectory'
   1525       result() {
   1526         %text:expand
   1527         #|$ mkdir -p -- ${PREFIX}/sub
   1528         #|$ scm_begin
   1529         #|$ scm_add sub/.age-recipients
   1530         #|$ do_reencrypt_dir ${PREFIX}/sub
   1531         #|$ scm_commit Set age recipients at sub
   1532       }
   1533       two_id() {
   1534         %text
   1535         #|identity 1
   1536         #|identity 2
   1537       }
   1538       When call do_init sub 'identity 1' 'identity 2'
   1539       The status should be success
   1540       The output should equal 'Password store recipients set at sub'
   1541       The error should equal "$(result)"
   1542       The file "${PREFIX}/sub/.age-recipients" should be exist
   1543       The contents of the file "${PREFIX}/sub/.age-recipients" should equal \
   1544         "$(two_id)"
   1545     End
   1546 
   1547     It 'can initialize without re-encryption'
   1548       DECISION=keep
   1549       result() {
   1550         %text:expand
   1551         #|$ mkdir -p -- ${PREFIX}
   1552         #|$ scm_begin
   1553         #|$ scm_add .age-recipients
   1554         #|$ scm_commit Set age recipients at store root
   1555       }
   1556       When call do_init '' identity
   1557       The status should be success
   1558       The output should equal 'Password store recipients set at store root'
   1559       The error should equal "$(result)"
   1560       The file "${PREFIX}/.age-recipients" should be exist
   1561       The contents of the file "${PREFIX}/.age-recipients" should equal \
   1562         'identity'
   1563     End
   1564   End
   1565 
   1566   Describe 'do_insert'
   1567     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1568 
   1569     do_encrypt() {
   1570       mocklog do_encrypt "$@"
   1571       @sed 's/^/> /' >&2
   1572     }
   1573 
   1574     dirname() { @dirname "$@"; }
   1575     head() { @head "$@"; }
   1576 
   1577     mkdir() { mocklog mkdir "$@"; }
   1578     scm_add() { mocklog scm_add "$@"; }
   1579     scm_begin() { mocklog scm_begin "$@"; }
   1580     scm_commit() { mocklog scm_commit "$@"; }
   1581 
   1582     setup() {
   1583       @mkdir -p "${PREFIX}"
   1584       %putsn data >"${PREFIX}/existing.age"
   1585     }
   1586 
   1587     cleanup() {
   1588       @rm -rf "${PREFIX}"
   1589     }
   1590 
   1591     BeforeEach setup
   1592     AfterEach cleanup
   1593 
   1594     It 'inserts a single line from standard input'
   1595       ECHO=yes
   1596       MULTILINE=no
   1597       OVERWRITE=yes
   1598       result() {
   1599         %text:expand
   1600         #|$ scm_begin
   1601         #|$ mkdir -p -- ${PREFIX}/subdir
   1602         #|$ do_encrypt subdir/new.age
   1603         #|> line 1
   1604         #|$ scm_add subdir/new.age
   1605         #|$ scm_commit Add given password for subdir/new to store.
   1606       }
   1607       Data
   1608         #|line 1
   1609         #|line 2
   1610         #|line 3
   1611       End
   1612 
   1613       When call do_insert 'subdir/new'
   1614       The status should be success
   1615       The output should equal 'Enter password for subdir/new: '
   1616       The error should equal "$(result)"
   1617     End
   1618 
   1619     It 'inserts the standard input until the first blank line'
   1620       MULTILINE=yes
   1621       OVERWRITE=yes
   1622       o_result() { %text
   1623         #|Enter contents of subdir/new and
   1624         #|press Ctrl+D or enter an empty line when finished:
   1625       }
   1626       result() {
   1627         %text:expand
   1628         #|$ scm_begin
   1629         #|$ mkdir -p -- ${PREFIX}/subdir
   1630         #|$ do_encrypt subdir/new.age
   1631         #|> line 1
   1632         #|> line 2
   1633         #|$ scm_add subdir/new.age
   1634         #|$ scm_commit Add given password for subdir/new to store.
   1635       }
   1636       Data
   1637         #|line 1
   1638         #|line 2
   1639         #|
   1640         #|line 3
   1641         #|line 4
   1642       End
   1643 
   1644       When call do_insert 'subdir/new'
   1645       The status should be success
   1646       The output should equal "$(o_result)"
   1647       The error should equal "$(result)"
   1648     End
   1649 
   1650     It 'inserts the whole standard input without blank line'
   1651       MULTILINE=yes
   1652       OVERWRITE=yes
   1653       o_result() { %text
   1654         #|Enter contents of subdir/new and
   1655         #|press Ctrl+D or enter an empty line when finished:
   1656       }
   1657       result() {
   1658         %text:expand
   1659         #|$ scm_begin
   1660         #|$ mkdir -p -- ${PREFIX}/subdir
   1661         #|$ do_encrypt subdir/new.age
   1662         #|> line 1
   1663         #|> line 2
   1664         #|> line 3
   1665         #|$ scm_add subdir/new.age
   1666         #|$ scm_commit Add given password for subdir/new to store.
   1667       }
   1668       Data
   1669         #|line 1
   1670         #|line 2
   1671         #|line 3
   1672       End
   1673 
   1674       When call do_insert 'subdir/new'
   1675       The status should be success
   1676       The output should equal "$(o_result)"
   1677       The error should equal "$(result)"
   1678     End
   1679 
   1680     It 'checks password confirmation before inserting it'
   1681       ECHO=no
   1682       MULTILINE=no
   1683       OVERWRITE=yes
   1684       stty() { true; }
   1685       o_result() {
   1686         %text | @sed 's/\$$//'
   1687         #|Enter password for subdir/new:  $
   1688         #|Retype password for subdir/new: $
   1689         #|Passwords don't match$
   1690         #|Enter password for subdir/new:  $
   1691         #|Retype password for subdir/new: $
   1692       }
   1693       e_result() {
   1694         %text:expand
   1695         #|$ scm_begin
   1696         #|$ mkdir -p -- ${PREFIX}/subdir
   1697         #|$ do_encrypt subdir/new.age
   1698         #|> line 3
   1699         #|$ scm_add subdir/new.age
   1700         #|$ scm_commit Add given password for subdir/new to store.
   1701       }
   1702       Data
   1703         #|line 1
   1704         #|line 2
   1705         #|line 3
   1706         #|line 3
   1707         #|line 5
   1708       End
   1709 
   1710       When call do_insert 'subdir/new'
   1711       The status should be success
   1712       The output should equal "$(o_result)"
   1713       The error should equal "$(e_result)"
   1714     End
   1715 
   1716     It 'asks confirmation before overwriting'
   1717       MULTILINE=yes
   1718       OVERWRITE=no
   1719       yesno() {
   1720         mocklog yesno "$@"
   1721         ANSWER=y
   1722       }
   1723       o_result() { %text
   1724         #|Enter contents of existing and
   1725         #|press Ctrl+D or enter an empty line when finished:
   1726       }
   1727       result() {
   1728         %text:expand
   1729         #|$ yesno An entry already exists for existing. Overwrite it?
   1730         #|$ scm_begin
   1731         #|$ mkdir -p -- ${PREFIX}
   1732         #|$ do_encrypt existing.age
   1733         #|> password
   1734         #|$ scm_add existing.age
   1735         #|$ scm_commit Add given password for existing to store.
   1736       }
   1737       Data 'password'
   1738 
   1739       When call do_insert 'existing'
   1740       The status should be success
   1741       The output should equal "$(o_result)"
   1742       The error should equal "$(result)"
   1743       The variable OVERWRITE should equal once
   1744     End
   1745 
   1746     It 'does not overwrite without confirmation'
   1747       MULTILINE=yes
   1748       OVERWRITE=no
   1749       yesno() {
   1750         mocklog yesno "$@"
   1751         ANSWER=n
   1752       }
   1753       result() {
   1754         %text:expand
   1755         #|$ yesno An entry already exists for existing. Overwrite it?
   1756       }
   1757       Data 'password'
   1758 
   1759       When call do_insert 'existing'
   1760       The status should be success
   1761       The output should be blank
   1762       The error should equal "$(result)"
   1763     End
   1764 
   1765     It 'does not ask confirmation before overwriting when forced'
   1766       MULTILINE=yes
   1767       OVERWRITE=yes
   1768       yesno() {
   1769         mocklog yesno "$@"
   1770         ANSWER=y
   1771       }
   1772       o_result() { %text
   1773         #|Enter contents of existing and
   1774         #|press Ctrl+D or enter an empty line when finished:
   1775       }
   1776       result() {
   1777         %text:expand
   1778         #|$ scm_begin
   1779         #|$ mkdir -p -- ${PREFIX}
   1780         #|$ do_encrypt existing.age
   1781         #|> password
   1782         #|$ scm_add existing.age
   1783         #|$ scm_commit Add given password for existing to store.
   1784       }
   1785       Data 'password'
   1786 
   1787       When call do_insert 'existing'
   1788       The status should be success
   1789       The output should equal "$(o_result)"
   1790       The error should equal "$(result)"
   1791     End
   1792   End
   1793 
   1794   Describe 'do_list'
   1795     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1796 
   1797     grep() { @grep "$@"; }
   1798 
   1799     setup() {
   1800       @mkdir -p "${PREFIX}/subdir/subsub" "${PREFIX}/empty" "${PREFIX}/other"
   1801       %putsn data >"${PREFIX}/root.age"
   1802       %putsn data >"${PREFIX}/subdir/hidden"
   1803       %putsn data >"${PREFIX}/subdir/subsub/old.gpg"
   1804       %putsn data >"${PREFIX}/other/lower.age"
   1805       %putsn data >"${PREFIX}/other/lower.gpg"
   1806       %putsn data >"${PREFIX}/subdir.gpg"
   1807     }
   1808 
   1809     cleanup() {
   1810       @rm -rf "${PREFIX}"
   1811     }
   1812 
   1813     BeforeEach setup
   1814     AfterEach cleanup
   1815 
   1816     It 'displays everything without a pattern'
   1817       result() {
   1818         %text
   1819         #|other/lower
   1820         #|other/lower.gpg
   1821         #|root
   1822         #|subdir/subsub/old
   1823         #|subdir.gpg
   1824       }
   1825       BEGIN_GPG_NAME=''
   1826       END_GPG_NAME=''
   1827       LIST_EMPTY='no'
   1828       When call do_list ''
   1829       The status should be success
   1830       The output should equal "$(result)"
   1831     End
   1832 
   1833     It 'displays everything, including empty directories'
   1834       result() {
   1835         %text
   1836         #|empty/
   1837         #|other/lower
   1838         #|other/lower.gpg
   1839         #|root
   1840         #|subdir/subsub/old
   1841         #|subdir.gpg
   1842       }
   1843       BEGIN_GPG_NAME=''
   1844       END_GPG_NAME=''
   1845       LIST_EMPTY=yes
   1846       When call do_list ''
   1847       The status should be success
   1848       The output should equal "$(result)"
   1849     End
   1850 
   1851     It 'displays only matching files'
   1852       result() {
   1853         %text
   1854         #|other/lower
   1855         #|other/lower.gpg
   1856         #|subdir/subsub/old
   1857       }
   1858       BEGIN_GPG_NAME=''
   1859       END_GPG_NAME=''
   1860       LIST_EMPTY='no'
   1861       When call do_list '' -i L
   1862       The status should be success
   1863       The output should equal "$(result)"
   1864     End
   1865 
   1866     It 'does not display matching directories'
   1867       BEGIN_GPG_NAME=''
   1868       END_GPG_NAME=''
   1869       LIST_EMPTY='no'
   1870       When call do_list '' t
   1871       The status should be success
   1872       The output should equal 'root'
   1873     End
   1874 
   1875     It 'might not display anything'
   1876       BEGIN_GPG_NAME=''
   1877       END_GPG_NAME=''
   1878       LIST_EMPTY='no'
   1879       When call do_list '' z
   1880       The status should be success
   1881       The output should equal ''
   1882     End
   1883 
   1884     It 'marks GPG names'
   1885       result() {
   1886         %text
   1887         #|other/lower
   1888         #|other/[lower.gpg]
   1889         #|root
   1890         #|subdir/subsub/[old]
   1891         #|[subdir.gpg]
   1892       }
   1893       BEGIN_GPG_NAME='['
   1894       END_GPG_NAME=']'
   1895       LIST_EMPTY='no'
   1896       When call do_list ''
   1897       The status should be success
   1898       The output should equal "$(result)"
   1899     End
   1900   End
   1901 
   1902   Describe 'do_list_or_show'
   1903     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1904 
   1905     do_decrypt() {
   1906       mocklog do_decrypt "$@"
   1907       %putsn data
   1908     }
   1909 
   1910     do_decrypt_gpg() {
   1911       mocklog do_decrypt_gpg "$@"
   1912       %putsn data
   1913     }
   1914 
   1915     do_list() { mocklog do_list "$@"; }
   1916 
   1917     do_show() {
   1918       @cat >/dev/null
   1919       mocklog do_show "$@"
   1920     }
   1921 
   1922     do_tree() { mocklog do_tree "$@"; }
   1923 
   1924     setup() {
   1925       @mkdir -p "${PREFIX}/subdir/subsub" "${PREFIX}/empty" "${PREFIX}/other"
   1926       %putsn data >"${PREFIX}/root.age"
   1927       %putsn data >"${PREFIX}/subdir/hidden"
   1928       %putsn data >"${PREFIX}/subdir/subsub/old.gpg"
   1929       %putsn data >"${PREFIX}/other/lower.age"
   1930     }
   1931 
   1932     cleanup() {
   1933       @rm -rf "${PREFIX}"
   1934     }
   1935 
   1936     BeforeEach setup
   1937     AfterEach cleanup
   1938 
   1939     It 'lists the whole store as a list'
   1940       LIST_VIEW=yes
   1941       When call do_list_or_show ''
   1942       The status should be success
   1943       The output should be blank
   1944       The error should equal "$ do_list "
   1945     End
   1946 
   1947     It 'lists the whole store as a tree'
   1948       LIST_VIEW=no
   1949       When call do_list_or_show ''
   1950       The status should be success
   1951       The output should equal 'Password Store'
   1952       The error should equal '$ do_tree '
   1953     End
   1954 
   1955     It 'shows a decrypted age file'
   1956       result() {
   1957         %text:expand
   1958         #|$ do_decrypt ${PREFIX}/other/lower.age
   1959         #|$ do_show other/lower
   1960       }
   1961       When call do_list_or_show 'other/lower'
   1962       The status should be success
   1963       The error should equal "$(result)"
   1964     End
   1965 
   1966     It 'shows a decrypted gpg file'
   1967       result() {
   1968         %text:expand
   1969         #|$ do_decrypt_gpg ${PREFIX}/subdir/subsub/old.gpg
   1970         #|$ do_show subdir/subsub/old
   1971       }
   1972       When call do_list_or_show 'subdir/subsub/old'
   1973       The status should be success
   1974       The error should equal "$(result)"
   1975     End
   1976 
   1977     It 'lists a subdirectory as a list'
   1978       LIST_VIEW=yes
   1979       When call do_list_or_show 'subdir'
   1980       The status should be success
   1981       The output should be blank
   1982       The error should equal "$ do_list subdir"
   1983     End
   1984 
   1985     It 'lists a subdirectory as a tree'
   1986       LIST_VIEW=no
   1987       When call do_list_or_show 'subdir'
   1988       The status should be success
   1989       The output should equal 'subdir'
   1990       The error should equal '$ do_tree subdir'
   1991     End
   1992 
   1993     It 'does not show a non-encrypted file'
   1994       When run do_list_or_show 'subdir/hidden'
   1995       The output should be blank
   1996       The error should equal \
   1997         'Error: subdir/hidden is not in the password store.'
   1998       The status should equal 1
   1999     End
   2000   End
   2001 
   2002   Describe 'do_reencrypt'
   2003     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   2004 
   2005     do_decrypt() {
   2006       mocklog do_decrypt "$@"
   2007       %putsn 'secret data'
   2008     }
   2009 
   2010     do_encrypt() {
   2011       @cat >/dev/null
   2012       mocklog do_encrypt "$@"
   2013     }
   2014 
   2015     mktemp() { %putsn "${2-$1}"; }
   2016     mv() { mocklog mv "$@"; }
   2017     scm_add() { mocklog scm_add "$@"; }
   2018     scm_begin() { mocklog scm_begin "$@"; }
   2019     scm_commit() { mocklog scm_commit "$@"; }
   2020 
   2021     setup() {
   2022       @mkdir -p "${PREFIX}/subdir/other" "${PREFIX}/subdir/subsub"
   2023       %putsn id >"${PREFIX}/subdir/other/.age-recipients"
   2024       %putsn data >"${PREFIX}/root.age"
   2025       %putsn data >"${PREFIX}/subdir/middle.age"
   2026       %putsn data >"${PREFIX}/subdir/other/unrelated.age"
   2027       %putsn data >"${PREFIX}/subdir/subsub/deep.age"
   2028     }
   2029 
   2030     cleanup() {
   2031       @rm -rf "${PREFIX}"
   2032     }
   2033 
   2034     BeforeEach setup
   2035     AfterEach cleanup
   2036 
   2037     It 're-encrypts a single file'
   2038       DECISION=default
   2039       result() {
   2040         %text:expand
   2041         #|$ scm_begin
   2042         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   2043         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   2044         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   2045         #|$ scm_add subdir/subsub/deep.age
   2046         #|$ scm_commit Re-encrypt subdir/subsub/deep
   2047       }
   2048       When call do_reencrypt subdir/subsub/deep
   2049       The status should be success
   2050       The output should be blank
   2051       The error should equal "$(result)"
   2052     End
   2053 
   2054     It 'recursively re-encrypts a directory'
   2055       DECISION=default
   2056       RECURSIVE=no
   2057       result() {
   2058         %text:expand
   2059         #|$ scm_begin
   2060         #|$ do_decrypt ${PREFIX}/subdir/middle.age
   2061         #|$ do_encrypt subdir/middle-XXXXXXXXX.age
   2062         #|$ mv -f -- ${PREFIX}/subdir/middle-XXXXXXXXX.age ${PREFIX}/subdir/middle.age
   2063         #|$ scm_add subdir/middle.age
   2064         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   2065         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   2066         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   2067         #|$ scm_add subdir/subsub/deep.age
   2068         #|$ scm_commit Re-encrypt subdir/
   2069       }
   2070       When call do_reencrypt subdir/
   2071       The status should be success
   2072       The output should be blank
   2073       The error should equal "$(result)"
   2074     End
   2075 
   2076     It 'recursively and deeply re-encrypts a directory'
   2077       DECISION=default
   2078       RECURSIVE=yes
   2079       result() {
   2080         %text:expand
   2081         #|$ scm_begin
   2082         #|$ do_decrypt ${PREFIX}/subdir/middle.age
   2083         #|$ do_encrypt subdir/middle-XXXXXXXXX.age
   2084         #|$ mv -f -- ${PREFIX}/subdir/middle-XXXXXXXXX.age ${PREFIX}/subdir/middle.age
   2085         #|$ scm_add subdir/middle.age
   2086         #|$ do_decrypt ${PREFIX}/subdir/other/unrelated.age
   2087         #|$ do_encrypt subdir/other/unrelated-XXXXXXXXX.age
   2088         #|$ mv -f -- ${PREFIX}/subdir/other/unrelated-XXXXXXXXX.age ${PREFIX}/subdir/other/unrelated.age
   2089         #|$ scm_add subdir/other/unrelated.age
   2090         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   2091         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   2092         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   2093         #|$ scm_add subdir/subsub/deep.age
   2094         #|$ scm_commit Re-encrypt subdir/
   2095       }
   2096       When call do_reencrypt subdir/
   2097       The status should be success
   2098       The output should be blank
   2099       The error should equal "$(result)"
   2100     End
   2101 
   2102     It 'recursively re-encrypts the whole store as /'
   2103       DECISION=default
   2104       RECURSIVE=no
   2105       result() {
   2106         %text:expand
   2107         #|$ scm_begin
   2108         #|$ do_decrypt ${PREFIX}/root.age
   2109         #|$ do_encrypt root-XXXXXXXXX.age
   2110         #|$ mv -f -- ${PREFIX}/root-XXXXXXXXX.age ${PREFIX}/root.age
   2111         #|$ scm_add root.age
   2112         #|$ do_decrypt ${PREFIX}/subdir/middle.age
   2113         #|$ do_encrypt subdir/middle-XXXXXXXXX.age
   2114         #|$ mv -f -- ${PREFIX}/subdir/middle-XXXXXXXXX.age ${PREFIX}/subdir/middle.age
   2115         #|$ scm_add subdir/middle.age
   2116         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   2117         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   2118         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   2119         #|$ scm_add subdir/subsub/deep.age
   2120         #|$ scm_commit Re-encrypt /
   2121       }
   2122       When call do_reencrypt /
   2123       The status should be success
   2124       The output should be blank
   2125       The error should equal "$(result)"
   2126     End
   2127 
   2128     It 'recursively re-encrypts the whole store as the empty string'
   2129       DECISION=default
   2130       RECURSIVE=no
   2131       result() {
   2132         %text:expand
   2133         #|$ scm_begin
   2134         #|$ do_decrypt ${PREFIX}/root.age
   2135         #|$ do_encrypt root-XXXXXXXXX.age
   2136         #|$ mv -f -- ${PREFIX}/root-XXXXXXXXX.age ${PREFIX}/root.age
   2137         #|$ scm_add root.age
   2138         #|$ do_decrypt ${PREFIX}/subdir/middle.age
   2139         #|$ do_encrypt subdir/middle-XXXXXXXXX.age
   2140         #|$ mv -f -- ${PREFIX}/subdir/middle-XXXXXXXXX.age ${PREFIX}/subdir/middle.age
   2141         #|$ scm_add subdir/middle.age
   2142         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   2143         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   2144         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   2145         #|$ scm_add subdir/subsub/deep.age
   2146         #|$ scm_commit Re-encrypt /
   2147       }
   2148       When call do_reencrypt ''
   2149       The status should be success
   2150       The output should be blank
   2151       The error should equal "$(result)"
   2152     End
   2153 
   2154     It 'asks for confirmation before each file'
   2155       DECISION=interactive
   2156       RECURSIVE=no
   2157       YESNO_NEXT=n
   2158       yesno() {
   2159         mocklog yesno "$@"
   2160         ANSWER="${YESNO_NEXT}"
   2161         YESNO_NEXT=y
   2162       }
   2163       result() {
   2164         %text:expand
   2165         #|$ scm_begin
   2166         #|$ yesno Re-encrypt subdir/middle?
   2167         #|$ yesno Re-encrypt subdir/subsub/deep?
   2168         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   2169         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   2170         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   2171         #|$ scm_add subdir/subsub/deep.age
   2172         #|$ scm_commit Re-encrypt subdir/
   2173       }
   2174       When call do_reencrypt subdir
   2175       The status should be success
   2176       The output should be blank
   2177       The error should equal "$(result)"
   2178     End
   2179 
   2180     It 'asks for confirmation before each file in deep re-encryption'
   2181       DECISION=interactive
   2182       RECURSIVE=yes
   2183       YESNO_NEXT=n
   2184       yesno() {
   2185         mocklog yesno "$@"
   2186         ANSWER="${YESNO_NEXT}"
   2187         YESNO_NEXT=y
   2188       }
   2189       result() {
   2190         %text:expand
   2191         #|$ scm_begin
   2192         #|$ yesno Re-encrypt subdir/middle?
   2193         #|$ yesno Re-encrypt subdir/other/unrelated?
   2194         #|$ do_decrypt ${PREFIX}/subdir/other/unrelated.age
   2195         #|$ do_encrypt subdir/other/unrelated-XXXXXXXXX.age
   2196         #|$ mv -f -- ${PREFIX}/subdir/other/unrelated-XXXXXXXXX.age ${PREFIX}/subdir/other/unrelated.age
   2197         #|$ scm_add subdir/other/unrelated.age
   2198         #|$ yesno Re-encrypt subdir/subsub/deep?
   2199         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   2200         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   2201         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   2202         #|$ scm_add subdir/subsub/deep.age
   2203         #|$ scm_commit Re-encrypt subdir/
   2204       }
   2205       When call do_reencrypt subdir
   2206       The status should be success
   2207       The output should be blank
   2208       The error should equal "$(result)"
   2209     End
   2210 
   2211     It 'reports a non-existent directory'
   2212       result() {
   2213         %text
   2214         #|$ scm_begin
   2215         #|Error: non-existent/ is not in the password store.
   2216       }
   2217       When run do_reencrypt non-existent/
   2218       The output should be blank
   2219       The error should equal "$(result)"
   2220       The status should equal 1
   2221     End
   2222 
   2223     It 'reports a non-existent file'
   2224       result() {
   2225         %text
   2226         #|$ scm_begin
   2227         #|Error: non-existent is not in the password store.
   2228       }
   2229       When run do_reencrypt non-existent
   2230       The output should be blank
   2231       The error should equal "$(result)"
   2232       The status should equal 1
   2233     End
   2234   End
   2235 
   2236   Describe 'do_show'
   2237     cleartext(){
   2238       %text
   2239       #|password line
   2240       #|extra line 1
   2241       #|extra line 2
   2242     }
   2243 
   2244     It 'shows a secret on standard output'
   2245       cat() { @cat; }
   2246       Data cleartext
   2247       SHOW=text
   2248       When call do_show
   2249       The status should be success
   2250       The output should equal "$(cleartext)"
   2251     End
   2252 
   2253     It 'pastes a secret into the clipboard'
   2254       head() { @head "$@"; }
   2255       tail() { @tail "$@"; }
   2256       tr() { @tr "$@"; }
   2257       platform_clip() { @cat >&2; }
   2258       Data cleartext
   2259       SELECTED_LINE=1
   2260       SHOW=clip
   2261       When call do_show title
   2262       The status should be success
   2263       The output should be blank
   2264       The error should equal 'password line'
   2265     End
   2266 
   2267     It 'shows a secret as a QR-code'
   2268       head() { @head "$@"; }
   2269       tail() { @tail "$@"; }
   2270       tr() { @tr "$@"; }
   2271       platform_qrcode() { @cat >&2; }
   2272       Data cleartext
   2273       SELECTED_LINE=1
   2274       SHOW=qrcode
   2275       When call do_show title
   2276       The status should be success
   2277       The output should be blank
   2278       The error should equal 'password line'
   2279     End
   2280 
   2281     It 'aborts on unexpected SHOW'
   2282       SHOW=bad
   2283       Data cleartext
   2284       When run do_show title
   2285       The output should be blank
   2286       The error should equal 'Unexpected SHOW value "bad"'
   2287       The status should equal 1
   2288     End
   2289   End
   2290 
   2291   Describe 'do_tree'
   2292     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   2293     BLUE_TEXT='(B)'
   2294     NORMAL_TEXT='(N)'
   2295     RED_TEXT='(R)'
   2296     TREE_T='T_'
   2297     TREE_L='L_'
   2298     TREE_I='I_'
   2299     TREE__='__'
   2300 
   2301     grep() { @grep "$@"; }
   2302 
   2303     setup() {
   2304       @mkdir -p "${PREFIX}/subdir/subsub" "${PREFIX}/empty" "${PREFIX}/other"
   2305       %putsn data >"${PREFIX}/root.age"
   2306       %putsn data >"${PREFIX}/subdir/hidden"
   2307       %putsn data >"${PREFIX}/subdir/subsub/old.gpg"
   2308       %putsn data >"${PREFIX}/other/upper.age"
   2309       %putsn data >"${PREFIX}/other/lower.age"
   2310     }
   2311 
   2312     cleanup() {
   2313       @rm -rf "${PREFIX}"
   2314     }
   2315 
   2316     BeforeEach setup
   2317     AfterEach cleanup
   2318 
   2319     It 'displays everything without a pattern'
   2320       result() {
   2321         %text
   2322         #|T_(B)empty(N)
   2323         #|T_(B)other(N)
   2324         #|I_T_lower
   2325         #|I_L_upper
   2326         #|T_root
   2327         #|L_(B)subdir(N)
   2328         #|__L_(B)subsub(N)
   2329         #|____L_(R)old(N)
   2330       }
   2331       When call do_tree ''
   2332       The status should be success
   2333       The output should equal "$(result)"
   2334     End
   2335 
   2336     It 'displays matching files and their non-matching parents'
   2337       result() {
   2338         %text
   2339         #|T_(B)other(N)
   2340         #|I_L_lower
   2341         #|L_(B)subdir(N)
   2342         #|__L_(B)subsub(N)
   2343         #|____L_(R)old(N)
   2344       }
   2345       When call do_tree '' -i L
   2346       The status should be success
   2347       The output should equal "$(result)"
   2348     End
   2349 
   2350     It 'does not display matching directories'
   2351       When call do_tree '' t
   2352       The status should be success
   2353       The output should equal 'L_root'
   2354     End
   2355 
   2356     It 'does not consider file extension when matching'
   2357       When call do_tree '' g
   2358       The status should be success
   2359       The output should equal ''
   2360     End
   2361 
   2362     It 'might not display anything'
   2363       When call do_tree '' z
   2364       The status should be success
   2365       The output should equal ''
   2366     End
   2367 
   2368     It 'defensively aborts on invalid prefix start'
   2369       When run do_tree_prefix '_XI_'
   2370       The output should be blank
   2371       The error should equal 'Invalid tree prefix: "XI_"'
   2372       The status should equal 1
   2373     End
   2374 
   2375     It 'defensively aborts on invalid prefix end'
   2376       When run do_tree_prefix '_IX'
   2377       The output should be blank
   2378       The error should equal 'Invalid tree prefix: "X"'
   2379       The status should equal 1
   2380     End
   2381   End
   2382 End