Skip to content

Commit 9571fa1

Browse files
authored
Merge pull request #92 from heroku/fix/optional-access-token
Fix scenarios where valid requests may not include accessToken in the header.
2 parents f9b6836 + 51a442c commit 9571fa1

File tree

6 files changed

+55
-19
lines changed

6 files changed

+55
-19
lines changed

docs/heroku_applink/context.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Classes
1212
# `ClientContext`
1313

1414
```python
15-
class ClientContext(*, orgheroku_applink.context.Org, data_apiheroku_applink.data_api.DataAPI, request_idstr, access_tokenstr, api_versionstr, namespacestr | None = None)
15+
class ClientContext(*, orgheroku_applink.context.Org, data_apiheroku_applink.data_api.DataAPI, request_idstr, access_tokenstr | None, api_versionstr, namespacestr | None = None)
1616
```
1717
Information about the Salesforce org that made the request.
1818

@@ -24,7 +24,7 @@ def from_header(header: str, connection: heroku_applink.connection.Connection)
2424

2525
## Instance variables
2626

27-
* `access_token: str`
27+
* `access_token: str | None`
2828
Valid access token for the current context org/user.
2929

3030
* `api_version: str`

docs/heroku_applink/data_api/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Classes
2020
# `DataAPI`
2121

2222
```python
23-
class DataAPI(*, org_domain_urlstr, api_versionstr, access_tokenstr, connectionheroku_applink.connection.Connection)
23+
class DataAPI(*, org_domain_urlstr, api_versionstr, access_tokenstr | None, connectionheroku_applink.connection.Connection)
2424
```
2525
Data API client to interact with data in a Salesforce org.
2626

docs/heroku_applink/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ For a list of exceptions, see:
184184
# `ClientContext`
185185

186186
```python
187-
class ClientContext(*, org: heroku_applink.context.Org, data_api: heroku_applink.data_api.DataAPI, request_id: str, access_token: str, api_version: str, namespace: str | None = None)
187+
class ClientContext(*, org: heroku_applink.context.Org, data_api: heroku_applink.data_api.DataAPI, request_id: str, access_token: str | None, api_version: str, namespace: str | None = None)
188188
```
189189
Information about the Salesforce org that made the request.
190190

@@ -196,7 +196,7 @@ def from_header(header: str, connection: heroku_applink.connection.Connection)
196196

197197
## Instance variables
198198

199-
* `access_token: str`
199+
* `access_token: str | None`
200200
Valid access token for the current context org/user.
201201

202202
* `api_version: str`

heroku_applink/context.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ class ClientContext:
6565

6666
org: Org
6767
"""Information about the Salesforce org and the user that made the request."""
68-
data_api: DataAPI
69-
"""An initialized data API client instance for interacting with data in the org."""
68+
data_api: DataAPI | None
69+
"""An initialized data API client instance for interacting with data in the org. None if no access token is available."""
7070
request_id: str
7171
"""Request ID from the Salesforce org."""
72-
access_token: str
72+
access_token: str | None
7373
"""Valid access token for the current context org/user."""
7474
api_version: str
7575
"""API version of the Salesforce component that made the request."""
@@ -81,6 +81,19 @@ def from_header(cls, header: str, connection: Connection):
8181
decoded = base64.b64decode(header)
8282
data = json.loads(decoded)
8383

84+
access_token = data.get("accessToken")
85+
86+
# Set data_api only if access token is available
87+
if access_token is None:
88+
data_api = None
89+
else:
90+
data_api = DataAPI(
91+
org_domain_url=data["orgDomainUrl"],
92+
api_version=data["apiVersion"],
93+
access_token=access_token,
94+
connection=connection,
95+
)
96+
8497
return cls(
8598
org=Org(
8699
id=data["orgId"],
@@ -91,15 +104,10 @@ def from_header(cls, header: str, connection: Connection):
91104
),
92105
),
93106
request_id=data["requestId"],
94-
access_token=data["accessToken"],
107+
access_token=access_token,
95108
api_version=data["apiVersion"],
96109
namespace=data.get("namespace"), # Use get() to handle None case
97-
data_api=DataAPI(
98-
org_domain_url=data["orgDomainUrl"],
99-
api_version=data["apiVersion"],
100-
access_token=data["accessToken"],
101-
connection=connection,
102-
),
110+
data_api=data_api,
103111
)
104112

105113
# ContextVars for request-scoped data

heroku_applink/data_api/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(
4545
*,
4646
org_domain_url: str,
4747
api_version: str,
48-
access_token: str,
48+
access_token: str | None,
4949
connection: Connection,
5050
) -> None:
5151
self._api_version = api_version
@@ -251,9 +251,10 @@ async def _download_file(self, url: str) -> bytes:
251251
return await response.read()
252252

253253
def _default_headers(self) -> dict[str, str]:
254-
return {
255-
"Authorization": f"Bearer {self.access_token}",
256-
}
254+
headers = {}
255+
if self.access_token is not None:
256+
headers["Authorization"] = f"Bearer {self.access_token}"
257+
return headers
257258

258259

259260
def _json_serialize(data: Any) -> BytesPayload:

tests/test_context.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def test_client_context_from_header():
7474
assert ctx.access_token == "access-token-xyz"
7575
assert ctx.api_version == "v57.0"
7676
assert ctx.namespace == "ns"
77+
assert ctx.data_api is not None # DataAPI should be created when access token is present
78+
assert ctx.data_api.access_token == "access-token-xyz" # DataAPI should have the access token
7779

7880
def test_client_context_from_header_invalid():
7981
# Provide bad Base64 encoded string
@@ -103,3 +105,28 @@ def test_client_context_from_header_with_null_namespace():
103105
assert ctx.access_token == "access-token-xyz"
104106
assert ctx.api_version == "v57.0"
105107
assert ctx.namespace is None
108+
109+
def test_client_context_from_header_missing_access_token():
110+
payload = {
111+
"orgId": "00DJS0000000123ABC",
112+
"orgDomainUrl": "https://example-domain.my.salesforce.com",
113+
"userContext": {
114+
"userId": "005JS000000H123",
115+
"username": "user@example.tld",
116+
},
117+
"requestId": "req-456",
118+
# Note: accessToken is intentionally missing
119+
"apiVersion": "v57.0",
120+
"namespace": "ns",
121+
}
122+
encoded = base64.b64encode(json.dumps(payload).encode()).decode()
123+
connection = Connection(Config.default())
124+
ctx = ClientContext.from_header(encoded, connection)
125+
126+
assert ctx.org.id == "00DJS0000000123ABC"
127+
assert ctx.org.user.username == "user@example.tld"
128+
assert ctx.request_id == "req-456"
129+
assert ctx.api_version == "v57.0"
130+
assert ctx.namespace == "ns"
131+
assert ctx.access_token is None # Should be None when missing
132+
assert ctx.data_api is None # DataAPI should not be created when no access token

0 commit comments

Comments
 (0)