1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
|
# Copyright Red Hat
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Authors: Adam Williamson <awilliam@redhat.com>
# these are all kinda inappropriate for pytest patterns
# pylint: disable=old-style-class, no-init, protected-access, no-self-use, unused-argument
# pylint: disable=invalid-name, too-few-public-methods, too-many-public-methods, too-many-lines
"""Tests for the main client code."""
from unittest import mock
import freezegun
import pytest
import requests
import openqa_client.client as oqc
import openqa_client.exceptions as oqe
class TestClient:
"""Tests for the client library."""
@pytest.mark.parametrize(
"config_hosts",
[
["localhost"],
["openqa.fedoraproject.org"],
["localhost", "openqa.fedoraproject.org"],
["openqa.fedoraproject.org", "localhost"],
["openqa.nokey.org", "localhost", "openqa.fedoraproject.org"],
["http://openqa.fedoraproject.org", "openqa.fedoraproject.org"],
["https://openqa.fedoraproject.org", "localhost"],
],
)
def test_config_hosts(self, config, config_hosts):
"""Test handling config files with various different hosts
specified (sometimes one, sometimes more).
"""
client = oqc.OpenQA_Client()
# we expect default scheme 'http' for localhost, specified
# scheme if there is one, 'https' for all else
if config_hosts[0] == "localhost":
scheme = "http://"
elif config_hosts[0].startswith("http"):
scheme = ""
else:
scheme = "https://"
assert client.baseurl == f"{scheme}{config_hosts[0]}"
assert client.session.headers["Accept"] == "json"
# this should be set for all but the 'nokey' case
if "nokey" in config_hosts[0]:
assert "X-API-Key" not in client.session.headers
else:
assert client.session.headers["X-API-Key"] == "aaaaaaaaaaaaaaaa"
assert client.apisecret == "bbbbbbbbbbbbbbbb"
# check we override the config file priority but use the key
# if server and scheme specified
client = oqc.OpenQA_Client(server="openqa.fedoraproject.org", scheme="http")
assert client.baseurl == "http://openqa.fedoraproject.org"
if "openqa.fedoraproject.org" in config_hosts:
assert client.session.headers["X-API-Key"] == "aaaaaaaaaaaaaaaa"
assert client.apisecret == "bbbbbbbbbbbbbbbb"
else:
assert "X-API-Key" not in client.session.headers
def test_noconfig_host(self, empty_config):
"""Test with empty config file (should use localhost)."""
client = oqc.OpenQA_Client()
assert client.baseurl == "http://localhost"
assert "X-API-Key" not in client.session.headers
@freezegun.freeze_time("2020-02-27")
def test_add_auth_headers(self, simple_config):
"""Test _add_auth_headers."""
client = oqc.OpenQA_Client()
# this weird build value tests tilde substitution in hash
params = {"build": "foo~", "latest": "1"}
# this (incorrect) URL tests space substitution in hash
request = requests.Request(
url=client.baseurl + "/api/v1/jobs ", method="GET", params=params
)
prepared = client.session.prepare_request(request)
authed = client._add_auth_headers(prepared)
assert prepared.headers != authed.headers
assert authed.headers["X-API-Hash"] == "71373f0a57118b120d1915ccc0a24ae2cc112ad3"
assert authed.headers["X-API-Microtime"] == "1582761600.0"
# with no key/secret, request should be returned unmodified
client = oqc.OpenQA_Client("localhost")
request = requests.Request(
url=client.baseurl + "/api/v1/jobs ", method="GET", params=params
)
prepared = client.session.prepare_request(request)
authed = client._add_auth_headers(prepared)
assert prepared.headers == authed.headers
@mock.patch("requests.sessions.Session.send", autospec=True)
def test_do_request_ok(self, fakesend, simple_config):
"""Test do_request (normal, success case)."""
# we have to set up a proper headers dict or mock gets lost in
# infinite recursion and eats all our RAM...
fakeresp = fakesend.return_value
fakeresp.headers = {"content-type": "text/json,encoding=utf-8"}
client = oqc.OpenQA_Client()
params = {"id": "1"}
request = requests.Request(url=client.baseurl + "/api/v1/jobs", method="GET", params=params)
client.do_request(request)
# check request was authed. Note: [0][0] is self
assert "X-API-Key" in fakesend.call_args[0][1].headers
assert "X-API-Hash" in fakesend.call_args[0][1].headers
assert "X-API-Microtime" in fakesend.call_args[0][1].headers
# check URL looks right
assert fakesend.call_args[0][1].url == "https://openqa.fedoraproject.org/api/v1/jobs?id=1"
# check we called .json() on the response
fakeresp = fakesend.return_value
assert len(fakeresp.method_calls) == 1
(callname, callargs, callkwargs) = fakeresp.method_calls[0]
assert callname == "json"
assert not callargs
assert not callkwargs
@mock.patch("requests.sessions.Session.send", autospec=True)
def test_do_request_ok_no_parse(self, fakesend, simple_config):
"""Test do_request (normal, success case, with parse=False)."""
client = oqc.OpenQA_Client()
params = {"id": "1"}
request = requests.Request(url=client.baseurl + "/api/v1/jobs", method="GET", params=params)
client.do_request(request, parse=False)
# check request was authed. Note: [0][0] is self
assert "X-API-Key" in fakesend.call_args[0][1].headers
assert "X-API-Hash" in fakesend.call_args[0][1].headers
assert "X-API-Microtime" in fakesend.call_args[0][1].headers
# check URL looks right
assert fakesend.call_args[0][1].url == "https://openqa.fedoraproject.org/api/v1/jobs?id=1"
# check we did not call .json() (or anything else) on response
fakeresp = fakesend.return_value
assert len(fakeresp.method_calls) == 0
@mock.patch("requests.sessions.Session.send", autospec=True)
def test_do_request_ok_yaml(self, fakesend, simple_config):
"""Test do_request (with YAML response)."""
# set up the response to return YAML and correct
# content-type header
fakeresp = fakesend.return_value
fakeresp.headers = {"content-type": "text/yaml,encoding=utf-8"}
fakeresp.text = "defaults:\n arm:\n machine: ARM"
client = oqc.OpenQA_Client()
request = requests.Request(
url=client.baseurl + "/api/v1/job_templates_scheduling/1", method="GET"
)
ret = client.do_request(request)
# check we did not call .json() on response
assert len(fakeresp.method_calls) == 0
# check we parsed the response
assert ret == {"defaults": {"arm": {"machine": "ARM"}}}
@mock.patch("requests.sessions.Session.send", autospec=True)
def test_do_request_not_changed(self, fakesend, simple_config):
"""Test do_request when receiving a 204 Not Changed reply"""
fakeresp = fakesend.return_value
fakeresp.status_code = 204
fakeresp.text = ""
client = oqc.OpenQA_Client()
request = requests.Request(
url=client.baseurl + "/api/v1/job_templates_scheduling/1", method="PUT"
)
ret = client.do_request(request)
assert len(fakeresp.method_calls) == 0, "no methods must be called on response"
assert ret == fakesend.return_value, "do_request should have returned the response itself"
@mock.patch("time.sleep", autospec=True)
@mock.patch("requests.sessions.Session.send", autospec=True)
def test_do_request_502(self, fakesend, fakesleep, simple_config):
"""Test do_request (response not OK and in the retry list,
default retries).
"""
fakeresp = fakesend.return_value
fakeresp.ok = False
fakeresp.status_code = 502
client = oqc.OpenQA_Client()
params = {"id": "1"}
request = requests.Request(url=client.baseurl + "/api/v1/jobs", method="GET", params=params)
# if response is not OK, we should raise RequestError
with pytest.raises(oqe.RequestError):
client.do_request(request)
# we should also have retried 5 times, with a wait based on 10
assert fakesend.call_count == 6
assert fakesleep.call_count == 5
sleeps = [call[0][0] for call in fakesleep.call_args_list]
assert sleeps == [10, 20, 40, 60, 60]
@mock.patch("time.sleep", autospec=True)
@mock.patch("requests.sessions.Session.send", autospec=True)
def test_do_request_404(self, fakesend, fakesleep, simple_config):
"""Test do_request (response not OK but not in the retry list,
default retries).
"""
fakeresp = fakesend.return_value
fakeresp.ok = False
fakeresp.status_code = 404
client = oqc.OpenQA_Client()
params = {"id": "1"}
request = requests.Request(url=client.baseurl + "/api/v1/jobs", method="GET", params=params)
# if response is not OK, we should raise RequestError
with pytest.raises(oqe.RequestError):
client.do_request(request)
# we should not have retried
assert fakesend.call_count == 1
assert fakesleep.call_count == 0
@mock.patch("time.sleep", autospec=True)
@mock.patch(
"requests.sessions.Session.send",
autospec=True,
side_effect=requests.exceptions.ConnectionError("foo"),
)
def test_do_request_error(self, fakesend, fakesleep, simple_config):
"""Test do_request (send raises exception, custom retries)."""
client = oqc.OpenQA_Client()
params = {"id": "1"}
request = requests.Request(url=client.baseurl + "/api/v1/jobs", method="GET", params=params)
# if send raises ConnectionError, we should raise ours
with pytest.raises(oqe.ConnectionError):
client.do_request(request, retries=2, wait=5)
# we should also have retried 2 times, with a wait based on 5
assert fakesend.call_count == 3
assert fakesleep.call_count == 2
sleeps = [call[0][0] for call in fakesleep.call_args_list]
assert sleeps == [5, 10]
@mock.patch("openqa_client.client.OpenQA_Client.do_request", autospec=True)
def test_openqa_request(self, fakedo, simple_config):
"""Test openqa_request."""
client = oqc.OpenQA_Client()
params = {"id": "1"}
client.openqa_request("get", "jobs", params=params, retries=2, wait=5)
# check we called do_request right. Note: [0][0] is self
assert fakedo.call_args[0][1].url == "https://openqa.fedoraproject.org/api/v1/jobs"
assert fakedo.call_args[0][1].params == {"id": "1"}
assert fakedo.call_args[1]["retries"] == 2
assert fakedo.call_args[1]["wait"] == 5
# check requests with no params work
fakedo.reset_mock()
client.openqa_request("get", "jobs", retries=2, wait=5)
assert fakedo.call_args[0][1].url == "https://openqa.fedoraproject.org/api/v1/jobs"
assert fakedo.call_args[0][1].params == {}
assert fakedo.call_args[1]["retries"] == 2
assert fakedo.call_args[1]["wait"] == 5
@mock.patch("time.sleep", autospec=True)
@mock.patch(
"requests.sessions.Session.send",
autospec=True,
side_effect=requests.exceptions.ConnectionError("foo"),
)
def test_openqa_request_retries(self, fakesend, fakesleep, simple_config):
"""Test the handling of wait & retries when using openqa_request."""
client = oqc.OpenQA_Client(retries=3)
with pytest.raises(oqe.ConnectionError):
client.openqa_request("get", "jobs", wait=42)
assert fakesend.call_count == 4, "expected the class global retries to be used"
assert fakesleep.call_count == 3
sleeps = [call[0][0] for call in fakesleep.call_args_list]
# sleep time is capped at 60s
assert sleeps == [42, 60, 60]
fakesend.reset_mock()
fakesleep.reset_mock()
with pytest.raises(oqe.ConnectionError):
client.openqa_request("get", "jobs", retries=1)
assert (
fakesend.call_count == 2
), "expected the retries value from the method to take precedence"
assert fakesleep.call_count == 1
sleeps = [call[0][0] for call in fakesleep.call_args_list]
assert sleeps == [10], "expected class default for wait to be used"
@mock.patch("openqa_client.client.OpenQA_Client.do_request", autospec=True)
def test_openqa_request_settings_addition(self, fakedo, simple_config):
"""Test openqa_request's handling of the 'settings' parameter."""
client = oqc.OpenQA_Client()
test_suite_params = {
"id": "1",
"name": "some_suite",
"settings": [
{
"key": "PUBLISH_HDD_1",
"value": "%DISTRI%-%VERSION%-%ARCH%-%BUILD%.qcow2",
},
{"key": "START_AFTER_TEST", "value": "fedora_rawhide_qcow2"},
],
}
client.openqa_request("POST", "test_suites", params=test_suite_params)
# check we called do_request right. Note: [0][0] is self
assert fakedo.call_args[0][1].url == "https://openqa.fedoraproject.org/api/v1/test_suites"
assert fakedo.call_args[0][1].params == {
"id": "1",
"name": "some_suite",
"settings[PUBLISH_HDD_1]": "%DISTRI%-%VERSION%-%ARCH%-%BUILD%.qcow2",
"settings[START_AFTER_TEST]": "fedora_rawhide_qcow2",
}
# check requests with a string payload
fakedo.reset_mock()
client.openqa_request("put", "test_suites", data="settings")
assert fakedo.call_args[0][1].params == {}
assert fakedo.call_args[0][1].data == "settings"
@mock.patch("openqa_client.client.OpenQA_Client.do_request", autospec=True)
def test_not_prepend_api_route(self, fakedo, simple_config):
"""Test openqa_request not prepending the /api/v1 string for absolute routes."""
client = oqc.OpenQA_Client()
client.openqa_request("GET", "/absolute_url")
assert fakedo.call_args[0][1].url == "https://openqa.fedoraproject.org/absolute_url"
@mock.patch("openqa_client.client.OpenQA_Client.openqa_request", autospec=True)
def test_find_clones(self, fakerequest, simple_config):
"""Test find_clones."""
client = oqc.OpenQA_Client()
# test data: three jobs with clones, one included in the data,
# two not
jobs = [
{"id": 1, "name": "foo", "result": "failed", "clone_id": 2},
{"id": 2, "name": "foo", "result": "passed", "clone_id": None},
{"id": 3, "name": "bar", "result": "failed", "clone_id": 4},
{"id": 5, "name": "moo", "result": "failed", "clone_id": 6},
]
# set the mock to return the additional jobs when we ask
fakerequest.return_value = {
"jobs": [
{"id": 4, "name": "bar", "result": "passed", "clone_id": None},
{"id": 6, "name": "moo", "result": "passed", "clone_id": None},
]
}
ret = client.find_clones(jobs)
assert ret == [
{"id": 2, "name": "foo", "result": "passed", "clone_id": None},
{"id": 4, "name": "bar", "result": "passed", "clone_id": None},
{"id": 6, "name": "moo", "result": "passed", "clone_id": None},
]
# check we actually requested the additional job correctly
assert fakerequest.call_count == 1
assert fakerequest.call_args[0][1] == "GET"
assert fakerequest.call_args[0][2] == "jobs"
assert fakerequest.call_args[1]["params"] == {"ids": "4,6"}
@mock.patch("openqa_client.client.OpenQA_Client.find_clones", autospec=True)
@mock.patch("openqa_client.client.OpenQA_Client.openqa_request", autospec=True)
def test_get_jobs(self, fakerequest, fakeclones, simple_config):
"""Test get_jobs."""
client = oqc.OpenQA_Client()
with pytest.raises(TypeError):
client.get_jobs()
client.get_jobs(jobs=[1, 2])
assert fakerequest.call_args[0][1] == "GET"
assert fakerequest.call_args[0][2] == "jobs"
assert fakerequest.call_args[1]["params"] == {"ids": "1,2", "latest": "1"}
assert fakeclones.call_count == 1
client.get_jobs(build="foo", filter_dupes=False)
assert fakerequest.call_args[0][1] == "GET"
assert fakerequest.call_args[0][2] == "jobs"
assert fakerequest.call_args[1]["params"] == {"build": "foo"}
assert fakeclones.call_count == 1
def test_client_errors(self):
"""Test creation of exceptions"""
template_error = '{"error":"fail"}'
err = oqe.RequestError("GET", "http://localhost", 404, template_error)
assert err.args[0] == "GET"
assert err.args[1] == "http://localhost"
assert err.args[2] == 404
assert err.method == "GET"
assert err.url == "http://localhost"
assert err.status_code == 404
assert err.text == template_error
err = oqe.ConnectionError("oh no")
assert err.args[0] == "oh no"
assert err.err == "oh no"
@mock.patch("openqa_client.client.OpenQA_Client.openqa_request", autospec=True)
def test_get_latest_build(self, fakerequest):
client = oqc.OpenQA_Client()
fakerequest.return_value = {"build_results": [{"all_passed": 0, "build": "1"}]}
ret = client.get_latest_build(42)
# returns default when no passed builds available
assert ret == ""
ret = client.get_latest_build(42, all_passed=False)
# returns build with all_passed=0 when all_passed is False
assert ret == "1"
fakerequest.return_value = {
"build_results": [
{"all_passed": 1, "build": "2"},
{"all_passed": 1, "build": "3"},
{"all_passed": 1, "build": "4"},
{"all_passed": 0, "build": "5"},
{"all_passed": 1, "build": "qq"},
{"all_passed": 1, "build": "001"},
]
}
ret = client.get_latest_build(42)
# returns latest passed build when all_passed flag set to True
assert ret == "4"
ret = client.get_latest_build(42, all_passed=False)
# returns latest failed build when all_passed flag set to False
assert ret == "5"
ret = client.get_latest_build(42, sort_key=len)
assert ret == "001"
|