diff --git a/app/models/rails/keyserver/key/pgp.rb b/app/models/rails/keyserver/key/pgp.rb index 66e8ccb..3d88e24 100644 --- a/app/models/rails/keyserver/key/pgp.rb +++ b/app/models/rails/keyserver/key/pgp.rb @@ -25,25 +25,14 @@ def url ) end + def to_gpgkey + # self.class.gpgkey_from_key_string(self.public) + @to_gpgkey ||= self.class.gpgkey_from_key_string(private || public).first + end + def derive_metadata_if_empty - # jsons usually has 2 items. Which one to use? - # Match keyid? fingerprint? grip? TODO: match by grip - # TODO: or, use the one with nil primary key grip? - # or. just pick one, doesn't matter??? if metadata.empty? - jsons = derive_rnp_jsons - primary_jsons = jsons.select { |j| j["primary key grip"].nil? } - primary_secret_json = primary_jsons.detect { |j| j["secret key"]["present"] } - primary_public_json = primary_jsons.detect { |j| j["public key"]["present"] } - - if primary_secret_json - json = primary_secret_json - if primary_public_json - json["public key"] = primary_public_json["public key"] - end - else - json = primary_public_json - end + json = to_gpgkey.as_json update_column(:metadata, json) end @@ -55,18 +44,6 @@ def save_expiration_date update_column(:expiration_date, expiry_date) end - def save_primary_key_grip - super - derive_metadata_if_empty - update_column(:primary_key_grip, metadata["primary key grip"]) - end - - def save_grip - super - derive_metadata_if_empty - update_column(:grip, metadata["grip"]) - end - def save_fingerprint super derive_metadata_if_empty @@ -83,48 +60,53 @@ def save_fingerprint # end def key_id - metadata["keyid"] + metadata["subkeys"][0]["keyid"] end def key_type - metadata["type"] + GPGME.gpgme_pubkey_algo_name metadata["subkeys"][0]["pubkey_algo"] end def generation_date - Time.at(metadata["creation time"]) + Time.at(metadata["subkeys"][0]["timestamp"]) end def expiry_date - Time.at(metadata["creation time"] + metadata["expiration"]) if expires? + # TODO: verify + if expires? + read_attribute(:expiration_date) || Time.at(to_gpgkey.expires) + end end def expires? - metadata["expiration"] != 0 + # puts "will expire...?" + # pp to_gpgkey + to_gpgkey.expires? end def expired? - expires? && expiry_date < Time.now + # metadata["expired"] != 0 + # expires? && expiry_date < Time.now + to_gpgkey.expired end def key_size - metadata["length"] + metadata["subkeys"][0]["length"].to_i end def fingerprint - read_attribute(:fingerprint) || metadata["fingerprint"] + # read_attribute(:fingerprint) || metadata["subkeys"][0]["fpr"] + read_attribute(:fingerprint) || to_gpgkey.fingerprint end - # TODO? are we aggregating all user ids of the same key group? def userids - metadata["userids"] + metadata["uids"].map { |uid| uid["uid"] } end - # TODO? same question as above def userid userids&.first end - # TODO? same question as above def email userid.match(/<(.*@.*)>/)[1] if userid end @@ -137,11 +119,12 @@ def first? # Else, return nil? empty collection? def subkeys return self.class.none unless primary? - self.class.where(primary_key_grip: grip) + # TODO: GPGME: meaningful to ask for subkeys? + # self.class.where(primary_key_grip: grip) end def primary? - primary_key_grip.blank? + metadata["subkeys"][0]["fpr"] == fingerprint end def has_public? @@ -160,47 +143,6 @@ def active? end alias active active? - # TODO: make it private.? - # Internal(?) method to serialize DB key record back to an Rnp key object. - def derive_rnp_keys - # First, collect all primary key / subkeys - # XXX: For Factory-created keys where metadata is incomplete: Don't rely - # on metadata alone. - # Export... using.... - # Query all keys for matching grips? - # 1) export self public & private to Rnp - [public, private].compact.map do |data| - rnp = self.class.load_key_string(data) - self.class.all_keys(rnp) - end.flatten - end - - def derive_rnp_jsons - # 2) Get back metadata for subkey / primary key - # derive_rnp_keys.map(&:json).uniq { |j| j.values_at('keyid') } - # XXX: half is public, half is secret - derive_rnp_keys.map(&:json) - end - - def all_related_grips - # 3) query DB for such extra grips - derive_rnp_json.map do |json| - ["primary key grip", "subkey grips"].map do |attr| - json[attr] - end.compact - end.flatten.uniq - end - - # TODO: needed? - def derive_related_records - self.class.where(grip: all_related_grips) - end - - # TODO: needed? - def derive_related_jsons - derive_related_records.map(&:metadata) - end - # TODO: Move these to config/initializers UID_KEY_EMAIL_FIRST = "notifications-noreply@example.com" UID_KEY_NAME_FIRST = "Rails Notifications" @@ -210,112 +152,230 @@ def derive_related_jsons UID_KEY_NAME_SECOND = "Rails Security" UID_KEY_COMMENT_SECOND = "for security advisories" + # - + class Fakelog + def puts(_stuff); end + + def write(_stuff); end + + def flush; end + end + class << self - def build_rnp - Rnp.new - end - attr_reader :rnp + def debug_log + @debug_log ||= Fakelog.new + # $stderr + end - # TODO: spec it - def build_rnp_and_load_keys(homedir = Rnp.default_homedir) - homedir_info = ::Rnp.homedir_info(homedir) - public_info, secret_info = homedir_info.values_at(:public, :secret) + # def get_generated_key(email: UID_KEY_EMAIL_FIRST) + # { + # public: public_key_from_keyring(email), + # secret: secret_key_from_keyring(email), + # } + # end - rnp = Rnp.new(public_info[:format], secret_info[:format]) + # URL: + # https://github.com/ueno/ruby-gpgme/blob/master/examples/genkey.rb + def progfunc(_hook, what, _type, current, total) + debug_log.write("#{what}: #{current}/#{total}\r") + debug_log.flush + end - [public_info, secret_info].each do |keyring_info| - input = ::Rnp::Input.from_path(keyring_info[:path]) - rnp.load_keys(format: keyring_info[:format], input: input) - end + # Return first public key with matching +email+ + # + # NOTE: "first" assume there are no other keys with the same + # email + def public_key_from_keyring(email) + # puts "pubemail is #{email}" + # Note: "first" assume there are no other keys with the same email + public_key = GPGME::Key.find(:public, email).first + public_key.export(armor: true).to_s + end - rnp + # Return first private key with matching +email+ + # + # NOTE: "first" assume there are no other keys with the same + # email + def secret_key_from_keyring(email) + # puts "secemail is #{email}" + secret_key = GPGME::Key.find(:secret, email).first + return nil unless secret_key + + # GPGME does not allow exporting of private keys + # Unsafe: + # `gpg --export-secret-keys -a #{secret_key.fingerprint}` + # Doesn't return STDOUT: + # system('gpg', *(%w[--export-secret-keys -a] << secret_key.fingerprint)) + f = IO.popen(%w[gpg --export-secret-keys -a] << secret_key.fingerprint) + f.readlines.join + ensure + f&.close end - # Load into default RNP instance as well as to a new RNP - # instance just to differentiate between imported ones from - # existing ones. - # TODO: spec it - def load_key_string(key_string) - rnp = Rnp.new - rnp.load_keys( - format: "GPG", - input: Rnp::Input.from_string(key_string), - public_keys: true, - secret_keys: true, + def add_uid_to_key(email: UID_KEY_EMAIL_FIRST) + ctx = GPGME::Ctx.new( + progress_callback: method(:progfunc), + passphrase_callback: method(:passfunc), ) + Thread.current["rk-gpg-editkey-working"] = true + ctx.edit_key(ctx.keys(email).first, method(:add_uid_editfunc)) + end + + # Necessary for editfunc + def passfunc(_hook, _uid_hint, _passphrase_info, _prev_was_bad, file_descriptor) + io = IO.for_fd(file_descriptor, "w") + # Returns empty passphrase + io.puts("") + io.flush + end + + # SECOND UID + def add_uid_params + { + "keyedit.prompt" => "adduid", + "keygen.name" => UID_KEY_NAME_SECOND, + "keygen.email" => UID_KEY_EMAIL_SECOND, + "keygen.comment" => UID_KEY_COMMENT_SECOND, + } + end + + def add_uid_editfunc(_hook, status, args, file_descriptor) + # return if fd == "-1" + case status + when GPGME::GPGME_STATUS_GET_BOOL + debug_log.puts("# GPGME_STATUS_GET_BOOL") + io = IO.for_fd(file_descriptor) + # we always answer yes here + io.puts("Y") + io.flush + when GPGME::GPGME_STATUS_GET_LINE, + GPGME::GPGME_STATUS_GET_HIDDEN + + debug_log.puts("# GPGME_STATUS_GET_(LINE/HIDDEN)") + debug_log.flush + + input = add_uid_params[args] + + if args == "keyedit.prompt" + if Thread.current["rk-gpg-editkey-working"] + Thread.current["rk-gpg-editkey-working"] = nil + else + input = "quit" + end + end - rnp + debug_log.puts(" $ #{args} => typing '#{input}'") + io = IO.for_fd(file_descriptor) + io.puts(input) + io.flush + when GPGME::GPGME_STATUS_GOT_IT + debug_log.puts("# GPGME_STATUS_GOT_IT") + when GPGME::GPGME_STATUS_GOOD_PASSPHRASE + debug_log.puts("# GPGME_STATUS_GOOD_PASSPHRASE, command complete") + when GPGME::GPGME_STATUS_EOF + debug_log.puts("# GPGME_STATUS_EOF, exiting now") + else + debug_log.puts("# error: unknown status from GPGME editkey. status(#{status}) args(#{args.inspect})") + end end # Actually save key_string into new record def import_key_string(key_string, activation_date: Time.now) - rnp = load_key_string(key_string) - all_keys(rnp).map do |raw| - metadata = raw.json + # puts "importing yo , #{key_string[0..10]}" + gpgkey_from_key_string(key_string).map do |raw| + metadata = raw.as_json + raw_expiration_date = metadata["subkeys"][0]["expires"] + expiration_date = raw_expiration_date == 0 ? nil : Time.at(raw_expiration_date) + + # require 'pry' + # binding.pry creation_hash = { - private: raw.secret_key_present? ? raw.secret_key_data : nil, - public: raw.public_key_present? ? raw.public_key_data : nil, + # TODO: + private: secret_key_from_keyring(raw.email), + public: public_key_from_keyring(raw.email), activation_date: activation_date, + expiration_date: expiration_date, metadata: metadata, - primary_key_grip: metadata["primary key grip"], - grip: metadata["grip"], - fingerprint: metadata["fingerprint"], + fingerprint: raw.fingerprint, }.reject { |_k, v| v.nil? } create(creation_hash) end end - def all_keys(rnp_instance) - rnp_instance.each_keyid.map { |k| rnp_instance.find_key(keyid: k) } + def gpgkey_from_key_string(key_string) + setup_gpghome + s = GPGME::Key.import(key_string) + # pp "imports plzx" + # pp s.imports + # require 'pry' + # binding.pry + GPGME::Key.find(:secret, s.imports.first.fpr) + + GPGME::Key.find(:public, s.imports.first.fpr) end # Expiration means the *duration*, not the actual point in # time. - # Use +key_expiration_time(rnp_key)+ for that purpose. - def key_validity_seconds(rnp_key) - rnp_key.json["expiration"] + # Use +key_expiration_time(gpg_key)+ for that purpose. + def key_validity_seconds(gpg_key) + gpg_key.as_json["expiration"] end private :key_validity_seconds - def key_creation_time(rnp_key) - Time.at(rnp_key.json["creation time"]) + def key_creation_time(gpg_key) + Time.at(gpg_key.as_json["creation time"]) end private :key_creation_time # +key_expiration_time+ is the actual point in time. # NOTE: This is different from the terminology used in RFC4880. # They use "expiration time" as the "validity period". - def key_expiration_time(rnp_key) - Time.at(key_creation_time(rnp_key) + key_validity_seconds(rnp_key)) + def key_expiration_time(gpg_key) + Time.at(key_creation_time(gpg_key) + key_validity_seconds(gpg_key)) end private :key_expiration_time - def key_expired?(rnp_key) - key_expiration_time(rnp_key) != 0 && - key_expiration_time(rnp_key) > Time.now + def key_expired?(gpg_key) + key_expiration_time(gpg_key) != 0 && + key_expiration_time(gpg_key) > Time.now end private :key_expired? + # URL: + # https://github.com/jkraemer/mail-gpg/blob/8ee91e49bdcff0a59a9952d45bb4f2c23525747d/Rakefile + def setup_gpghome + gpghome = Dir.mktmpdir("rails-keyserver-gpghome") + ENV["GNUPGHOME"] = gpghome + ENV["GPG_AGENT_INFO"] = "" # disable gpg agent + + Rails.logger.info "[rails-keyserver] created temporary GNUPGHOME at #{gpghome}" + debug_log.puts "[rails-keyserver] created temporary GNUPGHOME at #{gpghome}" + end + # URL: # http://security.stackexchange.com/questions/31594/what-is-a-good-general-purpose-gnupg-key-setup def generate_new_key( email: UID_KEY_EMAIL_FIRST, creation_date: Time.now ) - rnp = Rnp.new - generated = rnp.generate_key( - default_key_params(email: email, creation_date: creation_date), + ctx = GPGME::Ctx.new( + # progress_callback: method(:progfunc) + #passphrase_callback: method(:passfunc) + ) + ctx.genkey( + default_key_params(email: email, creation_date: creation_date), nil, nil ) activation_date = creation_date + pubkey = public_key_from_keyring(email) + seckey = secret_key_from_keyring(email) key_records = %i[primary sub].map do |key_type| raw = generated[key_type] metadata = raw.json creation_hash = { - private: raw.secret_key_present? ? raw.secret_key_data : nil, - public: raw.public_key_present? ? raw.public_key_data : nil, + private: seckey, + public: pubkey, activation_date: activation_date, metadata: metadata, primary_key_grip: metadata["primary key grip"], @@ -383,27 +443,22 @@ def default_key_params(email: UID_KEY_EMAIL_FIRST, creation_date:) expiry_date = creation_date + 1.year # expiry_date = creation_date + 365 * 60 * 60 * 24 - { - primary: { - type: "RSA", - length: 4096, - userid: "#{UID_KEY_NAME_FIRST}#{email.present? ? " <#{email}>" : ''} #{UID_KEY_COMMENT_FIRST}".strip, - usage: [:sign], - expiration: date_format(expiry_date), - # These are the ruby-rnp defaults: - # preferences: { 'ciphers' => %w[AES256 AES192 AES128 TRIPLEDES], - # 'hashes' => %w[SHA256 SHA384 SHA512 SHA224 SHA1], - # 'compression' => %w[ZLIB BZip2 ZIP Uncompressed] }, - preferences: { "ciphers" => %w[AES256 AES192 AES128 CAST5], - "hashes" => %w[SHA512 SHA384 SHA256 SHA224], - "compression" => %w[ZLIB BZip2 ZIP Uncompressed] }, - }, - sub: { - type: "RSA", - length: 4096, - usage: [:encrypt], - }, - } + <<~EOOPTS + + Key-Type: RSA + Key-Length: 4096 + Key-Usage: sign + Subkey-Type: RSA + Subkey-Length: 4096 + Subkey-Usage: encrypt + Name-Real: #{UID_KEY_NAME_FIRST} + Name-Comment: #{UID_KEY_COMMENT_FIRST} + Name-Email: #{email} + Expire-Date: #{gnupg_date_format(expiry_date)} + Creation-Date: #{gnupg_date_format(creation_date)} + Preferences: SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed + + EOOPTS end end end diff --git a/spec/controllers/rails/keyserver/keys_controller_spec.rb b/spec/controllers/rails/keyserver/keys_controller_spec.rb index 5081dfc..b493019 100644 --- a/spec/controllers/rails/keyserver/keys_controller_spec.rb +++ b/spec/controllers/rails/keyserver/keys_controller_spec.rb @@ -128,7 +128,6 @@ module Rails::Keyserver::Api::V1 end it "returns an ASCII-armoured public key" do - pending "implementation in ruby-rnp" expect(response.body).to match(/\A-----BEGIN PGP PUBLIC KEY BLOCK-----/) expect(response.body).to match(/-----END PGP PUBLIC KEY BLOCK-----(?:\n)?\z/) end @@ -438,7 +437,7 @@ module Rails::Keyserver::Api::V1 end end - let(:date_from) { Time.now - 3.years } + let(:date_from) { now - 3.years } let(:date_to) { date_from + 6.years } before do @@ -446,11 +445,11 @@ module Rails::Keyserver::Api::V1 key = timecopped(time) do # FactoryBot.create :rails_keyserver_key_pgp, # activation_date: time - (RK::Key::PGP.import_key_string key_string_1, - activation_date: time).first + RK::Key::PGP.import_key_string(key_string_1, + activation_date: time.round(0)).first end - expect(key.activation_date.to_i).to eq time.to_i + expect(key.activation_date.utc.round(0)).to eq time.utc.round(0) end action[] end @@ -550,7 +549,7 @@ module Rails::Keyserver::Api::V1 end end - let(:date_from) { Time.now - 3.years } + let(:date_from) { now - 3.years } let(:date_to) { date_from + 6.years } before do @@ -559,10 +558,10 @@ module Rails::Keyserver::Api::V1 # FactoryBot.create :rails_keyserver_key_pgp, # activation_date: time RK::Key::PGP.import_key_string(key_string_1, - activation_date: time).first + activation_date: time.round(0)).first end - expect(key.activation_date.to_i).to eq time.to_i + expect(key.activation_date.utc.round(0)).to eq time.utc.round(0) end action[] end diff --git a/spec/models/rails/key/pgp_spec.rb b/spec/models/rails/key/pgp_spec.rb index cfd93bc..214f950 100644 --- a/spec/models/rails/key/pgp_spec.rb +++ b/spec/models/rails/key/pgp_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "rails_helper" + RSpec.describe Rails::Keyserver::Key::PGP, type: :model do let(:pub_key_path_1) { "spec/data/gpg/spec.pub" } let(:pub_key_string_1) { File.read pub_key_path_1 } @@ -15,6 +17,28 @@ let(:key) { keys.first } + describe ".setup_gpghome" do + it "changes ENV['GNUPGHOME']" do + expect { described_class.setup_gpghome }.to change { ENV["GNUPGHOME"] } + end + + it "changes ENV['GNUPGHOME'] to a valid file path" do + ENV["GNUPGHOME"] = "not valid path" + expect(File).to_not be_readable File.expand_path(ENV["GNUPGHOME"]) + + described_class.setup_gpghome + expect(File).to be_readable File.expand_path(ENV["GNUPGHOME"]) + end + + it "resets ENV['GPG_AGENT_INFO']" do + ENV["GPG_AGENT_INFO"] = "non-nil" + expect(ENV["GPG_AGENT_INFO"]).to_not be_blank + + described_class.setup_gpghome + expect(ENV["GPG_AGENT_INFO"]).to be_blank + end + end + describe ".import_key_string" do # let(:key) do # FactoryBot.create :rails_keyserver_key_pgp @@ -38,7 +62,7 @@ # XXX: What about: # let(:key_found_from_imported_keys) { keys.first } - context "the internal rnp instance" do + context "the internal GPGME instance" do it "contains 2 keys" do expect(keys.length).to eq 2 end @@ -47,9 +71,9 @@ expect(keys.first.fingerprint).to eq first_imported_key.fingerprint end - it "contains a key with matching grip" do - expect(keys.first.grip).to eq first_imported_key.grip - end + # it "contains a key with matching grip" do + # expect(keys.first.grip).to eq first_imported_key.grip + # end it "contains a key with matching keyid" do expect(keys.first.key_id).to eq first_imported_key.key_id @@ -117,6 +141,18 @@ end end + describe ".passfunc" do + it "has arity of 5" do + expect(described_class.method(:passfunc).arity).to eq 5 + end + end + + describe ".progfunc" do + it "has arity of 5" do + expect(described_class.method(:progfunc).arity).to eq 5 + end + end + describe ".generate_new_key" do it "has an arity of 2" do # .arity returns -1 for variable params @@ -147,7 +183,7 @@ email = "test #{rand}" expect do described_class.generate_new_key(email: email) - end.to change { described_class.all.map(&:grip).length }.by(2) + end.to change { described_class.all.map(&:fingerprint).length }.by(2) end it 'increases the number of keys found in "userids"' do @@ -215,29 +251,14 @@ described_class.default_key_params(creation_date: creation_date, email: "random") end - it "returns a Hash" do - expect(result).to be_instance_of Hash - end - - it 'returns a Hash with "primary" and "sub"' do - expect(result). - to include(:primary).and include(:sub) - end - - it 'returns a "primary" Hash with :type, :length, :userid, :usage, :expiration' do - expect(result[:primary]).to( - %i[type length userid usage expiration]. - map { |key| include key }. - reduce { |acc, constraint| acc.and(constraint) }, - ) + it "returns a String" do + expect(result).to be_instance_of String end - it 'returns a "sub" Hash with :type, :length, :usage' do - expect(result[:sub]).to( - %i[type length usage]. - map { |key| include key }. - reduce { |acc, constraint| acc.and(constraint) }, - ) + context "with extra params" do + it "returns a String" do + expect(described_class.default_key_params(creation_date: DateTime.now, email: "there")).to be_instance_of String + end end end @@ -265,7 +286,7 @@ let(:email) { e } it "(#{e}) would contain it" do - expect(result[:primary][:userid]).to match(/#{e}/) + expect(result).to match(/Name-Email:\s*#{e}/) end end end @@ -278,24 +299,23 @@ ].each do |d| context "with different creation_date (#{d})" do let(:creation_date) { d } - let(:creation_date_int) { creation_date.to_i } + let(:creation_date_string) { creation_date.utc.iso8601.gsub(/-|:/, "")[0..-6] } - it "would contain it (#{d.to_i})" do - pending "Rnp Key creation parameters do not support creation date time?" - expect(result[:primary][:creation_date]).to eq creation_date_int + it "would contain it (#{d.utc.iso8601.gsub(/-|:/, '')[0..-6]})" do + expect(result).to match /Creation-Date:\s*#{creation_date_string}/ end - it "would contain a corresponding expiration time (#{(d + 1.year).to_i})" do - expect(result[:primary][:expiration]).to eq((creation_date + 1.year).to_i) + it "would contain a corresponding expiration time (#{(d + 1.year).utc.iso8601.gsub(/-|:/, '')[0..-6]})" do + expect(result).to match(/#{(creation_date + 1.year).utc.iso8601.gsub(/-|:/, "")[0..-6]}/) end end end - %i[ - UID_KEY_NAME_FIRST - UID_KEY_COMMENT_FIRST - UID_KEY_EMAIL_FIRST - ].each do |param_name| + { + "Name-Real" => "UID_KEY_NAME_FIRST", + "Name-Comment" => "UID_KEY_COMMENT_FIRST", + "Name-Email" => "UID_KEY_EMAIL_FIRST", + }.each do |field_name, param_name| [ "sdflkj", 23, @@ -308,8 +328,8 @@ stub_const("#{described_class}::#{param_name}", value) end - it "contains it in the :primary.:userid" do - expect(result[:primary][:userid]).to match(/#{value}/) + it "contains it in the #{field_name}" do + expect(result).to match(/#{field_name}:\s*#{value}/) end end end @@ -359,7 +379,7 @@ end end - xdescribe ".public_key_from_keyring" do + describe ".public_key_from_keyring" do it "has an arity of 1" do expect(described_class.method(:public_key_from_keyring).arity).to eq 1 end @@ -370,15 +390,15 @@ let(:email) { key.email } - it "returns a Rnp::Key" do - expect(result).to be_a Rnp::Key + it "returns a String" do + expect(result).to be_a String end context "for a non-existent email" do let(:email) { key.email + "1" } - it "returns nil" do - expect(result).to be_nil + it "raises NoMethodError" do + expect { result }.to raise_error NoMethodError end end end @@ -392,11 +412,11 @@ let(:result) do described_class.generate_new_key(email: email) - described_class.get_generated_key(email: email)[:secret] + described_class.secret_key_from_keyring(email) end - it "returns a Rnp::Key" do - expect(result).to be_a Rnp::Key + it "returns a String" do + expect(result).to be_a String end context "for a non-existent email" do @@ -459,17 +479,13 @@ end end - xdescribe ".add_uid_to_key" do + describe ".add_uid_to_key" do it "has an arity of 1" do - expect(described_class.method(:add_uid_to_key).parameters.length).to eq 2 + expect(described_class.method(:add_uid_to_key).parameters.length).to eq 1 end - it "has a specific set of parameters" do - expect(described_class.method(:add_uid_to_key).parameters).to( - [%i[keyreq userid], %i[key target_email]].map do |cons| - include cons - end.reduce { |acc, cons| acc.and(cons) }, - ) + it "has a specific set of optional parameters" do + expect(described_class.method(:add_uid_to_key).parameters).to include %i[key email] end context "with missing :userid" do @@ -480,13 +496,7 @@ context "with a random email" do it "raises TypeError" do - pending "It currently doesn't check for email address syntax" - expect do - described_class.add_uid_to_key( - userid: "Example Addition #{rand} ", - target_email: "random", - ) - end.to raise_error TypeError + expect { described_class.add_uid_to_key(email: "random") }.to raise_error TypeError end end @@ -494,28 +504,35 @@ it "succeeds without errors" do expect do described_class.add_uid_to_key( - userid: "Example Addition #{rand} ", + email: described_class::UID_KEY_EMAIL_FIRST, ) end.to_not raise_error end it "appears in .userids" do - generated_keys = described_class.generate_new_key( - email: described_class::UID_KEY_EMAIL_FIRST, - ) - key_grip = generated_keys[:primary].json["grip"] + # generated_keys = described_class.generate_new_key( + # email: described_class::UID_KEY_EMAIL_FIRST, + # ) measurement = lambda { - Set.new(described_class.send(:rnp).find_key( - grip: key_grip, - ).userids) + Set.new(described_class.all.to_a.select do |key| + key.userids.include?(described_class::UID_KEY_EMAIL_FIRST) + end) } - additional_userid = "Example Addition #{rand} " + additional_name = "Example Addition #{rand}" + additional_email = "valid@example.com" + additional_comment = "some comment" + additional_userid = "#{additional_name} (#{additional_comment}) <#{additional_email}>" + allow(described_class).to receive(:add_uid_params).and_return( + "keyedit.prompt" => "adduid", + "keygen.name" => additional_name, + "keygen.email" => additional_email, + "keygen.comment" => additional_comment, + ) set1 = measurement.call described_class.add_uid_to_key( - target_email: described_class::UID_KEY_EMAIL_FIRST, - userid: additional_userid, + email: described_class::UID_KEY_EMAIL_FIRST, ) set2 = measurement.call @@ -524,6 +541,107 @@ expect(diff.first).to eq additional_userid end end + + xit "calls :add_uid_editfunc" do + pending "example runs forever until interrupted" + expect(described_class).to receive(:add_uid_editfunc).once + described_class.add_uid_to_key + end + + xit "sets Thread.current['rk-gpg-editkey-working'] to 'true'" do + pending "Thread local variables cannot be tested" + expect { described_class.add_uid_to_key }.to change { + Thread.current["rk-gpg-editkey-working"] + }.to true + end + + it "sets Thread.current['rk-gpg-editkey-working'] to 'true'" do + expect(Thread.current).to receive(:[]=).with("rk-gpg-editkey-working", true) + described_class.add_uid_to_key + end + end + + describe ".add_uid_editfunc" do + + it "has an arity of 4" do + expect(described_class.method(:add_uid_editfunc).arity).to eq 4 + end + + # when GPGME::GPGME_STATUS_GET_BOOL + # debug_log.puts("# GPGME_STATUS_GET_BOOL") + # io = IO.for_fd(fd) + # # we always answer yes here + # io.puts("Y") + # io.flush + # when GPGME::GPGME_STATUS_GET_LINE, + # GPGME::GPGME_STATUS_GET_HIDDEN + # + # debug_log.puts("# GPGME_STATUS_GET_(LINE/HIDDEN)") + # debug_log.flush + # + # input = add_uid_params[args] + # + # if args == "keyedit.prompt" + # if Thread.current['rk-gpg-editkey-working'] + # Thread.current['rk-gpg-editkey-working'] = nil + # else + # input = "quit" + # end + # end + # + # debug_log.puts(" $ #{args} => typing '#{input}'") + # io = IO.for_fd(fd) + # io.puts(input) + # io.flush + + describe "when :status" do + { + GPGME::GPGME_STATUS_GOT_IT => "# GPGME_STATUS_GOT_IT", + GPGME::GPGME_STATUS_GOOD_PASSPHRASE => "# GPGME_STATUS_GOOD_PASSPHRASE, command complete", + GPGME::GPGME_STATUS_EOF => "# GPGME_STATUS_EOF, exiting now", + "unknown" => "# error: unknown status from GPGME editkey. status(unknown) args(2)", + }.each do |key, val| + context "is #{key}" do + let(:status) { key } + let(:action) { described_class.add_uid_editfunc(nil, status, 2, 4) } + it "debug_log.puts(#{val})" do + expect(described_class.debug_log).to receive(:puts).with(val) + action + end + + end + end + end + + it "reads from the key" do + pending "TBI mock IO" + # fd = IO.sysopen("/dev/tty", "w") + io = IO.new 2 + # io = IO.for_fd fd + + def io.write stuff + @buffer ||= "" + @buffer << stuff + end + + def io.gets + @buffer + end + + described_class.add_uid_editfunc(nil, GPGME::GPGME_STATUS_GET_BOOL, 2, 4) + expect(io.gets).to eq "Y\n" + end + end + + describe ".add_uid_params" do + it "returns a specific Hash" do + expect(described_class.add_uid_params).to eq ({ + "keyedit.prompt" => "adduid", + "keygen.name" => RK::Key::PGP::UID_KEY_NAME_SECOND, + "keygen.email" => RK::Key::PGP::UID_KEY_EMAIL_SECOND, + "keygen.comment" => RK::Key::PGP::UID_KEY_COMMENT_SECOND, + }) + end end describe "#expires?" do diff --git a/spec/models/rails/key_spec.rb b/spec/models/rails/key_spec.rb index 0fb5561..708f0ca 100644 --- a/spec/models/rails/key_spec.rb +++ b/spec/models/rails/key_spec.rb @@ -57,7 +57,7 @@ module Rails::Keyserver end end - describe ".grip" do + xdescribe ".grip" do before do 5.times do FactoryBot.create :rails_keyserver_key_pgp diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7489da4..9667910 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,14 +16,13 @@ # users commonly want. # -# URL: +# URL: # http://stackoverflow.com/questions/22860025/rails-4-mounted-engine-with-rspec-and-factory-girl-rails -ENGINE_RAILS_ROOT=File.join(__dir__, '../') -Dir[File.join(ENGINE_RAILS_ROOT, "spec/factories/**/*.rb")].each {|f| require f } +ENGINE_RAILS_ROOT = File.join(__dir__, "../") +Dir[File.join(ENGINE_RAILS_ROOT, "spec/factories/**/*.rb")].each { |f| require f } # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| - config.include FactoryBot::Syntax::Methods # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest @@ -55,53 +54,51 @@ # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - config.disable_monkey_patching! - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = 'doc' - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = 'doc' + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed end