Require a Google client
GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set, so the app no
longer has a path for a server without one. The Fly demos set
placeholders, since anyone who opens them shares the one demo user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
change
commit 6bf42a82124bf683c76c51aa58713fd8f234977f
author Claude <noreply@anthropic.com>
date
parent a1465e6f
diff --git a/AGENTS.md b/AGENTS.md
index 52ebd61..b86ff2d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,11 +85,11 @@ fly.toml           — Fly config for the main demo and PR previews
 
 - `DATABASE_URL` — SQLite path (default: `:memory:`)
 - `SESSION_SECRET` — session signing key (auto-generated if unset)
-- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` — a Google OAuth web client
-  with the YouTube Data API enabled, for connecting Google. Its redirect
-  URI is `<app URL>/settings/google/callback`. Without them YouTube lists
-  are not offered. A consent screen left in Testing mode expires refresh
-  tokens after 7 days.
+- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (required) — a Google OAuth
+  web client with the YouTube Data API enabled, for connecting Google.
+  Its redirect URI is `<app URL>/settings/google/callback`. A consent
+  screen left in Testing mode expires refresh tokens after 7 days. The
+  Fly demos set placeholders in `fly.toml`, since anyone can open them.
 
 Tests set `DATABASE_URL=:memory:` and a dummy Google client in
 `test/test_helper.rb`, and swap `RankKing::Google.transport` so no
diff --git a/fly.toml b/fly.toml
index f8e8383..1f9ced0 100644
--- a/fly.toml
+++ b/fly.toml
@@ -16,6 +16,14 @@ primary_region = 'sjc'
 
 [build]
 
+# The app requires a Google client. The demos get a placeholder rather
+# than the real one: anyone who opens them is the one demo user, so a
+# Google account connected there would be everyone's. Connect Google on
+# a demo fails at Google's consent screen.
+[env]
+  GOOGLE_CLIENT_ID = 'demo'
+  GOOGLE_CLIENT_SECRET = 'demo'
+
 [processes]
   app = 'rackup -o 0.0.0.0 demo.ru'
 
diff --git a/lib/rank_king/config.rb b/lib/rank_king/config.rb
index 89124f1..ffaae54 100644
--- a/lib/rank_king/config.rb
+++ b/lib/rank_king/config.rb
@@ -22,10 +22,9 @@ module RankKing
       new(
         database_url: env.fetch("DATABASE_URL", ":memory:"),
         session_secret: env.fetch("SESSION_SECRET") { SecureRandom.hex(64) },
-        # A Google OAuth client, for connecting YouTube. Without one the
-        # settings page says so and YouTube lists cannot be made.
-        google_client_id: env["GOOGLE_CLIENT_ID"],
-        google_client_secret: env["GOOGLE_CLIENT_SECRET"],
+        # A Google OAuth client, for connecting YouTube.
+        google_client_id: env.fetch("GOOGLE_CLIENT_ID"),
+        google_client_secret: env.fetch("GOOGLE_CLIENT_SECRET"),
       )
     end
   end
diff --git a/lib/rank_king/google.rb b/lib/rank_king/google.rb
index e3dcb4b..eee06bd 100644
--- a/lib/rank_king/google.rb
+++ b/lib/rank_king/google.rb
@@ -34,9 +34,6 @@ module RankKing
       @transport ||= ->(method, url, params, access_token) { request(method, url, params, access_token) }
     end
 
-    #: () -> bool
-    def self.configured? = !!(CONFIG.google_client_id && CONFIG.google_client_secret)
-
     # Where to send the user to grant read access to their YouTube
     # account. prompt=consent makes Google send a refresh token every time.
     #: (redirect_uri: String, state: String) -> String
@@ -115,7 +112,7 @@ module RankKing
 
     #: () -> Hash[Symbol, String]
     def self.client
-      { client_id: CONFIG.google_client_id.to_s, client_secret: CONFIG.google_client_secret.to_s }
+      { client_id: CONFIG.google_client_id, client_secret: CONFIG.google_client_secret }
     end
 
     #: (Symbol method, String url, Hash[Symbol, String] params, String? access_token) -> Hash[String, untyped]
diff --git a/lib/rank_king/sources/youtube.rb b/lib/rank_king/sources/youtube.rb
index c29ce4b..3cbaaf2 100644
--- a/lib/rank_king/sources/youtube.rb
+++ b/lib/rank_king/sources/youtube.rb
@@ -28,9 +28,9 @@ module RankKing
       def self.fields(user)
         account = user.google_account
         playlists = account && Google.playlists(Google.access_token(account.refresh_token))
-        Views::Sources::YouTubeFields.new(playlists:, configured: Google.configured?)
+        Views::Sources::YouTubeFields.new(playlists:)
       rescue Google::Error => e
-        Views::Sources::YouTubeFields.new(playlists: nil, configured: true, error: e.message)
+        Views::Sources::YouTubeFields.new(playlists: nil, error: e.message)
       end
 
       # Follows the playlist chosen on the new-list form and fetches it.
diff --git a/lib/rank_king/views/settings.rb b/lib/rank_king/views/settings.rb
index f0835f6..a5c63e8 100644
--- a/lib/rank_king/views/settings.rb
+++ b/lib/rank_king/views/settings.rb
@@ -9,9 +9,8 @@ module RankKing
     # The user's own settings, as against a list's. A future setting is a
     # new section here.
     class Settings < Phlex::HTML
-      def initialize(user:, google_configured:, csrf:)
+      def initialize(user:, csrf:)
         @user = user
-        @google_configured = google_configured
         @csrf = csrf
       end
 
@@ -23,11 +22,9 @@ module RankKing
             h2(class: "type-label") { "Google" }
             if @user.google_account
               render_connected
-            elsif @google_configured
+            else
               p { "Connect Google to make lists from your YouTube playlists, private ones included." }
               a(href: "/settings/google/connect", class: "btn", data_variant: "primary") { "Connect Google" }
-            else
-              p { "This server has no Google client set up. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to connect Google." }
             end
           end
         end
diff --git a/lib/rank_king/views/sources/youtube.rb b/lib/rank_king/views/sources/youtube.rb
index 43128a2..675ced5 100644
--- a/lib/rank_king/views/sources/youtube.rb
+++ b/lib/rank_king/views/sources/youtube.rb
@@ -9,9 +9,8 @@ module RankKing
       # of the user's playlists, once they have connected Google. playlists
       # is nil until then.
       class YouTubeFields < Phlex::HTML
-        def initialize(playlists:, configured:, error: nil)
+        def initialize(playlists:, error: nil)
           @playlists = playlists
-          @configured = configured
           @error = error
         end
 
@@ -30,14 +29,12 @@ module RankKing
               end
               small { "Private playlists are listed too. YouTube does not share Watch Later." }
             end
-          elsif @configured
+          else
             p do
               plain "Connect Google in "
               a(href: "/settings") { "your settings" }
               plain " to make a list from a YouTube playlist."
             end
-          else
-            p { "This server has no Google client set up, so YouTube lists cannot be made yet." }
           end
         end
       end
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index 982fd9b..7027849 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -41,7 +41,7 @@ module RankKing
 
       r.on "settings" do
         r.get true do
-          Views::Settings.new(user:, google_configured: Google.configured?, csrf: method(:csrf_token)).call
+          Views::Settings.new(user:, csrf: method(:csrf_token)).call
         end
 
         # Connecting Google is the OAuth dance: off to Google with a state
@@ -50,8 +50,6 @@ module RankKing
           redirect_uri = "#{r.base_url}/settings/google/callback"
 
           r.get "connect" do
-            r.halt 404 unless Google.configured?
-
             state = SecureRandom.hex(16)
             session["google_state"] = state
             r.redirect Google.authorize_url(redirect_uri:, state:)
diff --git a/sig/rank_king/config.rbs b/sig/rank_king/config.rbs
index 1625670..672805c 100644
--- a/sig/rank_king/config.rbs
+++ b/sig/rank_king/config.rbs
@@ -5,9 +5,9 @@ module RankKing
   class Config
     attr_reader database_url: String
     attr_reader session_secret: String
-    attr_reader google_client_id: String?
-    attr_reader google_client_secret: String?
+    attr_reader google_client_id: String
+    attr_reader google_client_secret: String
 
-    def self.new: (database_url: String, session_secret: String, google_client_id: String?, google_client_secret: String?) -> instance
+    def self.new: (database_url: String, session_secret: String, google_client_id: String, google_client_secret: String) -> instance
   end
 end
diff --git a/sig/rank_king/views.rbs b/sig/rank_king/views.rbs
index 8513dba..d8f44f7 100644
--- a/sig/rank_king/views.rbs
+++ b/sig/rank_king/views.rbs
@@ -9,7 +9,7 @@ module RankKing
     end
 
     class Settings
-      def initialize: (user: User, google_configured: bool, csrf: Method) -> void
+      def initialize: (user: User, csrf: Method) -> void
       def call: () -> String
     end
 
@@ -30,7 +30,7 @@ module RankKing
       end
 
       class YouTubeFields
-        def initialize: (playlists: Array[[String, String]]?, configured: bool, ?error: String?) -> void
+        def initialize: (playlists: Array[[String, String]]?, ?error: String?) -> void
         def call: () -> String
       end
     end
diff --git a/test/test_youtube.rb b/test/test_youtube.rb
index 33799bd..f3a6810 100644
--- a/test/test_youtube.rb
+++ b/test/test_youtube.rb
@@ -87,17 +87,6 @@ class TestYouTube < Minitest::Test
     assert_includes last_response.body, %(href="/settings/google/connect")
   end
 
-  def test_settings_says_when_google_is_not_set_up
-    unconfigured do
-      get "/settings"
-      refute_includes last_response.body, "/settings/google/connect"
-      assert_includes last_response.body, "GOOGLE_CLIENT_ID"
-
-      get "/settings/google/connect"
-      assert_equal 404, last_response.status
-    end
-  end
-
   def test_connecting_google_keeps_a_refresh_token
     get "/settings/google/connect"
     assert last_response.redirect?
@@ -282,17 +271,6 @@ class TestYouTube < Minitest::Test
 
   def user = RankKing::User.first(login: LOGIN)
 
-  # As if the server had no Google client.
-  def unconfigured
-    google = RankKing::Google.singleton_class
-    google.alias_method :__configured?, :configured?
-    google.define_method(:configured?) { false }
-    yield
-  ensure
-    google.alias_method :configured?, :__configured?
-    google.remove_method :__configured?
-  end
-
   def connect_google
     get "/settings/google/connect"
     state = URI.decode_www_form(URI(last_response.location).query).to_h["state"]