steps.rb (8112B)
1 MOSS="./moss.rb" 2 3 require 'tmpdir' 4 require 'fileutils' 5 6 def shell(s, permit_failure: false) 7 output = IO.popen(["bash", "-c", s], "r", 2 => 1) do |f| f.read end 8 (permit_failure || $?.exitstatus.zero?) or 9 raise "#{$?} : #{@i_see}" 10 output 11 end 12 13 IDENTITY_FILE = "fixtures/keys/me.key" 14 15 def store_path(s) 16 Pathname.new(ENV["MOSS_HOME"]).join("store", s) 17 end 18 19 def add_to_store(path, content) 20 fullpath = Pathname.new(store_path(path)) 21 fullpath.parent.mkpath 22 File.open(fullpath, "wb") do |f| 23 f.write(content) 24 end 25 end 26 27 def recipient_for_identity(identity_file) 28 File.open(identity_file).read.match(/(age1.+)$/).captures.first or 29 raise "can't get pubkey for identity #{identity_file}" 30 end 31 32 Given("I set MOSS_HOME to a unique temporary pathname") do 33 ENV["MOSS_HOME"] = Dir.mktmpdir 34 end 35 36 Given("I am using a temporary password store") do 37 ENV["MOSS_HOME"] = Dir.mktmpdir 38 39 add_to_store("../identity", File.read(IDENTITY_FILE)) 40 add_to_store(".recipients", recipient_for_identity(IDENTITY_FILE)) 41 end 42 43 Given("I am using the example password store") do 44 ENV["MOSS_HOME"] = "fixtures" 45 end 46 47 Given('there is a secret for {string}') do |name| 48 shell "#{MOSS} generate #{name} 20" 49 end 50 51 When("I generate a secret for {string} with length {int}") do |name, length| 52 @stored_secret_name = name 53 @i_see = shell "#{MOSS} generate #{name} #{length}" 54 end 55 56 When("I delete the secret for {string}") do |name| 57 @i_see = shell "#{MOSS} rm #{name}" 58 end 59 60 Then('{string} does not exist') do |path| 61 expect(store_path(path)).not_to exist 62 end 63 64 # yes, this step is a Given and a When with different text 65 Given("there is a secret for {string} with content {string}") do |name, content| 66 @stored_secret_name = name 67 shell "echo -n #{content} | #{MOSS} insert #{name.inspect}" 68 end 69 70 When("I store a secret for {string} with content {string}") do |name, content| 71 @stored_secret_name = name 72 shell "echo -n #{content} | #{MOSS} insert #{name.inspect}" 73 end 74 75 When("I force store a secret for {string} with content {string}") do |name, content| 76 @stored_secret_name = name 77 shell "echo -n #{content} | #{MOSS} insert #{name.inspect} --force" 78 end 79 80 Then("I cannot store a secret for {string} with content {string}") do |name, content| 81 @stored_secret_name = name 82 shell "echo -n #{content} | #{MOSS} insert #{name.inspect}", permit_failure: true 83 warn $? 84 end 85 86 Then("I see a {int} character string") do |len| 87 expect(@i_see.length).to eq len 88 end 89 90 Then("I see the string {string}") do |s| 91 expect(@i_see).to match(s) 92 end 93 94 Then("I see a QR code for {string}") do |s| 95 expect(@i_see).to eq(File.read("#{__dir__}/../fixtures/#{s}.qransi")) 96 end 97 98 Then("{string} plaintext is {string}") do |name, expected| 99 path_name = store_path(name) 100 contents = File.read(path_name) 101 expect(contents).to match /AGE ENCRYPTED FILE/ 102 plaintext = shell "age -i #{IDENTITY_FILE} -d #{path_name.to_s.inspect}" 103 expect(plaintext).to eq expected 104 end 105 106 def decrypt(secret_name, keyfile) 107 path_name = store_path(secret_name + '.age') 108 contents = File.read(path_name) 109 expect(contents).to match /AGE ENCRYPTED FILE/ 110 shell "age -i fixtures/keys/#{keyfile} -d #{path_name.to_s.inspect}" 111 end 112 113 Then("I can decrypt it with key {string} to {string}") do |keyfile, expected| 114 expect(decrypt(@stored_secret_name, keyfile)).to eq expected 115 end 116 117 Then("I can decrypt {string} with key {string} to {string}") do |secret_name, keyfile, expected| 118 expect(decrypt(secret_name, keyfile)).to eq expected 119 end 120 121 Then("I cannot decrypt it with key {string}") do |keyfile| 122 path_name = store_path(@stored_secret_name + '.age') 123 contents = File.read(path_name) 124 expect(contents).to match /AGE ENCRYPTED FILE/ 125 # we expect age to fail, redirect stderr to avoid messing up test output 126 shell "! age -i fixtures/keys/#{keyfile} -d #{path_name.to_s.inspect} 2>&1" 127 end 128 129 Then("{string} plaintext matches {word}") do |name, re| 130 path_name = store_path(name) 131 contents = File.read(path_name) 132 expect(contents).to match /AGE ENCRYPTED FILE/ 133 shell "age -i #{IDENTITY_FILE} -d #{path_name.to_s.inspect}" 134 expect(@i_see).to match Regexp.new(re) 135 end 136 137 When("I view the secret {string}") do |name| 138 @i_see = shell "#{MOSS} cat #{name}" 139 end 140 141 When("I view the secret {string} as QR code") do |name| 142 @i_see = shell "#{MOSS} qr #{name}" 143 end 144 145 When("I search for {string}") do |term| 146 @i_see = shell "#{MOSS} search #{term}" 147 end 148 149 When("I list the secrets") do 150 @i_see = shell "#{MOSS} list" 151 end 152 153 When("I edit {string}") do |name| 154 ENV["EDITOR"] = "./fixtures/fakedit.sh" 155 @i_see = shell "#{MOSS} edit #{name}" 156 end 157 158 Then("the editor opens a temporary file containing {string}") do |expected| 159 magic, pathname, content = @i_see.split("\0") 160 # we test for this magic string to make sure that our fake editor was 161 # invoked 162 expect(magic).to eq('fakedit-magic') 163 expect(pathname).to start_with(ENV['TMPDIR']) 164 expect(content).to eq(expected) 165 end 166 167 Given("there are recipient files in different subtrees") do |table| 168 table.hashes.each do |row| 169 keys = row["identity"].split(/, */) 170 recipients = keys.map { |k| 171 recipient_for_identity("fixtures/keys/#{k}") 172 }.join("\n") 173 add_to_store("#{row['pathname']}/.recipients", recipients) 174 end 175 end 176 177 When('I add a recipient for the identity {string}') do |keyfile| 178 add_to_store(".recipients", 179 store_path(".recipients").read + 180 "\n" + 181 recipient_for_identity("fixtures/keys/#{keyfile}")) 182 end 183 184 185 Given("the store is version-controlled") do 186 shell "cd #{ENV["MOSS_HOME"]}/store && git init --initial-branch=main" 187 end 188 189 Then("the change to {string} is committed to version control") do |name| 190 log = shell "cd #{ENV["MOSS_HOME"]}/store && git log #{name}" 191 expect(log).to match /new secret/ 192 end 193 194 Then("the removal of {string} is committed to version control") do |name| 195 log = shell "cd #{ENV["MOSS_HOME"]}/store && git log --follow -- #{name}" 196 expect(log).to match /removed secret/ 197 end 198 199 Given("I do not specify a store") do 200 ENV.delete('MOSS_HOME') 201 end 202 203 Then("the store directory is under XDG_DATA_HOME") do 204 ENV["XDG_DATA_HOME"] = "/tmp/#{6.times.map { rand(36).to_s(36) }.join}" 205 result = JSON.load(shell "#{MOSS} config") 206 store = result["store"] 207 expect(store).to start_with(ENV["XDG_DATA_HOME"]) 208 end 209 210 When("I create a moss instance with identity {string}") do |keyfile| 211 shell "#{MOSS} init fixtures/keys/#{keyfile}" 212 end 213 214 When("I interactively create a moss instance with identity {string} and passphrase {string}") do |keyfile, passphrase| 215 shell "expect moss-init-with-passphrase.expect fixtures/keys/#{keyfile} #{passphrase.inspect}" 216 end 217 218 Then("the instance store exists") do 219 expect(Pathname.new(ENV['MOSS_HOME']).join("store")).to be_directory 220 end 221 222 Then("the instance identity is {string}") do |keyfile| 223 expect(store_path("../identity").read).to eq File.read("fixtures/keys/#{keyfile}") 224 end 225 226 Then("the store root has .recipients for the identity {string}") do |keyfile| 227 expected = `age-keygen -y fixtures/keys/#{keyfile.inspect}` 228 expect(store_path(".recipients").read).to eq expected 229 end 230 231 Then("the store root has recipient {string}") do |recipient| 232 expect(store_path(".recipients").read).to match recipient 233 end 234 235 Then("I can run git {string}") do |command| 236 @i_see = shell "#{MOSS} git #{command}" 237 end 238 239 Then("the store file {string} is readable only by me") do |store_file| 240 pathname = store_path(store_file) 241 mode = File.stat(pathname).mode 242 expect(mode & 077).to be_zero 243 end 244 245 Then("my identity file is readable only by me") do 246 pathname= store_path("../identity") 247 mode = File.stat(pathname).mode 248 expect(mode & 077).to be_zero 249 end 250 251 When("I run {string}") do |command| 252 # don't fail if error returns, as we need to test that 253 @i_see = IO.popen(["bash", "-c", "#{command.sub(/\Amoss/, MOSS)} 2>&1"], 254 "r") do |f| f.read end 255 @exit_status = $?.exitstatus 256 end 257 258 Then("it shows a usage message for {string}") do |command| 259 expect(@exit_status).to be > 0 260 expect(@i_see).to match /usage: moss #{command} \<file\>/ 261 end 262 263 When('I re-encrypt the store') do 264 @i_see = shell "#{MOSS} rebuild" 265 end