# typed: false require "rails_helper" describe Domain::Fa::Job::UserPageJob do let(:http_client_mock) { instance_double("::Scraper::HttpClient") } before do Scraper::ClientFactory.http_client_mock = http_client_mock @log_entries = HttpClientMockHelpers.init_http_client_mock( http_client_mock, client_mock_config, ) end context "the user page has a user with a url name that has brackets" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/thesteamlemur/", status_code: 200, content_type: "text/html", contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_thesteamlemur.html", ), }, ] end it "creates the user" do expect do perform_now({ url_name: "thesteamlemur" }) end.to change { Domain::User::FaUser.find_by(url_name: "thesteamlemur") }.from(nil).to(be_present) end it "creates the recent watchers" do perform_now({ url_name: "thesteamlemur" }) user = Domain::User::FaUser.find_by(url_name: "thesteamlemur") expect(user.followed_by_users.count).to eq(7) expect(user.followed_by_users.map(&:url_name)).to include( "[sic]", "drakenbyte", "naynay", ) end end context "the user is disabled" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/christianallree/", status_code: 200, content_type: "text/html", contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_christianallree_account_disabled.html", ), }, ] end it "marks the user as disabled" do perform_now({ url_name: "christianallree" }) user = Domain::User::FaUser.find_by(url_name: "christianallree") expect(user.state).to eq("account_disabled") end end context "scanning a normal user" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/meesh/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Apr 7, 2023 18:26 UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_meesh.html", ), }, ] end let(:user) { Domain::User::FaUser.find_by(url_name: "meesh") } let(:faved_post_fa_ids) do [ 51_671_587, 51_660_483, 51_656_085, 51_656_135, 51_633_080, 51_633_041, 51_535_939, 51_631_696, 51_634_520, 51_632_374, 51_631_377, 51_631_461, 51_624_859, 51_618_150, 51_621_410, 51_618_586, 51_617_113, 51_613_740, 51_600_523, 51_594_821, ] end it "records the right stats" do perform_now({ url_name: "meesh" }) expect(user).to_not be_nil expect(user.num_pageviews).to eq(3_061_083) expect(user.num_submissions).to eq(1590) expect(user.num_favorites).to eq(1_422_886) expect(user.num_comments_recieved).to eq(47_931) expect(user.num_comments_given).to eq(17_741) expect(user.num_journals).to eq(5) expect(user.account_status).to eq("active") expect(user.registered_at).to eq(Time.parse("Dec 11, 2005 10:28 -08:00")) end it "enqueues a favs job scan" do perform_now({ url_name: "meesh" }) expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::FavsJob)).to match( [hash_including(user:, caused_by_entry: @log_entries[0])], ) end it "records favs with fav_id and explicit_time" do user = create(:domain_user_fa_user, url_name: "meesh") post = create(:domain_post_fa_post, fa_id: 51_617_113) # existing record that has a fav_fa_id but no date user.update_fav_model(post_id: post.id, fav_id: 1_234_567_890) perform_now({ url_name: "meesh" }) user_post_favs = user.user_post_favs expect(user_post_favs.count).to eq(20) expect(user_post_favs.map(&:post).map(&:fa_id)).to contain_exactly( *faved_post_fa_ids, ) fav_51671587 = user_post_favs.find { |f| f.post.fa_id == 51_671_587 } expect(fav_51671587.user).to eq(user) expect(fav_51671587.explicit_time).to eq( Time.parse("Apr 6, 2023 04:28 PM -07:00"), ) expect(fav_51671587.fav_id).to be_nil fav_51617113 = user_post_favs.find { |f| f.post.fa_id == 51_617_113 } expect(fav_51617113.user).to eq(user) expect(fav_51617113.explicit_time).to eq( Time.parse("Apr 2, 2023 02:49 PM -07:00"), ) expect(fav_51617113.fav_id).to eq(1_234_567_890) end context "the user does not yet exist" do it "the user is created" do expect do perform_now({ url_name: "meesh" }) end.to change { Domain::User::FaUser.find_by(url_name: "meesh") }.from(nil).to(be_present) end it "enqueues a user avatar job" do perform_now({ url_name: "meesh" }) expect(user).to_not be_nil avatar = user.avatar expect(avatar).to_not be_nil expect(avatar.url_str).to eq( "https://a.furaffinity.net/1635789297/meesh.gif", ) expect(avatar.state).to eq("pending") expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserAvatarJob), ).to match([hash_including(avatar:, caused_by_entry: @log_entries[0])]) end it "enqueues a gallery job" do perform_now({ url_name: "meesh" }) expect(user).to_not be_nil expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserGalleryJob), ).to match([hash_including(user:, caused_by_entry: @log_entries[0])]) end end context "the user exists" do let!(:user) { create(:domain_user_fa_user, url_name: "meesh") } context "gallery scan was recently performed" do before do user.scanned_gallery_at = 1.day.ago user.save! end it "does not enqueue a gallery job" do perform_now({ url_name: "meesh" }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserGalleryJob), ).to be_empty end end context "the gallery scan was not recently performed" do before do user.scanned_gallery_at = 10.years.ago user.save! end it "enqueues a gallery job" do perform_now({ url_name: "meesh" }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserGalleryJob), ).to match([hash_including(user:, caused_by_entry: @log_entries[0])]) end end end end context "all watched users fit in the recently watched section" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/llllvi/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Feb 27, 2025 04:10 PM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_llllvi_few_watched_users.html", ), }, ] end let(:user) { Domain::User::FaUser.find_by(url_name: "llllvi") } it "does not enqueue a follows job" do perform_now({ url_name: "llllvi" }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserFollowsJob), ).to be_empty end it "adds watched users to the user's followed_users" do perform_now({ url_name: "llllvi" }) expect(user.followed_users.count).to eq(6) expect(user.followed_users.map(&:url_name)).to match_array( %w[ koul artii aquadragon35 incredibleediblecalico nummynumz fidchellvore ], ) end it "marks scanned_follows_at as recent" do perform_now({ url_name: "llllvi" }) expect(user.scanned_follows_at).to be_within(3.seconds).of(Time.now) end it "does not add any users to followed_by_users" do perform_now({ url_name: "llllvi" }) expect(user.followed_by_users.count).to eq(0) end it "works when the user already has some followed users" do user = create(:domain_user_fa_user, url_name: "llllvi") followed_user = create(:domain_user_fa_user, url_name: "koul") user.followed_users << followed_user perform_now({ url_name: "llllvi" }) expect(user.followed_users.count).to eq(6) expect(user.followed_by_users.count).to eq(0) end end context "the user has no submissions" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/sealingthedeal/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Feb 27, 2025 02:48 PM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_sealingthedeal_no_submissions.html", ), }, ] end it "records the right number of submissions" do perform_now({ url_name: "sealingthedeal" }) user = Domain::User::FaUser.find_by(url_name: "sealingthedeal") expect(user).to_not be_nil expect(user.num_submissions).to eq(0) end it "does not enqueue a gallery job" do perform_now({ url_name: "sealingthedeal" }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserGalleryJob), ).to be_empty end end context "the user has a single scrap submission and few watchers" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/zzreg/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Feb 27, 2025 03:56 PM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_zzreg_one_scrap_submission.html", ), }, ] end let(:user) { Domain::User::FaUser.find_by(url_name: "zzreg") } it "records the right number of submissions" do perform_now({ url_name: "zzreg" }) expect(user.num_submissions).to eq(1) end it "enqueues a gallery job" do perform_now({ url_name: "zzreg" }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserGalleryJob), ).to match([hash_including(user:, caused_by_entry: @log_entries[0])]) end it "adds watchers to followed_by_users" do perform_now({ url_name: "zzreg" }) expect(user.followed_by_users.count).to eq(5) expect(user.followed_by_users.map(&:url_name)).to match_array( %w[noneedtothankme karenpls azureparagon zarmir iginger], ) end it "works when the user already has some followed_by_users" do user = create(:domain_user_fa_user, url_name: "zzreg") followed_by_user = create(:domain_user_fa_user, url_name: "noneedtothankme") user.followed_by_users << followed_by_user perform_now({ url_name: "zzreg" }) expect(user.followed_by_users.count).to eq(5) expect(user.followed_users.count).to eq(0) end it "marks scanned_followed_by_at as recent" do perform_now({ url_name: "zzreg" }) expect(user.scanned_followed_by_at).to be_within(3.seconds).of(Time.now) end it "does not mark scanned_follows_at as recent" do perform_now({ url_name: "zzreg" }) expect(user.scanned_follows_at).to be_nil end end context "the user has more than threshold watched users and they are all known" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/koul/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Mar 1, 2025 08:27 PM UTC"), contents: SpecUtil.read_fixture_file( # this user page indicates: # - 14 watched # - 25 watchers "domain/fa/user_page/user_page_koul_over_threshold_watchers.html", ), }, ] end let(:user) { create(:domain_user_fa_user, url_name: "koul") } let(:recent_watched_user_url_names) do %w[ spiritinchoco syrrvya knyazkolosok waspsalad moth-sprout suzamuri 2d10 lemithecat fr95 floki.midnight dizzy. kilobytefox ] end it "does not mark scanned_follows_at if no followed users are known" do expect do perform_now({ user: }) user.reload end.not_to change { user.scanned_follows_at } expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserFollowsJob), ).to match([hash_including(user:, caused_by_entry: @log_entries[0])]) end it "does not mark scanned_follows_at if page indicates more followed than are recorded" do # all in recent are marked as followed recent_watched_user_url_names.each do |url_name| user.followed_users << create( :domain_user_fa_user, url_name:, name: url_name, ) end expect(user.followed_users.count).to eq(12) # but the page indicates user watching 14 expect do perform_now({ user: }) user.reload end.not_to change { user.scanned_follows_at } expect(user.followed_users.count).to eq(12) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserFollowsJob), ).to match([hash_including(user:, caused_by_entry: @log_entries[0])]) end it "marks scanned_follows_at as recent if all followed users are known and number of followed matches page" do recent_watched_user_url_names.each do |url_name| user.followed_users << create(:domain_user_fa_user, url_name:) end user.followed_users << create(:domain_user_fa_user, url_name: "test1") user.followed_users << create(:domain_user_fa_user, url_name: "test2") expect(user.followed_users.count).to eq(14) expect do perform_now({ user: }) user.reload end.to change { user.scanned_follows_at }.to be_within(3.seconds).of( Time.now, ) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserFollowsJob), ).to be_empty end end context "the user is watched by more users than the threshold" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/koul/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Mar 1, 2025 08:27 PM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_koul_over_threshold_watchers.html", ), }, ] end let(:watched_by_user_url_names) do %w[ skifmutt k92 erri49 spaghetti779 nonuri mkyosh mystpaww retrorinx aurorethedire rasssss coyotesolo commissarisador ] end let(:user) { create(:domain_user_fa_user, url_name: "koul") } it "does not mark scanned_followed_by_at as recent if over threshold" do expect do perform_now({ user: }) user.reload end.not_to change { user.scanned_followed_by_at } end it "marks scanned_followed_by_at as recent if all watched by users are known and number of watched by matches page" do watched_by_user_url_names.each do |url_name| user.followed_by_users << create(:domain_user_fa_user, url_name:) end # 25 recorded, so make 25-12 more (25 - 12).times do |idx| user.followed_by_users << create( :domain_user_fa_user, url_name: "test#{idx}", ) end expect do perform_now({ user: }) user.reload end.to change { user.scanned_followed_by_at }.to be_within(3.seconds).of( Time.now, ) end end context "a user with recent gallery submissions" do let(:user) { create(:domain_user_fa_user, url_name: "kutua") } context "has one recent gallery submission, and user page indicates three total submissions" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/kutua/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Mar 2, 2025 09:59 AM UTC"), contents: SpecUtil.read_fixture_file( # one recent gallery submission, and three submissions indicated on user page "domain/fa/user_page/user_page_kutua_one_recent_three_total_gallery.html", ), }, ] end it "updates the num_submissions count" do expect do perform_now({ user: }) user.reload end.to change { user.num_submissions }.from(nil).to(3) end context "no submissions are yet known" do it "creates the recent submission" do perform_now({ user: }) expect(user.posts.count).to eq(1) post = user.posts.first expect(post.fa_id).to eq(60_073_062) end it "does not mark the gallery as scanned" do expect do perform_now({ user: }) user.reload end.not_to change { user.scanned_gallery_at } end it "enqueues a ScanPostJob for the recent submission" do perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob), ).to match( [ hash_including( post: user.posts.first, caused_by_entry: @log_entries[0], ), ], ) end it "enqueues a UserGalleryJob for the user" do perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserGalleryJob), ).to match([hash_including(user:, caused_by_entry: @log_entries[0])]) end end context "one unlisted submission is known" do let!(:unseen_post_1) do create( :domain_post_fa_post, fa_id: 12_345, creator: user, title: "Not In The Gallery", ) end shared_examples "unlisted submission in state" do |state| context "and is in the '#{state}' state" do before do unseen_post_1.state = state unseen_post_1.save! end it "enqueues a ScanPostJob only for the new gallery submission" do perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob), ).to match( [ hash_including( post: user.posts.find_by(fa_id: 60_073_062), caused_by_entry: @log_entries[0], ), ], ) end it "enqueues a UserGalleryJob for the user" do perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserGalleryJob), ).to match( [hash_including(user:, caused_by_entry: @log_entries[0])], ) end it "does not mark the gallery as scanned" do perform_now({ user: }) user.reload expect(user.scanned_gallery_at).to be_nil end end end # we do not know anything about the unseen submissions, so do # not re-enqueue them - only enqueue those in recent gallery section Domain::Post::FaPost.states.keys.each do |state| include_examples "unlisted submission in state", state end end context "all two unlisted submissions are known" do let!(:unseen_post_1) do create( :domain_post_fa_post, fa_id: 12_345, creator: user, title: "Not In The Gallery 1", ) end let!(:unseen_post_2) do create( :domain_post_fa_post, fa_id: 12_346, creator: user, title: "Not In The Gallery 2", ) end it "enqueues a ScanPostJob for the recent submission" do perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob), ).to match( [hash_including(post: user.posts.find_by(fa_id: 60_073_062))], ) end it "marks the gallery as scanned" do expect do perform_now({ user: }) user.reload end.to change { user.scanned_gallery_at }.to be_within(3.seconds).of( Time.now, ) end end end context "has one recent gallery submission, and user page indicates one total submission" do let(:client_mock_config) do [ { # one recent gallery submission, and one submission indicated on user page uri: "https://www.furaffinity.net/user/kutua/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Mar 2, 2025 09:59 AM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_kutua_one_recent_one_total_gallery.html", ), }, ] end context "the submission is not yet known" do it "updates the num_submissions count" do expect do perform_now({ user: }) user.reload end.to change { user.num_submissions }.from(nil).to(1) end it "creates the submission" do expect do perform_now({ user: }) user.reload end.to change { user.posts.count }.from(0).to(1) post = user.posts.first # only know the creator and fa_id when seen from the user page expect(post.fa_id).to eq(60_073_062) expect(post.title).to be_nil expect(post.description).to be_nil expect(post.file).to be_nil end it "marks the gallery as scanned" do perform_now({ user: }) expect(user.scanned_gallery_at).to be_within(3.seconds).of(Time.now) end it "enqueues a ScanPostJob" do perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob), ).to match( [ hash_including( post: user.posts.first, caused_by_entry: @log_entries[0], ), ], ) end end context "and recent gallery submissions are known" do let!(:post) do create( :domain_post_fa_post, fa_id: 60_073_062, state: "ok", creator: user, ) end shared_examples "force enqueues a ScanPostJob when post is in state" do |state| it "force enqueues a ScanPostJob when a post is in the '#{state}' state" do post.state = state post.save! perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob), ).to match([hash_including(post:, force_scan: true)]) end end shared_examples "force enqueues a ScanFileJob when post file is in state" do |state| it "force enqueues a ScanPostJob when a post's file in the '#{state}' state" do file = create(:domain_post_file, post:) file.state = state file.save! perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob), ).to match([hash_including(post:, force_scan: true)]) end end %w[removed scan_error file_error].each do |state| include_examples( "force enqueues a ScanPostJob when post is in state", state, ) end %w[file_error retryable_error terminal_error removed].each do |state| include_examples( "force enqueues a ScanFileJob when post file is in state", state, ) end it "force enqueues a ScanFileJob a post's file is in the 'pending' state and has a url" do post.state_ok! post.save! file = create( :domain_post_file, post:, url_str: "https://example.com/file.png", ) file.state_pending! file.save! perform_now({ user: }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanFileJob), ).to match([hash_including(post_file: file)]) end it "marks the gallery as scanned" do perform_now({ user: }) expect(user.scanned_gallery_at).to be_within(3.seconds).of(Time.now) end end end end context "the user has no recent favories" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/angu/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Feb 27, 2025 04:30 PM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_angu_no_recent_favorites.html", ), }, ] end let(:user) { Domain::User::FaUser.find_by(url_name: "angu") } it "does not enqueue a favs job" do perform_now({ url_name: "angu" }) expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::FavsJob)).to be_empty end it "marks scanned_favs_at as recent" do perform_now({ url_name: "angu" }) expect(user.scanned_favs_at).to be_within(3.seconds).of(Time.now) end it "records a favs scan" do perform_now({ url_name: "angu" }) expect(user.favs_scans.count).to eq(1) favs_scan = user.favs_scans.first expect(favs_scan.log_entry).to eq(@log_entries[0]) expect(favs_scan.num_added).to eq(0) end end context "all favorites fit in the recently faved section" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/lleaued/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Feb 27, 2025 04:35 PM"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_lleaued_few_recent_favorites.html", ), }, ] end let(:user) { Domain::User::FaUser.find_by(url_name: "lleaued") } it "does not enqueue a favs job" do perform_now({ url_name: "lleaued" }) expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::FavsJob)).to be_empty end it "marks scanned_favs_at as recent" do perform_now({ url_name: "lleaued" }) expect(user.scanned_favs_at).to be_within(3.seconds).of(Time.now) end it "adds posts to the user's favorites" do perform_now({ url_name: "lleaued" }) expect(user.faved_posts.count).to eq(1) expect(user.faved_posts.map(&:fa_id)).to eq([51_355_154]) end it "works when the user already has some favorites" do user = create(:domain_user_fa_user, url_name: "lleaued") post = create(:domain_post_fa_post, fa_id: 51_355_154) user.update_fav_model(post_id: post.id) perform_now({ url_name: "lleaued" }) expect(user.faved_posts.count).to eq(1) expect(user.faved_posts.map(&:fa_id)).to eq([51_355_154]) end it "records a favs scan" do perform_now({ url_name: "lleaued" }) expect(user.favs_scans.count).to eq(1) favs_scan = user.favs_scans.first expect(favs_scan.log_entry).to eq(@log_entries[0]) expect(favs_scan.num_added).to eq(1) end end context "more favorites than fits in the recent faved section" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/dilgear/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Jun 19, 2025 11:14 AM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_dilgear_many_recent_favorites.html", ), }, ] end let(:faved_post_fa_ids) do [ 49_881, 72_900, 60_088, 60_086, 58_000, 57_298, 57_282, 56_811, 55_187, 54_431, 58_488, 31_830, 31_425, 31_413, 31_409, 29_859, 28_961, 26_304, 24_977, 24_451, ] end it "records favs with fav_id and explicit_time" do perform_now({ url_name: "dilgear" }) user = Domain::User::FaUser.find_by(url_name: "dilgear") user_post_favs = user.user_post_favs expect(user_post_favs.count).to eq(20) expect(user_post_favs.map(&:post).map(&:fa_id)).to contain_exactly( *faved_post_fa_ids, ) fav_26304 = user_post_favs.find { |f| f.post.fa_id == 26_304 } expect(fav_26304.explicit_time).to eq( Time.parse("Feb 17, 2006 07:09 AM -08:00"), ) expect(fav_26304.fav_id).to be_nil end context "user has not had a favs scan in the past" do it "enqueues a favs job" do perform_now({ url_name: "dilgear" }) user = Domain::User::FaUser.find_by(url_name: "dilgear") expect(user.scanned_favs_at).to be_nil expect(user.user_post_favs.count).to eq(20) expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::FavsJob)).to match( [hash_including(user:, caused_by_entry: @log_entries[0])], ) end it "does not record a favs scan" do perform_now({ url_name: "dilgear" }) user = Domain::User::FaUser.find_by(url_name: "dilgear") expect(user.favs_scans.count).to eq(0) end end context "the user has had a favs scan in the past" do let(:scanned_favs_at) { 1.year.ago } let(:user) do create(:domain_user_fa_user, url_name: "dilgear", scanned_favs_at:) end let(:faved_post_fa_ids) do [ # newer favs 49_881, 72_900, 60_088, 60_086, 58_000, 57_298, 57_282, 56_811, 55_187, 54_431, 58_488, 31_830, 31_425, 31_413, 31_409, 29_859, 28_961, 26_304, 24_977, 24_451, # older favs ] end shared_examples "does not enqueue a favs job" do it "does not enqueue a favs job" do perform_now({ user: }) user.reload expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::FavsJob), ).to be_empty end end shared_examples "enqueues a favs job" do it "enqueues a favs job" do perform_now({ user: }) expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::FavsJob)).to match( [hash_including(user:, caused_by_entry: @log_entries[0])], ) end it "does not record a favs scan" do perform_now({ user: }) expect(user.favs_scans.count).to eq(0) end end shared_examples "marks scanned_favs_at as recent" do it "marks scanned_favs_at as recent" do expect do perform_now({ user: }) user.reload end.to change { user.scanned_favs_at }.to be_within(3.seconds).of( Time.now, ) end end shared_examples "does not mark scanned_favs_at as recent" do it "does not mark scanned_favs_at as recent" do expect { perform_now({ user: }) user.reload }.to_not change { user.scanned_favs_at } end end shared_examples "does not change the user's favorites" do it "does not change the user's favorites" do expect { perform_now({ user: }) user.reload }.to_not change { user.faved_posts.map(&:fa_id).sort } end end context "and the user has no known favorites" do include_examples "enqueues a favs job" include_examples "does not mark scanned_favs_at as recent" # include_examples "does not change the user's favorites" end context "all user's recent favorites are known" do before do faved_post_fa_ids.each do |fa_post_id| post = create(:domain_post_fa_post, fa_id: fa_post_id) user.update_fav_model(post_id: post.id) end end include_examples "does not enqueue a favs job" include_examples "marks scanned_favs_at as recent" # include_examples "does not change the user's favorites" end context "all but the last favorite are known" do before do faved_post_fa_ids[0..-2].each do |fa_post_id| post = create(:domain_post_fa_post, fa_id: fa_post_id) user.update_fav_model(post_id: post.id) end end include_examples "enqueues a favs job" include_examples "does not mark scanned_favs_at as recent" # include_examples "does not change the user's favorites" end context "favorites in the middle are unknown" do before do (faved_post_fa_ids[..5] + faved_post_fa_ids[8..]).each do |fa_post_id| post = create(:domain_post_fa_post, fa_id: fa_post_id) user.update_fav_model(post_id: post.id) end end include_examples "enqueues a favs job" include_examples "does not mark scanned_favs_at as recent" # include_examples "does not change the user's favorites" end context "favorites at the start are unknown" do before do faved_post_fa_ids[5..].each do |fa_post_id| post = create(:domain_post_fa_post, fa_id: fa_post_id) user.update_fav_model(post_id: post.id) end end include_examples "does not enqueue a favs job" include_examples "marks scanned_favs_at as recent" it "adds the new favorites to the user's favorites" do expect { perform_now({ user: }) }.to change { user.reload user.faved_posts.count }.by(5) expect(user.faved_posts.count).to eq(faved_post_fa_ids.count) expect(user.faved_posts.map(&:fa_id)).to contain_exactly( *faved_post_fa_ids, ) end end end end context "with a user with buggy favcount" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/marsdust/", status_code: 200, content_type: "text/html", requested_at: Time.parse("May 2, 2023 12:41 PM"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_marsdust.html", ), }, ] end it "records the right fav count" do perform_now({ url_name: "marsdust" }) user = Domain::User::FaUser.find_by(url_name: "marsdust") expect(user).to_not be_nil expect(user.avatar.url_str).to eq( "https://a.furaffinity.net/1424255659/marsdust.gif", ) expect(user.num_favorites).to eq(0) end end context "user with page that links to unseen users" do let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/angelpawqt/", status_code: 200, content_type: "text/html", requested_at: Time.parse("Feb 24, 2025 08:52 PM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_angelpawqt.html", ), }, ] end it "enqueues jobs for the unseen users" do perform_now({ url_name: "angelpawqt", skip_enqueue_found_links: false }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserPageJob), ).to include( hash_including( user: Domain::User::FaUser.find_by(url_name: "8bitstarshon1"), ), ) end end shared_examples "user not found" do |status_code| let(:client_mock_config) do [ { uri: "https://www.furaffinity.net/user/onefatpokemon/", status_code:, content_type: "text/html", requested_at: Time.parse("Mar 2, 2025 09:59 AM UTC"), contents: SpecUtil.read_fixture_file( "domain/fa/user_page/user_page_onefatpokemon_not_found.html", ), }, ] end it "does not enqueue a user page job" do perform_now({ url_name: "onefatpokemon" }) expect( SpecUtil.enqueued_job_args(Domain::Fa::Job::UserPageJob), ).to be_empty end it "marks the user as error" do perform_now({ url_name: "onefatpokemon" }) user = Domain::User::FaUser.find_by(url_name: "onefatpokemon") expect(user).to_not be_nil expect(user.state).to eq("error") end end context "user not found with 200 status code" do include_examples "user not found", 200 end context "user not found with 400 status code" do include_examples "user not found", 400 end end