Skip to content

Commit 3283ee8

Browse files
dblockCopilot
andcommitted
Refresh workspace metadata after successful authentication
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 71cdbcf commit 3283ee8

5 files changed

Lines changed: 108 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### 2.3.0 (Next)
44

5+
* [#191](https://github.com/slack-ruby/slack-ruby-bot-server/pull/191): Refresh workspace names and domains from successful authentication during pings, and expose `Team#update_info!` for standalone metadata refresh without additional scopes - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
56
* [#188](https://github.com/slack-ruby/slack-ruby-bot-server/pull/188): Reduce the database CI matrices from 22 to 12 jobs while retaining representative legacy ORM majors and modern Ruby/Grape 3/4 coverage - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
67
* [#189](https://github.com/slack-ruby/slack-ruby-bot-server/pull/189): Add a prefixed Team fabricator entry point and preserve application-defined `:team` fabricators in the legacy RSpec helper - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
78
* [#186](https://github.com/slack-ruby/slack-ruby-bot-server/pull/186): Document persisted custom Team attributes for both adapters and Rails concern loading/reloading - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Build a complete Slack bot service with Slack button integration, in Ruby.
2121
- [Mongoid Attributes](#mongoid-attributes)
2222
- [Active Record Attributes](#active-record-attributes)
2323
- [Rails Loading and Reloading](#rails-loading-and-reloading)
24+
- [Workspace Metadata](#workspace-metadata)
2425
- [OAuth Version and Scopes](#oauth-version-and-scopes)
2526
- [Slack App](#slack-app)
2627
- [API](#api)
@@ -185,6 +186,20 @@ With Active Record, use the migration above instead of declaring a field in the
185186

186187
The framework currently owns the global `Team` constant. An unrelated application model with the same name is a separate [name-collision limitation](https://github.com/slack-ruby/slack-ruby-bot-server/pull/83), not an extension mechanism.
187188

189+
#### Workspace Metadata
190+
191+
`Team#ping!` refreshes the stored workspace `name` and `domain` from its existing successful [`auth.test`](https://docs.slack.dev/reference/methods/auth.test/) response, without making an additional API request. `domain` is the first hostname label of the workspace URL (for example, `example` from `https://example.slack.com/`). Unchanged metadata is not written to the database. Authentication, malformed-response, and persistence failures propagate instead of returning a successful refresh.
192+
193+
Call `team.update_info!` to refresh only this information without fetching presence. `auth.test` requires no additional scopes, unlike `team.info`. To refresh when an installation starts, opt into an additional authentication request through a lifecycle callback:
194+
195+
```ruby
196+
SlackRubyBotServer::Service.instance.on :started do |team|
197+
team.update_info!
198+
end
199+
```
200+
201+
This callback is optional; no new authentication request is added to the default startup lifecycle. It refreshes workspace metadata, not transport connection state.
202+
188203
### OAuth Version and Scopes
189204

190205
Configure your app's [OAuth version](https://api.slack.com/authentication/oauth-v2) and [scopes](https://api.slack.com/legacy/oauth-scopes) as needed by your application.

‎UPGRADING.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Upgrading Slack-Ruby-Bot-Server
33

44
### Upgrading to >= 2.3.0
55

6+
#### Workspace Metadata Refresh
7+
8+
`Team#ping!` now persists the current workspace `name` and `domain` when they differ from the successful `auth.test` response. This adds no Slack request, but pings (including `/api/status`) can now write to the `teams` collection/table and surface persistence errors. `domain` is derived from the workspace URL's first hostname label. Applications needing a standalone refresh can call `team.update_info!`.
9+
610
#### Recommended Test Helper Migration
711

812
If your application defines its own `:team` fabricator, replace `require 'slack-ruby-bot-server/rspec'` with `require 'slack-ruby-bot-server/rspec/fabricators'`. This is recommended, not required: the new entry point registers only `:slack_ruby_bot_server_team`, leaving `:team` available regardless of whether your application's fabricator is loaded before or after the helper.

‎lib/slack-ruby-bot-server/models/team/methods.rb‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'uri'
2+
13
module Methods
24
extend ActiveSupport::Concern
35

@@ -34,6 +36,7 @@ def ping!
3436
client = Slack::Web::Client.new(token: token)
3537

3638
auth = client.auth_test
39+
update_info!(auth)
3740

3841
presence = begin
3942
client.users_getPresence(user: auth['user_id'])
@@ -47,6 +50,21 @@ def ping!
4750
}
4851
end
4952

53+
def update_info!(auth = nil)
54+
auth ||= Slack::Web::Client.new(token: token).auth_test
55+
raise ArgumentError, 'Slack authentication belongs to a different workspace.' unless auth.fetch('team_id') == team_id
56+
57+
name = auth.fetch('team')
58+
raise ArgumentError, 'Slack authentication returned an invalid workspace name.' unless name.is_a?(String) && !name.empty?
59+
60+
url = URI.parse(auth.fetch('url'))
61+
raise ArgumentError, 'Slack authentication returned an invalid workspace URL.' unless url.is_a?(URI::HTTP) && url.host
62+
63+
attributes = { name: name, domain: url.host.split('.').first }
64+
update!(attributes) if attributes.any? { |key, value| public_send(key) != value }
65+
auth
66+
end
67+
5068
def ping_if_active!
5169
return unless active?
5270

‎spec/models/team_spec.rb‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,76 @@
11
require 'spec_helper'
22

33
describe Team do
4+
context 'workspace metadata' do
5+
let(:team) { Fabricate(:team, name: 'Old name', domain: 'old-domain') }
6+
let(:auth) do
7+
Slack::Messages::Message.new(
8+
'ok' => true,
9+
'team_id' => team.team_id,
10+
'team' => 'New workspace name',
11+
'url' => 'https://new-domain.slack.com/',
12+
'user_id' => 'UBOT'
13+
)
14+
end
15+
let(:slack_client) { instance_double(Slack::Web::Client) }
16+
let(:presence) { { 'presence' => 'active' } }
17+
18+
before do
19+
allow(Slack::Web::Client).to receive(:new).with(token: team.token).and_return(slack_client)
20+
allow(slack_client).to receive(:auth_test).and_return(auth)
21+
allow(slack_client).to receive(:users_getPresence).with(user: 'UBOT').and_return(presence)
22+
end
23+
24+
it 'persists current workspace info from a ping without an extra API request' do
25+
expect(slack_client).to receive(:auth_test).once.and_return(auth)
26+
expect(team.ping!).to eq(auth: auth, presence: presence)
27+
expect(team.reload.name).to eq 'New workspace name'
28+
expect(team.domain).to eq 'new-domain'
29+
end
30+
31+
it 'can refresh workspace info without fetching presence' do
32+
expect(slack_client).not_to receive(:users_getPresence)
33+
expect(team.update_info!).to eq auth
34+
expect(team.reload.name).to eq 'New workspace name'
35+
expect(team.domain).to eq 'new-domain'
36+
end
37+
38+
it 'does not write an unchanged workspace' do
39+
team.update!(name: 'New workspace name', domain: 'new-domain')
40+
expect(team).not_to receive(:update!)
41+
team.ping!
42+
end
43+
44+
it 'refreshes info even when presence requires an additional scope' do
45+
allow(slack_client).to receive(:users_getPresence).and_raise(Slack::Web::Api::Errors::MissingScope, 'missing_scope')
46+
expect(team.ping!).to eq(auth: auth, presence: nil)
47+
expect(team.reload.domain).to eq 'new-domain'
48+
end
49+
50+
it 'does not change stored info when Slack authentication fails' do
51+
allow(slack_client).to receive(:auth_test).and_raise(Slack::Web::Api::Errors::SlackError, 'invalid_auth')
52+
expect { team.ping! }.to raise_error(Slack::Web::Api::Errors::SlackError, 'invalid_auth')
53+
expect(team.reload.name).to eq 'Old name'
54+
expect(team.domain).to eq 'old-domain'
55+
end
56+
57+
it 'rejects a response for another workspace without changing stored info' do
58+
expect do
59+
team.update_info!(auth.merge('team_id' => 'TOTHER'))
60+
end.to raise_error(ArgumentError, 'Slack authentication belongs to a different workspace.')
61+
expect(team.reload.name).to eq 'Old name'
62+
expect(team.domain).to eq 'old-domain'
63+
end
64+
65+
it 'rejects malformed workspace URLs without partially updating the name' do
66+
expect do
67+
team.update_info!(auth.merge('url' => '/not-a-workspace'))
68+
end.to raise_error(ArgumentError, 'Slack authentication returned an invalid workspace URL.')
69+
expect(team.reload.name).to eq 'Old name'
70+
expect(team.domain).to eq 'old-domain'
71+
end
72+
end
73+
474
context '#purge!' do
575
let!(:active_team) { Fabricate(:team) }
676
let!(:inactive_team) { Fabricate(:team, active: false) }

0 commit comments

Comments
 (0)