Instead of storing the expiry in the cache, query for it prior to
returning the client from cache.
This is both:
- More accurate as it uses the actual expiry from Taskcluster
- Catches the case where the client no longer exists (e.g, was deleted
on the server by some other means)
At the cost of an extra TC API call, but it'll be fast.
Differential Revision: https://phabricator.services.mozilla.com/D309068
166 lines
5.2 KiB
Python
166 lines
5.2 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import json
|
|
import os
|
|
import threading
|
|
import urllib.parse
|
|
import urllib.request
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import mozunit
|
|
import pytest
|
|
from tryselect.util.taskcluster import (
|
|
TC_CREDENTIALS_EXPIRY_DAYS,
|
|
TC_ROOT_URL,
|
|
_scopes_key,
|
|
get_client,
|
|
)
|
|
|
|
import taskcluster as tc_module
|
|
|
|
DEFAULT_SCOPES = ["some:scope"]
|
|
BROWSER_CLIENT_ID = "browser-client"
|
|
BROWSER_ACCESS_TOKEN = "browser-token"
|
|
TC_CLIENT_URL = f"{TC_ROOT_URL}/api/auth/v1/clients/cached-client"
|
|
|
|
|
|
def _expires_iso(offset_s):
|
|
dt = datetime.now(tz=timezone.utc) + timedelta(seconds=offset_s)
|
|
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
|
|
|
|
def _register_tc_client(
|
|
rsps,
|
|
disabled=False,
|
|
expires_offset_s=TC_CREDENTIALS_EXPIRY_DAYS * 86400,
|
|
status=200,
|
|
):
|
|
if status != 200:
|
|
rsps.add(rsps.GET, TC_CLIENT_URL, status=status, json={})
|
|
else:
|
|
rsps.add(
|
|
rsps.GET,
|
|
TC_CLIENT_URL,
|
|
json={"disabled": disabled, "expires": _expires_iso(expires_offset_s)},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def credentials_file(tmp_path, monkeypatch):
|
|
creds_path = tmp_path / "tc_credentials.json"
|
|
monkeypatch.setattr(
|
|
"tryselect.util.taskcluster._get_credentials_file", lambda: creds_path
|
|
)
|
|
return creds_path
|
|
|
|
|
|
def make_cache(credentials_file, scopes=None):
|
|
scopes = scopes or DEFAULT_SCOPES
|
|
credentials_file.write_text(
|
|
json.dumps({
|
|
_scopes_key(scopes): {
|
|
"clientId": "cached-client",
|
|
"accessToken": "cached-token",
|
|
}
|
|
})
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def run_get_client(monkeypatch, responses):
|
|
def fake_webbrowser_open(url):
|
|
params = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
|
|
callback_url = params["callback_url"][0]
|
|
qs = urllib.parse.urlencode({
|
|
"clientId": BROWSER_CLIENT_ID,
|
|
"accessToken": BROWSER_ACCESS_TOKEN,
|
|
})
|
|
|
|
def send_creds():
|
|
try:
|
|
urllib.request.urlopen(f"{callback_url}?{qs}", timeout=10)
|
|
except Exception:
|
|
pass
|
|
|
|
threading.Thread(target=send_creds, daemon=True).start()
|
|
|
|
def inner(service="queue", scopes=None, env=None):
|
|
scopes = scopes or DEFAULT_SCOPES
|
|
env = env or {}
|
|
monkeypatch.setattr(os, "environ", env)
|
|
monkeypatch.setattr("webbrowser.open", fake_webbrowser_open)
|
|
return get_client(service, scopes)
|
|
|
|
return inner
|
|
|
|
|
|
def test_get_client_automation(run_get_client):
|
|
result = run_get_client(
|
|
env={
|
|
"MOZ_AUTOMATION": "1",
|
|
"TASKCLUSTER_CLIENT_ID": "env-client",
|
|
"TASKCLUSTER_ACCESS_TOKEN": "env-token",
|
|
"TASKCLUSTER_ROOT_URL": "https://tc.example.com",
|
|
},
|
|
)
|
|
assert isinstance(result, tc_module.Queue)
|
|
assert result.options["credentials"]["clientId"] == b"env-client"
|
|
assert result.options["credentials"]["accessToken"] == b"env-token"
|
|
|
|
|
|
def test_get_client_cache_hit(credentials_file, run_get_client, responses):
|
|
make_cache(credentials_file)
|
|
_register_tc_client(responses)
|
|
result = run_get_client()
|
|
assert isinstance(result, tc_module.Queue)
|
|
assert result.options["rootUrl"] == TC_ROOT_URL
|
|
assert result.options["credentials"]["clientId"] == b"cached-client"
|
|
assert result.options["credentials"]["accessToken"] == b"cached-token"
|
|
|
|
|
|
def test_get_client_cache_expired(credentials_file, run_get_client, responses):
|
|
make_cache(credentials_file)
|
|
_register_tc_client(responses, expires_offset_s=200)
|
|
result = run_get_client()
|
|
assert isinstance(result, tc_module.Queue)
|
|
assert result.options["rootUrl"] == TC_ROOT_URL
|
|
assert result.options["credentials"]["clientId"] == b"browser-client"
|
|
assert result.options["credentials"]["accessToken"] == b"browser-token"
|
|
|
|
|
|
def test_get_client_browser_auth(credentials_file, run_get_client):
|
|
assert not credentials_file.exists()
|
|
result = run_get_client()
|
|
assert isinstance(result, tc_module.Queue)
|
|
assert result.options["rootUrl"] == TC_ROOT_URL
|
|
assert result.options["credentials"]["clientId"] == b"browser-client"
|
|
assert result.options["credentials"]["accessToken"] == b"browser-token"
|
|
assert credentials_file.is_file()
|
|
|
|
|
|
def test_get_client_deleted_tc_client(credentials_file, run_get_client, responses):
|
|
make_cache(credentials_file)
|
|
_register_tc_client(responses, status=404)
|
|
result = run_get_client()
|
|
assert result.options["credentials"]["clientId"] == b"browser-client"
|
|
|
|
|
|
def test_get_client_auth_failure(credentials_file, run_get_client, responses):
|
|
make_cache(credentials_file)
|
|
_register_tc_client(responses, status=401)
|
|
result = run_get_client()
|
|
assert result.options["credentials"]["clientId"] == b"browser-client"
|
|
|
|
|
|
def test_get_client_disabled(credentials_file, run_get_client, responses):
|
|
make_cache(credentials_file)
|
|
_register_tc_client(responses, disabled=True)
|
|
result = run_get_client()
|
|
assert result.options["credentials"]["clientId"] == b"browser-client"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
mozunit.main()
|