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


      1 # pashage - age-backed POSIX password manager
      2 # Copyright (C) 2024  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 EDITOR exit code'
    862       exit42() { mocklog editor "$@"; return 42; }
    863       EDITOR=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}/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       start_do_grep(){
   1449         ( cd "${PREFIX}" && do_grep '' "$@" )
   1450       }
   1451       When call start_do_grep ot
   1452       The status should be success
   1453       The output should equal "$(result)"
   1454     End
   1455 
   1456     It 'outputs all the matching lines'
   1457       result(){
   1458         %text
   1459         #|(B)subdir/(G)match(N):
   1460         #|other
   1461         #|suffix
   1462       }
   1463       start_do_grep(){
   1464         ( cd "${PREFIX}" && do_grep '' "$@" )
   1465       }
   1466       When call start_do_grep -vea
   1467       The status should be success
   1468       The output should equal "$(result)"
   1469     End
   1470   End
   1471 
   1472   Describe 'do_init'
   1473     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1474     DECISION=default
   1475     OVERWRITE=no
   1476 
   1477     do_reencrypt_dir() { mocklog do_reencrypt_dir "$@"; }
   1478     mkdir() { mocklog mkdir "$@"; @mkdir "$@"; }
   1479     scm_add() { mocklog scm_add "$@"; }
   1480     scm_begin() { mocklog scm_begin "$@"; }
   1481     scm_commit() { mocklog scm_commit "$@"; }
   1482 
   1483     cleanup() {
   1484       @rm -rf "${PREFIX}"
   1485     }
   1486 
   1487     AfterEach cleanup
   1488 
   1489     It 'initializes the store'
   1490       result() {
   1491         %text:expand
   1492         #|$ mkdir -p -- ${PREFIX}
   1493         #|$ scm_begin
   1494         #|$ scm_add .age-recipients
   1495         #|$ do_reencrypt_dir ${PREFIX}
   1496         #|$ scm_commit Set age recipients at store root
   1497       }
   1498       When call do_init '' identity
   1499       The status should be success
   1500       The output should equal 'Password store recipients set at store root'
   1501       The error should equal "$(result)"
   1502       The file "${PREFIX}/.age-recipients" should be exist
   1503       The contents of the file "${PREFIX}/.age-recipients" should equal \
   1504         'identity'
   1505     End
   1506 
   1507     It 'initializes a subdirectory'
   1508       result() {
   1509         %text:expand
   1510         #|$ mkdir -p -- ${PREFIX}/sub
   1511         #|$ scm_begin
   1512         #|$ scm_add sub/.age-recipients
   1513         #|$ do_reencrypt_dir ${PREFIX}/sub
   1514         #|$ scm_commit Set age recipients at sub
   1515       }
   1516       two_id() {
   1517         %text
   1518         #|identity 1
   1519         #|identity 2
   1520       }
   1521       When call do_init sub 'identity 1' 'identity 2'
   1522       The status should be success
   1523       The output should equal 'Password store recipients set at sub'
   1524       The error should equal "$(result)"
   1525       The file "${PREFIX}/sub/.age-recipients" should be exist
   1526       The contents of the file "${PREFIX}/sub/.age-recipients" should equal \
   1527         "$(two_id)"
   1528     End
   1529 
   1530     It 'can initialize without re-encryption'
   1531       DECISION=keep
   1532       result() {
   1533         %text:expand
   1534         #|$ mkdir -p -- ${PREFIX}
   1535         #|$ scm_begin
   1536         #|$ scm_add .age-recipients
   1537         #|$ scm_commit Set age recipients at store root
   1538       }
   1539       When call do_init '' identity
   1540       The status should be success
   1541       The output should equal 'Password store recipients set at store root'
   1542       The error should equal "$(result)"
   1543       The file "${PREFIX}/.age-recipients" should be exist
   1544       The contents of the file "${PREFIX}/.age-recipients" should equal \
   1545         'identity'
   1546     End
   1547   End
   1548 
   1549   Describe 'do_insert'
   1550     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1551 
   1552     do_encrypt() {
   1553       mocklog do_encrypt "$@"
   1554       @sed 's/^/> /' >&2
   1555     }
   1556 
   1557     dirname() { @dirname "$@"; }
   1558     head() { @head "$@"; }
   1559 
   1560     mkdir() { mocklog mkdir "$@"; }
   1561     scm_add() { mocklog scm_add "$@"; }
   1562     scm_begin() { mocklog scm_begin "$@"; }
   1563     scm_commit() { mocklog scm_commit "$@"; }
   1564 
   1565     setup() {
   1566       @mkdir -p "${PREFIX}"
   1567       %putsn data >"${PREFIX}/existing.age"
   1568     }
   1569 
   1570     cleanup() {
   1571       @rm -rf "${PREFIX}"
   1572     }
   1573 
   1574     BeforeEach setup
   1575     AfterEach cleanup
   1576 
   1577     It 'inserts a single line from standard input'
   1578       ECHO=yes
   1579       MULTILINE=no
   1580       OVERWRITE=yes
   1581       result() {
   1582         %text:expand
   1583         #|$ scm_begin
   1584         #|$ mkdir -p -- ${PREFIX}/subdir
   1585         #|$ do_encrypt subdir/new.age
   1586         #|> line 1
   1587         #|$ scm_add subdir/new.age
   1588         #|$ scm_commit Add given password for subdir/new to store.
   1589       }
   1590       Data
   1591         #|line 1
   1592         #|line 2
   1593         #|line 3
   1594       End
   1595 
   1596       When call do_insert 'subdir/new'
   1597       The status should be success
   1598       The output should equal 'Enter password for subdir/new: '
   1599       The error should equal "$(result)"
   1600     End
   1601 
   1602     It 'inserts the standard input until the first blank line'
   1603       MULTILINE=yes
   1604       OVERWRITE=yes
   1605       o_result() { %text
   1606         #|Enter contents of subdir/new and
   1607         #|press Ctrl+D or enter an empty line when finished:
   1608       }
   1609       result() {
   1610         %text:expand
   1611         #|$ scm_begin
   1612         #|$ mkdir -p -- ${PREFIX}/subdir
   1613         #|$ do_encrypt subdir/new.age
   1614         #|> line 1
   1615         #|> line 2
   1616         #|$ scm_add subdir/new.age
   1617         #|$ scm_commit Add given password for subdir/new to store.
   1618       }
   1619       Data
   1620         #|line 1
   1621         #|line 2
   1622         #|
   1623         #|line 3
   1624         #|line 4
   1625       End
   1626 
   1627       When call do_insert 'subdir/new'
   1628       The status should be success
   1629       The output should equal "$(o_result)"
   1630       The error should equal "$(result)"
   1631     End
   1632 
   1633     It 'inserts the whole standard input without blank line'
   1634       MULTILINE=yes
   1635       OVERWRITE=yes
   1636       o_result() { %text
   1637         #|Enter contents of subdir/new and
   1638         #|press Ctrl+D or enter an empty line when finished:
   1639       }
   1640       result() {
   1641         %text:expand
   1642         #|$ scm_begin
   1643         #|$ mkdir -p -- ${PREFIX}/subdir
   1644         #|$ do_encrypt subdir/new.age
   1645         #|> line 1
   1646         #|> line 2
   1647         #|> line 3
   1648         #|$ scm_add subdir/new.age
   1649         #|$ scm_commit Add given password for subdir/new to store.
   1650       }
   1651       Data
   1652         #|line 1
   1653         #|line 2
   1654         #|line 3
   1655       End
   1656 
   1657       When call do_insert 'subdir/new'
   1658       The status should be success
   1659       The output should equal "$(o_result)"
   1660       The error should equal "$(result)"
   1661     End
   1662 
   1663     It 'checks password confirmation before inserting it'
   1664       ECHO=no
   1665       MULTILINE=no
   1666       OVERWRITE=yes
   1667       stty() { true; }
   1668       o_result() {
   1669         %text | @sed 's/\$$//'
   1670         #|Enter password for subdir/new:  $
   1671         #|Retype password for subdir/new: $
   1672         #|Passwords don't match$
   1673         #|Enter password for subdir/new:  $
   1674         #|Retype password for subdir/new: $
   1675       }
   1676       e_result() {
   1677         %text:expand
   1678         #|$ scm_begin
   1679         #|$ mkdir -p -- ${PREFIX}/subdir
   1680         #|$ do_encrypt subdir/new.age
   1681         #|> line 3
   1682         #|$ scm_add subdir/new.age
   1683         #|$ scm_commit Add given password for subdir/new to store.
   1684       }
   1685       Data
   1686         #|line 1
   1687         #|line 2
   1688         #|line 3
   1689         #|line 3
   1690         #|line 5
   1691       End
   1692 
   1693       When call do_insert 'subdir/new'
   1694       The status should be success
   1695       The output should equal "$(o_result)"
   1696       The error should equal "$(e_result)"
   1697     End
   1698 
   1699     It 'asks confirmation before overwriting'
   1700       MULTILINE=yes
   1701       OVERWRITE=no
   1702       yesno() {
   1703         mocklog yesno "$@"
   1704         ANSWER=y
   1705       }
   1706       o_result() { %text
   1707         #|Enter contents of existing and
   1708         #|press Ctrl+D or enter an empty line when finished:
   1709       }
   1710       result() {
   1711         %text:expand
   1712         #|$ yesno An entry already exists for existing. Overwrite it?
   1713         #|$ scm_begin
   1714         #|$ mkdir -p -- ${PREFIX}
   1715         #|$ do_encrypt existing.age
   1716         #|> password
   1717         #|$ scm_add existing.age
   1718         #|$ scm_commit Add given password for existing to store.
   1719       }
   1720       Data 'password'
   1721 
   1722       When call do_insert 'existing'
   1723       The status should be success
   1724       The output should equal "$(o_result)"
   1725       The error should equal "$(result)"
   1726       The variable OVERWRITE should equal once
   1727     End
   1728 
   1729     It 'does not overwrite without confirmation'
   1730       MULTILINE=yes
   1731       OVERWRITE=no
   1732       yesno() {
   1733         mocklog yesno "$@"
   1734         ANSWER=n
   1735       }
   1736       result() {
   1737         %text:expand
   1738         #|$ yesno An entry already exists for existing. Overwrite it?
   1739       }
   1740       Data 'password'
   1741 
   1742       When call do_insert 'existing'
   1743       The status should be success
   1744       The output should be blank
   1745       The error should equal "$(result)"
   1746     End
   1747 
   1748     It 'does not ask confirmation before overwriting when forced'
   1749       MULTILINE=yes
   1750       OVERWRITE=yes
   1751       yesno() {
   1752         mocklog yesno "$@"
   1753         ANSWER=y
   1754       }
   1755       o_result() { %text
   1756         #|Enter contents of existing and
   1757         #|press Ctrl+D or enter an empty line when finished:
   1758       }
   1759       result() {
   1760         %text:expand
   1761         #|$ scm_begin
   1762         #|$ mkdir -p -- ${PREFIX}
   1763         #|$ do_encrypt existing.age
   1764         #|> password
   1765         #|$ scm_add existing.age
   1766         #|$ scm_commit Add given password for existing to store.
   1767       }
   1768       Data 'password'
   1769 
   1770       When call do_insert 'existing'
   1771       The status should be success
   1772       The output should equal "$(o_result)"
   1773       The error should equal "$(result)"
   1774     End
   1775   End
   1776 
   1777   Describe 'do_list_or_show'
   1778     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1779 
   1780     do_decrypt() {
   1781       mocklog do_decrypt "$@"
   1782       %putsn data
   1783     }
   1784 
   1785     do_decrypt_gpg() {
   1786       mocklog do_decrypt_gpg "$@"
   1787       %putsn data
   1788     }
   1789 
   1790     do_show() {
   1791       @cat >/dev/null
   1792       mocklog do_show "$@"
   1793     }
   1794 
   1795     do_tree() { mocklog do_tree "$@"; }
   1796 
   1797     setup() {
   1798       @mkdir -p "${PREFIX}/subdir/subsub" "${PREFIX}/empty" "${PREFIX}/other"
   1799       %putsn data >"${PREFIX}/root.age"
   1800       %putsn data >"${PREFIX}/subdir/hidden"
   1801       %putsn data >"${PREFIX}/subdir/subsub/old.gpg"
   1802       %putsn data >"${PREFIX}/other/lower.age"
   1803     }
   1804 
   1805     cleanup() {
   1806       @rm -rf "${PREFIX}"
   1807     }
   1808 
   1809     BeforeEach setup
   1810     AfterEach cleanup
   1811 
   1812     It 'lists the whole store'
   1813       When call do_list_or_show ''
   1814       The status should be success
   1815       The output should be blank
   1816       The error should equal "$ do_tree ${PREFIX} Password Store"
   1817     End
   1818 
   1819     It 'shows a decrypted age file'
   1820       result() {
   1821         %text:expand
   1822         #|$ do_decrypt ${PREFIX}/other/lower.age
   1823         #|$ do_show other/lower
   1824       }
   1825       When call do_list_or_show 'other/lower'
   1826       The status should be success
   1827       The error should equal "$(result)"
   1828     End
   1829 
   1830     It 'shows a decrypted gpg file'
   1831       result() {
   1832         %text:expand
   1833         #|$ do_decrypt_gpg ${PREFIX}/subdir/subsub/old.gpg
   1834         #|$ do_show subdir/subsub/old
   1835       }
   1836       When call do_list_or_show 'subdir/subsub/old'
   1837       The status should be success
   1838       The error should equal "$(result)"
   1839     End
   1840 
   1841     It 'lists a subdirectory'
   1842       When call do_list_or_show 'subdir'
   1843       The status should be success
   1844       The output should be blank
   1845       The error should equal "$ do_tree ${PREFIX}/subdir subdir"
   1846     End
   1847 
   1848     It 'does not show a non-encrypted file'
   1849       When run do_list_or_show 'subdir/hidden'
   1850       The output should be blank
   1851       The error should equal \
   1852         'Error: subdir/hidden is not in the password store.'
   1853       The status should equal 1
   1854     End
   1855   End
   1856 
   1857   Describe 'do_reencrypt'
   1858     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   1859     DECISION=default
   1860 
   1861     do_decrypt() {
   1862       mocklog do_decrypt "$@"
   1863       %putsn 'secret data'
   1864     }
   1865 
   1866     do_encrypt() {
   1867       @cat >/dev/null
   1868       mocklog do_encrypt "$@"
   1869     }
   1870 
   1871     mktemp() { %putsn "${2-$1}"; }
   1872     mv() { mocklog mv "$@"; }
   1873     scm_add() { mocklog scm_add "$@"; }
   1874     scm_begin() { mocklog scm_begin "$@"; }
   1875     scm_commit() { mocklog scm_commit "$@"; }
   1876 
   1877     setup() {
   1878       @mkdir -p "${PREFIX}/subdir/subsub"
   1879       %putsn data >"${PREFIX}/root.age"
   1880       %putsn data >"${PREFIX}/subdir/middle.age"
   1881       %putsn data >"${PREFIX}/subdir/subsub/deep.age"
   1882     }
   1883 
   1884     cleanup() {
   1885       @rm -rf "${PREFIX}"
   1886     }
   1887 
   1888     BeforeEach setup
   1889     AfterEach cleanup
   1890 
   1891     It 're-encrypts a single file'
   1892       result() {
   1893         %text:expand
   1894         #|$ scm_begin
   1895         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   1896         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   1897         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   1898         #|$ scm_add subdir/subsub/deep.age
   1899         #|$ scm_commit Re-encrypt subdir/subsub/deep
   1900       }
   1901       When call do_reencrypt subdir/subsub/deep
   1902       The status should be success
   1903       The output should be blank
   1904       The error should equal "$(result)"
   1905     End
   1906 
   1907     It 'recursively re-encrypts a directory'
   1908       result() {
   1909         %text:expand
   1910         #|$ scm_begin
   1911         #|$ do_decrypt ${PREFIX}/subdir/middle.age
   1912         #|$ do_encrypt subdir/middle-XXXXXXXXX.age
   1913         #|$ mv -f -- ${PREFIX}/subdir/middle-XXXXXXXXX.age ${PREFIX}/subdir/middle.age
   1914         #|$ scm_add subdir/middle.age
   1915         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   1916         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   1917         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   1918         #|$ scm_add subdir/subsub/deep.age
   1919         #|$ scm_commit Re-encrypt subdir/
   1920       }
   1921       When call do_reencrypt subdir/
   1922       The status should be success
   1923       The output should be blank
   1924       The error should equal "$(result)"
   1925     End
   1926 
   1927     It 'recursively re-encrypts the whole store as /'
   1928       result() {
   1929         %text:expand
   1930         #|$ scm_begin
   1931         #|$ do_decrypt ${PREFIX}/root.age
   1932         #|$ do_encrypt root-XXXXXXXXX.age
   1933         #|$ mv -f -- ${PREFIX}/root-XXXXXXXXX.age ${PREFIX}/root.age
   1934         #|$ scm_add root.age
   1935         #|$ do_decrypt ${PREFIX}/subdir/middle.age
   1936         #|$ do_encrypt subdir/middle-XXXXXXXXX.age
   1937         #|$ mv -f -- ${PREFIX}/subdir/middle-XXXXXXXXX.age ${PREFIX}/subdir/middle.age
   1938         #|$ scm_add subdir/middle.age
   1939         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   1940         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   1941         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   1942         #|$ scm_add subdir/subsub/deep.age
   1943         #|$ scm_commit Re-encrypt /
   1944       }
   1945       When call do_reencrypt /
   1946       The status should be success
   1947       The output should be blank
   1948       The error should equal "$(result)"
   1949     End
   1950 
   1951     It 'recursively re-encrypts the whole store as the empty string'
   1952       result() {
   1953         %text:expand
   1954         #|$ scm_begin
   1955         #|$ do_decrypt ${PREFIX}/root.age
   1956         #|$ do_encrypt root-XXXXXXXXX.age
   1957         #|$ mv -f -- ${PREFIX}/root-XXXXXXXXX.age ${PREFIX}/root.age
   1958         #|$ scm_add root.age
   1959         #|$ do_decrypt ${PREFIX}/subdir/middle.age
   1960         #|$ do_encrypt subdir/middle-XXXXXXXXX.age
   1961         #|$ mv -f -- ${PREFIX}/subdir/middle-XXXXXXXXX.age ${PREFIX}/subdir/middle.age
   1962         #|$ scm_add subdir/middle.age
   1963         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   1964         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   1965         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   1966         #|$ scm_add subdir/subsub/deep.age
   1967         #|$ scm_commit Re-encrypt /
   1968       }
   1969       When call do_reencrypt ''
   1970       The status should be success
   1971       The output should be blank
   1972       The error should equal "$(result)"
   1973     End
   1974 
   1975     It 'asks for confirmation before each file'
   1976       DECISION=interactive
   1977       YESNO_NEXT=n
   1978       yesno() {
   1979         mocklog yesno "$@"
   1980         ANSWER="${YESNO_NEXT}"
   1981         YESNO_NEXT=y
   1982       }
   1983       result() {
   1984         %text:expand
   1985         #|$ scm_begin
   1986         #|$ yesno Re-encrypt subdir/middle?
   1987         #|$ yesno Re-encrypt subdir/subsub/deep?
   1988         #|$ do_decrypt ${PREFIX}/subdir/subsub/deep.age
   1989         #|$ do_encrypt subdir/subsub/deep-XXXXXXXXX.age
   1990         #|$ mv -f -- ${PREFIX}/subdir/subsub/deep-XXXXXXXXX.age ${PREFIX}/subdir/subsub/deep.age
   1991         #|$ scm_add subdir/subsub/deep.age
   1992         #|$ scm_commit Re-encrypt subdir/
   1993       }
   1994       When call do_reencrypt subdir
   1995       The status should be success
   1996       The output should be blank
   1997       The error should equal "$(result)"
   1998     End
   1999 
   2000     It 'reports a non-existent directory'
   2001       result() {
   2002         %text
   2003         #|$ scm_begin
   2004         #|Error: non-existent/ is not in the password store.
   2005       }
   2006       When run do_reencrypt non-existent/
   2007       The output should be blank
   2008       The error should equal "$(result)"
   2009       The status should equal 1
   2010     End
   2011 
   2012     It 'reports a non-existent file'
   2013       result() {
   2014         %text
   2015         #|$ scm_begin
   2016         #|Error: non-existent is not in the password store.
   2017       }
   2018       When run do_reencrypt non-existent
   2019       The output should be blank
   2020       The error should equal "$(result)"
   2021       The status should equal 1
   2022     End
   2023   End
   2024 
   2025   Describe 'do_show'
   2026     cleartext(){
   2027       %text
   2028       #|password line
   2029       #|extra line 1
   2030       #|extra line 2
   2031     }
   2032 
   2033     It 'shows a secret on standard output'
   2034       cat() { @cat; }
   2035       Data cleartext
   2036       SHOW=text
   2037       When call do_show
   2038       The status should be success
   2039       The output should equal "$(cleartext)"
   2040     End
   2041 
   2042     It 'pastes a secret into the clipboard'
   2043       head() { @head "$@"; }
   2044       tail() { @tail "$@"; }
   2045       tr() { @tr "$@"; }
   2046       platform_clip() { @cat >&2; }
   2047       Data cleartext
   2048       SELECTED_LINE=1
   2049       SHOW=clip
   2050       When call do_show title
   2051       The status should be success
   2052       The output should be blank
   2053       The error should equal 'password line'
   2054     End
   2055 
   2056     It 'shows a secret as a QR-code'
   2057       head() { @head "$@"; }
   2058       tail() { @tail "$@"; }
   2059       tr() { @tr "$@"; }
   2060       platform_qrcode() { @cat >&2; }
   2061       Data cleartext
   2062       SELECTED_LINE=1
   2063       SHOW=qrcode
   2064       When call do_show title
   2065       The status should be success
   2066       The output should be blank
   2067       The error should equal 'password line'
   2068     End
   2069 
   2070     It 'aborts on unexpected SHOW'
   2071       SHOW=bad
   2072       Data cleartext
   2073       When run do_show title
   2074       The output should be blank
   2075       The error should equal 'Unexpected SHOW value "bad"'
   2076       The status should equal 1
   2077     End
   2078   End
   2079 
   2080   Describe 'do_tree'
   2081     PREFIX="${SHELLSPEC_WORKDIR}/prefix"
   2082     BLUE_TEXT='(B)'
   2083     NORMAL_TEXT='(N)'
   2084     RED_TEXT='(R)'
   2085     TREE_T='T_'
   2086     TREE_L='L_'
   2087     TREE_I='I_'
   2088     TREE__='__'
   2089 
   2090     grep() { @grep "$@"; }
   2091 
   2092     setup() {
   2093       @mkdir -p "${PREFIX}/subdir/subsub" "${PREFIX}/empty" "${PREFIX}/other"
   2094       %putsn data >"${PREFIX}/root.age"
   2095       %putsn data >"${PREFIX}/subdir/hidden"
   2096       %putsn data >"${PREFIX}/subdir/subsub/old.gpg"
   2097       %putsn data >"${PREFIX}/other/lower.age"
   2098     }
   2099 
   2100     cleanup() {
   2101       @rm -rf "${PREFIX}"
   2102     }
   2103 
   2104     BeforeEach setup
   2105     AfterEach cleanup
   2106 
   2107     It 'displays everything without a pattern'
   2108       result() {
   2109         %text
   2110         #|Title
   2111         #|T_(B)empty(N)
   2112         #|T_(B)other(N)
   2113         #|I_L_lower
   2114         #|T_root
   2115         #|L_(B)subdir(N)
   2116         #|__L_(B)subsub(N)
   2117         #|____L_(R)old(N)
   2118       }
   2119       When call do_tree "${PREFIX}" 'Title'
   2120       The status should be success
   2121       The output should equal "$(result)"
   2122     End
   2123 
   2124     It 'displays matching files and their non-matching parents'
   2125       result() {
   2126         %text
   2127         #|Title
   2128         #|T_(B)other(N)
   2129         #|I_L_lower
   2130         #|L_(B)subdir(N)
   2131         #|__L_(B)subsub(N)
   2132         #|____L_(R)old(N)
   2133       }
   2134       When call do_tree "${PREFIX}" 'Title' -i L
   2135       The status should be success
   2136       The output should equal "$(result)"
   2137     End
   2138 
   2139     It 'does not display matching directories'
   2140       result() {
   2141         %text
   2142         #|Title
   2143         #|L_root
   2144       }
   2145       When call do_tree "${PREFIX}" 'Title' t
   2146       The status should be success
   2147       The output should equal "$(result)"
   2148     End
   2149 
   2150     It 'might not display anything'
   2151       When call do_tree "${PREFIX}" 'Title' z
   2152       The status should be success
   2153       The output should equal ''
   2154     End
   2155 
   2156     It 'does not display an empty title'
   2157       When call do_tree "${PREFIX}" '' t
   2158       The status should be success
   2159       The output should equal 'L_root'
   2160     End
   2161   End
   2162 End