Skip to content

Commit 8881453

Browse files
committed
move cli-specific auth to CliSession, add tests for CliSession
1 parent e703ad3 commit 8881453

File tree

5 files changed

+114
-22
lines changed

5 files changed

+114
-22
lines changed

planet/cli/data.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,8 @@
3737

3838
@asynccontextmanager
3939
async def data_client(ctx):
40-
auth = ctx.obj['AUTH']
41-
base_url = ctx.obj['BASE_URL']
42-
async with CliSession(auth=auth) as sess:
43-
cl = DataClient(sess, base_url=base_url)
40+
async with CliSession() as sess:
41+
cl = DataClient(sess, base_url=ctx.obj['BASE_URL'])
4442
yield cl
4543

4644

@@ -52,7 +50,6 @@ async def data_client(ctx):
5250
help='Assign custom base Orders API URL.')
5351
def data(ctx, base_url):
5452
'''Commands for interacting with the Data API'''
55-
ctx.obj['AUTH'] = None
5653
ctx.obj['BASE_URL'] = base_url
5754

5855

planet/cli/orders.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@
3131

3232
@asynccontextmanager
3333
async def orders_client(ctx):
34-
auth = ctx.obj['AUTH']
3534
base_url = ctx.obj['BASE_URL']
36-
async with CliSession(auth=auth) as sess:
35+
async with CliSession() as sess:
3736
cl = OrdersClient(sess, base_url=base_url)
3837
yield cl
3938

@@ -46,7 +45,6 @@ async def orders_client(ctx):
4645
help='Assign custom base Orders API URL.')
4746
def orders(ctx, base_url):
4847
'''Commands for interacting with the Orders API'''
49-
ctx.obj['AUTH'] = None
5048
ctx.obj['BASE_URL'] = base_url
5149

5250

planet/cli/session.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
"""CLI HTTP/auth sessions."""
22

3-
from planet.auth import AuthType
3+
from planet.auth import Auth
44
from planet.http import Session
5-
from typing import Optional
65

76

87
class CliSession(Session):
9-
10-
def __init__(self, auth: Optional[AuthType] = None):
11-
super().__init__(auth)
8+
"""Session with CLI-specific auth and identifying header"""
9+
def __init__(self):
10+
super().__init__(Auth.from_file())
1211
self._client.headers.update({'X-Planet-App': 'python-cli'})

planet/cli/subscriptions.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
@click.pass_context
1515
def subscriptions(ctx):
1616
'''Commands for interacting with the Subscriptions API'''
17-
# None means that order of precedence is 1) environment variable,
18-
# 2) secret file.
19-
ctx.obj['AUTH'] = None
17+
pass
2018

2119

2220
# We want our command to be known as "list" on the command line but
@@ -40,7 +38,7 @@ def subscriptions(ctx):
4038
@coro
4139
async def list_subscriptions_cmd(ctx, status, limit, pretty):
4240
"""Prints a sequence of JSON-encoded Subscription descriptions."""
43-
async with CliSession(auth=ctx.obj['AUTH']) as session:
41+
async with CliSession() as session:
4442
client = SubscriptionsClient(session)
4543
subs_aiter = client.list_subscriptions_aiter(status=status,
4644
limit=limit)
@@ -77,7 +75,7 @@ def parse_request(ctx, param, value: str) -> dict:
7775
@coro
7876
async def create_subscription_cmd(ctx, request, pretty):
7977
"""Submits a subscription request and prints the API response."""
80-
async with CliSession(auth=ctx.obj['AUTH']) as session:
78+
async with CliSession() as session:
8179
client = SubscriptionsClient(session)
8280
sub = await client.create_subscription(request)
8381
echo_json(sub, pretty)
@@ -91,7 +89,7 @@ async def create_subscription_cmd(ctx, request, pretty):
9189
@coro
9290
async def cancel_subscription_cmd(ctx, subscription_id, pretty):
9391
"""Cancels a subscription and prints the API response."""
94-
async with CliSession(auth=ctx.obj['AUTH']) as session:
92+
async with CliSession() as session:
9593
client = SubscriptionsClient(session)
9694
sub = await client.cancel_subscription(subscription_id)
9795
echo_json(sub, pretty)
@@ -106,7 +104,7 @@ async def cancel_subscription_cmd(ctx, subscription_id, pretty):
106104
@coro
107105
async def update_subscription_cmd(ctx, subscription_id, request, pretty):
108106
"""Updates a subscription and prints the API response."""
109-
async with CliSession(auth=ctx.obj['AUTH']) as session:
107+
async with CliSession() as session:
110108
client = SubscriptionsClient(session)
111109
sub = await client.update_subscription(subscription_id, request)
112110
echo_json(sub, pretty)
@@ -120,7 +118,7 @@ async def update_subscription_cmd(ctx, subscription_id, request, pretty):
120118
@coro
121119
async def describe_subscription_cmd(ctx, subscription_id, pretty):
122120
"""Gets the description of a subscription and prints the API response."""
123-
async with CliSession(auth=ctx.obj['AUTH']) as session:
121+
async with CliSession() as session:
124122
client = SubscriptionsClient(session)
125123
sub = await client.get_subscription(subscription_id)
126124
echo_json(sub, pretty)
@@ -156,7 +154,7 @@ async def list_subscription_results_cmd(ctx,
156154
status,
157155
limit):
158156
"""Gets results of a subscription and prints the API response."""
159-
async with CliSession(auth=ctx.obj['AUTH']) as session:
157+
async with CliSession() as session:
160158
client = SubscriptionsClient(session)
161159
results_aiter = client.get_results_aiter(subscription_id,
162160
status=status,

tests/unit/test_cli_session.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Copyright 2022 Planet Labs PBC.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4+
# use this file except in compliance with the License. You may obtain a copy of
5+
# the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations under
13+
# the License.
14+
import base64
15+
from http import HTTPStatus
16+
import json
17+
18+
import httpx
19+
import respx
20+
21+
import pytest
22+
23+
# from planet.auth import _SecretFile
24+
from planet import auth
25+
from planet.cli import session
26+
from planet.exceptions import AuthException
27+
28+
TEST_URL = 'mock://mock.com'
29+
30+
31+
# skip the global mock of _SecretFile.read
32+
# for this module
33+
@pytest.fixture(autouse=True, scope='module')
34+
def test_secretfile_read():
35+
return
36+
37+
38+
@pytest.fixture()
39+
def test_valid_secretfile(tmp_path, monkeypatch):
40+
# write our own secret file
41+
secret_path = f'{tmp_path}/secret.test'
42+
monkeypatch.setattr(auth, 'SECRET_FILE_PATH', secret_path)
43+
with open(secret_path, 'w') as fp:
44+
json.dump({'key': 'clisessiontest'}, fp)
45+
46+
47+
@respx.mock
48+
@pytest.mark.asyncio
49+
async def test_CliSession_headers(test_valid_secretfile):
50+
async with session.CliSession() as sess:
51+
route = respx.get(TEST_URL)
52+
route.return_value = httpx.Response(HTTPStatus.OK)
53+
54+
await sess.request(method='GET', url=TEST_URL)
55+
56+
# the proper headers are included and they have the expected values
57+
received_request = route.calls.last.request
58+
assert received_request.headers['x-planet-app'] == 'python-cli'
59+
assert 'planet-client-python/' in received_request.headers[
60+
'user-agent']
61+
62+
63+
@respx.mock
64+
@pytest.mark.asyncio
65+
async def test_CliSession_auth_valid(test_valid_secretfile):
66+
async with session.CliSession() as sess:
67+
route = respx.get(TEST_URL)
68+
route.return_value = httpx.Response(HTTPStatus.OK)
69+
70+
await sess.request(method='GET', url=TEST_URL)
71+
72+
# the proper headers are included and they have the expected values
73+
received_request = route.calls.last.request
74+
credentials = received_request.headers['authorization'].strip(
75+
'Authorization: Basic ')
76+
assert base64.b64decode(credentials) == b'clisessiontest:'
77+
78+
79+
@respx.mock
80+
@pytest.mark.asyncio
81+
async def test_CliSession_auth_invalid(tmp_path, monkeypatch):
82+
# write invalid secret file
83+
secret_path = f'{tmp_path}/secret.test'
84+
monkeypatch.setattr(auth, 'SECRET_FILE_PATH', secret_path)
85+
with open(secret_path, 'w') as fp:
86+
json.dump({'invalidkey': 'clisessiontest'}, fp)
87+
88+
with pytest.raises(AuthException):
89+
session.CliSession()
90+
91+
92+
@respx.mock
93+
@pytest.mark.asyncio
94+
async def test_CliSession_auth_nofile(tmp_path, monkeypatch):
95+
# point to non-existant file
96+
secret_path = f'{tmp_path}/doesnotexist.test'
97+
monkeypatch.setattr(auth, 'SECRET_FILE_PATH', secret_path)
98+
99+
with pytest.raises(AuthException):
100+
session.CliSession()

0 commit comments

Comments
 (0)