Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ jobs:
strategy:
matrix:
ruby:
- '3.2.2'
- '2.6'
- '2.7'
- '3.0'
- '3.1'
- '3.2'
- '3.3'
- '3.4'
- '4.0'

steps:
- uses: actions/checkout@v3
Expand Down
28 changes: 28 additions & 0 deletions spec/car_archive_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require 'base64'
require 'cbor'

describe Skyfall::CarArchive do
it "should convert nested CBOR tagged values to CID links" do
tag = CBOR::Tagged.new(42, "\x00" + ("a" * 32))
data = { "link" => tag }

described_class.convert_data(data)

data["link"]["$link"].should be_a(Skyfall::CID)
end

it "should convert binary strings to bytes objects" do
bytes = "\x00\x01".b
data = { "payload" => bytes }

described_class.convert_data(data)

data["payload"]["$bytes"].should eq(Base64.encode64(bytes).chomp.gsub(/=+$/, ""))
end

it "should raise when converting unexpected value types" do
expect { described_class.convert_data("bad") }.to raise_error(Skyfall::DecodeError)
end
end
46 changes: 46 additions & 0 deletions spec/cid_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

describe Skyfall::CID do
it "should build from a CBOR tag" do
data = "\x00" + ("a" * 32)
tag = Struct.new(:value).new(data)
Comment on lines +5 to +6

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use full CID bytes in CBOR tag fixture

The CBOR tag fixture here uses a 32‑byte payload ("\x00" + ("a" * 32)), but CIDv1 bytes are 36 bytes (version + codec + multihash prefix + 32‑byte digest). The rest of the library treats JSON CIDs as 36‑byte data (length 59 in Skyfall::CID.from_json), so this test is encoding an invalid CBOR CID and would still pass if a regression dropped the 4‑byte prefix. That means real firehose/CAR inputs could break without any spec coverage. Consider building the tag with full CID bytes to match real data.

Useful? React with 👍 / 👎.


cid = described_class.from_cbor_tag(tag)

cid.data.should eq("a" * 32)
end

it "should raise when CBOR tag is invalid" do
tag = Struct.new(:value).new("\x01bad")

expect { described_class.from_cbor_tag(tag) }.to raise_error(Skyfall::DecodeError)
end

it "should build from JSON string" do
cid = described_class.new("b" * 36)

parsed = described_class.from_json(cid.to_s)

parsed.should eq(cid)
end

it "should raise when JSON CID has unexpected length" do
expect { described_class.from_json("bshort") }.to raise_error(Skyfall::DecodeError)
end

it "should raise when JSON CID has invalid prefix" do
expect { described_class.from_json("z" + ("a" * 58)) }.to raise_error(Skyfall::DecodeError)
end

it "should return a multibase string" do
cid = described_class.new("b" * 36)

cid.to_s.should start_with("b")
end

it "should inspect with CID wrapper" do
cid = described_class.new("b" * 36)

cid.inspect.should include("CID(\"")
end
end
19 changes: 19 additions & 0 deletions spec/collection_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

describe Skyfall::Collection do
it "should map known collection names to short codes" do
described_class.short_code(described_class::BSKY_POST).should eq(:bsky_post)
end

it "should return :unknown for unknown collections" do
described_class.short_code("app.bsky.unknown").should eq(:unknown)
end

it "should map short codes back to collection names" do
described_class.from_short_code(:bsky_like).should eq(described_class::BSKY_LIKE)
end

it "should return nil for unknown short codes" do
described_class.from_short_code(:nonexistent).should be_nil
end
end
19 changes: 19 additions & 0 deletions spec/errors_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

describe Skyfall::ReactorActiveError do
it "should include a helpful message" do
error = described_class.new

error.message.should include("EventMachine reactor thread")
end
end

describe Skyfall::SubscriptionError do
it "should expose error details" do
error = described_class.new("Boom", "Something happened")

error.error_type.should eq("Boom")
error.error_message.should eq("Something happened")
error.message.should include("Boom")
end
end
74 changes: 74 additions & 0 deletions spec/firehose_message_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require 'cbor'

describe Skyfall::Firehose::Message do
def encode_message(type, data)
CBOR.encode(type) + CBOR.encode(data)
end

it "should parse commit messages" do
type = { "op" => 1, "t" => "#commit" }
data = {
"seq" => 1,
"repo" => "did:example:repo",
"commit" => CBOR::Tagged.new(42, "\x00" + ("a" * 32)),
"blocks" => "car",
"ops" => [],
"time" => "2024-01-01T00:00:00Z"
}

message = described_class.new(encode_message(type, data))

message.should be_a(Skyfall::Firehose::CommitMessage)
message.type.should eq(:commit)
message.repo.should eq("did:example:repo")
message.seq.should eq(1)
end

it "should parse account messages" do
type = { "op" => 1, "t" => "#account" }
data = {
"seq" => 2,
"did" => "did:example:acct",
"time" => "2024-01-01T00:00:00Z",
"active" => true
}

message = described_class.new(encode_message(type, data))

message.should be_a(Skyfall::Firehose::AccountMessage)
message.active?.should be(true)
end

it "should parse info messages" do
type = { "op" => 1, "t" => "#info" }
data = { "name" => "OutdatedCursor", "message" => "Old" }

message = described_class.new(encode_message(type, data))

message.should be_a(Skyfall::Firehose::InfoMessage)
message.to_s.should include("OutdatedCursor")
end

it "should treat unknown messages as unknown" do
type = { "op" => 1, "t" => "#mystery" }
data = { "seq" => 3 }

message = described_class.new(encode_message(type, data))

message.should be_a(Skyfall::Firehose::UnknownMessage)
message.unknown?.should be(true)
end

it "should raise when error is present" do
type = { "op" => 1, "t" => "#commit" }
data = { "error" => "Boom", "message" => "Bad" }

expect { described_class.new(encode_message(type, data)) }.to raise_error(Skyfall::SubscriptionError)
end

it "should raise on invalid message format" do
expect { described_class.new(CBOR.encode({})) }.to raise_error(Skyfall::DecodeError)
end
end
55 changes: 55 additions & 0 deletions spec/firehose_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

describe Skyfall::Firehose do
it "should default to subscribe repos" do
firehose = described_class.new("example.com")

firehose.send(:build_websocket_url).should eq("wss://example.com/xrpc/#{Skyfall::Firehose::SUBSCRIBE_REPOS}")
end

it "should accept a cursor as the second argument" do
firehose = described_class.new("example.com", 12)

firehose.cursor.should eq(12)
firehose.send(:build_websocket_url).should eq("wss://example.com/xrpc/#{Skyfall::Firehose::SUBSCRIBE_REPOS}?cursor=12")
end

it "should accept named endpoints" do
firehose = described_class.new("example.com", :subscribe_labels)

firehose.send(:build_websocket_url).should eq("wss://example.com/xrpc/#{Skyfall::Firehose::SUBSCRIBE_LABELS}")
end

it "should reject unknown endpoints" do
expect { described_class.new("example.com", :unknown) }.to raise_error(ArgumentError)
end

it "should reject invalid cursor" do
expect { described_class.new("example.com", :subscribe_repos, "abc") }.to raise_error(ArgumentError)
end

it "should handle messages and update cursor" do
firehose = described_class.new("example.com")
event = Struct.new(:data).new("payload")
received = nil

firehose.on_message { |msg| received = msg }

message = mock(seq: 99)
Skyfall::Firehose::Message.expects(:new).with("payload").returns(message)

firehose.send(:handle_message, event)

received.should eq(message)
firehose.cursor.should eq(99)
end

it "should clear cursor when no message handler is set" do
firehose = described_class.new("example.com")
event = Struct.new(:data).new("payload")

firehose.send(:handle_message, event)

firehose.cursor.should be_nil
end
end
54 changes: 54 additions & 0 deletions spec/jetstream_message_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

require 'json'

describe Skyfall::Jetstream::Message do
it "should parse commit messages" do
json = {
"kind" => "commit",
"did" => "did:example:repo",
"time_us" => 123,
"commit" => {
"collection" => "app.bsky.feed.post",
"rkey" => "key",
"operation" => "create",
"record" => { "text" => "Hello" }
}
}

message = described_class.new(JSON.dump(json))

message.should be_a(Skyfall::Jetstream::CommitMessage)
message.type.should eq(:commit)
message.operation.action.should eq(:create)
end

it "should parse identity messages" do
json = {
"kind" => "identity",
"did" => "did:example:repo",
"time_us" => 123,
"identity" => { "handle" => "alice.test" }
}

message = described_class.new(JSON.dump(json))

message.should be_a(Skyfall::Jetstream::IdentityMessage)
message.handle.should eq("alice.test")
end

it "should parse unknown message types" do
json = { "kind" => "mystery", "did" => "did:example:repo", "time_us" => 123 }

message = described_class.new(JSON.dump(json))

message.should be_a(Skyfall::Jetstream::UnknownMessage)
message.unknown?.should be(true)
end

it "should raise when required fields are missing" do
json = { "kind" => "commit" }

expect { described_class.new(JSON.dump(json)) }.to raise_error(Skyfall::DecodeError)
end
end
52 changes: 52 additions & 0 deletions spec/jetstream_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

describe Skyfall::Jetstream do
it "should build a subscribe url without params" do
stream = described_class.new("example.com")

stream.send(:build_websocket_url).should eq("wss://example.com/subscribe")
end

it "should include cursor and params" do
stream = described_class.new("example.com", wanted_collections: :bsky_post, cursor: 42)

stream.send(:build_websocket_url).should eq("wss://example.com/subscribe?wantedCollections=app.bsky.feed.post&cursor=42")
end

it "should reject unknown params" do
expect { described_class.new("example.com", unknown: true) }.to raise_error(ArgumentError)
end

it "should reject invalid dids" do
expect { described_class.new("example.com", wanted_dids: ["bad"]) }.to raise_error(ArgumentError)
end

it "should reject unsupported options" do
expect { described_class.new("example.com", compress: true) }.to raise_error(ArgumentError)
end

it "should handle messages and update cursor" do
stream = described_class.new("example.com")
event = Struct.new(:data).new("payload")
received = nil

stream.on_message { |msg| received = msg }

message = mock(time_us: 123_456)
Skyfall::Jetstream::Message.expects(:new).with("payload").returns(message)

stream.send(:handle_message, event)

received.should eq(message)
stream.cursor.should eq(123_456)
end

it "should clear cursor when no message handler is set" do
stream = described_class.new("example.com")
event = Struct.new(:data).new("payload")

stream.send(:handle_message, event)

stream.cursor.should be_nil
end
end
Loading