def test_negative_create_missing_data(src_type, cleanup, shared_client, field): """Attempt to create a source missing various data. The requests should be met with a 4XX response. :id: 4b176997-0be2-4bd8-81fd-8b4ef5045382 :description: Attempt to create sources missing required data. :steps: Attempt to create a source missing: a) a name b) hosts c) credential id's :expectedresults: Error is thrown and no new source is created. """ cred = Credential(cred_type=src_type, client=shared_client, password=uuid4()) cred.create() cleanup.append(cred) src = Source( source_type=src_type, client=api.Client(response_handler=api.echo_handler), hosts=["localhost"], credential_ids=[cred._id], ) # remove field from payload delattr(src, field) create_response = src.create() assert create_response.status_code == 400 if create_response.status_code in [200, 201]: cleanup.append(src)
def test_negative_create_key_and_pass(cleanup, isolated_filesystem): """Attempt to create a network credential with sshkey and password. The request should be met with a 4XX response. :id: 22a2ca65-5f9d-4c43-89ad-d7ab53223896 :description: Create a network credential with username, sshkey, and password. :steps: Send POST with necessary data to the credential api endpoint. :expectedresults: Error is thrown and no new network credential is created. """ ssh_keyfile = Path(uuid4()) ssh_keyfile.touch() client = api.Client(api.echo_handler) cred = Credential( cred_type="network", client=client, ssh_keyfile=str(ssh_keyfile.resolve()), password=uuid4(), ) response = cred.create() assert response.status_code == 400 assert "either a password or an ssh_keyfile, not both" in response.text assert cred._id is None
def test_delete(src_type, cleanup, shared_client): """After creating several {network, vcenter} sources, delete one. :id: 24d811b1-655d-4278-ab9f-64ca46a7121b :description: Test that we can delete an individual {network, vcenter} source by id :steps: 1) Create collection of {network, vcenter} sources, saving the information. 2) Send a DELETE request to destroy individual source 3) Confirm that all sources are on the server except the deleted one. 4) Repeat until all sources are deleted. :expectedresults: All sources are present on the server except the deleted source. """ created_srcs = [] num_srcs = random.randint(2, 20) echo_client = api.Client(response_handler=api.echo_handler) for i in range(num_srcs): # gen_valid_srcs will take care of cleanup created_srcs.append(gen_valid_source(cleanup, src_type, "localhost")) for i in range(num_srcs): delete_src = created_srcs.pop() delete_src.delete() delete_src.client = echo_client delete_response = delete_src.read() assert delete_response.status_code == 404 for p in created_srcs: assert_matches_server(p)
def test_negative_create_invalid_data(src_type, cleanup, shared_client, data): """Attempt to create a source with invalid data. The requests should be met with a 4XX response. :id: e8754fd4-8d03-4899-bfde-0fc587d78ae1 :description: Attempt to create sources missing required data. :steps: Attempt to create a source with invalid: a) creds b) host c) name :expectedresults: Error is thrown and no new source is created. """ cred = Credential(cred_type=src_type, client=shared_client, password=uuid4()) cred.create() cleanup.append(cred) data["credential_ids"] = cred._id if not data["credential_ids"] else [-1] src = Source( source_type=src_type, client=api.Client(response_handler=api.echo_handler), # unpack parametrized arguments **data ) create_response = src.create() assert create_response.status_code == 400 if create_response.status_code in [200, 201]: cleanup.append(src)
def sort_and_delete(trash): """Sort and delete a list of QPCObject typed items in the correct order.""" creds = [] sources = [] scans = [] # first sort into types because we have to delete scans before sources # and sources before scans for obj in trash: if isinstance(obj, Credential): creds.append(obj) continue if isinstance(obj, Source): sources.append(obj) continue if isinstance(obj, Scan): scans.append(obj) continue client = api.Client(response_handler=api.echo_handler) for collection in [scans, sources, creds]: for obj in collection: # Override client to use a fresh one. It may have been a while # since the object was created and its token may be invalid. obj.client = client # Only assert that we do not hit an internal server error, in case # for some reason the object was allready cleaned up by the test response = obj.delete() assert response.status_code < 500, response.content
def assert_source_update_fails(original_data, source): """Assert that the update method on this source fails. :param original_data: This should be the json you expect to match the server. This can be collected from your object via source.fields() before altering the object with the invalid data. """ # replace whatever client the source had with one that won't raise # exceptions orig_client = source.client source.client = api.Client(response_handler=api.echo_handler) update_response = source.update() assert update_response.status_code == 400 server_data = source.read().json() for key, value in server_data.items(): if key == "options" and original_data.get(key) is None: continue if key == "credentials": # the server creds are dicts with other data besides the id cred_ids = [] for cred in value: cred_ids.append(cred.get("id")) assert sorted(original_data.get(key)) == sorted(cred_ids) else: assert original_data.get(key) == value # give the source its original client back source.client = orig_client
def test_type_mismatch(src_type, cleanup, shared_client): """Attempt to create sources with credentials of the wrong type. For example, if we create a 'network' typed credential, we can not create a 'vcenter' typed source using this credential. :id: 89bc1bb5-127b-48da-b106-82cd1ef7e00a :description: Test that we cannot create a source with the wrong type of credential. :steps: 1) Create a credential of one type 2) Attempt to create a source with that credential of a different type :expectedresults: An error is thrown and no new source is created. :caseautomation: notautomated """ src = gen_valid_source(cleanup, src_type, "localhost", create=False) other_types = set(QPC_SOURCE_TYPES).difference(set((src_type,))) other_cred = Credential( password=uuid4(), cred_type=random.choice(list(other_types)) ) other_cred.create() cleanup.append(other_cred) src.credentials = [other_cred._id] src.client = api.Client(api.echo_handler) create_response = src.create() assert create_response.status_code == 400 if create_response.status_code in [200, 201]: cleanup.append(src) assert "source_type" in create_response.json().keys()
def test_create_override_config(self): """If a base url is specified, we use that instead of config file.""" with mock.patch.object(config, "_CONFIG", self.config): other_host = "http://hostname.com" client = api.Client(url=other_host, authenticate=False) cfg_host = self.config["qpc"]["hostname"] self.assertNotEqual(cfg_host, client.url) self.assertEqual(other_host, client.url)
def test_create_no_config(self): """If a base url is specified we use it.""" with mock.patch.object(config, "_CONFIG", {}): self.assertEqual(config.get_config(), {}) other_host = "http://hostname.com" client = api.Client(url=other_host, authenticate=False) self.assertNotEqual("http://example.com/api/v1/", client.url) self.assertEqual(other_host, client.url)
def shared_client(): """Yeild a single instance of api.Client() to a test. yeilds an api.Client() instance with the standard return code handler. .. warning:: If you intend to change the return code handler, it would be best not to use the shared client to avoid possible problems when running tests in parallel. """ client = api.Client() yield client
def get_report_entity_src_ids(entity): """Given an entity from a deployment report, find the source ids.""" entity_src_ids = [] client = api.Client() for s in entity["sources"]: s_info = client.get("sources/?name={}".format(s["source_name"])) # the source is returned in a list, but there should only be one item # because names should be unique s_info = s_info.json().get("results", {}) assert len(s_info) == 1 s_info = s_info[0] if s_info.get("id"): entity_src_ids.append(s_info.get("id")) return entity_src_ids
def test_equivalent(self): """If a hostname is specified in the config file, we use it.""" with mock.patch.object(config, "_CONFIG", self.config): client = api.Client(authenticate=False) h = Credential( cred_type="network", username=MOCK_CREDENTIAL["username"], name=MOCK_CREDENTIAL["name"], client=client, ) h._id = MOCK_CREDENTIAL["id"] self.assertTrue(h.equivalent(MOCK_CREDENTIAL)) self.assertTrue(h.equivalent(h)) with self.assertRaises(TypeError): h.equivalent([])
def test_negative_update_to_invalid(shared_client, cleanup, isolated_filesystem): """Attempt to update valid credential with invalid data. :id: c34ea917-ee36-4b93-8907-24a5f87bbed3 :description: Create valid network credentials, then attempt to update to be invalid. :steps: 1) Create valid credentials with passwords or sshkey. 2) Update the network credentials: a) using both password and sshkey b) missing both password and sshkey :expectedresults: Error codes are returned and the network credentials are not updated. """ sshkeyfile_name = utils.uuid4() tmp_dir = os.path.basename(os.getcwd()) sshkeyfile = Path(sshkeyfile_name) sshkeyfile.touch() cred = Credential( cred_type="network", client=shared_client, ssh_keyfile=f"/sshkeys/{tmp_dir}/{sshkeyfile_name}", ) cred.create() # add the id to the list to destroy after the test is done cleanup.append(cred) assert_matches_server(cred) cred.client = api.Client(api.echo_handler) # Try to update with both sshkeyfile and password cred.password = uuid4() response = cred.update() assert response.status_code == 400 assert "either a password or an ssh_keyfile, not both" in response.text cred.password = None assert_matches_server(cred) # Try to update with both sshkeyfile and password missing old = cred.ssh_keyfile del cred.ssh_keyfile response = cred.update() assert response.status_code == 400 assert "must have either a password or an ssh_keyfile" in response.text cred.ssh_keyfile = old assert_matches_server(cred)
def test_negative_update_invalid(src_type, shared_client, cleanup, scan_host, invalid_host): """Create a host manager source and then update it with invalid data. :id: d57d8481-54e3-4d9a-b330-80edc9364f37 :description: Create host manager source of single host and credential, then attempt to update it with multiple {hosts, credentials} :steps: 1) Create a valid host manager credential and source 2) Attempt to update with multiple {hosts, credentials} :expectedresults: An error is thrown and no new host is created. """ # initialize & create original credential & source pwd_cred = Credential(cred_type=src_type, client=shared_client, password=uuid4()) pwd_cred.create() src = Source( source_type=src_type, client=shared_client, hosts=scan_host, credential_ids=[pwd_cred._id], ) src.create() # Create extra credential for update cred2 = Credential(cred_type="network", client=shared_client, password=uuid4()) cred2.create() # add the ids to the lists to destroy after the test is done cleanup.extend([pwd_cred, src, cred2]) original_data = copy.deepcopy(src.fields()) src.client = api.Client(api.echo_handler) # Try to update with multiple credentials src.credentials = [pwd_cred._id, cred2._id] assert_source_update_fails(original_data, src) # Try to update with multiple hosts src.hosts = invalid_host assert_source_update_fails(original_data, src) # Try to update with multiple hosts & creds src.hosts = invalid_host src.credentials = [pwd_cred._id, cred2._id]
def test_equivalent_network(self): """If a hostname is specified in the config file, we use it.""" with mock.patch.object(config, "_CONFIG", self.config): client = api.Client(authenticate=False) src = Source( source_type="network", name=MOCK_SOURCE["name"], hosts=MOCK_SOURCE["hosts"], credential_ids=[MOCK_SOURCE["credentials"][0]["id"]], client=client, ) src._id = MOCK_SOURCE["id"] self.assertTrue(src.equivalent(MOCK_SOURCE)) self.assertTrue(src.equivalent(src)) with self.assertRaises(TypeError): src.equivalent([])
def test_response_handler(self): """Test that when we get a 4xx or 5xx response, an error is raised.""" with mock.patch.object(config, "_CONFIG", self.config): self.assertEqual(config.get_config(), self.config) client = api.Client(authenticate=False) mock_request = mock.Mock( body='{"Test Body"}', path_url="/example/path/", headers='{"Test Header"}', text="Some text", ) mock_response = mock.Mock(status_code=404) mock_response.request = mock_request mock_response.json = MagicMock(return_value=json.dumps( '{"The resource you requested was not found"}')) with self.subTest(msg="Test code handler"): client.response_handler = api.code_handler with self.assertRaises(requests.exceptions.HTTPError): client.response_handler(mock_response) with self.subTest(msg="Test json handler"): client.response_handler = api.json_handler with self.assertRaises(requests.exceptions.HTTPError): client.response_handler(mock_response) with self.subTest(msg="Test echo handler"): client.response_handler = api.echo_handler # no error should be raised with the echo handler client.response_handler(mock_response) # not all responses have valid json mock_response.json = MagicMock(return_value="Not valid json") with self.subTest(msg="Test code handler without json available"): client.response_handler = api.code_handler with self.assertRaises(requests.exceptions.HTTPError): client.response_handler(mock_response) with self.subTest(msg="Test json handler without json available"): client.response_handler = api.json_handler with self.assertRaises(requests.exceptions.HTTPError): client.response_handler(mock_response) with self.subTest(msg="Test echo handler without json available"): client.response_handler = api.echo_handler # no error should be raised with the echo handler client.response_handler(mock_response)
def test_negative_update_invalid( shared_client, cleanup, isolated_filesystem, scan_host ): """Create a network source and then update it with invalid data. :id: e0d72f2b-2490-445e-b646-f06ceb4ad23f :description: Create network source of single host and credential, then attempt to update it with multiple invalid {hosts, credentials} :steps: 1) Create a valid network credential and source 2) Attempt to update with multiple invalid {hosts, credentials} :expectedresults: An error is thrown and no new host is created. """ sshkeyfile_name = utils.uuid4() tmp_dir = os.path.basename(os.getcwd()) sshkeyfile = Path(sshkeyfile_name) sshkeyfile.touch() net_cred = Credential( cred_type=NETWORK_TYPE, client=shared_client, ssh_keyfile=f"/sshkeys/{tmp_dir}/{sshkeyfile_name}", ) net_cred.create() sat_cred = Credential(cred_type="satellite", client=shared_client, password=uuid4()) sat_cred.create() src = Source( source_type=NETWORK_TYPE, client=shared_client, hosts=[scan_host], credential_ids=[net_cred._id], ) src.create() # add the ids to the lists to destroy after the test is done cleanup.extend([net_cred, sat_cred, src]) original_data = copy.deepcopy(src.fields()) src.client = api.Client(api.echo_handler) # Try to update with invalid credential type src.credentials = [sat_cred._id] assert_source_update_fails(original_data, src) src.hosts = ["1**2@33^"] assert_source_update_fails(original_data, src)
def test_equivalent_satellite(self): """If a hostname is specified in the config file, we use it.""" with mock.patch.object(config, "_CONFIG", self.config): client = api.Client(authenticate=False) p = Source( source_type="satellite", name=MOCK_SAT6_SOURCE["name"], hosts=MOCK_SAT6_SOURCE["hosts"], credential_ids=[MOCK_SAT6_SOURCE["credentials"][0]["id"]], options=MOCK_SAT6_SOURCE["options"], port=443, client=client, ) p._id = MOCK_SAT6_SOURCE["id"] self.assertTrue(p.equivalent(MOCK_SAT6_SOURCE)) self.assertTrue(p.equivalent(p)) with self.assertRaises(TypeError): p.equivalent([])
def test_negative_disable_optional_products(scan_type, shared_client, cleanup): """Attempt to disable optional products with non-acceptable booleans. :id: 2adb483c-578d-4131-b426-9f772c1803de :description: Create a scan with bad input for optional products :steps: 1) Create a network credential 2) Create network source using the network credential. 3) Create a scan using the network source. When creating the scan disable the optional products with bad values. :expectedresults: The scan is not created. """ num_products = random.randint(1, len(QPC_OPTIONAL_PRODUCTS)) product_combinations = combinations(QPC_OPTIONAL_PRODUCTS, num_products) for combo in product_combinations: source_ids = [] for _ in range(random.randint(1, 10)): src_type = random.choice(QPC_SOURCE_TYPES) src = gen_valid_source(cleanup, src_type, "localhost") source_ids.append(src._id) scan = Scan(source_ids=source_ids, scan_type=scan_type, client=shared_client) products = {p: True for p in QPC_OPTIONAL_PRODUCTS if p not in combo} bad_choice = random.choice(["hamburger", "87", 42, "*"]) products.update({p: bad_choice for p in combo}) scan.options.update({OPTIONAL_PROD: products}) echo_client = api.Client(response_handler=api.echo_handler) scan.client = echo_client create_response = scan.create() if create_response.status_code == 201: cleanup.append(scan) raise AssertionError( "Optional products should be disabled and\n" "enabled with booleans. If invalid input is given, the user\n" "should be alerted." "The following data was provided: {}".format(products) ) assert create_response.status_code == 400 message = create_response.json().get("options", {}) assert message.get(OPTIONAL_PROD) is not None
def test_negative_invalid_port(src_type, bad_port, cleanup, shared_client): """Test that we are prevented from using a nonsense value for the port. :id: e64df701-5819-4e80-a5d2-d26cbc6f71a7 :description: Test that sources cannot be created with bad values for the port. :steps: 1) Create a credential 2) Attempt to create a source of the same type and specify a custom port with various nonsense values like 'foo**' or -1 or a Boolean :expectedresults: The source is not created :caseautomation: notautomated """ src = gen_valid_source(cleanup, src_type, "localhost", create=False) src.port = bad_port src.client = api.Client(api.echo_handler) create_response = src.create() assert create_response.status_code == 400 if create_response.status_code in [200, 201]: cleanup.append(src) # assert that the server tells us what we did wrong assert "port" in create_response.json().keys()
def assert_merge_fails(ids, errors_found, report): """Assert that the merge method on the given report fails. :param ids: The scan job identifiers to pass through to the merge function. :param report: The report object :param errors_found: A list of any errors encountered """ # replace whatever client the report had with one that won't raise # exceptions orig_client = report.client report.client = api.Client(response_handler=api.echo_handler) merge_response = report.create_from_merge(ids) if merge_response.status_code != 400: errors_found.append( "Merging scan job identifiers {ids} resulted in a response" "status code of {response_status} when it should have resulted" "in a status code of 400.".format( ids=ids, response_status=merge_response.status_code)) # give the report its original client back report.client = orig_client return errors_found
def assert_source_create_fails(source, source_type=""): """Assert that the create method of this source fails. :param source: The source object. """ # replace whatever client the source had with one that won't raise # exceptions orig_client = source.client source.client = api.Client(response_handler=api.echo_handler) create_response = source.create() assert create_response.status_code == 400 expected_errors = [ { "hosts": ["Source of type vcenter must have a single hosts."] }, { "credentials": ["Source of type vcenter must have a single credential."] }, { "hosts": ["Source of type satellite must have a single hosts."] }, { "credentials": ["Source of type satellite must have a single credential."] }, { "exclude_hosts": [ "The exclude_hosts option is not valid for source of type " + source_type + "." ] }, ] response = create_response.json() assert response in expected_errors # give the source its original client back source.client = orig_client
def test_delete_with_dependencies(src_type, cleanup, shared_client): """Test that cannot delete sources if other objects depend on them. :id: 76d79090-a3f7-4750-a8b8-eaf6c2ed4b89 :description: Test that sources cannot be deleted if they are members of a scan. :steps: 1) Create a valid source and one or more scans that use it. 2) Attempt to delete the source, this should fail 3) Delete the scan(s) 4) Assert that we can now delete the source :expectedresults: The source is not created :caseautomation: notautomated """ src1 = gen_valid_source(cleanup, src_type, "localhost") src2 = gen_valid_source(cleanup, src_type, "localhost") scns = [] for i in range(random.randint(2, 6)): if i % 2 == 0: scn = Scan(source_ids=[src1._id]) else: scn = Scan(source_ids=[src1._id, src2._id]) scns.append(scn) scn.create() cleanup.append(scn) src1.client = api.Client(api.echo_handler) # this should fail del_response = src1.delete() assert del_response.status_code == 400 # now delete scan, and then we should be allowed to delete source for scn in scns: scn.delete() cleanup.remove(scn) src1.client = shared_client src1.delete() cleanup.remove(src1)
def __init__(self, client=None, _id=None): """Provide shared methods for QPC model objects.""" # we want to allow for an empty string name self._id = _id self.client = client if client else api.Client() self.endpoint = ""
def test_products_found_deployment_report(scan_info): """Test that products reported as present are correct for the source. :id: d5d424bb-8183-4b60-b21a-1b4ed1d879c0 :description: Test that products indicated as present are correctly identified. :steps: 1) Request the json report for the scan. 2) Assert that any products marked as present are expected to be found as is listed in the configuration file for the source. :expectedresults: There are inspection results for each source we scanned and any products found are correctly identified. """ result = get_scan_result(scan_info["name"]) report_id = result["report_id"] if not report_id: pytest.xfail(reason="No report id was returned from scan " "named {scan_name}".format(scan_name=scan_info["name"])) report = api.Client().get( "reports/{}/deployments".format(report_id)).json() assert report.get("status") == "completed", report report = report.get("system_fingerprints") errors_found = [] for entity in report: all_found_products = [] present_products = [] for product in entity.get("products"): name = "".join(product["name"].lower().split()) if product["presence"] == "present": present_products.append(name) if product["presence"] in ["present", "potential"]: all_found_products.append(name) for source_to_product_map in result["expected_products"]: src_id = list(source_to_product_map.keys())[0] entity_src_ids = get_report_entity_src_ids(entity) hostname = result["source_id_to_hostname"][src_id] ex_products = source_to_product_map[src_id] expected_product_names = [ prod for prod in ex_products.keys() if prod != "distribution" ] if src_id in entity_src_ids: # We assert that products marked as present are expected # We do not assert that products marked as potential must # actually be on server unexpected_products = [] for prod_name in present_products: # Assert that products marked "present" # Are actually expected on machine if prod_name not in expected_product_names: unexpected_products.append(prod_name) # after inpsecting all found products, # raise assertion error for all unexpected products if len(unexpected_products) > 0: errors_found.append( "Found {found_products} but only expected to find\n" "{expected_products} on {host_found_on}.\n" "All information about the entity was as follows\n" "{entity_info}".format( found_products=unexpected_products, expected_products=expected_product_names, host_found_on=hostname, entity_info=pformat(entity), )) assert len(errors_found) == 0, ( "Found {num} unexpected products!\n" "Errors are listed below: \n {errors}.\n" "Full results for this scan were: {scan_results}".format( num=len(errors_found), errors="\n\n======================================\n\n".join( errors_found), scan_results=pformat(result), ))
def test_OS_found_deployment_report(scan_info): """Test that OS identified are correct for the source. :id: 0b16331c-2431-498a-9e84-65b3d66e4001 :description: Test that OS type and version are correctly identified. :steps: 1) Request the json report for the scan. 2) Assert that the OS identified is expected to be found as is listed in the configuration file for the source. :expectedresults: There are inspection results for each source we scanned and the operating system is correctly identified. """ result = get_scan_result(scan_info["name"]) if scan_info["name"].lower() == 'sat6': pytest.skip("Skipping sat6 run until Quipucords Issue #2039 " "is resolved") report_id = result["report_id"] if not report_id: pytest.xfail(reason="No report id was returned from scan " "named {scan_name}".format(scan_name=scan_info["name"])) report = api.Client().get( "reports/{}/deployments".format(report_id)).json() assert report.get("status") == "completed", report report = report.get("system_fingerprints") errors_found = [] for entity in report: for source_to_product_map in result["expected_products"]: src_id = list(source_to_product_map.keys())[0] entity_src_ids = get_report_entity_src_ids(entity) hostname = result["source_id_to_hostname"][src_id] ex_products = source_to_product_map[src_id] expected_distro = ex_products["distribution"].get("name", "").lower() expected_version = ex_products["distribution"].get("version", "").lower() # The key may exist but the value be None if entity.get("os_name") is None: found_distro = "" else: found_distro = entity.get("os_name").lower() if entity.get("os_version") is None: found_version = "" else: found_version = entity.get("os_version").lower() if src_id in entity_src_ids: # We assert that the expected distro's name is at least # contained in the found name. # For example, if "Red Hat" is listed in config file, # It will pass if "Red Hat Enterprise Linux Server" is found if expected_distro not in found_distro: errors_found.append( "Expected OS named {0} for source {1} but" "found OS named {2}".format(expected_distro, hostname, found_distro)) # We assert that the expected distro's version is at least # contained in the found version. # For example, if "6.9" is listed in config file, # It will pass if "6.9 (Santiago)" is found if expected_version not in found_version: errors_found.append( "Expected OS version {0} for source {1} but" "found OS version {2}".format(expected_version, hostname, found_version)) assert len(errors_found) == 0, ( "Found {num} unexpected OS names and/or versions!\n" "Errors are listed below: \n {errors}.\n" "Full results for this scan were: {scan_results}".format( num=len(errors_found), errors="\n\n======================================\n\n".join( errors_found), scan_results=pformat(result), ))
def test_create_with_config(self): """If a hostname is specified in the config file, we use it.""" with mock.patch.object(config, "_CONFIG", self.config): self.assertEqual(config.get_config(), self.config) client = api.Client(authenticate=False) self.assertEqual(client.url, "http://example.com/api/v1/")
def test_invalid_hostname(self): """Raise an error if no config entry is found and no url specified.""" with mock.patch.object(config, "_CONFIG", self.invalid_config): self.assertEqual(config.get_config(), self.invalid_config) with self.assertRaises(exceptions.QPCBaseUrlNotFound): api.Client(authenticate=False)