Add avatar downloading to Bluesky scan user job
- Modified process_user_avatar method to enqueue Domain::UserAvatarJob for avatar downloads - Made Domain::UserAvatarJob concrete (removed abstract!) with generic HTTP client - Added smart avatar management: handles new avatars, URL changes, and pending re-enqueues - Added comprehensive test coverage for all avatar scenarios - Updated HTTP mocking in specs to use HttpClientMockHelpers pattern - Fixed caused_by_entry handling for chained HTTP requests - Updated .cursorrules with proper HTTP mocking documentation including caused_by_entry: :any The job now automatically downloads user avatars when scanning Bluesky users.
This commit is contained in:
@@ -83,30 +83,32 @@ RSpec.describe Domain::Bluesky::Job::ScanUserJob do
|
||||
}.to_json
|
||||
end
|
||||
|
||||
before do
|
||||
# Mock profile API call
|
||||
expect(http_client_mock).to receive(:get).with(
|
||||
"https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=#{user.did}&collection=app.bsky.actor.profile&rkey=self",
|
||||
anything,
|
||||
).and_return(
|
||||
double(
|
||||
let(:client_mock_config) do
|
||||
[
|
||||
{
|
||||
uri:
|
||||
"https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=#{user.did}&collection=app.bsky.actor.profile&rkey=self",
|
||||
status_code: 200,
|
||||
body: profile_response_body,
|
||||
log_entry: double,
|
||||
),
|
||||
)
|
||||
content_type: "application/json",
|
||||
contents: profile_response_body,
|
||||
},
|
||||
{
|
||||
uri:
|
||||
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=#{user.did}&collection=app.bsky.feed.post&limit=100",
|
||||
status_code: 200,
|
||||
content_type: "application/json",
|
||||
contents: posts_response_body,
|
||||
caused_by_entry: :any, # Accept any caused_by_entry for the posts call
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
# Mock posts API call
|
||||
expect(http_client_mock).to receive(:get).with(
|
||||
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=#{user.did}&collection=app.bsky.feed.post&limit=100",
|
||||
anything,
|
||||
).and_return(
|
||||
double(
|
||||
status_code: 200,
|
||||
body: posts_response_body,
|
||||
log_entry: double,
|
||||
),
|
||||
)
|
||||
before do
|
||||
@log_entries =
|
||||
HttpClientMockHelpers.init_http_client_mock(
|
||||
http_client_mock,
|
||||
client_mock_config,
|
||||
)
|
||||
|
||||
# Mock static file job enqueueing - allow it but don't require it
|
||||
allow(Domain::StaticFileJob).to receive(:perform_later)
|
||||
@@ -123,7 +125,7 @@ RSpec.describe Domain::Bluesky::Job::ScanUserJob do
|
||||
expect(user.state).to eq("ok")
|
||||
end
|
||||
|
||||
it "creates avatar for user" do
|
||||
it "creates avatar for user with pending state" do
|
||||
expect { perform_now({ user: user }) }.to change {
|
||||
user.reload.avatar.present?
|
||||
}.from(false).to(true)
|
||||
@@ -132,6 +134,21 @@ RSpec.describe Domain::Bluesky::Job::ScanUserJob do
|
||||
expect(avatar.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreiavatar123",
|
||||
)
|
||||
expect(avatar.state).to eq("pending")
|
||||
end
|
||||
|
||||
it "enqueues Domain::UserAvatarJob when creating new avatar" do
|
||||
# Clear any existing enqueued jobs first
|
||||
SpecUtil.clear_enqueued_jobs!
|
||||
|
||||
perform_now({ user: user })
|
||||
|
||||
avatar = user.reload.avatar
|
||||
expect(avatar).to be_present
|
||||
|
||||
# Check that UserAvatarJob was enqueued
|
||||
enqueued_jobs = SpecUtil.enqueued_job_args(Domain::UserAvatarJob)
|
||||
expect(enqueued_jobs).to contain_exactly(hash_including(avatar: avatar))
|
||||
end
|
||||
|
||||
it "creates posts with media and associated files" do
|
||||
@@ -166,6 +183,127 @@ RSpec.describe Domain::Bluesky::Job::ScanUserJob do
|
||||
end
|
||||
end
|
||||
|
||||
context "avatar handling scenarios" do
|
||||
let(:profile_response_body) do
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.actor.profile/self",
|
||||
"cid" => "bafyreiabc123",
|
||||
"value" => {
|
||||
"displayName" => "Test User",
|
||||
"description" => "A test user profile",
|
||||
"avatar" => {
|
||||
"ref" => {
|
||||
"$link" => "bafkreiavatar123",
|
||||
},
|
||||
"mimeType" => "image/jpeg",
|
||||
"size" => 50_000,
|
||||
},
|
||||
},
|
||||
}.to_json
|
||||
end
|
||||
|
||||
let(:posts_response_body) { { "records" => [], "cursor" => nil }.to_json }
|
||||
|
||||
let(:client_mock_config) do
|
||||
[
|
||||
{
|
||||
uri:
|
||||
"https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=#{user.did}&collection=app.bsky.actor.profile&rkey=self",
|
||||
status_code: 200,
|
||||
content_type: "application/json",
|
||||
contents: profile_response_body,
|
||||
},
|
||||
{
|
||||
uri:
|
||||
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=#{user.did}&collection=app.bsky.feed.post&limit=100",
|
||||
status_code: 200,
|
||||
content_type: "application/json",
|
||||
contents: posts_response_body,
|
||||
caused_by_entry: :any, # Accept any caused_by_entry for the posts call
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
@log_entries =
|
||||
HttpClientMockHelpers.init_http_client_mock(
|
||||
http_client_mock,
|
||||
client_mock_config,
|
||||
)
|
||||
end
|
||||
|
||||
context "when user has existing avatar with same URL" do
|
||||
let!(:existing_avatar) do
|
||||
user.create_avatar!(
|
||||
url_str:
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreiavatar123",
|
||||
state: "ok",
|
||||
)
|
||||
end
|
||||
|
||||
it "does not enqueue job for already downloaded avatar with same URL" do
|
||||
perform_now({ user: user })
|
||||
|
||||
# Avatar should remain unchanged
|
||||
existing_avatar.reload
|
||||
expect(existing_avatar.state).to eq("ok")
|
||||
|
||||
# Should not enqueue any avatar job
|
||||
enqueued_jobs = SpecUtil.enqueued_job_args(Domain::UserAvatarJob)
|
||||
expect(enqueued_jobs).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when user has existing avatar with different URL" do
|
||||
let!(:existing_avatar) do
|
||||
user.create_avatar!(
|
||||
url_str:
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=oldavatar456",
|
||||
state: "ok",
|
||||
)
|
||||
end
|
||||
|
||||
it "updates avatar URL and enqueues job" do
|
||||
perform_now({ user: user })
|
||||
|
||||
existing_avatar.reload
|
||||
expect(existing_avatar.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreiavatar123",
|
||||
)
|
||||
expect(existing_avatar.state).to eq("pending")
|
||||
|
||||
# Should enqueue avatar job with the existing avatar
|
||||
enqueued_jobs = SpecUtil.enqueued_job_args(Domain::UserAvatarJob)
|
||||
expect(enqueued_jobs).to contain_exactly(
|
||||
hash_including(avatar: existing_avatar),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user has existing avatar in pending state" do
|
||||
let!(:existing_avatar) do
|
||||
user.create_avatar!(
|
||||
url_str:
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreiavatar123",
|
||||
state: "pending",
|
||||
)
|
||||
end
|
||||
|
||||
it "re-enqueues job for pending avatar with same URL" do
|
||||
perform_now({ user: user })
|
||||
|
||||
existing_avatar.reload
|
||||
expect(existing_avatar.state).to eq("pending")
|
||||
|
||||
# Should re-enqueue avatar job
|
||||
enqueued_jobs = SpecUtil.enqueued_job_args(Domain::UserAvatarJob)
|
||||
expect(enqueued_jobs).to contain_exactly(
|
||||
hash_including(avatar: existing_avatar),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when user already scanned recently" do
|
||||
before do
|
||||
user.update!(scanned_profile_at: 1.day.ago, scanned_posts_at: 1.day.ago)
|
||||
|
||||
Reference in New Issue
Block a user