Add Bluesky post helper with facet rendering and external link support
- Add BlueskyPostHelper for rendering Bluesky post facets (mentions, links, hashtags) - Implement facet parsing and rendering with proper styling - Add external link partial for non-Bluesky URLs - Update DisplayedFile and PostFiles components to handle Bluesky posts - Add comprehensive test coverage for helper methods - Update scan user job to handle Bluesky-specific data
This commit is contained in:
479
spec/helpers/domain/bluesky_post_helper_spec.rb
Normal file
479
spec/helpers/domain/bluesky_post_helper_spec.rb
Normal file
@@ -0,0 +1,479 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Domain::BlueskyPostHelper, type: :helper do
|
||||
# Mock policies to avoid Devise authentication errors
|
||||
before do
|
||||
allow(helper).to receive(:policy).and_return(
|
||||
double("PostPolicy", view_file?: true),
|
||||
)
|
||||
allow(helper).to receive(:policy).and_return(
|
||||
double("UserPolicy", view_file?: true),
|
||||
)
|
||||
end
|
||||
|
||||
describe "#render_bsky_post_facets" do
|
||||
let(:post_text) do
|
||||
"Hello @alice.bsky.social! Check out https://bsky.app/profile/bob.bsky.social/post/abc123 and #hashtag"
|
||||
end
|
||||
|
||||
context "when facets is nil" do
|
||||
it "returns the original text unchanged" do
|
||||
result =
|
||||
helper.render_bsky_post_facets("Simple text without facets", nil)
|
||||
expect(result).to eq("Simple text without facets")
|
||||
end
|
||||
end
|
||||
|
||||
context "when facets is empty array" do
|
||||
it "returns the original text unchanged" do
|
||||
result = helper.render_bsky_post_facets(post_text, [])
|
||||
expect(result).to eq(post_text)
|
||||
end
|
||||
end
|
||||
|
||||
context "with mention facets" do
|
||||
let(:mentioned_user) do
|
||||
create(
|
||||
:domain_user_bluesky_user,
|
||||
handle: "alice.bsky.social",
|
||||
did: "did:plc:alice123",
|
||||
)
|
||||
end
|
||||
let(:text) { "Hello @alice.bsky.social!" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 6,
|
||||
"byteEnd" => 24,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#mention",
|
||||
"did" => "did:plc:alice123",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
context "when mentioned user exists in database" do
|
||||
before { mentioned_user } # Ensure user exists
|
||||
|
||||
it "renders inline_link_domain_user partial" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/inline_link_domain_user",
|
||||
locals:
|
||||
hash_including(
|
||||
user: mentioned_user,
|
||||
link_text: "@alice.bsky.social",
|
||||
),
|
||||
).and_return("[external user link]")
|
||||
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Hello [external user link]!")
|
||||
end
|
||||
end
|
||||
|
||||
context "when mentioned user does not exist in database" do
|
||||
it "renders external link" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/external_link",
|
||||
locals:
|
||||
hash_including(
|
||||
link_text: "@alice.bsky.social",
|
||||
url: "https://bsky.app/profile/did:plc:alice123",
|
||||
),
|
||||
).and_return("[external user link]")
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Hello [external user link]!")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with link facets" do
|
||||
let(:linked_post) do
|
||||
create(
|
||||
:domain_post_bluesky_post,
|
||||
rkey: "abc123",
|
||||
at_uri: "at://did:plc:bob123/app.bsky.feed.post/abc123",
|
||||
)
|
||||
end
|
||||
let(:text) do
|
||||
"Check out https://bsky.app/profile/bob.bsky.social/post/abc123"
|
||||
end
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 10,
|
||||
"byteEnd" => 62,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#link",
|
||||
"uri" => "https://bsky.app/profile/bob.bsky.social/post/abc123",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
context "when linked post exists in database" do
|
||||
before { linked_post } # Ensure post exists
|
||||
|
||||
it "renders inline_link_domain_post partial" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/inline_link_domain_post",
|
||||
locals:
|
||||
hash_including(
|
||||
post: linked_post,
|
||||
link_text:
|
||||
"https://bsky.app/profile/bob.bsky.social/post/abc123",
|
||||
),
|
||||
).and_return("[external post link]")
|
||||
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Check out [external post link]")
|
||||
end
|
||||
end
|
||||
|
||||
context "when linked post does not exist in database" do
|
||||
it "renders external link" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/external_link",
|
||||
locals:
|
||||
hash_including(
|
||||
link_text:
|
||||
"https://bsky.app/profile/bob.bsky.social/post/abc123",
|
||||
url: "https://bsky.app/profile/bob.bsky.social/post/abc123",
|
||||
),
|
||||
).and_return("[external post link]")
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Check out [external post link]")
|
||||
end
|
||||
end
|
||||
|
||||
context "when link is not a Bluesky post link" do
|
||||
let(:text) { "Visit https://example.com" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 6,
|
||||
"byteEnd" => 25,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#link",
|
||||
"uri" => "https://example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "renders external link" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/external_link",
|
||||
locals:
|
||||
hash_including(
|
||||
link_text: "https://example.com",
|
||||
url: "https://example.com",
|
||||
),
|
||||
).and_return("[external link]")
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Visit [external link]")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with hashtag facets" do
|
||||
let(:text) { "Love this #art" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 10,
|
||||
"byteEnd" => 14,
|
||||
},
|
||||
"features" => [
|
||||
{ "$type" => "app.bsky.richtext.facet#tag", "tag" => "art" },
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "renders hashtag as external link to Bluesky search" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/external_link",
|
||||
locals:
|
||||
hash_including(
|
||||
link_text: "#art",
|
||||
url: "https://bsky.app/hashtag/art",
|
||||
),
|
||||
).and_return("[external hashtag link]")
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Love this [external hashtag link]")
|
||||
end
|
||||
end
|
||||
|
||||
context "with multiple facets" do
|
||||
let(:mentioned_user) do
|
||||
create(
|
||||
:domain_user_bluesky_user,
|
||||
handle: "alice.bsky.social",
|
||||
did: "did:plc:alice123",
|
||||
)
|
||||
end
|
||||
let(:linked_post) do
|
||||
create(
|
||||
:domain_post_bluesky_post,
|
||||
rkey: "abc123",
|
||||
at_uri: "at://did:plc:bob123/app.bsky.feed.post/abc123",
|
||||
)
|
||||
end
|
||||
let(:text) do
|
||||
"Hey @alice.bsky.social, check https://bsky.app/profile/bob.bsky.social/post/abc123 #cool"
|
||||
end
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 4,
|
||||
"byteEnd" => 22,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#mention",
|
||||
"did" => "did:plc:alice123",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 30,
|
||||
"byteEnd" => 82,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#link",
|
||||
"uri" => "https://bsky.app/profile/bob.bsky.social/post/abc123",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 83,
|
||||
"byteEnd" => 88,
|
||||
},
|
||||
"features" => [
|
||||
{ "$type" => "app.bsky.richtext.facet#tag", "tag" => "cool" },
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
mentioned_user
|
||||
linked_post
|
||||
end
|
||||
|
||||
it "processes all facets correctly" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/inline_link_domain_user",
|
||||
locals: hash_including(user: mentioned_user),
|
||||
).and_return("[external user link]")
|
||||
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/inline_link_domain_post",
|
||||
locals: hash_including(post: linked_post),
|
||||
).and_return("[external post link]")
|
||||
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/external_link",
|
||||
locals:
|
||||
hash_including(
|
||||
link_text: "#cool",
|
||||
url: "https://bsky.app/hashtag/cool",
|
||||
),
|
||||
).and_return("[external hashtag link]")
|
||||
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
|
||||
expect(result).to include("[external user link]")
|
||||
expect(result).to include("[external post link]")
|
||||
expect(result).to include("[external hashtag link]")
|
||||
end
|
||||
end
|
||||
|
||||
context "with overlapping facets" do
|
||||
let(:text) { "Check @alice.bsky.social" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 6,
|
||||
"byteEnd" => 24,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#mention",
|
||||
"did" => "did:plc:alice123",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 10,
|
||||
"byteEnd" => 20,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#link",
|
||||
"uri" => "https://example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "handles overlapping facets by processing in order and skipping conflicts" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/external_link",
|
||||
locals:
|
||||
hash_including(
|
||||
link_text: "@alice.bsky.social",
|
||||
url: "https://bsky.app/profile/did:plc:alice123",
|
||||
),
|
||||
).and_return("[external user link]")
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
# Should process the first facet and skip the overlapping one
|
||||
expect(result).to eq("Check [external user link]")
|
||||
end
|
||||
end
|
||||
|
||||
context "with UTF-8 characters" do
|
||||
let(:text) { "Hello 🌟 @alice.bsky.social! 🎉" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 11,
|
||||
"byteEnd" => 29,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#mention",
|
||||
"did" => "did:plc:alice123",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "correctly handles byte offsets with UTF-8 characters" do
|
||||
expect(helper).to receive(:render).with(
|
||||
partial: "domain/has_description_html/external_link",
|
||||
locals:
|
||||
hash_including(
|
||||
link_text: "@alice.bsky.social",
|
||||
url: "https://bsky.app/profile/did:plc:alice123",
|
||||
),
|
||||
).and_return("[external user link]")
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Hello 🌟 [external user link]! 🎉")
|
||||
end
|
||||
end
|
||||
|
||||
context "with malformed facet data" do
|
||||
let(:text) { "Hello @alice.bsky.social!" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 6,
|
||||
}, # Missing byteEnd
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#mention",
|
||||
"did" => "did:plc:alice123",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "gracefully handles malformed facets and returns original text" do
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Hello @alice.bsky.social!")
|
||||
end
|
||||
end
|
||||
|
||||
context "with invalid byte ranges" do
|
||||
let(:text) { "Short" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 0,
|
||||
"byteEnd" => 100,
|
||||
}, # Beyond text length
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#mention",
|
||||
"did" => "did:plc:alice123",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "gracefully handles invalid byte ranges and returns original text" do
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Short")
|
||||
end
|
||||
end
|
||||
|
||||
context "with unknown facet types" do
|
||||
let(:text) { "Hello world!" }
|
||||
let(:facets) do
|
||||
[
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet",
|
||||
"index" => {
|
||||
"byteStart" => 0,
|
||||
"byteEnd" => 5,
|
||||
},
|
||||
"features" => [
|
||||
{
|
||||
"$type" => "app.bsky.richtext.facet#unknown",
|
||||
"data" => "some_data",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "ignores unknown facet types and returns original text" do
|
||||
result = helper.render_bsky_post_facets(text, facets)
|
||||
expect(result).to eq("Hello world!")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user