diff --git a/.rubocop.yml b/.rubocop.yml index 01f4f98..f005830 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,3 +8,15 @@ Style/StringLiteralsInInterpolation: Layout/LineLength: Max: 190 + +Metrics/BlockLength: + Max: 60 + +Metrics/ParameterLists: + Max: 8 + +Metrics/MethodLength: + Max: 13 + +Metrics/BlockLength: + Max: 80 diff --git a/Gemfile b/Gemfile index baeb205..3eec88f 100644 --- a/Gemfile +++ b/Gemfile @@ -9,8 +9,8 @@ gem "pry" gem "webmock" -gem "sinatra" gem "puma" +gem "sinatra" gem "rake", "~> 13.0" diff --git a/activitypub.gemspec b/activitypub.gemspec index 71123cc..7aa5d55 100644 --- a/activitypub.gemspec +++ b/activitypub.gemspec @@ -10,10 +10,10 @@ Gem::Specification.new do |spec| spec.summary = "A Ruby library for the ActivityPub protocol." spec.description = "A comprehensive library for building and parsing ActivityPub content in Ruby." - spec.homepage = "https://github.com/rauversion/activitypub_ruby" + spec.homepage = "https://github.com/rauversion/activitypub" spec.license = "MIT" - spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") + spec.required_ruby_version = Gem::Requirement.new(">= 2.4") # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" diff --git a/app.rb b/app.rb index 4a3fbfa..eeca2c4 100644 --- a/app.rb +++ b/app.rb @@ -1,4 +1,6 @@ -require 'sinatra' +# frozen_string_literal: true + +require "sinatra" require_relative "lib/activitypub" # For simplicity, we'll hardcode actor_id and generate a keypair on startup @@ -6,7 +8,7 @@ keypair = ActivityPub::Signature.generate_keypair outbox = ActivityPub::Outbox.new(actor_id: actor_id, private_key: keypair[:private]) -post '/actors/1/outbox' do +post "/actors/1/outbox" do target_inbox_url = params[:inbox_url] # expect the client to provide target inbox URL activity_data = params[:activity_data] # and the activity data @@ -14,9 +16,9 @@ [response.code.to_i, response.body] end -post '/actors/1/inbox' do +post "/actors/1/inbox" do # This is where other actors would send activities to our actor - incoming_activity = JSON.parse(params['incoming_activity']) # JSON.parse(request.body.read) + incoming_activity = JSON.parse(params["incoming_activity"]) # JSON.parse(request.body.read) # For demonstration purposes, we'll just print the activity to the console puts "Received activity: #{incoming_activity}" @@ -25,7 +27,7 @@ [200, "Activity received"] end -get '/' do +get "/" do %( "ActivityPub Sinatra Example"

Send an Activity to Outbox

diff --git a/lib/activitypub.rb b/lib/activitypub.rb index e2ffa96..7ec03f0 100644 --- a/lib/activitypub.rb +++ b/lib/activitypub.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require "json" -require 'time' -require 'base64' +require "time" +require "base64" require_relative "activitypub/version" require_relative "activitypub/activity" require_relative "activitypub/object" diff --git a/lib/activitypub/activity.rb b/lib/activitypub/activity.rb index 0adfa75..518e743 100644 --- a/lib/activitypub/activity.rb +++ b/lib/activitypub/activity.rb @@ -1,4 +1,10 @@ +# frozen_string_literal: true + module ActivityPub + # The Activity class represents a specific action or event in the ActivityPub protocol. + # It encapsulates details of the action being taken and any associated metadata. + # This class provides methods for creating, validating, and processing activities + # in accordance with the ActivityPub standard. class Activity # The type of the activity, e.g., 'Create', 'Like', 'Follow', etc. attr_accessor :type @@ -26,7 +32,7 @@ def initialize(type:, actor:, object:, **other_attributes) # Convert the activity into a hash representation. def to_h { - '@context': 'https://www.w3.org/ns/activitystreams', + '@context': "https://www.w3.org/ns/activitystreams", type: @type, actor: @actor, object: @object @@ -43,9 +49,9 @@ def to_json(*args) # Load an activity from a hash. def self.from_h(hash) new( - type: hash['type'], - actor: hash['actor'], - object: hash['object'] + type: hash["type"], + actor: hash["actor"], + object: hash["object"] # Handle any other attributes as needed. # ... ) diff --git a/lib/activitypub/actor.rb b/lib/activitypub/actor.rb index 16df718..bb2d5f6 100644 --- a/lib/activitypub/actor.rb +++ b/lib/activitypub/actor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # The ActivityPub::Actor represents individual and # collective entities which perform activities. # Commonly used actor types include Person, Organization, @@ -7,10 +9,15 @@ # Here's a basic representation for the ActivityPub::Actor: module ActivityPub + # The Actor class represents participants in the ActivityPub network. + # An Actor can be an individual user, a group, a service, or any other entity + # that performs actions on the network. This class handles details related to + # actor identity, public keys, endpoints, and other metadata essential for + # secure and accurate communication within the ActivityPub protocol. class Actor attr_accessor :id, :type, :name, :preferred_username, :inbox, :outbox, :followers, :following - def initialize(id:, type: 'Person', name:, preferred_username:, inbox:, outbox:, followers: nil, following: nil) + def initialize(id:, type: "Person", name:, preferred_username:, inbox:, outbox:, followers: nil, following: nil) @id = id @type = type @name = name @@ -23,7 +30,7 @@ def initialize(id:, type: 'Person', name:, preferred_username:, inbox:, outbox:, def to_h { - '@context': 'https://www.w3.org/ns/activitystreams', + '@context': "https://www.w3.org/ns/activitystreams", id: @id, type: @type, name: @name, @@ -41,14 +48,14 @@ def to_json(*args) def self.from_h(hash) new( - id: hash['id'], - type: hash['type'] || 'Person', - name: hash['name'], - preferred_username: hash['preferredUsername'], - inbox: hash['inbox'], - outbox: hash['outbox'], - followers: hash['followers'], - following: hash['following'] + id: hash["id"], + type: hash["type"] || "Person", + name: hash["name"], + preferred_username: hash["preferredUsername"], + inbox: hash["inbox"], + outbox: hash["outbox"], + followers: hash["followers"], + following: hash["following"] ) end end diff --git a/lib/activitypub/inbox.rb b/lib/activitypub/inbox.rb index a8a13a7..36bc6a2 100644 --- a/lib/activitypub/inbox.rb +++ b/lib/activitypub/inbox.rb @@ -1,7 +1,12 @@ -require 'net/http' -require 'json' +# frozen_string_literal: true + +require "net/http" +require "json" module ActivityPub + # The Inbox class manages the reception of activities from other actors. + # It is responsible for validating incoming activities, verifying signatures, + # and processing or storing them as needed in the context of the ActivityPub protocol. class Inbox attr_reader :actor_id @@ -17,9 +22,7 @@ def accept_activity(activity_data, headers) signature = headers["Signature"] # Verify the activity's signature - unless ActivityPub::Signature.verify?(activity_data, signature, public_key) - raise "Invalid signature" - end + raise "Invalid signature" unless ActivityPub::Signature.verify?(activity_data, signature, public_key) # Process activity process_activity(activity_data) @@ -35,7 +38,7 @@ def fetch_actor_public_key(actor_id) # This assumes the public key is stored under a 'publicKey' key in the actor's profile. # This structure may vary based on the implementation details of the server hosting the actor's profile. - profile['publicKey']['publicKeyPem'] + profile["publicKey"]["publicKeyPem"] end def process_activity(activity_data) diff --git a/lib/activitypub/object.rb b/lib/activitypub/object.rb index 41a93f7..41e96fe 100644 --- a/lib/activitypub/object.rb +++ b/lib/activitypub/object.rb @@ -1,8 +1,13 @@ +# frozen_string_literal: true + module ActivityPub + # The Object class represents the primary data structure of the ActivityPub protocol. + # It provides a foundational element from which specific ActivityPub types (like "Note" or "Event") derive. + # This class facilitates the creation, validation, and manipulation of ActivityPub objects. class Object attr_accessor :id, :type, :published, :updated, :content, :attributed_to - def initialize(id:, type: 'Object', published: nil, updated: nil, content: nil, attributed_to: nil) + def initialize(id:, type: "Object", published: nil, updated: nil, content: nil, attributed_to: nil) @id = id @type = type @published = published || Time.now.utc.iso8601 @@ -13,7 +18,7 @@ def initialize(id:, type: 'Object', published: nil, updated: nil, content: nil, def to_h { - '@context': 'https://www.w3.org/ns/activitystreams', + '@context': "https://www.w3.org/ns/activitystreams", id: @id, type: @type, published: @published, @@ -29,22 +34,20 @@ def to_json(*args) def self.from_h(hash) new( - id: hash['id'], - type: hash['type'] || 'Object', - published: hash['published'], - updated: hash['updated'], - content: hash['content'], - attributed_to: hash['attributedTo'] + id: hash["id"], + type: hash["type"] || "Object", + published: hash["published"], + updated: hash["updated"], + content: hash["content"], + attributed_to: hash["attributedTo"] ) end end end -=begin - obj = ActivityPub::Object.new(id: 'https://example.com/objects/1', content: 'Hello world!') -puts obj.to_json - -loaded_obj = ActivityPub::Object.from_h(JSON.parse(obj.to_json)) -puts loaded_obj.content # => "Hello world!" - -=end +# obj = ActivityPub::Object.new(id: 'https://example.com/objects/1', content: 'Hello world!') +# puts obj.to_json +# +# loaded_obj = ActivityPub::Object.from_h(JSON.parse(obj.to_json)) +# puts loaded_obj.content # => "Hello world!" +# diff --git a/lib/activitypub/outbox.rb b/lib/activitypub/outbox.rb index 7486c56..b629ada 100644 --- a/lib/activitypub/outbox.rb +++ b/lib/activitypub/outbox.rb @@ -1,6 +1,12 @@ -require 'net/http' +# frozen_string_literal: true + +require "net/http" module ActivityPub + # The Outbox class is responsible for sending activities to other actors. + # It wraps the process of preparing an activity, signing it, and then delivering it to + # the recipient's inbox. This ensures that messages sent via ActivityPub are + # authenticated and securely delivered. class Outbox attr_reader :actor_id, :private_key @@ -28,7 +34,7 @@ def send_activity(target_inbox_url, activity_data) def post_activity(inbox_url, activity_data, signature) uri = URI(inbox_url) http = Net::HTTP.new(uri.host, uri.port) - request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/ld+json', 'Signature' => signature }) + request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/ld+json", "Signature" => signature }) request.body = activity_data http.request(request) end diff --git a/lib/activitypub/signature.rb b/lib/activitypub/signature.rb index b05bfee..3625205 100644 --- a/lib/activitypub/signature.rb +++ b/lib/activitypub/signature.rb @@ -1,4 +1,6 @@ -require 'openssl' +# frozen_string_literal: true + +require "openssl" # Digital signatures are essential in federated networks # like ActivityPub to ensure data integrity and authenticity. @@ -6,6 +8,9 @@ # in the ActivityPub community is the RSA-SHA256 algorithm. module ActivityPub + # This class handles the generation and verification of ActivityPub signatures. + # It ensures that outgoing messages are authenticated and incoming messages + # are verified against a known public key. class Signature # Generates a new RSA private and public key pair def self.generate_keypair @@ -17,7 +22,7 @@ def self.generate_keypair def self.sign(data, private_key_pem) private_key = OpenSSL::PKey::RSA.new(private_key_pem) signature = private_key.sign(OpenSSL::Digest::SHA256.new, data) - Base64.encode64(signature).gsub("\n", '') + Base64.encode64(signature).gsub("\n", "") end # Verifies the signature with the provided public key diff --git a/spec/activity_spec.rb b/spec/activity_spec.rb index 1ec5d4d..7aa5e40 100644 --- a/spec/activity_spec.rb +++ b/spec/activity_spec.rb @@ -1,36 +1,38 @@ +# frozen_string_literal: true + RSpec.describe ActivityPub::Activity do - describe '#initialize' do - subject { described_class.new(type: 'Like', actor: 'https://example.com/users/alice', object: 'https://example.com/posts/1') } + describe "#initialize" do + subject { described_class.new(type: "Like", actor: "https://example.com/users/alice", object: "https://example.com/posts/1") } - it 'initializes with given attributes' do - expect(subject.type).to eq('Like') - expect(subject.actor).to eq('https://example.com/users/alice') - expect(subject.object).to eq('https://example.com/posts/1') + it "initializes with given attributes" do + expect(subject.type).to eq("Like") + expect(subject.actor).to eq("https://example.com/users/alice") + expect(subject.object).to eq("https://example.com/posts/1") end end - describe '#to_json' do - subject { described_class.new(type: 'Like', actor: 'https://example.com/users/alice', object: 'https://example.com/posts/1') } + describe "#to_json" do + subject { described_class.new(type: "Like", actor: "https://example.com/users/alice", object: "https://example.com/posts/1") } - it 'converts the activity to a JSON representation' do + it "converts the activity to a JSON representation" do json_output = subject.to_json parsed_output = JSON.parse(json_output) - expect(parsed_output['type']).to eq('Like') - expect(parsed_output['actor']).to eq('https://example.com/users/alice') - expect(parsed_output['object']).to eq('https://example.com/posts/1') + expect(parsed_output["type"]).to eq("Like") + expect(parsed_output["actor"]).to eq("https://example.com/users/alice") + expect(parsed_output["object"]).to eq("https://example.com/posts/1") end end - describe '.from_h' do - let(:activity_hash) { { 'type' => 'Like', 'actor' => 'https://example.com/users/alice', 'object' => 'https://example.com/posts/1' } } + describe ".from_h" do + let(:activity_hash) { { "type" => "Like", "actor" => "https://example.com/users/alice", "object" => "https://example.com/posts/1" } } - it 'loads an activity from a hash' do + it "loads an activity from a hash" do activity = described_class.from_h(activity_hash) - expect(activity.type).to eq('Like') - expect(activity.actor).to eq('https://example.com/users/alice') - expect(activity.object).to eq('https://example.com/posts/1') + expect(activity.type).to eq("Like") + expect(activity.actor).to eq("https://example.com/users/alice") + expect(activity.object).to eq("https://example.com/posts/1") end end end diff --git a/spec/actor_spec.rb b/spec/actor_spec.rb index 4e0cd5d..9e0ddc6 100644 --- a/spec/actor_spec.rb +++ b/spec/actor_spec.rb @@ -1,70 +1,72 @@ +# frozen_string_literal: true + RSpec.describe ActivityPub::Actor do - describe '#initialize' do + describe "#initialize" do subject do described_class.new( - id: 'https://example.com/users/alice', - name: 'Alice', - preferred_username: 'alice', - inbox: 'https://example.com/users/alice/inbox', - outbox: 'https://example.com/users/alice/outbox' + id: "https://example.com/users/alice", + name: "Alice", + preferred_username: "alice", + inbox: "https://example.com/users/alice/inbox", + outbox: "https://example.com/users/alice/outbox" ) end - it 'initializes with given attributes' do - expect(subject.id).to eq('https://example.com/users/alice') - expect(subject.type).to eq('Person') - expect(subject.name).to eq('Alice') - expect(subject.preferred_username).to eq('alice') - expect(subject.inbox).to eq('https://example.com/users/alice/inbox') - expect(subject.outbox).to eq('https://example.com/users/alice/outbox') + it "initializes with given attributes" do + expect(subject.id).to eq("https://example.com/users/alice") + expect(subject.type).to eq("Person") + expect(subject.name).to eq("Alice") + expect(subject.preferred_username).to eq("alice") + expect(subject.inbox).to eq("https://example.com/users/alice/inbox") + expect(subject.outbox).to eq("https://example.com/users/alice/outbox") end end - describe '#to_json' do + describe "#to_json" do subject do described_class.new( - id: 'https://example.com/users/alice', - name: 'Alice', - preferred_username: 'alice', - inbox: 'https://example.com/users/alice/inbox', - outbox: 'https://example.com/users/alice/outbox' + id: "https://example.com/users/alice", + name: "Alice", + preferred_username: "alice", + inbox: "https://example.com/users/alice/inbox", + outbox: "https://example.com/users/alice/outbox" ) end - it 'converts the actor to a JSON representation' do + it "converts the actor to a JSON representation" do json_output = subject.to_json parsed_output = JSON.parse(json_output) - expect(parsed_output['id']).to eq('https://example.com/users/alice') - expect(parsed_output['type']).to eq('Person') - expect(parsed_output['name']).to eq('Alice') - expect(parsed_output['preferredUsername']).to eq('alice') - expect(parsed_output['inbox']).to eq('https://example.com/users/alice/inbox') - expect(parsed_output['outbox']).to eq('https://example.com/users/alice/outbox') + expect(parsed_output["id"]).to eq("https://example.com/users/alice") + expect(parsed_output["type"]).to eq("Person") + expect(parsed_output["name"]).to eq("Alice") + expect(parsed_output["preferredUsername"]).to eq("alice") + expect(parsed_output["inbox"]).to eq("https://example.com/users/alice/inbox") + expect(parsed_output["outbox"]).to eq("https://example.com/users/alice/outbox") end end - describe '.from_h' do + describe ".from_h" do let(:actor_hash) do { - 'id' => 'https://example.com/users/alice', - 'type' => 'Person', - 'name' => 'Alice', - 'preferredUsername' => 'alice', - 'inbox' => 'https://example.com/users/alice/inbox', - 'outbox' => 'https://example.com/users/alice/outbox' + "id" => "https://example.com/users/alice", + "type" => "Person", + "name" => "Alice", + "preferredUsername" => "alice", + "inbox" => "https://example.com/users/alice/inbox", + "outbox" => "https://example.com/users/alice/outbox" } end - it 'loads an actor from a hash' do + it "loads an actor from a hash" do actor = described_class.from_h(actor_hash) - expect(actor.id).to eq('https://example.com/users/alice') - expect(actor.type).to eq('Person') - expect(actor.name).to eq('Alice') - expect(actor.preferred_username).to eq('alice') - expect(actor.inbox).to eq('https://example.com/users/alice/inbox') - expect(actor.outbox).to eq('https://example.com/users/alice/outbox') + expect(actor.id).to eq("https://example.com/users/alice") + expect(actor.type).to eq("Person") + expect(actor.name).to eq("Alice") + expect(actor.preferred_username).to eq("alice") + expect(actor.inbox).to eq("https://example.com/users/alice/inbox") + expect(actor.outbox).to eq("https://example.com/users/alice/outbox") end end end diff --git a/spec/inbox_spec.rb b/spec/inbox_spec.rb index 6c64374..d74bd9a 100644 --- a/spec/inbox_spec.rb +++ b/spec/inbox_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # spec/activitypub/inbox_spec.rb RSpec.describe ActivityPub::Inbox do @@ -22,12 +24,12 @@ allow(Net::HTTP).to receive(:get).with(URI(actor_id)).and_return(public_key_response) end - describe '#accept_activity' do - it 'accepts and processes a valid activity' do + describe "#accept_activity" do + it "accepts and processes a valid activity" do expect { subject.accept_activity(activity_data, headers) }.not_to raise_error end - it 'raises an error for an activity with an invalid signature' do + it "raises an error for an activity with an invalid signature" do tampered_headers = headers.merge("Signature" => "INVALID_SIGNATURE") expect { subject.accept_activity(activity_data, tampered_headers) }.to raise_error("Invalid signature") diff --git a/spec/object_spec.rb b/spec/object_spec.rb index 58d8abb..8f59281 100644 --- a/spec/object_spec.rb +++ b/spec/object_spec.rb @@ -1,44 +1,46 @@ +# frozen_string_literal: true + # spec/activitypub/object_spec.rb RSpec.describe ActivityPub::Object do let(:current_time) { Time.now.utc.iso8601 } - describe '#initialize' do - subject { described_class.new(id: 'https://example.com/objects/1', content: 'Hello world!') } + describe "#initialize" do + subject { described_class.new(id: "https://example.com/objects/1", content: "Hello world!") } - it 'initializes with given attributes' do - expect(subject.id).to eq('https://example.com/objects/1') - expect(subject.type).to eq('Object') - expect(subject.content).to eq('Hello world!') + it "initializes with given attributes" do + expect(subject.id).to eq("https://example.com/objects/1") + expect(subject.type).to eq("Object") + expect(subject.content).to eq("Hello world!") expect(subject.published).to be_a(String) # Checking if it's a formatted string for simplicity. In a real-world scenario, you might want to parse and validate. expect(subject.updated).to be_a(String) end end - describe '#to_json' do - subject { described_class.new(id: 'https://example.com/objects/1', content: 'Hello world!', published: current_time, updated: current_time) } + describe "#to_json" do + subject { described_class.new(id: "https://example.com/objects/1", content: "Hello world!", published: current_time, updated: current_time) } - it 'converts the object to a JSON representation' do + it "converts the object to a JSON representation" do json_output = subject.to_json parsed_output = JSON.parse(json_output) - expect(parsed_output['id']).to eq('https://example.com/objects/1') - expect(parsed_output['type']).to eq('Object') - expect(parsed_output['content']).to eq('Hello world!') - expect(parsed_output['published']).to eq(current_time) - expect(parsed_output['updated']).to eq(current_time) + expect(parsed_output["id"]).to eq("https://example.com/objects/1") + expect(parsed_output["type"]).to eq("Object") + expect(parsed_output["content"]).to eq("Hello world!") + expect(parsed_output["published"]).to eq(current_time) + expect(parsed_output["updated"]).to eq(current_time) end end - describe '.from_h' do - let(:object_hash) { { 'id' => 'https://example.com/objects/1', 'type' => 'Object', 'content' => 'Hello world!', 'published' => current_time, 'updated' => current_time } } + describe ".from_h" do + let(:object_hash) { { "id" => "https://example.com/objects/1", "type" => "Object", "content" => "Hello world!", "published" => current_time, "updated" => current_time } } - it 'loads an object from a hash' do + it "loads an object from a hash" do obj = described_class.from_h(object_hash) - expect(obj.id).to eq('https://example.com/objects/1') - expect(obj.type).to eq('Object') - expect(obj.content).to eq('Hello world!') + expect(obj.id).to eq("https://example.com/objects/1") + expect(obj.type).to eq("Object") + expect(obj.content).to eq("Hello world!") expect(obj.published).to eq(current_time) expect(obj.updated).to eq(current_time) end diff --git a/spec/outbox_spec.rb b/spec/outbox_spec.rb index 80c29f1..a983062 100644 --- a/spec/outbox_spec.rb +++ b/spec/outbox_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # spec/activitypub/outbox_spec.rb RSpec.describe ActivityPub::Outbox do @@ -14,18 +16,18 @@ .with( body: activity_data, headers: { - 'Accept' => '*/*', - 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', - 'Content-Type' => 'application/ld+json', - 'User-Agent' => 'Ruby', - 'Signature' => ActivityPub::Signature.sign(activity_data, keypair[:private]) + "Accept" => "*/*", + "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "Content-Type" => "application/ld+json", + "User-Agent" => "Ruby", + "Signature" => ActivityPub::Signature.sign(activity_data, keypair[:private]) } ) .to_return(status: 200, body: "", headers: {}) end - describe '#send_activity' do - it 'sends the activity to the target inbox' do + describe "#send_activity" do + it "sends the activity to the target inbox" do response = subject.send_activity(inbox_url, activity_data) expect(response.code).to eq("200") end diff --git a/spec/signature_spec.rb b/spec/signature_spec.rb index bfdcb4b..2978722 100644 --- a/spec/signature_spec.rb +++ b/spec/signature_spec.rb @@ -1,34 +1,36 @@ +# frozen_string_literal: true + # spec/activitypub/signature_spec.rb RSpec.describe ActivityPub::Signature do let(:keypair) { described_class.generate_keypair } let(:data) { "Important data that needs to be signed" } - describe '.generate_keypair' do - it 'generates a valid RSA key pair' do + describe ".generate_keypair" do + it "generates a valid RSA key pair" do expect(keypair[:private]).to be_a(String) expect(keypair[:public]).to be_a(String) end end - describe '.sign' do + describe ".sign" do let(:signature) { described_class.sign(data, keypair[:private]) } - it 'signs data using a private key' do + it "signs data using a private key" do expect(signature).to be_a(String) # Note: In a real-world scenario, you'd have more comprehensive checks # or perhaps try verifying the signature as a test. end end - describe '.verify?' do + describe ".verify?" do let(:signature) { described_class.sign(data, keypair[:private]) } - it 'verifies the signed data with the correct public key' do + it "verifies the signed data with the correct public key" do expect(described_class.verify?(data, signature, keypair[:public])).to be_truthy end - it 'returns false for incorrect data' do + it "returns false for incorrect data" do expect(described_class.verify?("Tampered data", signature, keypair[:public])).to be_falsey end end