diff --git a/Envfile b/Envfile
index cac827daa..fb2897eb2 100644
--- a/Envfile
+++ b/Envfile
@@ -144,6 +144,11 @@ variable :TWILIO_ACCOUNT_SID, :String, default: "Optional"
variable :TWILIO_VIDEO_API_KEY, :String, default: "Optional"
variable :TWILIO_VIDEO_API_SECRET, :String, default: "Optional"
+# For Twitch live stream integration
+variable :TWITCH_CLIENT_ID, :String, default: "Optional"
+variable :TWITCH_CLIENT_SECRET, :String, default: "Optional"
+variable :TWITCH_WEBHOOK_SECRET, :String, default: "Optional"
+
# Trending tags on home page
variable :TRENDING_TAGS, :String, default: "git,beginners"
diff --git a/app/assets/stylesheets/scaffolds.scss b/app/assets/stylesheets/scaffolds.scss
index 8c0f6aa06..f746b6ceb 100644
--- a/app/assets/stylesheets/scaffolds.scss
+++ b/app/assets/stylesheets/scaffolds.scss
@@ -18,7 +18,7 @@ body {
&:active {
outline: 0;
}
-}
+}
.ptr--ptr{
@@ -38,7 +38,7 @@ body {
overflow:hidden;
min-height:88vh;
visibility: visible;
- &.stories-index,&.notifications-index,&.stories-search,&.podcast_episodes-index,&.reading_list_items-index,.tags-index{
+ &.stories-index,&.notifications-index,&.stories-search,&.podcast_episodes-index,&.reading_list_items-index,.tags-index,&.twitch_live_streams-show{
margin-top:68px;
}
}
diff --git a/app/controllers/twitch_live_streams_controller.rb b/app/controllers/twitch_live_streams_controller.rb
new file mode 100644
index 000000000..789267f0b
--- /dev/null
+++ b/app/controllers/twitch_live_streams_controller.rb
@@ -0,0 +1,12 @@
+class TwitchLiveStreamsController < ApplicationController
+ before_action :set_cache_control_headers
+
+ def show
+ @user = User.find_by!(username: params[:username].tr("@", "").downcase)
+ if @user.twitch_username.present?
+ render :show
+ else
+ render :no_twitch
+ end
+ end
+end
diff --git a/app/controllers/twitch_stream_updates_controller.rb b/app/controllers/twitch_stream_updates_controller.rb
new file mode 100644
index 000000000..93206983e
--- /dev/null
+++ b/app/controllers/twitch_stream_updates_controller.rb
@@ -0,0 +1,44 @@
+class TwitchStreamUpdatesController < ApplicationController
+ skip_before_action :verify_authenticity_token
+
+ def show
+ if params["hub.mode"] == "denied"
+ airbrake_logger.error("Twitch Webhook was denied: #{params.permit('hub.mode', 'hub.reason', 'hub.topic').to_json}")
+ head :no_content
+ else
+ render plain: params["hub.challenge"]
+ end
+ end
+
+ def create
+ head :no_content
+
+ unless secret_verified?
+ airbrake_logger.warn("Twitch Webhook Recieved for which the webhook could not be verified")
+ return
+ end
+
+ user = User.find(params[:user_id])
+
+ if params[:data].first.present?
+ user.update!(currently_streaming_on: :twitch)
+ else
+ user.update!(currently_streaming_on: nil)
+ end
+ end
+
+ private
+
+ def airbrake_logger
+ Airbrake::AirbrakeLogger.new(Rails.logger)
+ end
+
+ def secret_verified?
+ twitch_sha = request.headers["x-hub-signature"]
+ digest = Digest::SHA256.new
+ digest << ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
+ digest << request.raw_post
+
+ twitch_sha == "sha256=#{digest.hexdigest}"
+ end
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 3e64ffdcd..770a9ea3b 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -24,6 +24,7 @@ class UsersController < ApplicationController
# raise permitted_attributes(@user).to_s
if @user.update(permitted_attributes(@user))
RssReader.new.delay.fetch_user(@user) if @user.feed_url.present?
+ Streams::TwitchWebhookRegistrationJob.perform_later(@user.id) if @user.twitch_username.present?
notice = "Your profile was successfully updated."
if @user.export_requested?
notice += " The export will be emailed to you shortly."
diff --git a/app/jobs/streams/twitch_webhook_registration_job.rb b/app/jobs/streams/twitch_webhook_registration_job.rb
new file mode 100644
index 000000000..7a312d643
--- /dev/null
+++ b/app/jobs/streams/twitch_webhook_registration_job.rb
@@ -0,0 +1,12 @@
+module Streams
+ class TwitchWebhookRegistrationJob < ApplicationJob
+ queue_as :twitch_webhook_registration
+
+ def perform(user_id, service = TwitchWebhook::Register)
+ user = User.find_by(id: user_id)
+ return if user.blank? || user.twitch_username.blank?
+
+ service.call(user)
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 18985d726..f567d27b9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -130,6 +130,7 @@ class User < ApplicationRecord
validates :mentee_description, :mentor_description,
length: { maximum: 1000 }
validates :inbox_type, inclusion: { in: %w[open private] }
+ validates :currently_streaming_on, inclusion: { in: %w[twitch] }, allow_nil: true
validate :conditionally_validate_summary
validate :validate_mastodon_url
validate :validate_feed_url, if: :feed_url_changed?
@@ -145,7 +146,7 @@ class User < ApplicationRecord
before_update :mentorship_status_update
before_validation :set_username
# make sure usernames are not empty, to be able to use the database unique index
- before_validation :verify_twitter_username, :verify_github_username, :verify_email
+ before_validation :verify_twitter_username, :verify_github_username, :verify_email, :verify_twitch_username
before_validation :set_config_input
before_validation :downcase_email
before_validation :check_for_username_change
@@ -400,6 +401,14 @@ class User < ApplicationRecord
roles.where(name: "tag_moderator").any?
end
+ def currently_streaming?
+ currently_streaming_on.present?
+ end
+
+ def currently_streaming_on_twitch?
+ currently_streaming_on == "twitch"
+ end
+
private
def send_welcome_notification
@@ -418,6 +427,10 @@ class User < ApplicationRecord
self.email = nil if email == ""
end
+ def verify_twitch_username
+ self.twitch_username = nil if twitch_username == ""
+ end
+
def set_username
set_temp_username if username.blank?
self.username = username&.downcase
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index dbcae75f8..1e780f9e5 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -117,6 +117,7 @@ class UserPolicy < ApplicationPolicy
summary
text_color_hex
twitch_url
+ twitch_username
username
website_url
export_requested]
diff --git a/app/services/streams/twitch_access_token/get.rb b/app/services/streams/twitch_access_token/get.rb
new file mode 100644
index 000000000..ecf648dc3
--- /dev/null
+++ b/app/services/streams/twitch_access_token/get.rb
@@ -0,0 +1,35 @@
+module Streams
+ module TwitchAccessToken
+ class Get
+ ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY = :twitch_access_token_with_expiration
+ def self.call
+ new.call
+ end
+
+ def call
+ token, exp = Rails.cache.fetch(ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY)
+
+ if token.nil? || Time.zone.now >= exp
+ token, exp = get_new_token
+ Rails.cache.write(ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY, [token, exp])
+ end
+
+ token
+ end
+
+ private
+
+ def get_new_token
+ resp = HTTParty.post(
+ "https://id.twitch.tv/oauth2/token",
+ body: {
+ client_id: ApplicationConfig["TWITCH_CLIENT_ID"],
+ client_secret: ApplicationConfig["TWITCH_CLIENT_SECRET"],
+ grant_type: :client_credentials
+ },
+ )
+ [resp["access_token"], resp["expires_in"].seconds.from_now]
+ end
+ end
+ end
+end
diff --git a/app/services/streams/twitch_webhook/register.rb b/app/services/streams/twitch_webhook/register.rb
new file mode 100644
index 000000000..8a5bdd6fa
--- /dev/null
+++ b/app/services/streams/twitch_webhook/register.rb
@@ -0,0 +1,49 @@
+module Streams
+ module TwitchWebhook
+ class Register
+ WEBHOOK_LEASE_SECONDS = 7.days.to_i
+
+ def initialize(user, access_token_service = TwitchAccessToken::Get)
+ @user = user
+ @access_token_service = access_token_service
+ end
+
+ def self.call(*args)
+ new(*args).call
+ end
+
+ def call
+ user_resp = HTTParty.get("https://api.twitch.tv/helix/users", query: { login: user.twitch_username }, headers: authentication_request_headers)
+ twitch_user_id = user_resp["data"].first["id"]
+
+ HTTParty.post(
+ "https://api.twitch.tv/helix/webhooks/hub",
+ body: webhook_request_body(twitch_user_id),
+ headers: authentication_request_headers,
+ )
+ end
+
+ private
+
+ attr_reader :user, :access_token_service
+
+ def webhook_request_body(twitch_user_id)
+ {
+ "hub.callback" => twitch_stream_updates_url_for_user(user),
+ "hub.mode" => "subscribe",
+ "hub.lease_seconds" => WEBHOOK_LEASE_SECONDS,
+ "hub.topic" => "https://api.twitch.tv/helix/streams?user_id=#{twitch_user_id}",
+ "hub.secret" => ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
+ }.to_json
+ end
+
+ def authentication_request_headers
+ { "Authorization" => "Bearer #{access_token_service.call}" }
+ end
+
+ def twitch_stream_updates_url_for_user(user)
+ Rails.application.routes.url_helpers.user_twitch_stream_updates_url(user_id: user.id, host: ApplicationConfig["APP_DOMAIN"])
+ end
+ end
+ end
+end
diff --git a/app/views/twitch_live_streams/no_twitch.html.erb b/app/views/twitch_live_streams/no_twitch.html.erb
new file mode 100644
index 000000000..98f0d4d36
--- /dev/null
+++ b/app/views/twitch_live_streams/no_twitch.html.erb
@@ -0,0 +1,3 @@
+
+ This user does not use Twitch
+
diff --git a/app/views/twitch_live_streams/show.html.erb b/app/views/twitch_live_streams/show.html.erb
new file mode 100644
index 000000000..c4415a9f5
--- /dev/null
+++ b/app/views/twitch_live_streams/show.html.erb
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/app/views/users/_profile.html.erb b/app/views/users/_profile.html.erb
index fbb416fb4..29a8e7a85 100644
--- a/app/views/users/_profile.html.erb
+++ b/app/views/users/_profile.html.erb
@@ -150,6 +150,11 @@
<%= f.label :twitch_url, "Twitch URL" %>
<%= f.url_field :twitch_url %>
+ Streaming
+
+ <%= f.label :twitch_username, "Twitch User Name" %>
+ <%= f.text_field :twitch_username %>
+
<%= f.hidden_field :tab, value: @tab %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index 2f5bb907c..64185471a 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -105,7 +105,11 @@
<%= inline_svg("gitlab.svg", class: "icon-img") %>
<% end %>
- <% if @user.twitch_url? %>
+ <% if @user.twitch_username? %>
+ <%= link_to twitch_live_stream_path(username: @user.username) do %>
+ <%= inline_svg("twitch-logo.svg", class: "icon-img", id: "icon-twitch") %>
+ <% end %>
+ <% elsif @user.twitch_url? %>
<%= inline_svg("twitch-logo.svg", class: "icon-img") %>
@@ -139,6 +143,15 @@
<% end %>
+<% if @user.currently_streaming_on_twitch? %>
+
+<% end %>
+
"
diff --git a/config/routes.rb b/config/routes.rb
index c8050502b..a190967b6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -108,7 +108,10 @@ Rails.application.routes.draw do
resources :article_mutes, only: %i[update]
resources :comments, only: %i[create update destroy]
resources :comment_mutes, only: %i[update]
- resources :users, only: [:update]
+ resources :users, only: [:update] do
+ resource :twitch_stream_updates, only: %i[show create]
+ end
+ resources :twitch_live_streams, only: :show, param: :username
resources :reactions, only: %i[index create]
resources :feedback_messages, only: %i[index create]
get "/reports/:slug", to: "feedback_messages#show"
diff --git a/config/sample_application.yml b/config/sample_application.yml
index a829dae51..a45287457 100644
--- a/config/sample_application.yml
+++ b/config/sample_application.yml
@@ -15,3 +15,7 @@ PUSHER_APP_ID:
PUSHER_KEY:
PUSHER_SECRET:
PUSHER_CLUSTER:
+
+TWITCH_CLIENT_ID:
+TWITCH_CLIENT_SECRET:
+TWITCH_WEBHOOK_SECRET:
diff --git a/db/migrate/20190420000607_add_twitch_columns_to_user.rb b/db/migrate/20190420000607_add_twitch_columns_to_user.rb
new file mode 100644
index 000000000..a85303a76
--- /dev/null
+++ b/db/migrate/20190420000607_add_twitch_columns_to_user.rb
@@ -0,0 +1,8 @@
+class AddTwitchColumnsToUser < ActiveRecord::Migration[5.2]
+ def change
+ change_table :users do |t|
+ t.string :twitch_username, index: true, unique: true
+ t.string :currently_streaming_on
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0efe783d9..2f418a3e4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
@@ -809,6 +811,7 @@ ActiveRecord::Schema.define(version: 2019_04_25_210432) do
t.inet "current_sign_in_ip"
t.string "currently_hacking_on"
t.string "currently_learning"
+ t.string "currently_streaming_on"
t.boolean "display_sponsors", default: true
t.string "dribbble_url"
t.string "editor_version", default: "v1"
@@ -920,6 +923,7 @@ ActiveRecord::Schema.define(version: 2019_04_25_210432) do
t.string "text_only_name"
t.string "top_languages"
t.string "twitch_url"
+ t.string "twitch_username"
t.datetime "twitter_created_at"
t.integer "twitter_followers_count"
t.integer "twitter_following_count"
diff --git a/lib/tasks/twitch.rake b/lib/tasks/twitch.rake
new file mode 100644
index 000000000..5b4e903ef
--- /dev/null
+++ b/lib/tasks/twitch.rake
@@ -0,0 +1,8 @@
+namespace :twitch do
+ desc "Register for Webhooks for all User's Registered with Twitch"
+ task wehbook_register_all: :environment do
+ User.where.not(twitch_username: nil).find_each do |user|
+ Streams::TwitchWebhookRegistrationJob.perform_later(user.id)
+ end
+ end
+end
diff --git a/spec/jobs/streams/twitch_webhook_registration_job_spec.rb b/spec/jobs/streams/twitch_webhook_registration_job_spec.rb
new file mode 100644
index 000000000..59d11d665
--- /dev/null
+++ b/spec/jobs/streams/twitch_webhook_registration_job_spec.rb
@@ -0,0 +1,33 @@
+require "rails_helper"
+
+RSpec.describe Streams::TwitchWebhookRegistrationJob, type: :job do
+ let(:user) { create(:user, twitch_username: "test-username") }
+
+ let(:service) { double }
+
+ before do
+ allow(service).to receive(:call)
+ end
+
+ context "when the user does NOT have a twitch username present" do
+ let(:user) { create(:user) }
+
+ it "noops" do
+ described_class.perform_now(user.id, service)
+
+ expect(service).not_to have_received(:call)
+ end
+ end
+
+ it "noops when the id passed does not belong to a user" do
+ described_class.perform_now(987_654_321, service)
+
+ expect(service).not_to have_received(:call)
+ end
+
+ it "registers for webhooks" do
+ described_class.perform_now(user.id, service)
+
+ expect(service).to have_received(:call).with(user)
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index d4f00a55c..bad0f576e 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -57,6 +57,8 @@ VCR.configure do |config|
)
end
+RSpec::Matchers.define_negated_matcher :not_change, :change
+
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.fixture_path = "#{::Rails.root}/spec/fixtures"
diff --git a/spec/requests/twitch_stream_updates_spec.rb b/spec/requests/twitch_stream_updates_spec.rb
new file mode 100644
index 000000000..cc0f752ea
--- /dev/null
+++ b/spec/requests/twitch_stream_updates_spec.rb
@@ -0,0 +1,127 @@
+require "rails_helper"
+
+RSpec.describe "TwitchStramUpdates", type: :request do
+ let(:user) { create(:user, twitch_username: "my-twtich-username", currently_streaming_on: currently_streaming_on) }
+ let(:currently_streaming_on) { nil }
+
+ describe "GET /users/:user_id/twitch_stream_updates" do
+ context "when the subscription was successfull" do
+ let(:challenge) { "FAKE_CHALLENGE" }
+ let(:twitch_webhook_subscription_params) do
+ {
+ "hub.mode" => "subscribe",
+ "hub.topic" => "SOME_TOPIC_URL",
+ "hub.lease_seconds" => "864000",
+ "hub.challenge" => challenge
+ }
+ end
+
+ it "returns the challenge" do
+ get "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_subscription_params
+
+ expect(response.body).to eq challenge
+ end
+ end
+
+ context "when the subscription is denied" do
+ let(:twitch_webhook_subscription_params) do
+ {
+ "hub.mode" => "denied",
+ "hub.topic" => "SOME_TOPIC_URL",
+ "hub.reason" => "unauthorized"
+ }
+ end
+
+ it "returns a 204 and logs" do
+ get "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_subscription_params
+
+ expect(response.status).to eq 204
+ end
+ end
+ end
+
+ describe "POST /users/:user_id/twitch_stream_updates" do
+ context "when the user was not streaming and starts streaming" do
+ let(:currently_streaming_on) { nil }
+
+ let(:twitch_webhook_params) do
+ {
+ data: [{
+ id: "0123456789",
+ user_id: "5678",
+ user_name: "wjdtkdqhs",
+ game_id: "21779",
+ community_ids: [],
+ type: "live",
+ title: "Best Stream Ever",
+ viewer_count: 417,
+ started_at: "2017-12-01T10:09:45Z",
+ language: "en",
+ thumbnail_url: "https://link/to/thumbnail.jpg"
+ }]
+ }
+ end
+ let(:twitch_webhook_secret_sha) do
+ digest = Digest::SHA256.new
+ digest << ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
+ digest << twitch_webhook_params.to_json
+ "sha256=#{digest.hexdigest}"
+ end
+
+ it "updates the Users twitch streaming status" do
+ expect { post "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_params.to_json, headers: { "Content-Type" => "application/json", "X-Hub-Signature" => twitch_webhook_secret_sha } }.
+ to change { user.reload.currently_streaming? }.from(false).to(true).
+ and change { user.reload.currently_streaming_on_twitch? }.from(false).to(true)
+ end
+ end
+
+ context "when the webhook secret was NOT verified" do
+ let(:twitch_webhook_params) do
+ {
+ data: [{
+ id: "0123456789",
+ user_id: "5678",
+ user_name: "wjdtkdqhs",
+ game_id: "21779",
+ community_ids: [],
+ type: "live",
+ title: "Best Stream Ever",
+ viewer_count: 417,
+ started_at: "2017-12-01T10:09:45Z",
+ language: "en",
+ thumbnail_url: "https://link/to/thumbnail.jpg"
+ }]
+ }
+ end
+ let(:twitch_webhook_secret_sha) { "sha256=BAD_HASH" }
+
+ it "noops" do
+ expect { post "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_params.to_json, headers: { "Content-Type" => "application/json", "X-Hub-Signature" => twitch_webhook_secret_sha } }.
+ to not_change { user.reload.currently_streaming? }.from(false).
+ and not_change { user.reload.currently_streaming_on_twitch? }.from(false)
+ end
+ end
+ end
+
+ context "when the user was streaming and stops" do
+ let(:currently_streaming_on) { :twitch }
+
+ let(:twitch_webhook_params) do
+ {
+ data: []
+ }
+ end
+ let(:twitch_webhook_secret_sha) do
+ digest = Digest::SHA256.new
+ digest << ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
+ digest << twitch_webhook_params.to_json
+ "sha256=#{digest.hexdigest}"
+ end
+
+ it "updates the Users twitch streaming status" do
+ expect { post "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_params.to_json, headers: { "Content-Type" => "application/json", "X-Hub-Signature" => twitch_webhook_secret_sha } }.
+ to change { user.reload.currently_streaming? }.from(true).to(false).
+ and change { user.reload.currently_streaming_on_twitch? }.from(true).to(false)
+ end
+ end
+end
diff --git a/spec/services/streams/twitch_access_token/get_spec.rb b/spec/services/streams/twitch_access_token/get_spec.rb
new file mode 100644
index 000000000..0d1f016a3
--- /dev/null
+++ b/spec/services/streams/twitch_access_token/get_spec.rb
@@ -0,0 +1,50 @@
+require "rails_helper"
+
+RSpec.describe Streams::TwitchAccessToken::Get, type: :service do
+ describe ".access_token" do
+ let(:expected_twitch_token_body) do
+ {
+ client_id: "FAKE_TWITCH_CLIENT_ID",
+ client_secret: "FAKE_TWITCH_CLIENT_SECRET",
+ grant_type: "client_credentials"
+ }
+ end
+ let!(:twitch_token_stubbed_route) do
+ stub_request(:post, "https://id.twitch.tv/oauth2/token").
+ with(body: expected_twitch_token_body).
+ and_return(body: { access_token: "FAKE_BRAND_NEW_TWITCH_TOKEN", expires_in: 5_184_000 }.to_json, headers: { "Content-Type" => "application/json" })
+ end
+
+ before do
+ allow(ApplicationConfig).to receive(:[]).and_call_original
+ allow(ApplicationConfig).to receive(:[]).with("TWITCH_CLIENT_ID").and_return("FAKE_TWITCH_CLIENT_ID")
+ allow(ApplicationConfig).to receive(:[]).with("TWITCH_CLIENT_SECRET").and_return("FAKE_TWITCH_CLIENT_SECRET")
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache.lookup_store(:memory_store))
+ end
+
+ context "when there is an unexpired token in the cache" do
+ it "returns the cached token" do
+ Rails.cache.write(described_class::ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY, ["FAKE_UNEXPIRED_TWITCH_TOKEN", 15.days.from_now])
+
+ expect(described_class.call).to eq "FAKE_UNEXPIRED_TWITCH_TOKEN"
+ expect(twitch_token_stubbed_route).not_to have_been_requested
+ end
+ end
+
+ context "when there is an expired token in the cache" do
+ it "requests a new token and caches it" do
+ Rails.cache.write(described_class::ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY, ["FAKE_EXPIRED_TWITCH_TOKEN", 15.days.ago])
+
+ expect(described_class.call).to eq "FAKE_BRAND_NEW_TWITCH_TOKEN"
+ expect(twitch_token_stubbed_route).to have_been_requested
+ end
+ end
+
+ context "when the token is not in the cache" do
+ it "requests a new token and caches it" do
+ expect(described_class.call).to eq "FAKE_BRAND_NEW_TWITCH_TOKEN"
+ expect(twitch_token_stubbed_route).to have_been_requested
+ end
+ end
+ end
+end
diff --git a/spec/services/streams/twitch_webhook/register_spec.rb b/spec/services/streams/twitch_webhook/register_spec.rb
new file mode 100644
index 000000000..e3b39dbbf
--- /dev/null
+++ b/spec/services/streams/twitch_webhook/register_spec.rb
@@ -0,0 +1,40 @@
+require "rails_helper"
+
+RSpec.describe Streams::TwitchWebhook::Register, type: :service do
+ describe "::call" do
+ let(:twitch_access_token_get) { instance_double(Streams::TwitchAccessToken::Get, call: "FAKE_TWITCH_TOKEN") }
+ let(:user) { create(:user, twitch_username: "test-username") }
+
+ let(:expected_headers) do
+ { "Authorization" => "Bearer FAKE_TWITCH_TOKEN" }
+ end
+ let(:expected_twitch_webhook_params) do
+ {
+ "hub.callback" => "http://#{ApplicationConfig['APP_DOMAIN']}/users/#{user.id}/twitch_stream_updates",
+ "hub.mode" => "subscribe",
+ "hub.lease_seconds" => 604_800,
+ "hub.topic" => "https://api.twitch.tv/helix/streams?user_id=654321",
+ "hub.secret" => ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
+ }.to_json
+ end
+ let!(:twitch_webhook_registration_stubbed_route) do
+ stub_request(:post, "https://api.twitch.tv/helix/webhooks/hub").
+ with(body: expected_twitch_webhook_params, headers: expected_headers).
+ and_return(status: 204)
+ end
+
+ let(:expected_twitch_user_params) { { login: "test-username" } }
+ let!(:twitch_user_stubbed_route) do
+ stub_request(:get, "https://api.twitch.tv/helix/users").
+ with(query: expected_twitch_user_params, headers: expected_headers).
+ and_return(body: { data: [{ id: 654_321 }] }.to_json, headers: { "Content-Type" => "application/json" })
+ end
+
+ it "registers for webhooks" do
+ described_class.call(user, twitch_access_token_get)
+
+ expect(twitch_webhook_registration_stubbed_route).to have_been_requested
+ expect(twitch_user_stubbed_route).to have_been_requested
+ end
+ end
+end