Skip to content

Commit 5b1b9de

Browse files
committed
spec it
1 parent 52adcae commit 5b1b9de

File tree

3 files changed

+318
-0
lines changed

3 files changed

+318
-0
lines changed

spec/factories/oauth_tokens.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FactoryBot.define do
2+
factory :oauth_token do
3+
association :resource_owner, factory: :identity
4+
association :application, factory: :program
5+
token { OAuthToken.generate }
6+
scopes { "basic_info" }
7+
expires_in { nil }
8+
revoked_at { nil }
9+
10+
trait :with_all_scopes do
11+
scopes { "verification_status basic_info email name slack_id legal_name address" }
12+
end
13+
14+
trait :expired do
15+
expires_in { -1 }
16+
end
17+
18+
trait :revoked do
19+
revoked_at { 1.hour.ago }
20+
end
21+
end
22+
end

spec/factories/programs.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FactoryBot.define do
2+
factory :program do
3+
sequence(:name) { |n| "Test Program #{n}" }
4+
sequence(:uid) { |n| SecureRandom.hex(16) }
5+
secret { SecureRandom.hex(32) }
6+
redirect_uri { "https://example.com/callback" }
7+
scopes { "basic_info email name" }
8+
active { true }
9+
10+
trait :with_all_scopes do
11+
scopes { "verification_status basic_info email name slack_id legal_name address" }
12+
end
13+
14+
trait :inactive do
15+
active { false }
16+
end
17+
end
18+
end
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "API::V1::Identities", type: :request do
4+
let(:program) { create(:program, :with_all_scopes) }
5+
let(:identity) { create(:identity, :with_address) }
6+
7+
describe "GET /api/v1/me" do
8+
context "with OAuth token" do
9+
let(:token) { create(:oauth_token, resource_owner: identity, application: program, scopes: "basic_info email") }
10+
11+
it "returns identity data for authorized scopes only" do
12+
get "/api/v1/me", headers: { "Authorization" => "Bearer #{token.token}" }
13+
14+
expect(response).to have_http_status(:ok)
15+
json = JSON.parse(response.body)
16+
ident = json["identity"]
17+
18+
# basic_info scope authorized
19+
expect(ident["first_name"]).to eq(identity.first_name)
20+
expect(ident["primary_email"]).to eq(identity.primary_email)
21+
22+
# legal_name scope NOT authorized
23+
expect(ident).not_to have_key("legal_first_name")
24+
expect(ident).not_to have_key("legal_last_name")
25+
26+
# address scope NOT authorized
27+
expect(ident).not_to have_key("addresses")
28+
end
29+
30+
it "returns legal_name when that scope is authorized" do
31+
token.update!(scopes: "basic_info legal_name")
32+
33+
get "/api/v1/me", headers: { "Authorization" => "Bearer #{token.token}" }
34+
35+
json = JSON.parse(response.body)
36+
ident = json["identity"]
37+
38+
expect(ident["legal_first_name"]).to eq(identity.legal_first_name)
39+
expect(ident["legal_last_name"]).to eq(identity.legal_last_name)
40+
end
41+
42+
it "returns 401 for revoked token" do
43+
token.update!(revoked_at: 1.hour.ago)
44+
45+
get "/api/v1/me", headers: { "Authorization" => "Bearer #{token.token}" }
46+
47+
expect(response).to have_http_status(:unauthorized)
48+
end
49+
50+
it "returns 401 for inactive program" do
51+
program.update!(active: false)
52+
53+
get "/api/v1/me", headers: { "Authorization" => "Bearer #{token.token}" }
54+
55+
expect(response).to have_http_status(:unauthorized)
56+
end
57+
end
58+
59+
context "with program key" do
60+
it "returns 404 (no current_identity when acting as program)" do
61+
get "/api/v1/me", headers: { "Authorization" => "Bearer #{program.program_key}" }
62+
63+
expect(response).to have_http_status(:not_found)
64+
end
65+
end
66+
67+
context "without authentication" do
68+
it "returns 401" do
69+
get "/api/v1/me"
70+
71+
expect(response).to have_http_status(:unauthorized)
72+
end
73+
end
74+
end
75+
76+
describe "GET /api/v1/identities/:id" do
77+
context "with program key" do
78+
before do
79+
# Create access grant to add identity to program.identities
80+
Doorkeeper::AccessGrant.create!(
81+
resource_owner: identity,
82+
application: program,
83+
token: SecureRandom.hex(32),
84+
expires_in: 600,
85+
redirect_uri: program.redirect_uri,
86+
scopes: "basic_info email"
87+
)
88+
# Create access token for scope authorization check
89+
create(:oauth_token, resource_owner: identity, application: program, scopes: "basic_info email")
90+
end
91+
92+
it "returns identity data for scopes the identity authorized" do
93+
get "/api/v1/identities/#{identity.public_id}",
94+
headers: { "Authorization" => "Bearer #{program.program_key}" }
95+
96+
expect(response).to have_http_status(:ok)
97+
json = JSON.parse(response.body)
98+
ident = json["identity"]
99+
100+
# basic_info scope authorized by identity
101+
expect(ident["first_name"]).to eq(identity.first_name)
102+
expect(ident["primary_email"]).to eq(identity.primary_email)
103+
104+
# legal_name scope NOT authorized by identity
105+
expect(ident).not_to have_key("legal_first_name")
106+
end
107+
108+
it "respects per-identity scope authorization" do
109+
# Create second identity with different scopes
110+
identity2 = create(:identity)
111+
Doorkeeper::AccessGrant.create!(
112+
resource_owner: identity2,
113+
application: program,
114+
token: SecureRandom.hex(32),
115+
expires_in: 600,
116+
redirect_uri: program.redirect_uri,
117+
scopes: "legal_name"
118+
)
119+
create(:oauth_token, resource_owner: identity2, application: program, scopes: "legal_name")
120+
121+
# Check first identity - has basic_info, not legal_name
122+
get "/api/v1/identities/#{identity.public_id}",
123+
headers: { "Authorization" => "Bearer #{program.program_key}" }
124+
125+
json = JSON.parse(response.body)
126+
expect(json["identity"]["first_name"]).to eq(identity.first_name)
127+
expect(json["identity"]).not_to have_key("legal_first_name")
128+
129+
# Check second identity - has legal_name, not basic_info
130+
get "/api/v1/identities/#{identity2.public_id}",
131+
headers: { "Authorization" => "Bearer #{program.program_key}" }
132+
133+
json = JSON.parse(response.body)
134+
expect(json["identity"]).not_to have_key("first_name")
135+
expect(json["identity"]["legal_first_name"]).to eq(identity2.legal_first_name)
136+
end
137+
138+
it "returns only id when identity has no matching scope authorizations" do
139+
# Create identity with access grant but no access tokens
140+
identity_no_auth = create(:identity)
141+
Doorkeeper::AccessGrant.create!(
142+
resource_owner: identity_no_auth,
143+
application: program,
144+
token: SecureRandom.hex(32),
145+
expires_in: 600,
146+
redirect_uri: program.redirect_uri,
147+
scopes: "basic_info"
148+
)
149+
# No access token created - identity never completed OAuth
150+
151+
get "/api/v1/identities/#{identity_no_auth.public_id}",
152+
headers: { "Authorization" => "Bearer #{program.program_key}" }
153+
154+
expect(response).to have_http_status(:ok)
155+
json = JSON.parse(response.body)
156+
# Only id should be present, no PII
157+
expect(json["identity"].keys).to eq(["id"])
158+
end
159+
160+
it "cannot access identity not associated with program" do
161+
unrelated_identity = create(:identity)
162+
163+
get "/api/v1/identities/#{unrelated_identity.public_id}",
164+
headers: { "Authorization" => "Bearer #{program.program_key}" }
165+
166+
expect(response).to have_http_status(:not_found)
167+
end
168+
end
169+
170+
context "with OAuth token" do
171+
let(:token) { create(:oauth_token, resource_owner: identity, application: program, scopes: "basic_info") }
172+
173+
it "returns 403 (only program keys can access show)" do
174+
get "/api/v1/identities/#{identity.public_id}",
175+
headers: { "Authorization" => "Bearer #{token.token}" }
176+
177+
expect(response).to have_http_status(:forbidden)
178+
end
179+
end
180+
end
181+
182+
describe "GET /api/v1/identities" do
183+
context "with program key" do
184+
let!(:identity1) { create(:identity) }
185+
let!(:identity2) { create(:identity) }
186+
187+
before do
188+
# identity1 authorized basic_info
189+
Doorkeeper::AccessGrant.create!(
190+
resource_owner: identity1,
191+
application: program,
192+
token: SecureRandom.hex(32),
193+
expires_in: 600,
194+
redirect_uri: program.redirect_uri,
195+
scopes: "basic_info"
196+
)
197+
create(:oauth_token, resource_owner: identity1, application: program, scopes: "basic_info")
198+
199+
# identity2 authorized legal_name only
200+
Doorkeeper::AccessGrant.create!(
201+
resource_owner: identity2,
202+
application: program,
203+
token: SecureRandom.hex(32),
204+
expires_in: 600,
205+
redirect_uri: program.redirect_uri,
206+
scopes: "legal_name"
207+
)
208+
create(:oauth_token, resource_owner: identity2, application: program, scopes: "legal_name")
209+
end
210+
211+
it "returns all program identities with per-identity scope filtering" do
212+
get "/api/v1/identities", headers: { "Authorization" => "Bearer #{program.program_key}" }
213+
214+
expect(response).to have_http_status(:ok)
215+
json = JSON.parse(response.body)
216+
identities = json["identities"]
217+
218+
expect(identities.length).to eq(2)
219+
220+
ident1 = identities.find { |i| i["id"] == identity1.public_id }
221+
ident2 = identities.find { |i| i["id"] == identity2.public_id }
222+
223+
# identity1 has basic_info
224+
expect(ident1["first_name"]).to eq(identity1.first_name)
225+
expect(ident1).not_to have_key("legal_first_name")
226+
227+
# identity2 has legal_name
228+
expect(ident2).not_to have_key("first_name")
229+
expect(ident2["legal_first_name"]).to eq(identity2.legal_first_name)
230+
end
231+
232+
233+
end
234+
235+
context "with OAuth token" do
236+
let(:token) { create(:oauth_token, resource_owner: identity, application: program, scopes: "basic_info") }
237+
238+
it "returns 403 (only program keys can access index)" do
239+
get "/api/v1/identities", headers: { "Authorization" => "Bearer #{token.token}" }
240+
241+
expect(response).to have_http_status(:forbidden)
242+
end
243+
end
244+
end
245+
246+
describe "scope enforcement edge cases" do
247+
context "program requests scope it doesn't have configured" do
248+
let(:limited_program) { create(:program, scopes: "email") }
249+
250+
before do
251+
Doorkeeper::AccessGrant.create!(
252+
resource_owner: identity,
253+
application: limited_program,
254+
token: SecureRandom.hex(32),
255+
expires_in: 600,
256+
redirect_uri: limited_program.redirect_uri,
257+
scopes: "email basic_info"
258+
)
259+
create(:oauth_token, resource_owner: identity, application: limited_program, scopes: "email basic_info")
260+
end
261+
262+
it "only returns data for scopes the program has AND identity authorized" do
263+
get "/api/v1/identities/#{identity.public_id}",
264+
headers: { "Authorization" => "Bearer #{limited_program.program_key}" }
265+
266+
expect(response).to have_http_status(:ok)
267+
json = JSON.parse(response.body)
268+
ident = json["identity"]
269+
270+
# email: program has it, identity authorized it
271+
expect(ident["primary_email"]).to eq(identity.primary_email)
272+
273+
# basic_info: identity authorized but program doesn't have it
274+
expect(ident).not_to have_key("first_name")
275+
end
276+
end
277+
end
278+
end

0 commit comments

Comments
 (0)