diff --git a/app/black_box/black_box.rb b/app/black_box/black_box.rb index a2c37383d..eb60d0693 100644 --- a/app/black_box/black_box.rb +++ b/app/black_box/black_box.rb @@ -1,8 +1,6 @@ class BlackBox class << self - def article_hotness_score(article) - return (article.featured_number || 10_000) / 10_000 unless Rails.env.production? - + def article_hotness_score(article, function_caller = FunctionCaller) usable_date = article.crossposted_at || article.published_at reaction_points = article.score super_super_recent_bonus = usable_date > 1.hour.ago ? 28 : 0 @@ -11,8 +9,8 @@ class BlackBox today_bonus = usable_date > 26.hours.ago ? 395 : 0 two_day_bonus = usable_date > 48.hours.ago ? 330 : 0 four_day_bonus = usable_date > 96.hours.ago ? 330 : 0 - FunctionCaller.new("blackbox-production-articleHotness", - { article: article, user: article.user }.to_json).call + + function_caller.call("blackbox-production-articleHotness", + { article: article, user: article.user }.to_json).to_i + reaction_points + recency_bonus + super_recent_bonus + super_super_recent_bonus + today_bonus + two_day_bonus + four_day_bonus end @@ -24,13 +22,12 @@ class BlackBox (rep_points + descendants_points + bonus_points - spaminess_rating).to_i end - def calculate_spaminess(story) + def calculate_spaminess(story, function_caller = FunctionCaller) # accepts comment or article as story - return 0 unless Rails.env.production? return 100 unless story.user - FunctionCaller.new("blackbox-production-spamScore", - { story: story, user: story.user }.to_json).call + function_caller.call("blackbox-production-spamScore", + { story: story, user: story.user }.to_json).to_i end private diff --git a/app/labor/function_caller.rb b/app/labor/function_caller.rb index 8ae2a08ed..2fe44eb4b 100644 --- a/app/labor/function_caller.rb +++ b/app/labor/function_caller.rb @@ -1,12 +1,20 @@ class FunctionCaller - attr_accessor :function_name, :payload - def initialize(function_name, payload) + def initialize(function_name, payload, aws_lambda_client = AWS_LAMBDA) @function_name = function_name @payload = payload + @aws_lambda_client = aws_lambda_client + end + + def self.call(*args) + new(*args).call end def call - response = AWS_LAMBDA.invoke(function_name: function_name, payload: payload) + response = aws_lambda_client.invoke(function_name: function_name, payload: payload) JSON.parse(JSON.parse(response.payload.as_json[0])["body"])["message"] end + + private + + attr_reader :function_name, :payload, :aws_lambda_client end diff --git a/app/services/aws/fake_client.rb b/app/services/aws/fake_client.rb new file mode 100644 index 000000000..9b0ad5de4 --- /dev/null +++ b/app/services/aws/fake_client.rb @@ -0,0 +1,10 @@ +# Fake Aws client is used in non-production environments to prevent actual calls to AWS lambda +module Aws + class FakeClient + def initialize; end + + def invoke(*) + OpenStruct.new(payload: [{ body: { message: 0 }.to_json }.to_json]) + end + end +end diff --git a/config/initializers/aws_sdk.rb b/config/initializers/aws_sdk.rb index b57e02f4b..88a8400a8 100644 --- a/config/initializers/aws_sdk.rb +++ b/config/initializers/aws_sdk.rb @@ -1,5 +1,9 @@ -AWS_LAMBDA = Aws::Lambda::Client.new( - region: ApplicationConfig["AWS_DEFAULT_REGION"], - access_key_id: ApplicationConfig["AWS_SDK_KEY"], - secret_access_key: ApplicationConfig["AWS_SDK_SECRET"], -) +AWS_LAMBDA = if Rails.env.production? + Aws::Lambda::Client.new( + region: ApplicationConfig["AWS_DEFAULT_REGION"], + access_key_id: ApplicationConfig["AWS_SDK_KEY"], + secret_access_key: ApplicationConfig["AWS_SDK_SECRET"], + ) + else + AWS_LAMBDA = Aws::FakeClient.new + end diff --git a/spec/black_box/black_box_spec.rb b/spec/black_box/black_box_spec.rb new file mode 100644 index 000000000..f4302ce2e --- /dev/null +++ b/spec/black_box/black_box_spec.rb @@ -0,0 +1,77 @@ +require "rails_helper" + +RSpec.describe BlackBox do + let!(:function_caller) { double } + + describe "#article_hotness_score" do + let!(:article) { create(:article, published_at: Time.current) } + + it "calls function caller" do + allow(function_caller).to receive(:call).and_return(5) + described_class.article_hotness_score(article, function_caller) + expect(function_caller).to have_received(:call).once + end + + it "doesn't fail when function caller returns nil" do + allow(function_caller).to receive(:call).and_return(nil) + described_class.article_hotness_score(article, function_caller) + end + + it "returns the correct value" do + article.update_column(:score, 99) + allow(function_caller).to receive(:call).and_return(5) + # recent bonuses (28 + 31 + 80 + 395 + 330 + 330 = 1194) + # + score (99) + # + value from the function caller (5) + score = described_class.article_hotness_score(article, function_caller) + expect(score).to eq(1298) + end + end + + describe "#comment_quality_score" do + let(:comment) { create(:comment, commentable: create(:article), body_markdown: "```#{'hello, world! ' * 20}```") } + + before do + reaction = create(:reaction, reactable: comment) + reaction.update_column(:points, 20) + reaction2 = create(:reaction, reactable: comment) + reaction2.update_column(:points, 2) + end + + it "returns the correct score" do + # rep_points + descendants_points + bonus_points - spaminess_rating + # rep_points - 22 + # descendants_points - 0 + # bonus_points - 2 + 1 = 3 + # spaminess_rating - 0 + # 22 + 0 + 3 - 0 = 25 + expect(described_class.comment_quality_score(comment)).to eq(25) + end + end + + describe "#calculate_spaminess" do + let(:user) { build(:user) } + let(:comment) { build(:comment, user: user) } + + before do + allow(function_caller).to receive(:call).and_return(1) + end + + it "returns 100 if there is no user" do + story = instance_double("Comment", user: nil) + expect(described_class.calculate_spaminess(story, function_caller)).to eq(100) + expect(function_caller).not_to have_received(:call) + end + + it "calls the function_caller" do + described_class.calculate_spaminess(comment, function_caller) + expect(function_caller).to have_received(:call).with("blackbox-production-spamScore", + { story: comment, user: user }.to_json).once + end + + it "returns the value that the caller returns" do + spaminess = described_class.calculate_spaminess(comment, function_caller) + expect(spaminess).to eq(1) + end + end +end diff --git a/spec/labor/coupon_generator_spec.rb b/spec/labor/coupon_generator_spec.rb index 9282d9799..a0096d7d9 100644 --- a/spec/labor/coupon_generator_spec.rb +++ b/spec/labor/coupon_generator_spec.rb @@ -5,10 +5,8 @@ RSpec.describe CouponGenerator, vcr: {} do describe "#expect" do it "generates code with proper prefix ( [version]_[code] )" do - VCR.use_cassette "coupon_generator_1" do - version = versions.sample - expect(described_class.new(1, version).generate).to include("#{version}_") - end + version = versions.sample + expect(described_class.new(1, version).generate).to include("#{version}_") end end end diff --git a/spec/labor/function_caller_spec.rb b/spec/labor/function_caller_spec.rb new file mode 100644 index 000000000..5c67302e8 --- /dev/null +++ b/spec/labor/function_caller_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe FunctionCaller do + let(:payload) { { user_id: 1, name: "hello" } } + let(:dummy_client) { double } + let(:result) { OpenStruct.new(payload: [{ body: { message: "hi" }.to_json }.to_json]) } + + before do + allow(dummy_client).to receive(:invoke).and_return(result) + end + + it "calls the aws_lambda_client" do + described_class.call("some_function", payload, dummy_client) + expect(dummy_client).to have_received(:invoke).with(function_name: "some_function", payload: payload) + end + + it "returns parsed value" do + value = described_class.call("some_function", payload, dummy_client) + expect(value).to eq("hi") + end +end