From 9641cf0443cdac644dd2010938a86a60a81a18e0 Mon Sep 17 00:00:00 2001 From: Robert Murray <1809160+rob-murray@users.noreply.github.com> Date: Thu, 19 Dec 2019 18:11:10 +0000 Subject: [PATCH] Support Jeykyll page variables (#27) * Support Jeykyll page variables (wip) * wip * Fix specs * Any 2.n.x version * tryn to get it workin * wip; updating tests * Finish tests * Tidy * Version 2.1.0 * Readme update * update text --- .gitignore | 1 + .travis.yml | 14 +- Gemfile | 1 + README.md | 29 ++- Rakefile | 1 + jekyll-twitter-plugin.gemspec | 9 +- lib/jekyll-twitter-plugin.rb | 68 ++++-- spec/api_request_spec.rb | 3 +- spec/integration_tests.rb | 12 +- spec/spec_helper.rb | 4 +- spec/support/jekyll_template.rb | 1 + spec/support/shared_contexts.rb | 22 -- spec/twitter_tag_spec.rb | 399 +++++++++++++++++++++++++------- 13 files changed, 417 insertions(+), 147 deletions(-) delete mode 100644 spec/support/shared_contexts.rb diff --git a/.gitignore b/.gitignore index a4a3bdf..6d2a6d9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ Gemfile.lock .env output_test.html .tweet-cache +test_app/ diff --git a/.travis.yml b/.travis.yml index 68f4479..a2efd96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: ruby sudo: false rvm: - - 2.0.0 - - 2.1.0 - - 2.2.0 - - 2.3.0 - - 2.4.0 - - 2.5.0 - - 2.6.0 + - 2.0 + - 2.1 + - 2.2 + - 2.3 + - 2.4 + - 2.5 + - 2.6 - ruby-head matrix: allow_failures: diff --git a/Gemfile b/Gemfile index bf99f9a..e0d7b4a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,5 @@ # frozen_string_literal: true + source "https://rubygems.org" # Specify your gem's dependencies in jekyll-twitter-plugin.gemspec diff --git a/README.md b/README.md index 5ebe698..3757eb5 100644 --- a/README.md +++ b/README.md @@ -98,12 +98,31 @@ To use the plugin, in your source content use the tag `twitter` and then pass ad {% twitter https://twitter.com/jekyllrb maxwidth=500 limit=5 %} ``` -| Argument | Required? | Description | -|---|---|---| -| `plugin_type` | Yes | Either `twitter` or `twitternocache` (same as `twitter` but does not cache api responses) | -| `twitter_url` | Yes | The Twitter URL to use, check below for supported URLs. | -| `*options` | No | Parameters for the API separated by spaces. Refer below and to respective Twitter API documentation for available parameters. | +| Argument | Required? | Description | +| --- | --- | --- | +| `plugin_type` | Yes | Either `twitter` or `twitternocache` (same as `twitter` but does not cache api responses) | +| `twitter_url` | Yes | The Twitter URL to use, check below for supported URLs. | +| `*options` | No | Parameters for the API separated by spaces. Refer below and to respective Twitter API documentation for available parameters. | +#### Custom variables + +In addition to passing the Twitter URL directly to the plugin, you can also use [Front Matter](https://jekyllrb.com/docs/front-matter/) to store URLs as page variables. This allows you to re-use view configuration or partials by keeping the Twitter URL(s) separate to page content. + +```liquid +--- +title: My page +tweets: + - https://twitter.com/dhh/status/1162426045405921282 + - https://twitter.com/rails/status/1205565185739673600 +a_tweet: https://twitter.com/rubygems/status/518821243320287232 +--- + +{% for tweet in page.tweets %} + {% twitter tweet align=right width=350 %} +{% endfor %} + +{% twitter page.a_tweet %} +``` ### Supported Twitter URLs diff --git a/Rakefile b/Rakefile index bb28611..5263b58 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,3 @@ # frozen_string_literal: true + require "bundler/gem_tasks" diff --git a/jekyll-twitter-plugin.gemspec b/jekyll-twitter-plugin.gemspec index 66cc3c1..f1588a8 100644 --- a/jekyll-twitter-plugin.gemspec +++ b/jekyll-twitter-plugin.gemspec @@ -1,11 +1,11 @@ -# coding: utf-8 # frozen_string_literal: true -lib = File.expand_path("../lib", __FILE__) + +lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) Gem::Specification.new do |spec| spec.name = "jekyll-twitter-plugin" - spec.version = "2.0.0" + spec.version = "2.1.0" spec.authors = ["Rob Murray"] spec.email = ["robmurray17@gmail.com"] spec.summary = "A Liquid tag plugin for Jekyll blogging engine that embeds Tweets, Timelines and more from Twitter API." @@ -18,8 +18,9 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_development_dependency "bundler" + spec.add_development_dependency "byebug" if RUBY_VERSION >= "2.0" spec.add_development_dependency "rake" spec.add_development_dependency "rspec" + spec.add_development_dependency "rubocop" spec.add_development_dependency "webmock" - spec.add_development_dependency "byebug" if RUBY_VERSION >= "2.0" end diff --git a/lib/jekyll-twitter-plugin.rb b/lib/jekyll-twitter-plugin.rb index 713bb8b..71a5304 100644 --- a/lib/jekyll-twitter-plugin.rb +++ b/lib/jekyll-twitter-plugin.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "fileutils" require "net/http" require "uri" @@ -30,23 +31,22 @@ def initialize(path) end def read(key) - file_to_read = cache_file(cache_filename(key)) + file_to_read = cache_file(key) JSON.parse(File.read(file_to_read)) if File.exist?(file_to_read) end def write(key, data) - file_to_write = cache_file(cache_filename(key)) - data_to_write = JSON.generate data.to_h + file_to_write = cache_file(key) File.open(file_to_write, "w") do |f| - f.write(data_to_write) + f.write(JSON.generate(data.to_h)) end end private - def cache_file(filename) - File.join(@cache_folder, filename) + def cache_file(key) + File.join(@cache_folder, cache_filename(key)) end def cache_filename(cache_key) @@ -77,7 +77,6 @@ def fetch(api_request) end handle_response(api_request, response) - rescue Timeout::Error => e ErrorResponse.new(api_request, e.class.name).to_h end @@ -144,11 +143,22 @@ class TwitterTag < Liquid::Tag ERROR_BODY_TEXT = "

Tweet could not be processed

".freeze OEMBED_ARG = "oembed".freeze + URL_OR_STRING_PARAM = /^("|')?(http|https):\/\//i + attr_writer :cache # for testing def initialize(_name, params, _tokens) super - @api_request = parse_params(params) + + # Test if first arg is a URL or starts with oembed, + # otherwise its a Jekyll variable. TODO: remove oembed after deprecation cycle + if params =~ URL_OR_STRING_PARAM || params.to_s.start_with?(OEMBED_ARG) + @fetch_from_context = false + @api_request = parse_params_from_string(params) + else + @fetch_from_context = true + @variable_params = normalize_string_params(params) + end end # Class that implements caching strategy @@ -160,6 +170,12 @@ def self.cache_klass # Return html string for Jekyll engine # @api public def render(context) + if fetch_from_context? + variable_name, *params = @variable_params + tweet_url = context[variable_name] + @api_request = parse_params_from_array [tweet_url, *params] + end + api_secrets_deprecation_warning(context) # TODO: remove after deprecation cycle response = cached_response || live_response html_output_for(response) @@ -167,6 +183,10 @@ def render(context) private + def fetch_from_context? + @fetch_from_context + end + def cache @cache ||= self.class.cache_klass.new("./.tweet-cache") end @@ -199,11 +219,19 @@ def cached_response build_response(response) unless response.nil? end + def parse_params_from_string(str) + args = normalize_string_params(str) + parse_params(args) + end + + def parse_params_from_array(arr) + parse_params(arr) + end + # Return an `ApiRequest` with the url and arguments # @api private - def parse_params(params) - args = params.split(/\s+/).map(&:strip) - invalid_args!(args) unless args.any? + def parse_params(args) + invalid_args!(args) unless args.compact.any? if args[0].to_s == OEMBED_ARG # TODO: remove after deprecation cycle arguments_deprecation_warning(args) @@ -211,25 +239,31 @@ def parse_params(params) end url, *api_args = args - ApiRequest.new(url, parse_args(api_args)) + ApiRequest.new(url, hash_from_args(api_args)) + end + + # Take input arguments, remove quotes & return array of argument values + # @api private + def normalize_string_params(str) + str.to_s.gsub(/"|'/, "").split(/\s+/).map(&:strip) end # Transform 'a=b x=y' tag arguments into { "a" => "b", "x" => "y" } # @api private - def parse_args(args) - args.each_with_object({}) do |arg, params| + def hash_from_args(args) + args.each_with_object({}) do |arg, results| k, v = arg.split("=").map(&:strip) if k && v v = Regexp.last_match[1] if v =~ /^'(.*)'$/ - params[k] = v + results[k] = v end end end # Format a response hash # @api private - def build_response(h) - OpenStruct.new(h) + def build_response(response_hash) + OpenStruct.new(response_hash) end # TODO: remove after deprecation cycle diff --git a/spec/api_request_spec.rb b/spec/api_request_spec.rb index abb6b80..e765f78 100644 --- a/spec/api_request_spec.rb +++ b/spec/api_request_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + RSpec.describe TwitterJekyll::ApiRequest do subject(:api_request) { described_class.new(url, params) } let(:url) { "https://twitter.com/twitter_user/status/12345" } @@ -23,7 +24,7 @@ let(:params) { { align: "right" } } it "has encoded query params" do - expect(URI.decode_www_form(uri.query)).to match_array [["url", url], %w(align right)] + expect(URI.decode_www_form(uri.query)).to match_array [["url", url], %w[align right]] end end diff --git a/spec/integration_tests.rb b/spec/integration_tests.rb index 265315a..8759c88 100644 --- a/spec/integration_tests.rb +++ b/spec/integration_tests.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true # Basic integration example - run code to produce html output # -# * Requires .env populated with valid Twitter API creds. -# $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) require_relative "./support/jekyll_template" require "jekyll-twitter-plugin" @@ -53,9 +51,9 @@ def render attr_reader :options, :jekyll_context - def render_twitter_tag(params) - say_with_colour "Fetching with params: #{params}", :yellow - TwitterJekyll::TwitterTag.new(nil, params, nil).render(jekyll_context) + def render_twitter_tag(option) + say_with_colour "Fetching with option: #{option}", :yellow + TwitterJekyll::TwitterTag.new(nil, option, nil).render(jekyll_context) end def template @@ -75,9 +73,9 @@ def template end def main - rederer = TwitterRenderer.new(OPTIONS) + renderer = TwitterRenderer.new(OPTIONS) File.open(OUTPUT_FILENAME, "w") do |f| - f.write rederer.render + f.write renderer.render end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f4856cc..ffc5776 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) require "webmock/rspec" require "support/jekyll_template" -require "support/shared_contexts" require "jekyll-twitter-plugin" require "byebug" if RUBY_VERSION >= "2.0" diff --git a/spec/support/jekyll_template.rb b/spec/support/jekyll_template.rb index de6cad6..450d3fd 100644 --- a/spec/support/jekyll_template.rb +++ b/spec/support/jekyll_template.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Hack ...or stub Liquid classes and methods used in plugin module Liquid class Tag diff --git a/spec/support/shared_contexts.rb b/spec/support/shared_contexts.rb deleted file mode 100644 index 27caa15..0000000 --- a/spec/support/shared_contexts.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true -RSpec.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 - -RSpec.shared_context "with a normal request and response" do - let(:arguments) { "https://twitter.com/twitter_user/status/12345" } - let(:response) { { html: "

tweet html

" } } - - before do - allow(api_client).to receive(:fetch).and_return(response) - allow(TwitterJekyll::ApiClient).to receive(:new).and_return(api_client) - end -end diff --git a/spec/twitter_tag_spec.rb b/spec/twitter_tag_spec.rb index 9e226e3..8d1bcf1 100644 --- a/spec/twitter_tag_spec.rb +++ b/spec/twitter_tag_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true + RSpec.describe TwitterJekyll::TwitterTag do - let(:context) { empty_jekyll_context } - let(:arguments) { "" } let(:api_response_hash) do { "url" => "https://twitter.com/twitter_user/status/12345", @@ -17,17 +16,77 @@ "version" => "1.0" } end - subject { described_class.new(nil, arguments, nil) } - describe "output from oembed request" do + 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) @@ -35,128 +94,298 @@ expect_output_to_match_tag_content(output, api_response_hash.fetch("html")) end end + end - context "without cached response" do - let(:cache) { double("TwitterJekyll::FileCache") } + shared_examples "it handles api responses" do + context "with successful api request" do before do - subject.cache = cache - allow(cache).to receive(:read).with(an_instance_of(String)).and_return(nil) + stub_api_request(status: 200, body: api_response_hash.to_json, headers: {}) end - 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) - 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 - output = subject.render(context) - expect_output_to_match_tag_content(output, api_response_hash.fetch("html")) - end + context "with a status not found api request" do + before do + stub_api_request(status: [404, "Not Found"], body: "", headers: {}) 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)) - 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 - output = subject.render(context) - expect_output_to_have_error(output, "Not Found") - end + context "with a status request not permitted api request" do + before do + stub_api_request(status: [403, "Forbidden"], body: "", headers: {}) 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)) - 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 - output = subject.render(context) - expect_output_to_have_error(output, "Forbidden") - end + context "with a server error api request" do + before do + stub_api_request(status: [500, "Internal Server Error"], body: "", headers: {}) 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)) - 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 - output = subject.render(context) - expect_output_to_have_error(output, "Internal Server Error") - end + context "with api request that times out" do + before do + stub_api.to_timeout 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)) - 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 - output = subject.render(context) - expect_output_to_have_error(output, "Net::OpenTimeout") - 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 the oembed api type as the first argument" do - let(:arguments) { "oembed https://twitter.com/twitter_user/status/12345" } - before do - stub_api_request(status: 200, body: api_response_hash.to_json, headers: {}) + 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) - it "renders response from api and writes to cache" do - expect(cache).to receive(:write).with(an_instance_of(String), api_response_hash) + subject = described_class.new(nil, arguments, nil) + subject.render(context) - output = subject.render(context) - expect_output_to_match_tag_content(output, api_response_hash.fetch("html")) + 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 "parsing arguments" do - context "without any arguments" do - let(:arguments) { "" } + describe "output from usage with front matter var" do + include_context "called with a page var" - it "raises an exception" do - expect_to_raise_invalid_args_error(arguments) do - tag = described_class.new(nil, arguments, nil) - tag.render(context) + 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 - context "with the oembed api type as the first argument" do - let(:arguments) { "oembed https://twitter.com/twitter_user/status/12345" } + describe "output from usage in a loop with a local var" do + include_context "called in a loop with a local var" - it "uses correct twitter url and warns of deprecation" do + 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) - expect(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", {}).and_call_original + allow(TwitterJekyll::ApiRequest).to receive(:new).with("https://twitter.com/twitter_user/status/12345", {}).and_call_original + allow(cache).to receive(:write) - expect do - tag = described_class.new(nil, arguments, nil) - tag.render(context) - end.to output(/Passing 'oembed' as the first argument is not required anymore/).to_stderr + 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" - include_context "with a normal request and response" - let(:api_client) { api_client_double } + + 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: {}) }) } @@ -177,7 +406,7 @@ context "with api secrets provided by Jekyll config" do let(:context) do - api_secrets = %w(consumer_key consumer_secret access_token access_token_secret) + 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 }) }) @@ -220,7 +449,13 @@ def stub_api end def empty_jekyll_context - double("context", registers: { site: double(config: {}) }) + 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 @@ -237,10 +472,10 @@ def expect_output_to_have_error(actual, error, tweet_url = "https://twitter.com/ expect_output_to_match_tag_content(actual, "

There was a '#{error}' error fetching URL: '#{tweet_url}'

") end - def expect_to_raise_invalid_args_error(options) + def expect_to_raise_invalid_args_error(arguments) raise unless block_given? - message = "Invalid arguments '#{options}' passed to 'jekyll-twitter-plugin'. Please see 'https://github.com/rob-murray/jekyll-twitter-plugin' for usage." + 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)