gds-sso 20.0.0 → 21.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +14 -1
- data/config/routes.rb +4 -3
- data/lib/gds-sso/api_access.rb +27 -1
- data/lib/gds-sso/authorised_user_constraint.rb +2 -3
- data/lib/gds-sso/bearer_token.rb +6 -1
- data/lib/gds-sso/config.rb +7 -0
- data/lib/gds-sso/controller_methods.rb +1 -9
- data/lib/gds-sso/failure_app.rb +6 -7
- data/lib/gds-sso/version.rb +1 -1
- data/lib/gds-sso/warden_config.rb +30 -3
- data/lib/gds-sso.rb +6 -0
- data/spec/internal/config/routes.rb +1 -1
- data/spec/request/api/user_spec.rb +144 -0
- data/spec/request/authentication_spec.rb +77 -0
- data/spec/request/demo_app_spec.rb +264 -0
- data/spec/spec_helper.rb +16 -7
- data/spec/support/request_helpers.rb +45 -0
- data/spec/unit/api_access_spec.rb +80 -8
- data/spec/unit/authorised_user_constraint_spec.rb +8 -31
- data/spec/unit/bearer_token_spec.rb +8 -0
- data/spec/unit/gds_sso_spec.rb +37 -0
- data/spec/unit/mock_bearer_token_spec.rb +55 -16
- data/spec/unit/session_serialisation_spec.rb +5 -4
- metadata +34 -30
- data/spec/controller/api_user_controller_spec.rb +0 -114
- data/spec/support/timecop.rb +0 -7
- data/spec/system/authentication_and_authorisation_spec.rb +0 -172
- /data/spec/{controller → unit}/controller_methods_spec.rb +0 -0
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gds-sso
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 21.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GOV.UK Dev
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: oauth2
|
@@ -65,6 +65,20 @@ dependencies:
|
|
65
65
|
- - ">="
|
66
66
|
- !ruby/object:Gem::Version
|
67
67
|
version: '5'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rack
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
68
82
|
- !ruby/object:Gem::Dependency
|
69
83
|
name: rails
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,19 +122,19 @@ dependencies:
|
|
108
122
|
- !ruby/object:Gem::Version
|
109
123
|
version: 0.0.1
|
110
124
|
- !ruby/object:Gem::Dependency
|
111
|
-
name:
|
125
|
+
name: climate_control
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
113
127
|
requirements:
|
114
128
|
- - "~>"
|
115
129
|
- !ruby/object:Gem::Version
|
116
|
-
version: '
|
130
|
+
version: '1'
|
117
131
|
type: :development
|
118
132
|
prerelease: false
|
119
133
|
version_requirements: !ruby/object:Gem::Requirement
|
120
134
|
requirements:
|
121
135
|
- - "~>"
|
122
136
|
- !ruby/object:Gem::Version
|
123
|
-
version: '
|
137
|
+
version: '1'
|
124
138
|
- !ruby/object:Gem::Dependency
|
125
139
|
name: combustion
|
126
140
|
requirement: !ruby/object:Gem::Requirement
|
@@ -169,14 +183,14 @@ dependencies:
|
|
169
183
|
requirements:
|
170
184
|
- - '='
|
171
185
|
- !ruby/object:Gem::Version
|
172
|
-
version: 5.
|
186
|
+
version: 5.1.18
|
173
187
|
type: :development
|
174
188
|
prerelease: false
|
175
189
|
version_requirements: !ruby/object:Gem::Requirement
|
176
190
|
requirements:
|
177
191
|
- - '='
|
178
192
|
- !ruby/object:Gem::Version
|
179
|
-
version: 5.
|
193
|
+
version: 5.1.18
|
180
194
|
- !ruby/object:Gem::Dependency
|
181
195
|
name: sqlite3
|
182
196
|
requirement: !ruby/object:Gem::Requirement
|
@@ -191,20 +205,6 @@ dependencies:
|
|
191
205
|
- - "~>"
|
192
206
|
- !ruby/object:Gem::Version
|
193
207
|
version: '2.1'
|
194
|
-
- !ruby/object:Gem::Dependency
|
195
|
-
name: timecop
|
196
|
-
requirement: !ruby/object:Gem::Requirement
|
197
|
-
requirements:
|
198
|
-
- - "~>"
|
199
|
-
- !ruby/object:Gem::Version
|
200
|
-
version: '0.9'
|
201
|
-
type: :development
|
202
|
-
prerelease: false
|
203
|
-
version_requirements: !ruby/object:Gem::Requirement
|
204
|
-
requirements:
|
205
|
-
- - "~>"
|
206
|
-
- !ruby/object:Gem::Version
|
207
|
-
version: '0.9'
|
208
208
|
- !ruby/object:Gem::Dependency
|
209
209
|
name: webmock
|
210
210
|
requirement: !ruby/object:Gem::Requirement
|
@@ -250,8 +250,6 @@ files:
|
|
250
250
|
- lib/gds-sso/version.rb
|
251
251
|
- lib/gds-sso/warden_config.rb
|
252
252
|
- lib/omniauth/strategies/gds.rb
|
253
|
-
- spec/controller/api_user_controller_spec.rb
|
254
|
-
- spec/controller/controller_methods_spec.rb
|
255
253
|
- spec/internal/app/assets/config/manifest.js
|
256
254
|
- spec/internal/app/controllers/application_controller.rb
|
257
255
|
- spec/internal/app/controllers/example_controller.rb
|
@@ -260,17 +258,21 @@ files:
|
|
260
258
|
- spec/internal/config/initializers/gds-sso.rb
|
261
259
|
- spec/internal/config/routes.rb
|
262
260
|
- spec/internal/db/schema.rb
|
261
|
+
- spec/request/api/user_spec.rb
|
262
|
+
- spec/request/authentication_spec.rb
|
263
|
+
- spec/request/demo_app_spec.rb
|
263
264
|
- spec/spec_helper.rb
|
264
265
|
- spec/support/controller_spy.rb
|
266
|
+
- spec/support/request_helpers.rb
|
265
267
|
- spec/support/serializable_user.rb
|
266
268
|
- spec/support/test_user.rb
|
267
|
-
- spec/support/timecop.rb
|
268
|
-
- spec/system/authentication_and_authorisation_spec.rb
|
269
269
|
- spec/unit/api_access_spec.rb
|
270
270
|
- spec/unit/authorise_user_spec.rb
|
271
271
|
- spec/unit/authorised_user_constraint_spec.rb
|
272
272
|
- spec/unit/bearer_token_spec.rb
|
273
273
|
- spec/unit/config_spec.rb
|
274
|
+
- spec/unit/controller_methods_spec.rb
|
275
|
+
- spec/unit/gds_sso_spec.rb
|
274
276
|
- spec/unit/mock_bearer_token_spec.rb
|
275
277
|
- spec/unit/railtie_spec.rb
|
276
278
|
- spec/unit/session_serialisation_spec.rb
|
@@ -293,12 +295,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
293
295
|
- !ruby/object:Gem::Version
|
294
296
|
version: '0'
|
295
297
|
requirements: []
|
296
|
-
rubygems_version: 3.
|
298
|
+
rubygems_version: 3.7.1
|
297
299
|
specification_version: 4
|
298
300
|
summary: Client for GDS' OAuth 2-based SSO
|
299
301
|
test_files:
|
300
|
-
- spec/controller/api_user_controller_spec.rb
|
301
|
-
- spec/controller/controller_methods_spec.rb
|
302
302
|
- spec/internal/app/assets/config/manifest.js
|
303
303
|
- spec/internal/app/controllers/application_controller.rb
|
304
304
|
- spec/internal/app/controllers/example_controller.rb
|
@@ -307,17 +307,21 @@ test_files:
|
|
307
307
|
- spec/internal/config/initializers/gds-sso.rb
|
308
308
|
- spec/internal/config/routes.rb
|
309
309
|
- spec/internal/db/schema.rb
|
310
|
+
- spec/request/api/user_spec.rb
|
311
|
+
- spec/request/authentication_spec.rb
|
312
|
+
- spec/request/demo_app_spec.rb
|
310
313
|
- spec/spec_helper.rb
|
311
314
|
- spec/support/controller_spy.rb
|
315
|
+
- spec/support/request_helpers.rb
|
312
316
|
- spec/support/serializable_user.rb
|
313
317
|
- spec/support/test_user.rb
|
314
|
-
- spec/support/timecop.rb
|
315
|
-
- spec/system/authentication_and_authorisation_spec.rb
|
316
318
|
- spec/unit/api_access_spec.rb
|
317
319
|
- spec/unit/authorise_user_spec.rb
|
318
320
|
- spec/unit/authorised_user_constraint_spec.rb
|
319
321
|
- spec/unit/bearer_token_spec.rb
|
320
322
|
- spec/unit/config_spec.rb
|
323
|
+
- spec/unit/controller_methods_spec.rb
|
324
|
+
- spec/unit/gds_sso_spec.rb
|
321
325
|
- spec/unit/mock_bearer_token_spec.rb
|
322
326
|
- spec/unit/railtie_spec.rb
|
323
327
|
- spec/unit/session_serialisation_spec.rb
|
@@ -1,114 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
def user_update_json
|
4
|
-
{
|
5
|
-
"user" => {
|
6
|
-
"uid" => @user_to_update.uid,
|
7
|
-
"name" => "Joshua Marshall",
|
8
|
-
"email" => "user@domain.com",
|
9
|
-
"permissions" => ["signin", "new permission"],
|
10
|
-
"organisation_slug" => "justice-league",
|
11
|
-
"organisation_content_id" => "aae1319e-5788-4677-998c-f1a53af528d0",
|
12
|
-
"disabled" => false,
|
13
|
-
},
|
14
|
-
}.to_json
|
15
|
-
end
|
16
|
-
|
17
|
-
describe Api::UserController, type: :controller do
|
18
|
-
before :each do
|
19
|
-
user_to_update_attrs = [{
|
20
|
-
uid: "a1s2d3#{rand(10_000)}",
|
21
|
-
email: "old@domain.com",
|
22
|
-
name: "Moshua Jarshall",
|
23
|
-
permissions: %w[signin],
|
24
|
-
}]
|
25
|
-
|
26
|
-
signon_sso_push_user_attrs = [{
|
27
|
-
uid: "a1s2d3#{rand(10_000)}",
|
28
|
-
email: "ssopushuser@legit.com",
|
29
|
-
name: "SSO Push user",
|
30
|
-
permissions: %w[signin user_update_permission],
|
31
|
-
}]
|
32
|
-
|
33
|
-
@user_to_update = User.create!(*user_to_update_attrs)
|
34
|
-
@signon_sso_push_user = User.create!(*signon_sso_push_user_attrs)
|
35
|
-
end
|
36
|
-
|
37
|
-
describe "PUT update" do
|
38
|
-
it "should deny access to anybody but the API user (or a user with 'user_update_permission')" do
|
39
|
-
malicious_user = User.new({
|
40
|
-
uid: "2",
|
41
|
-
name: "User",
|
42
|
-
permissions: %w[signin],
|
43
|
-
})
|
44
|
-
|
45
|
-
request.env["warden"] = double("stub warden", authenticate!: true, authenticated?: true, user: malicious_user)
|
46
|
-
|
47
|
-
request.env["RAW_POST_DATA"] = user_update_json
|
48
|
-
put :update, body: user_update_json, params: { uid: @user_to_update.uid }
|
49
|
-
|
50
|
-
expect(response.status).to eq(403)
|
51
|
-
end
|
52
|
-
|
53
|
-
it "should create/update the user record in the same way as the OAuth callback" do
|
54
|
-
# Test that it authenticates
|
55
|
-
request.env["warden"] = double("mock warden")
|
56
|
-
expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
|
57
|
-
expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
|
58
|
-
expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
|
59
|
-
|
60
|
-
request.env["RAW_POST_DATA"] = user_update_json
|
61
|
-
put :update, body: user_update_json, params: { uid: @user_to_update.uid }
|
62
|
-
|
63
|
-
@user_to_update.reload
|
64
|
-
expect(@user_to_update.name).to eq("Joshua Marshall")
|
65
|
-
expect(@user_to_update.email).to eq("user@domain.com")
|
66
|
-
expect(@user_to_update.permissions).to eq(["signin", "new permission"])
|
67
|
-
expect(@user_to_update.organisation_slug).to eq("justice-league")
|
68
|
-
expect(@user_to_update.organisation_content_id).to eq("aae1319e-5788-4677-998c-f1a53af528d0")
|
69
|
-
expect(response.content_type).to eq("text/plain")
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe "POST reauth" do
|
74
|
-
it "should deny access to anybody but the API user (or a user with 'user_update_permission')" do
|
75
|
-
malicious_user = User.new({
|
76
|
-
uid: "2",
|
77
|
-
name: "User",
|
78
|
-
permissions: %w[signin],
|
79
|
-
})
|
80
|
-
|
81
|
-
request.env["warden"] = double("stub warden", authenticate!: true, authenticated?: true, user: malicious_user)
|
82
|
-
|
83
|
-
post :reauth, params: { uid: @user_to_update.uid }
|
84
|
-
|
85
|
-
expect(response.status).to eq(403)
|
86
|
-
end
|
87
|
-
|
88
|
-
it "should return success if user record doesn't exist" do
|
89
|
-
request.env["warden"] = double("mock warden")
|
90
|
-
expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
|
91
|
-
expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
|
92
|
-
expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
|
93
|
-
|
94
|
-
post :reauth, params: { uid: "nonexistent-user" }
|
95
|
-
|
96
|
-
expect(response.status).to eq(200)
|
97
|
-
expect(response.content_type).to eq("text/plain")
|
98
|
-
end
|
99
|
-
|
100
|
-
it "should set remotely_signed_out to true on the user" do
|
101
|
-
# Test that it authenticates
|
102
|
-
request.env["warden"] = double("mock warden")
|
103
|
-
expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
|
104
|
-
expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
|
105
|
-
expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
|
106
|
-
|
107
|
-
post :reauth, params: { uid: @user_to_update.uid }
|
108
|
-
|
109
|
-
@user_to_update.reload
|
110
|
-
expect(@user_to_update).to be_remotely_signed_out
|
111
|
-
expect(response.content_type).to eq("text/plain")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
data/spec/support/timecop.rb
DELETED
@@ -1,172 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
RSpec.describe "Authenication and authorisation" do
|
4
|
-
context "omniauth request phase" do
|
5
|
-
let(:redirect_url) { URI.parse(page.response_headers["Location"]) }
|
6
|
-
let(:authorize_params) { Rack::Utils.parse_query(redirect_url.query) }
|
7
|
-
|
8
|
-
before do
|
9
|
-
visit "/auth/gds"
|
10
|
-
end
|
11
|
-
|
12
|
-
it "includes pkce code_challenge_method in request for /oauth/authorize" do
|
13
|
-
expect(redirect_url.path).to eql("/oauth/authorize")
|
14
|
-
expect(authorize_params["code_challenge_method"]).to eq("S256")
|
15
|
-
end
|
16
|
-
|
17
|
-
it "includes pkce code_challenge in request for /oauth/authorize" do
|
18
|
-
expect(redirect_url.path).to eql("/oauth/authorize")
|
19
|
-
expect(authorize_params["code_challenge"]).to be_present
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
context "omniauth callback phase" do
|
24
|
-
it "includes pkce code_verifier in request for /oauth/access_token" do
|
25
|
-
visit "/auth/gds"
|
26
|
-
|
27
|
-
redirect_url = URI.parse(page.response_headers["Location"])
|
28
|
-
expect(redirect_url.path).to eql("/oauth/authorize")
|
29
|
-
state = Rack::Utils.parse_query(redirect_url.query)["state"]
|
30
|
-
|
31
|
-
stub_request(:post, "http://signon/oauth/access_token")
|
32
|
-
|
33
|
-
visit "/auth/gds/callback?state=#{state}"
|
34
|
-
|
35
|
-
expect(WebMock).to have_requested(:post, "http://signon/oauth/access_token")
|
36
|
-
.with(body: hash_including({ "code_verifier" => /.*/ }))
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
context "when accessing a route that doesn't require permissions or authentication" do
|
41
|
-
it "allows access" do
|
42
|
-
visit "/not-restricted"
|
43
|
-
expect(page).to have_content("jabberwocky")
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
context "when accessing a route that requires authentication" do
|
48
|
-
it "redirects an unauthenticated request to signon" do
|
49
|
-
# We manually follow the redirects because we have configured capybara
|
50
|
-
# to not follow redirects (and thus allow testing an external redirect)
|
51
|
-
visit "/restricted"
|
52
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
53
|
-
visit page.response_headers["Location"]
|
54
|
-
expect(page.response_headers["Location"]).to match("http://signon/oauth/authorize")
|
55
|
-
end
|
56
|
-
|
57
|
-
it "allows access for an authenticated user" do
|
58
|
-
stub_signon_authenticated
|
59
|
-
|
60
|
-
visit "/restricted"
|
61
|
-
expect(page).to have_content("restricted kablooie")
|
62
|
-
end
|
63
|
-
|
64
|
-
it "restricts access if a user is authenticated but remotely signed out" do
|
65
|
-
stub_signon_authenticated
|
66
|
-
User.last.set_remotely_signed_out!
|
67
|
-
|
68
|
-
visit "/restricted"
|
69
|
-
expect(page.status_code).to eql(302)
|
70
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
71
|
-
end
|
72
|
-
|
73
|
-
it "restricts access if a user is authenticated but session has expired" do
|
74
|
-
stub_signon_authenticated
|
75
|
-
|
76
|
-
Timecop.travel(Time.now.utc + GDS::SSO::Config.auth_valid_for + 5.minutes) do
|
77
|
-
visit "/restricted"
|
78
|
-
expect(page.status_code).to eql(302)
|
79
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
it "allows access when given a valid bearer token" do
|
84
|
-
stub_signon_user_request
|
85
|
-
page.driver.header("Authorization", "Bearer 123")
|
86
|
-
|
87
|
-
visit "/restricted"
|
88
|
-
expect(page).to have_content("restricted kablooie")
|
89
|
-
end
|
90
|
-
|
91
|
-
it "restricts access when given an invalid bearer token" do
|
92
|
-
stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
|
93
|
-
.to_return(status: 401)
|
94
|
-
page.driver.header("Authorization", "Bearer 123")
|
95
|
-
|
96
|
-
visit "/restricted"
|
97
|
-
expect(page.status_code).to eq(401)
|
98
|
-
end
|
99
|
-
|
100
|
-
it "returns a 401 when a bearer token is missing and the app is api_only" do
|
101
|
-
allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
|
102
|
-
|
103
|
-
visit "/restricted"
|
104
|
-
expect(page.status_code).to eq(401)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
context "when accessing a route that requires a permission" do
|
109
|
-
it "allows access when an authenticated user has the permission" do
|
110
|
-
stub_signon_authenticated(permissions: %w[execute])
|
111
|
-
visit "/this-requires-execute-permission"
|
112
|
-
expect(page).to have_content("you have execute permission")
|
113
|
-
end
|
114
|
-
|
115
|
-
it "restricts access when an authenticated user lacks the permission" do
|
116
|
-
stub_signon_authenticated
|
117
|
-
visit "/this-requires-execute-permission"
|
118
|
-
expect(page.status_code).to eq(403)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
context "when accessing a route that is restricted by the authorised user constraint" do
|
123
|
-
it "allows access when an authenticated user has correct permissions" do
|
124
|
-
stub_signon_authenticated(permissions: %w[execute])
|
125
|
-
visit "/constraint-restricted"
|
126
|
-
expect(page).to have_content("constraint restricted")
|
127
|
-
end
|
128
|
-
|
129
|
-
it "redirects an unauthenticated request to signon" do
|
130
|
-
visit "/constraint-restricted"
|
131
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
132
|
-
visit page.response_headers["Location"]
|
133
|
-
expect(page.response_headers["Location"]).to match("http://signon/oauth/authorize")
|
134
|
-
end
|
135
|
-
|
136
|
-
it "restricts access when an authenticated user does not have the correct permissions" do
|
137
|
-
stub_signon_authenticated(permissions: %w[no-access])
|
138
|
-
visit "/constraint-restricted"
|
139
|
-
expect(page.status_code).to eq(403)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def stub_signon_authenticated(permissions: [])
|
144
|
-
# visit restricted page to trigger redirect URL to record state attribute
|
145
|
-
visit "/auth/gds"
|
146
|
-
state = CGI.parse(URI.parse(page.response_headers["Location"]).query)
|
147
|
-
.then { |query| query["state"].first }
|
148
|
-
|
149
|
-
stub_request(:post, "http://signon/oauth/access_token")
|
150
|
-
.to_return(body: { access_token: "token" }.to_json,
|
151
|
-
headers: { content_type: "application/json" })
|
152
|
-
|
153
|
-
stub_signon_user_request(permissions:)
|
154
|
-
|
155
|
-
visit "/auth/gds/callback?code=code&state=#{state}"
|
156
|
-
end
|
157
|
-
|
158
|
-
def stub_signon_user_request(permissions: [])
|
159
|
-
stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
|
160
|
-
.to_return(
|
161
|
-
body: {
|
162
|
-
user: {
|
163
|
-
uid: "123",
|
164
|
-
email: "test-user@example.com",
|
165
|
-
name: "Test User",
|
166
|
-
permissions:,
|
167
|
-
},
|
168
|
-
}.to_json,
|
169
|
-
headers: { content_type: "application/json" },
|
170
|
-
)
|
171
|
-
end
|
172
|
-
end
|
File without changes
|