# frozen_string_literal: true RSpec.describe TwitterJekyll::TwitterTag do let(:api_response_hash) do { "url" => "https://twitter.com/twitter_user/status/12345", "author_name" => "twitter user", "author_url" => "https://twitter.com/twitter_user", "html" => "<p>tweet html</p>", "width" => 550, "height" => nil, "type" => "rich", "cache_age" => "3153600000", "provider_name" => "Twitter", "provider_url" => "https://twitter.com", "version" => "1.0" } end shared_context "without cached response" do let(:cache) { null_cache } before do subject.cache = cache end def null_cache double("TwitterJekyll::NullCache", read: nil, write: nil) end end shared_context "called with deprecated oembed argument url" do # {% twitter oembed https://twitter.com/twitter_user/status/12345 option=value %} let(:arguments) { "oembed https://twitter.com/twitter_user/status/12345" } let(:context) { empty_jekyll_context } subject { described_class.new(nil, arguments, nil) } end shared_context "called with url" do # {% twitter https://twitter.com/twitter_user/status/12345 option=value %} let(:arguments) { "https://twitter.com/twitter_user/status/12345" } let(:context) { empty_jekyll_context } subject { described_class.new(nil, arguments, nil) } end shared_context "called with a page var" do # {% twitter page.tweet option=value %} let(:arguments) { "page.tweet" } let(:context) { jekyll_context_with(arguments, params) } let(:params) { "https://twitter.com/twitter_user/status/12345" } subject { described_class.new(nil, arguments, nil) } end shared_context "called in a loop with a local var" do # This is the same as above but ensures that same instance can render different contexts # {% for tweet in page.tweets %} # {% twitter tweet option=value %} # {% endfor %} let(:arguments) { "tweet" } let(:context) { jekyll_context_with(arguments, params) } let(:params) { "https://twitter.com/twitter_user/status/12345" } subject { described_class.new(nil, arguments, nil) } end shared_examples "it does not allow empty arguments" do context "without any arguments" do let(:arguments) { "" } it "raises an exception" do expect_to_raise_invalid_args_error(arguments) do subject.render(context) end end end end shared_examples "it uses a cached response" do context "with cached response" do let(:cache) { double("TwitterJekyll::FileCache") } before do subject.cache = cache end let(:arguments) { "https://twitter.com/twitter_user/status/12345" } it "renders response from cache" do expect(cache).to receive(:read).with(an_instance_of(String)).and_return(api_response_hash) output = subject.render(context) expect_output_to_match_tag_content(output, api_response_hash.fetch("html")) end end end shared_examples "it handles api responses" do context "with successful api request" do before do stub_api_request(status: 200, body: api_response_hash.to_json, headers: {}) end it "renders response from api and writes to cache" do expect(cache).to receive(:write).with(an_instance_of(String), api_response_hash) output = subject.render(context) expect_output_to_match_tag_content(output, api_response_hash.fetch("html")) end end context "with a status not found api request" do before do stub_api_request(status: [404, "Not Found"], body: "", headers: {}) end it "renders error response and writes to cache" do expect(cache).to receive(:write).with(an_instance_of(String), an_instance_of(Hash)) output = subject.render(context) expect_output_to_have_error(output, "Not Found") end end context "with a status request not permitted api request" do before do stub_api_request(status: [403, "Forbidden"], body: "", headers: {}) end it "renders error response and writes to cache" do expect(cache).to receive(:write).with(an_instance_of(String), an_instance_of(Hash)) output = subject.render(context) expect_output_to_have_error(output, "Forbidden") end end context "with a server error api request" do before do stub_api_request(status: [500, "Internal Server Error"], body: "", headers: {}) end it "renders error response and writes to cache" do expect(cache).to receive(:write).with(an_instance_of(String), an_instance_of(Hash)) output = subject.render(context) expect_output_to_have_error(output, "Internal Server Error") end end context "with api request that times out" do before do stub_api.to_timeout end it "renders error response and writes to cache" do expect(cache).to receive(:write).with(an_instance_of(String), an_instance_of(Hash)) output = subject.render(context) expect_output_to_have_error(output, "Net::OpenTimeout") end end end describe "output from oembed request" do include_context "called with deprecated oembed argument url" it_behaves_like "it does not allow empty arguments" it_behaves_like "it uses a cached response" context "without cached response" do include_context "without cached response" it "uses correct twitter url and warns of deprecation" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", {}).and_call_original allow(cache).to receive(:write) expect do subject = described_class.new(nil, arguments, nil) subject.render(context) end.to output(/Passing 'oembed' as the first argument is not required anymore/).to_stderr expect(TwitterJekyll::ApiRequest).to have_received(:new).with("https://twitter.com/twitter_user/status/12345", {}) end context "with options" do let(:arguments) { "oembed https://twitter.com/twitter_user/status/12345 align=right width=350" } it "passes options to api" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", "align" => "right", "width" => "350").and_call_original allow(cache).to receive(:write) subject = described_class.new(nil, arguments, nil) subject.cache = cache subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new) .with("https://twitter.com/twitter_user/status/12345", "align" => "right", "width" => "350") end end it_behaves_like "it handles api responses" end end describe "output from url request" do include_context "called with url" it_behaves_like "it does not allow empty arguments" it_behaves_like "it uses a cached response" context "without cached response" do include_context "without cached response" it "uses correct twitter url" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", {}).and_call_original allow(cache).to receive(:write) subject = described_class.new(nil, arguments, nil) subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new).with("https://twitter.com/twitter_user/status/12345", {}) end context "with options" do let(:arguments) { "https://twitter.com/twitter_user/status/12345 align=right width=350" } it "passes options to api" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", "align" => "right", "width" => "350").and_call_original allow(cache).to receive(:write) subject = described_class.new(nil, arguments, nil) subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new) .with("https://twitter.com/twitter_user/status/12345", "align" => "right", "width" => "350") end end it_behaves_like "it handles api responses" end end describe "output from usage with front matter var" do include_context "called with a page var" it_behaves_like "it does not allow empty arguments" do let(:arguments) { "page.tweet" } let(:context) { empty_jekyll_context } let(:params) { "" } end it_behaves_like "it uses a cached response" context "without cached response" do include_context "without cached response" it "uses correct twitter url" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", {}).and_call_original allow(cache).to receive(:write) subject = described_class.new(nil, arguments, nil) subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new).with("https://twitter.com/twitter_user/status/12345", {}) end context "with options" do let(:arguments) { "page.tweet align=left width=400" } let(:context) { jekyll_context_with("page.tweet", params) } let(:params) { "https://twitter.com/twitter_user/status/12345" } it "passes options to api" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", "align" => "left", "width" => "400").and_call_original allow(cache).to receive(:write) subject = described_class.new(nil, arguments, nil) subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new) .with("https://twitter.com/twitter_user/status/12345", "align" => "left", "width" => "400") end end it_behaves_like "it handles api responses" end end describe "output from usage in a loop with a local var" do include_context "called in a loop with a local var" it_behaves_like "it does not allow empty arguments" do let(:arguments) { "tweet" } let(:context) { empty_jekyll_context } let(:params) { "" } end it_behaves_like "it uses a cached response" context "without cached response" do include_context "without cached response" it "uses correct twitter url" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", {}).and_call_original allow(cache).to receive(:write) subject = described_class.new(nil, arguments, nil) subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new).with("https://twitter.com/twitter_user/status/12345", {}) end it "handles many contexts passed to same instance" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/first_url", {}).and_call_original allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/second_url", {}).and_call_original allow(cache).to receive(:write) context = double("context", registers: { site: double(config: {}) }).tap do |c| allow(c).to receive(:[]).with(arguments).and_return( "https://twitter.com/twitter_user/status/first_url", "https://twitter.com/twitter_user/status/second_url" ) end subject = described_class.new(nil, arguments, nil) subject.render(context) subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new).with("https://twitter.com/twitter_user/status/first_url", {}) expect(TwitterJekyll::ApiRequest).to have_received(:new).with("https://twitter.com/twitter_user/status/second_url", {}) end context "with options" do let(:arguments) { "tweet align=middle width=500" } let(:context) { jekyll_context_with("tweet", params) } let(:params) { "https://twitter.com/twitter_user/status/12345" } it "passes options to api" do api_client = api_client_double allow(api_client).to receive(:fetch).and_return({}) allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", "align" => "middle", "width" => "500").and_call_original allow(cache).to receive(:write) subject = described_class.new(nil, arguments, nil) subject.render(context) expect(TwitterJekyll::ApiRequest).to have_received(:new) .with("https://twitter.com/twitter_user/status/12345", "align" => "middle", "width" => "500") end end it_behaves_like "it handles api responses" end end describe "parsing api secrets" do include_context "called with url" include_context "without cached response" before do stub_api_request(status: 200, body: api_response_hash.to_json, headers: {}) end context "with api secrets provided by ENV" do let(:context) { double("context", registers: { site: double(config: {}) }) } before do stub_const("ENV", "TWITTER_CONSUMER_KEY" => "consumer_key", "TWITTER_CONSUMER_SECRET" => "consumer_secret", "TWITTER_ACCESS_TOKEN" => "access_token", "TWITTER_ACCESS_TOKEN_SECRET" => "access_token_secret") end it "warns of deprecation" do expect do tag = described_class.new(nil, arguments, nil) tag.render(context) end.to output(/Found Twitter API keys in ENV, this library does not require these keys anymore/).to_stderr end end context "with api secrets provided by Jekyll config" do let(:context) do api_secrets = %w[consumer_key consumer_secret access_token access_token_secret] .each_with_object({}) { |secret, h| h[secret] = secret } double("context", registers: { site: double(config: { "twitter" => api_secrets }) }) end before do stub_const("ENV", {}) end it "warns of deprecation" do expect do tag = described_class.new(nil, arguments, nil) tag.render(context) end.to output(/Found Twitter API keys in Jekyll _config.yml, this library does not require these keys anymore/).to_stderr end end context "with no api secrets provided" do let(:context) { empty_jekyll_context } before do stub_const("ENV", {}) end it "does not warn" do expect do subject.render(context) end.to_not output.to_stderr end end end private def stub_api_request(response) stub_api .to_return(response) end def stub_api stub_request(:get, /publish.twitter.com/) end def empty_jekyll_context double("context", registers: { site: double(config: {}) }, :[] => nil) end def jekyll_context_with(var, params) double("context", registers: { site: double(config: {}) }).tap do |c| allow(c).to receive(:[]).with(var).and_return(params) end end def api_client_double double("TwitterJekyll::ApiClient") end def expect_output_to_match_tag_content(actual, content) expect(actual).to eq( "<div class='jekyll-twitter-plugin'>#{content}</div>" ) end def expect_output_to_have_error(actual, error, tweet_url = "https://twitter.com/twitter_user/status/12345") expect_output_to_match_tag_content(actual, "<p>There was a '#{error}' error fetching URL: '#{tweet_url}'</p>") end def expect_to_raise_invalid_args_error(arguments) raise unless block_given? message = "Invalid arguments '#{arguments}' passed to 'jekyll-twitter-plugin'. Please see 'https://github.com/rob-murray/jekyll-twitter-plugin' for usage." expect do yield end.to raise_error(ArgumentError, message) end end