1+ from collections import Counter
2+
13from checkout_sdk .oauth_scopes import OAuthScopes
24
35
@@ -16,3 +18,82 @@ def test_should_expose_documented_balances_scope_values(self):
1618 assert OAuthScopes .BALANCES .value == 'balances'
1719 assert OAuthScopes .BALANCES_VIEW .value == 'balances:view'
1820 assert OAuthScopes .BALANCES_TOP_UP_INSTRUCTIONS .value == 'balances:top-up-instructions'
21+
22+ def test_should_expose_documented_values_for_scopes_added_in_spec_sync (self ):
23+ """The scopes added when this enum was synced against the spec.
24+
25+ None of these four are declared in clientCredentials.scopes: they appear only in the
26+ per-operation security requirements of GET/POST /compliance-requests/{payment_id} and
27+ GET /tokens/{tokenId}/metadata. An enum built from the declared map alone would miss them.
28+ """
29+ assert OAuthScopes .COMPLIANCE_REQUESTS .value == 'compliance-requests'
30+ assert OAuthScopes .COMPLIANCE_REQUESTS_READ .value == 'compliance-requests:read'
31+ assert OAuthScopes .COMPLIANCE_REQUESTS_RESPOND .value == 'compliance-requests:respond'
32+ assert OAuthScopes .VAULT_TOKENS_METADATA .value == 'vault:tokens-metadata'
33+
34+ def test_should_expose_the_agentic_commerce_inventory_scope (self ):
35+ """agentic:inventory is declared in clientCredentials.scopes ("Manage agentic commerce
36+ inventory and reservations"), and it is the OAuth requirement of the ten /inventory/*
37+ operations behind the beta agentic-commerce inventory and reservations endpoints.
38+ """
39+ assert OAuthScopes .AGENTIC_INVENTORY .value == 'agentic:inventory'
40+
41+ def test_should_retain_the_legacy_scopes_the_spec_omits (self ):
42+ """These five appear nowhere in the spec, so a spec-driven sweep would delete them.
43+
44+ They are kept deliberately: the authorization server still grants them and callers still
45+ request them. marketplace is the proof -- the sandbox payouts client is provisioned for it
46+ and answers a request for accounts with invalid_scope.
47+ """
48+ assert OAuthScopes .ISSUING_CARD_MGMT .value == 'issuing:card-mgmt'
49+ assert OAuthScopes .ISSUING_CLIENT .value == 'issuing:client'
50+ assert OAuthScopes .MARKETPLACE .value == 'marketplace'
51+ assert OAuthScopes .MIDDLEWARE_GATEWAY .value == 'middleware:gateway'
52+ assert OAuthScopes .MIDDLEWARE_PAYMENT_CONTEXT .value == 'middleware:payment-context'
53+
54+ def test_should_distinguish_the_two_payment_context_scopes (self ):
55+ """PAYMENT_CONTEXT and GATEWAY_PAYMENT_CONTEXTS read alike but are unrelated scopes.
56+
57+ The spec requires the former for GET /payment-contexts/{id} and the latter for
58+ POST /payment-contexts. 'Payment Context' is the only scope whose value contains a space and
59+ a capital letter, which is almost certainly a spec authoring defect -- asserted verbatim
60+ because that is the value the authorization server is documented to accept.
61+ """
62+ assert OAuthScopes .PAYMENT_CONTEXT .value == 'Payment Context'
63+ assert OAuthScopes .GATEWAY_PAYMENT_CONTEXTS .value == 'gateway:payment-contexts'
64+
65+ def test_should_expose_a_non_blank_wire_value_for_every_member (self ):
66+ """A blank value is not caught by the assertions above, which only read members they name.
67+
68+ oauth_credentials.py joins the requested scopes with a space, so a blank member would be
69+ sent as an empty entry and the token endpoint would reject the whole request, costing the
70+ caller every other scope it asked for.
71+ """
72+ blank = [scope .name for scope in OAuthScopes if not scope .value .strip ()]
73+ assert blank == []
74+
75+ def test_should_not_reuse_a_wire_value_across_members (self ):
76+ """A duplicate wire value means one of the two members is a copy-paste error.
77+
78+ Python's Enum hides this far better than the other SDKs' constructs do: the second member
79+ to declare a value becomes an *alias* of the first rather than a member of its own, so
80+ `OAuthScopes.VAULT_TOKENS_METADATA is OAuthScopes.VAULT_TOKENIZATION` would simply be True
81+ and the scope the aliased name was meant to carry would be unreachable, with nothing
82+ failing loudly.
83+
84+ This must iterate __members__, not the enum: iteration *skips* aliases, so counting values
85+ that way can never see a duplicate and the assertion would hold vacuously.
86+ """
87+ duplicates = [value for value , count in
88+ Counter (scope .value for scope in OAuthScopes .__members__ .values ()).items ()
89+ if count > 1 ]
90+ assert duplicates == []
91+
92+ def test_should_declare_members_in_alphabetical_order (self ):
93+ """Members are kept alphabetical so the next spec sync produces a readable diff.
94+
95+ Underscores are ignored when comparing, which is what puts PAYMENT_CONTEXT,
96+ PAYMENT_SESSIONS and PAYMENTS_SEARCH in that order, matching the other Checkout SDKs.
97+ """
98+ declared = [scope .name .replace ('_' , '' ).lower () for scope in OAuthScopes ]
99+ assert declared == sorted (declared )
0 commit comments