diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3d03663..115acbe 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-10-01 17:50:25 UTC using RuboCop version 1.81.1. +# on 2026-10-25 21:30:32 UTC using RuboCop version 1.81.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -44,7 +44,7 @@ RSpec/ContextWording: - 'spec/slack-api-explorer/commands/slack_spec.rb' - 'spec/support/api/endpoints/it_behaves_like_a_cursor_api.rb' -# Offense count: 12 +# Offense count: 14 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 27 @@ -67,7 +67,7 @@ RSpec/LetSetup: RSpec/MessageSpies: EnforcedStyle: receive -# Offense count: 26 +# Offense count: 29 RSpec/MultipleExpectations: Max: 7 diff --git a/CHANGELOG.md b/CHANGELOG.md index eba3099..2e23ed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Changelog +* 2026/10/25: Upgrade slack-ruby-bot-server to 2.3.0 and rack-server-pages to 0.3.0, version local website assets, and centrally configure private, revalidating page caching without changing Slack installation or API behavior - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent). * 2023/01/16: Replaced Travis-CI with GHA - [@dblock](https://github.com/dblock). * 2018/05/10: Use ERB instead of static pages - [@dblock](https://github.com/dblock). * 2017/03/14: Use slack-ruby-bot-server - [@dblock](https://github.com/dblock). diff --git a/Gemfile b/Gemfile index 0dfdb99..e0450fd 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,8 @@ gem 'ostruct' gem 'puma' gem 'rack' gem 'rack-robotz' -gem 'rack-server-pages' +gem 'rack-server-pages', '~> 0.3.0' +gem 'slack-ruby-bot-server', '~> 2.3.0' gem 'slack-ruby-bot-server-mailchimp' gem 'slack-ruby-bot-server-rtm' diff --git a/Gemfile.lock b/Gemfile.lock index 1fe1419..a614210 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -229,7 +229,7 @@ GEM rack-rewrite (1.5.1) rack-robotz (0.0.4) rack - rack-server-pages (0.2.1) + rack-server-pages (0.3.0) rack rack-test (2.2.0) rack (>= 1.3) @@ -305,7 +305,7 @@ GEM activesupport hashie slack-ruby-client (>= 0.14.0) - slack-ruby-bot-server (2.2.0) + slack-ruby-bot-server (2.3.0) async foreman grape @@ -390,7 +390,7 @@ DEPENDENCIES puma rack rack-robotz - rack-server-pages + rack-server-pages (~> 0.3.0) rack-test rackup rake @@ -400,6 +400,7 @@ DEPENDENCIES rubocop-rake rubocop-rspec selenium-webdriver + slack-ruby-bot-server (~> 2.3.0) slack-ruby-bot-server-mailchimp slack-ruby-bot-server-rtm vcr diff --git a/README.md b/README.md index 51c3453..fb84cac 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ Post a message into a channel. ![](public/img/post.gif) +## Website asset caching + +Local scripts and images use content-hashed `asset_path` URLs from rack-server-pages 0.3.0. The custom middleware applies slack-ruby-bot-server 2.3.0's `config.server_pages` callback, setting `Cache-Control: private, no-cache` for rendered pages so browsers revalidate and discover changed asset URLs. Static files retain the middleware's content ETags and timestamp validation defaults, without long-lived or immutable caching. Versioned URLs are cache keys, not archives; old query strings still serve current files. + ## Usage ### Help diff --git a/config/initializers/slack_ruby_bot_server.rb b/config/initializers/slack_ruby_bot_server.rb new file mode 100644 index 0000000..57efe59 --- /dev/null +++ b/config/initializers/slack_ruby_bot_server.rb @@ -0,0 +1,5 @@ +SlackRubyBotServer.configure do |config| + config.server_pages do |pages| + pages.cache_control = 'private, no-cache' + end +end diff --git a/public/help.html b/public/help.html.erb similarity index 97% rename from public/help.html rename to public/help.html.erb index 780d604..d788041 100644 --- a/public/help.html +++ b/public/help.html.erb @@ -6,7 +6,7 @@

- +

Help

@@ -141,4 +141,3 @@

Help

- diff --git a/public/index.html.erb b/public/index.html.erb index bbbfa21..0760a99 100644 --- a/public/index.html.erb +++ b/public/index.html.erb @@ -1,14 +1,14 @@ API Explorer for Slack - - - - + + + +

- +

Whoa, an API explorer for the Slack Web API.

@@ -19,7 +19,7 @@

 

- +

Tweet @@ -43,4 +43,3 @@

- diff --git a/public/privacy.html b/public/privacy.html.erb similarity index 98% rename from public/privacy.html rename to public/privacy.html.erb index 1cb4824..82b644d 100644 --- a/public/privacy.html +++ b/public/privacy.html.erb @@ -6,7 +6,7 @@

- +

tl;dr

diff --git a/slack-api-explorer/api/middleware.rb b/slack-api-explorer/api/middleware.rb index e9be9df..1027b72 100644 --- a/slack-api-explorer/api/middleware.rb +++ b/slack-api-explorer/api/middleware.rb @@ -23,7 +23,9 @@ def self.instance use Rack::Robotz, 'User-Agent' => '*', 'Disallow' => '/api' - use Rack::ServerPages + use Rack::ServerPages do |pages| + SlackRubyBotServer.config.server_pages&.call(pages) + end run Api::Middleware.new }.to_app diff --git a/spec/api/middleware_spec.rb b/spec/api/middleware_spec.rb new file mode 100644 index 0000000..8c7861d --- /dev/null +++ b/spec/api/middleware_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Api::Middleware do + include Rack::Test::Methods + + let(:app) { described_class.instance } + + ['/', '/help.html', '/privacy.html'].each do |path| + it "serves #{path} with versioned assets and private revalidation" do + get path + + assets = Capybara.string(last_response.body).all('script[src^="/"], img[src^="/"]', visible: :all) + expect(last_response.status).to eq 200 + expect(last_response.headers['Cache-Control']).to eq 'private, no-cache' + expect(assets).not_to be_empty + expect(assets.map { |asset| asset[:src] }).to all(match(/\?v=[a-f0-9]{64}\z/)) + end + end + + it 'revalidates static assets using their content ETag' do + get '/scripts/register.js' + etag = last_response.headers['ETag'] + expect(etag).to eq %("#{Digest::SHA256.file('public/scripts/register.js').hexdigest}") + + get '/scripts/register.js', {}, 'HTTP_IF_NONE_MATCH' => etag + expect(last_response.status).to eq 304 + expect(last_response.body).to be_empty + end + + it 'returns current bytes when an old ETag accompanies a matching timestamp' do + get '/scripts/register.js', {}, 'HTTP_IF_NONE_MATCH' => '"old-content"', + 'HTTP_IF_MODIFIED_SINCE' => File.mtime('public/scripts/register.js').httpdate + + expect(last_response.status).to eq 200 + expect(last_response.body).to eq File.read('public/scripts/register.js') + end +end diff --git a/spec/integration/website_spec.rb b/spec/integration/website_spec.rb new file mode 100644 index 0000000..96c9bdd --- /dev/null +++ b/spec/integration/website_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe 'Website', :js, type: :feature do + it 'loads the content-versioned logo after a reload' do + visit '/help.html' + page.refresh + + expect(page).to have_css('img[src^="/img/icon.png?v="]') do |image| + image.evaluate_script('this.complete && this.naturalWidth > 0') + end + end +end