631 lines
19 KiB
Ruby
631 lines
19 KiB
Ruby
# 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
|
|
|
|
# Shared test data used across multiple contexts
|
|
let(:linked_user) do
|
|
create(
|
|
:domain_user_bluesky_user,
|
|
handle: "bob.bsky.social",
|
|
did: "did:plc:bob123",
|
|
)
|
|
end
|
|
let(:linked_post) do
|
|
create(
|
|
:domain_post_bluesky_post,
|
|
rkey: "abc123",
|
|
at_uri: "at://did:plc:bob123/app.bsky.feed.post/abc123",
|
|
)
|
|
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 newlines in text" do
|
|
let(:text_with_newlines) { "Line 1\nLine 2\nLine 3" }
|
|
let(:facets) do
|
|
[
|
|
{
|
|
"$type" => "app.bsky.richtext.facet",
|
|
"index" => {
|
|
"byteStart" => 0,
|
|
"byteEnd" => 6,
|
|
},
|
|
"features" => [
|
|
{
|
|
"$type" => "app.bsky.richtext.facet#mention",
|
|
"did" => "did:plc:alice123",
|
|
},
|
|
],
|
|
},
|
|
]
|
|
end
|
|
|
|
it "converts newlines to <br /> tags in text parts" do
|
|
expect(helper).to receive(:render).with(
|
|
partial: "domain/has_description_html/external_link",
|
|
locals:
|
|
hash_including(
|
|
link_text: "Line 1",
|
|
url: "https://bsky.app/profile/did:plc:alice123",
|
|
),
|
|
).and_return("[external user link]")
|
|
|
|
result = helper.render_bsky_post_facets(text_with_newlines, facets)
|
|
expect(result).to eq("[external user link]<br />Line 2<br />Line 3")
|
|
end
|
|
|
|
context "with newlines in text without facets" do
|
|
it "keeps newlines unchanged when no facets are present" do
|
|
result = helper.render_bsky_post_facets(text_with_newlines, [])
|
|
expect(result).to eq("Line 1\nLine 2\nLine 3")
|
|
end
|
|
end
|
|
|
|
context "with newlines around facets" do
|
|
let(:text_with_newlines_around_facets) do
|
|
"Before\n@alice.bsky.social\nAfter"
|
|
end
|
|
let(:facets) do
|
|
[
|
|
{
|
|
"$type" => "app.bsky.richtext.facet",
|
|
"index" => {
|
|
"byteStart" => 7,
|
|
"byteEnd" => 25,
|
|
},
|
|
"features" => [
|
|
{
|
|
"$type" => "app.bsky.richtext.facet#mention",
|
|
"did" => "did:plc:alice123",
|
|
},
|
|
],
|
|
},
|
|
]
|
|
end
|
|
|
|
it "converts newlines to <br /> tags while preserving facets" 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_with_newlines_around_facets,
|
|
facets,
|
|
)
|
|
expect(result).to eq("Before<br />[external user link]<br />After")
|
|
end
|
|
end
|
|
|
|
context "with multiple text parts containing newlines" do
|
|
let(:text_with_multiple_newlines) do
|
|
"Start\n@alice.bsky.social\nMiddle\n@bob.bsky.social\nEnd"
|
|
end
|
|
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" => 32,
|
|
"byteEnd" => 48,
|
|
},
|
|
"features" => [
|
|
{
|
|
"$type" => "app.bsky.richtext.facet#mention",
|
|
"did" => "did:plc:bob123",
|
|
},
|
|
],
|
|
},
|
|
]
|
|
end
|
|
|
|
it "converts newlines in all text parts to <br /> tags" 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("[alice link]")
|
|
|
|
expect(helper).to receive(:render).with(
|
|
partial: "domain/has_description_html/external_link",
|
|
locals:
|
|
hash_including(
|
|
link_text: "@bob.bsky.social",
|
|
url: "https://bsky.app/profile/did:plc:bob123",
|
|
),
|
|
).and_return("[bob link]")
|
|
|
|
result =
|
|
helper.render_bsky_post_facets(text_with_multiple_newlines, facets)
|
|
expect(result).to eq(
|
|
"Start<br />[alice link]<br />Middle<br />[bob link]<br />End",
|
|
)
|
|
end
|
|
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(: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 do
|
|
linked_user # Ensure user exists
|
|
linked_post # Ensure post exists
|
|
end
|
|
|
|
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(: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_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
|