def migrate(self): if isK8S(): return cmd = ["usr/bin/tenantadm", "migrate"] self.container_manager.execute(self.cid, cmd)
def cleanup(self): if isK8S(): return dbs = self.client.list_database_names() dbs = [d for d in dbs if d not in ["local", "admin", "config", "workflows"]] for d in dbs: self.client.drop_database(d)
def propagate_inventory_statuses(self, tenant_id=None): if isK8S(): return cmd = ["usr/bin/deviceauth", "propagate-inventory-statuses"] if tenant_id is not None: cmd.extend(["--tenant_id", tenant_id]) self.container_manager.execute(self.cid, cmd)
def migrate(self, tenant_id=None): if isK8S(): return cmd = [self.path, "migrate"] if tenant_id is not None: cmd.extend(["--tenant", tenant_id]) self.container_manager.execute(self.cid, cmd)
def __init__(self, microservice, containers_namespace, container_manager): if isK8S(): self.container_manager = KubernetesNamespace() base_filter = microservice elif container_manager is None: self.container_manager = DockerNamespace(containers_namespace) base_filter = microservice + "_1" else: self.container_manager = container_manager base_filter = microservice + "_1" self.cid = self.container_manager.getid([base_filter])
def call( self, method, url, body=None, data=None, path_params={}, qs_params={}, headers={}, auth=None, files=None, ): url = self.__make_url(url) url = self.__subst_path_params(url, path_params) try: p = None if isK8S() and url.startswith("http://mender-"): host_forward_port = get_free_tcp_port() host = self.host.split(":", 1)[0] port = self.host.split(":", 1)[1] if ":" in self.host else "80" cmd = [ "kubectl", "port-forward", "service/" + host, "%d:%s" % (host_forward_port, port), ] p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) url = ("http://localhost:%d/" % host_forward_port) + url.split( "/", 3)[-1] wait_for_port(port=host_forward_port, host="localhost", timeout=10.0) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=InsecureRequestWarning) return requests.request( method, url, json=body, data=data, params=qs_params, headers=self.__make_headers(headers), auth=auth, verify=False, files=files, ) finally: if p is not None: p.terminate()
def update_tenant(tid, addons=None, plan=None, container_manager=None): """Call internal PUT tenantadm/tenants/{tid}""" update = {} if addons is not None: update["addons"] = tenantadm.make_addons(addons) if plan is not None: update["plan"] = plan tenantadm_host = ( tenantadm.HOST if isK8S() or container_manager is None else container_manager.get_ip_of_service("mender-tenantadm")[0] + ":8080" ) tadm = ApiClient(tenantadm.URL_INTERNAL, host=tenantadm_host, schema="http://") res = tadm.call( "PUT", tenantadm.URL_INTERNAL_TENANT, body=update, path_params={"tid": tid}, ) assert res.status_code == 202
def call( self, method, url, body=None, data=None, path_params={}, qs_params={}, headers={}, auth=None, files=None, ): url = self.__make_url(url) url = self.__subst_path_params(url, path_params) try: p = None if isK8S() and url.startswith("http://mender-"): host = self.host.split(":", 1)[0] port = self.host.split(":", 1)[1] if ":" in self.host else "80" cmd = [ "kubectl", "port-forward", "service/" + host, "8080:%s" % port ] p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) url = "http://localhost:8080/" + url.split("/", 3)[-1] # wait a few seconds to let the port-forwarding fully initialize time.sleep(3) return requests.request( method, url, json=body, data=data, params=qs_params, headers=self.__make_headers(headers), auth=auth, verify=False, files=files, ) finally: if p is not None: p.terminate()
def wait_for_traefik(gateway_host, routers=[]): """ Wait until provided routers are installed. Prevents race conditions where services are already up but traefik hasn't yet registered their routers. This causes subtle timing issues. By default checks the basic routers (incl. deployments - startup so time consuming, in practice it guarantees success). """ if isK8S(): return if routers == []: rnames = [ "deployments@docker", "deploymentsMgmt@docker", "minio@docker", "deviceauth@docker", "deviceauthMgmt@docker", "inventoryMgmt@docker", "inventoryMgmtV1@docker", "useradm@docker", "useradmLogin@docker", "deviceauth@docker", "deviceauthMgmt@docker", "inventoryV1@docker", ] else: rnames = routers[:] for _ in redo.retrier(attempts=5, sleeptime=10): try: r = requests.get( "http://{}:8080/api/http/routers".format(gateway_host)) assert r.status_code == 200 cur_routers = [x["name"] for x in r.json()] if set(cur_routers).issuperset(set(rnames)): break except requests.exceptions.ConnectionError as ex: print("connection error while waiting for routers - but that's ok") else: assert False, "timeout hit waiting for traefik routers {}".format( rnames)
def _get_endpoint_url(url): global forward_port if isK8S() and url.startswith("http://mender-"): url_parsed = urlparse(url) host = url_parsed.hostname port = url_parsed.port _, host_forward_port = processes.get(host, (None, None)) if host_forward_port is None: forward_port += 1 host_forward_port = forward_port cmd = [ "kubectl", "port-forward", "service/" + host, "%d:%d" % (host_forward_port, port), ] p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) processes[host] = (p, host_forward_port) # wait a few seconds to let the port-forwarding fully initialize time.sleep(3) url = ("http://localhost:%d" % host_forward_port) + url_parsed.path return url
def _get_endpoint_url(url): global forward_port if isK8S() and url.startswith("http://mender-"): url_parsed = urlparse(url) host = url_parsed.hostname port = url_parsed.port _, host_forward_port = processes.get(host, (None, None)) if host_forward_port is None: host_forward_port = get_free_tcp_port() cmd = [ "kubectl", "port-forward", "service/" + host, "%d:%d" % (host_forward_port, port), ] p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) processes[host] = (p, host_forward_port) wait_for_port(port=host_forward_port, host="localhost", timeout=10.0) url = ("http://localhost:%d" % host_forward_port) + url_parsed.path return url
class _TestRemoteTerminalBase: def test_regular_protocol_commands(self, docker_env): self.assert_env(docker_env) with docker_env.devconnect.get_websocket() as ws: # Start shell. shell = proto_shell.ProtoShell(ws) body = shell.startShell() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL assert body == proto_shell.MSG_BODY_SHELL_STARTED # Drain any initial output from the prompt. It should end in either "# " # (root) or "$ " (user). output = shell.recvOutput() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL assert output[-2:].decode() in [ "# ", "$ ", ], "Could not detect shell prompt." # Starting the shell again should be a no-op. It should return that # it is already started, as long as the shell limit is 1. MEN-4240. body = shell.startShell() assert shell.protomsg.props["status"] == protomsg.PROP_STATUS_ERROR assert body == b"failed to start shell: shell is already running" # Make sure we do not get any new output, it should be the same shell as before. output = shell.recvOutput() assert ( output == b"" ), "Unexpected output received when relauncing already launched shell." # Test if a simple command works. shell.sendInput("ls /\n".encode()) output = shell.recvOutput() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL output = output.decode() assert "usr" in output assert "etc" in output # Try to stop shell. body = shell.stopShell() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL assert body is None # Repeat stopping and verify the error body = shell.stopShell() assert shell.protomsg.props["status"] == protomsg.PROP_STATUS_ERROR assert b"session not found" in body, body # Make sure we can not send anything to the shell. shell.sendInput("ls /\n".encode()) output = shell.recvOutput() assert shell.protomsg.props["status"] == protomsg.PROP_STATUS_ERROR output = output.decode() assert "usr" not in output assert "etc" not in output assert "session not found" in output, output # Start it again. shell.startShell() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL # Drain any initial output from the prompt. It should end in either "# " # (root) or "$ " (user). output = shell.recvOutput() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL assert output[-2:].decode() in [ "# ", "$ ", ], "Could not detect shell prompt." def test_dbus_reconnect(self, docker_env): self.assert_env(docker_env) with docker_env.devconnect.get_websocket(): # Nothing to do, just connecting successfully is enough. pass # Test that mender-connect recovers if it initially has no DBus # connection. This is important because we don't have DBus activation # enabled in the systemd service file, so it's a race condition who gets # to the DBus service first. client_service_name = docker_env.device.get_client_service_name() docker_env.device.run( f"systemctl --job-mode=ignore-dependencies stop {client_service_name}" ) docker_env.device.run( "systemctl --job-mode=ignore-dependencies restart mender-connect") time.sleep(10) # At this point, mender-connect will already have queried DBus. docker_env.device.run( f"systemctl --job-mode=ignore-dependencies start {client_service_name}" ) with docker_env.devconnect.get_websocket(): # Nothing to do, just connecting successfully is enough. pass @pytest.mark.skipif( isK8S(), reason="not testable in a staging or production environment") def test_websocket_reconnect(self, docker_env): self.assert_env(docker_env) with docker_env.devconnect.get_websocket(): # Nothing to do, just connecting successfully is enough. pass # Test that mender-connect recovers if it loses the connection to deviceconnect. docker_env.restart_service("mender-deviceconnect") time.sleep(10) with docker_env.devconnect.get_websocket(): # Nothing to do, just connecting successfully is enough. pass def test_bogus_shell_message(self, docker_env): self.assert_env(docker_env) with docker_env.devconnect.get_websocket() as ws: prot = protomsg.ProtoMsg(proto_shell.PROTO_TYPE_SHELL) prot.clear() prot.setTyp("bogusmessage") msg = prot.encode(b"") ws.send(msg) msg = ws.recv() prot.decode(msg) assert prot.props["status"] == protomsg.PROP_STATUS_ERROR assert prot.protoType == proto_shell.PROTO_TYPE_SHELL assert prot.typ == "bogusmessage" def test_session_recording(self, docker_env): self.assert_env(docker_env) def get_cmd(ws, timeout=1): pmsg = protomsg.ProtoMsg(proto_shell.PROTO_TYPE_SHELL) body = b"" try: while True: msg = ws.recv(timeout) b = pmsg.decode(msg) if pmsg.typ == proto_shell.MSG_TYPE_SHELL_COMMAND: body += b except TimeoutError: return body session_id = "" session_bytes = b"" with docker_env.devconnect.get_websocket() as ws: # Start shell. shell = proto_shell.ProtoShell(ws) body = shell.startShell() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL assert body == proto_shell.MSG_BODY_SHELL_STARTED assert shell.sid is not None session_id = shell.sid """ Record a series of commands """ shell.sendInput("echo 'now you see me'\n".encode()) session_bytes += get_cmd(ws) # Disable echo time.sleep(1) shell.sendInput("stty -echo\n".encode()) time.sleep(1) session_bytes += get_cmd(ws) shell.sendInput('echo "now you don\'t" > /dev/null\n'.encode()) session_bytes += get_cmd(ws) shell.sendInput("# Invisible comment\n".encode()) session_bytes += get_cmd(ws) # Turn echo back on time.sleep(1) shell.sendInput("stty echo\n".encode()) time.sleep(1) session_bytes += get_cmd(ws) shell.sendInput("echo 'and now echo is back on'\n".encode()) session_bytes += get_cmd(ws) body = shell.stopShell() assert shell.protomsg.props[ "status"] == protomsg.PROP_STATUS_NORMAL assert body is None # Sleep for a second to make sure the session log propagate to the DB. time.sleep(1) playback_bytes = b"" with docker_env.devconnect.get_playback_websocket(session_id, sleep_ms=0) as ws: playback_bytes = get_cmd(ws) assert playback_bytes == session_bytes assert b"now you see me" in playback_bytes assert b"echo 'now you see me'" in playback_bytes # Check that the commands after echo was disabled is not present in the log assert b"# Invisible comment" not in playback_bytes assert b'echo "now you don\'t" > /dev/null' not in playback_bytes # ... and after echo is enabled assert b"echo 'and now echo is back on'" in playback_bytes def assert_env(self, docker_env): """Check extra env vars used by base test funcs - make sure they're set. Mostly important for custom setups. """ assert (docker_env.device is not None), "docker_env must have a designated 'device'" assert ( docker_env.devconnect is not None), "docker_env must have a set up 'devconnect' instance"
def test_saved_filters_dynamic(self, clean_mongo, test_case): """ Check that the saved filters return the correct set of devices when the inventory changes dynamically. That is, when devices modify their inventory or get removed entirely. """ devauthm = ApiClient(deviceauth.URL_MGMT) usradmm = ApiClient(useradm.URL_MGMT) invm_v2 = ApiClient(inventory_v2.URL_MGMT) invd = ApiClient(inventory.URL_DEV) # Initial device inventory setup inventories = [ {"deb": "squeeze", "py3": 3.3, "py2": 2.7, "idx": 0}, {"deb": "squeeze", "py3": 3.5, "py2": 2.7, "idx": 1}, {"deb": "wheezy", "py2": 2.7, "idx": 2}, {"deb": "wheezy", "py2": 2.7, "idx": 3}, {"deb": "wheezy", "py3": 3.5, "py2": 2.7, "idx": 4}, {"deb": "wheezy", "py3": 3.6, "py2": 2.7, "idx": 5}, {"deb": "wheezy", "py3": 3.6, "idx": 6}, {"deb": "jessie", "py3": 3.7, "idx": 7}, {"deb": "jessie", "py3": 3.7, "idx": 8}, {"deb": "jessie", "py3": 3.8, "idx": 9}, {"deb": "jessie", "py3": 3.8, "idx": 10}, {"deb": "jessie", "py3": 3.8, "idx": 11}, {"deb": "buster", "py3": 3.8, "idx": 12}, {"deb": "buster", "py3": 3.8, "idx": 13}, {"deb": "buster", "py3": 3.8, "idx": 14}, {"deb": "buster", "py3": 3.8, "idx": 15}, ] self.logger.info("Running test case: %s" % test_case["name"]) # Setup tenant and (initial) device set. uuidv4 = str(uuid.uuid4()) tenant, username, password = ( "test.mender.io-" + uuidv4, "some.user+" + uuidv4 + "@example.com", "secretsecret", ) tenant = create_org(tenant, username, password, "enterprise") rsp = usradmm.call( "POST", useradm.URL_LOGIN, auth=(tenant.users[0].name, tenant.users[0].pwd), ) assert rsp.status_code == 200 tenant.api_token = rsp.text # Create accepted devices with inventory. add_devices_to_tenant(tenant, inventories) # Save test filter. rsp = invm_v2.with_auth(tenant.api_token).call( "POST", inventory_v2.URL_SAVED_FILTERS, body=test_case["filter_req"], ) assert rsp.status_code == 201, ( "Failed to save filter, received status code: %d" % rsp.status_code ) filter_id = rsp.headers.get("Location").split("/")[-1] # Check that we get the exected devices from the set. rsp = invm_v2.with_auth(tenant.api_token).call( "GET", inventory_v2.URL_SAVED_FILTER_SEARCH.format(id=filter_id) ) assert rsp.status_code == 200 devs_recv = sorted([dev["id"] for dev in rsp.json()]) devs_exct = sorted( [dev.id for dev in filter(test_case["filter"], tenant.devices)] ) assert devs_recv == devs_exct, ( "Unexpected device set returned by saved filters, " + "expected: %s, received: %s" % (devs_recv, devs_exct) ) # Perform perturbations to the device set. # Temporary dict representation mapping device.id -> device devices = {} for dev in tenant.devices: devices[dev.id] = dev # Starting with modifications for fltr, change in test_case["mods"]: for dev in filter(fltr, devices.values()): for k, v in change.items(): dev.inventory[k] = v rsp = invd.with_auth(dev.token).call( "PATCH", inventory.URL_DEVICE_ATTRIBUTES, body=dict_to_inventoryattrs(dev.inventory), ) assert rsp.status_code == 200 devices[dev.id] = dev # when running against staging, wait 5 seconds to avoid hitting # the rate limits for the devices (one inventory update / 5 seconds) isK8S() and time.sleep(5.0) # Remove devices for fltr in test_case["remove"]: for dev in filter(fltr, list(devices.values())): devauthm.with_auth(tenant.api_token).call( "DELETE", deviceauth.URL_DEVICE.format(id=dev.id) ) devices.pop(dev.id) tenant.devices = list(devices.values()) # Add new devices add_devices_to_tenant(tenant, test_case["new"]) # Check that we get the exected devices from the perturbed set. rsp = invm_v2.with_auth(tenant.api_token).call( "GET", inventory_v2.URL_SAVED_FILTER_SEARCH.format(id=filter_id), qs_params={"per_page": len(tenant.devices) + 1}, ) assert rsp.status_code == 200 devs_recv = sorted([dev["id"] for dev in rsp.json()]) devs_exct = sorted( [dev.id for dev in filter(test_case["filter"], tenant.devices)] ) assert devs_recv == devs_exct, ( "Unexpected device set returned by saved filters, " + "expected: %s, received: %s" % (devs_recv, devs_exct) )
class TestPasswordResetEnterprise: uc = ApiClient(useradm.URL_MGMT) @pytest.mark.skipif( isK8S(), reason="not testable in a staging or production environment" ) def test_password_reset(self, clean_mongo, smtp_mock): uuidv4 = str(uuid.uuid4()) tenant, email, password = ( "test.mender.io-" + uuidv4, "some.user+" + uuidv4 + "@example.com", "secretsecret", ) create_org(tenant, email, password) new_password = "******" r = self.uc.post(useradm.URL_PASSWORD_RESET_START, body={"email": email}) assert r.status_code == 202 # wait for the password reset email message = None for i in range(15): messages = smtp_mock.filtered_messages(email) if len(messages) > 0: message = messages[0] break time.sleep(1) # be sure we received the email assert message is not None assert message.data != "" # extract the secret hash from the link match = re.search( r"https://hosted.mender.io/ui/#/password/([a-z0-9\-]+)", message.data.decode("utf-8"), ) secret_hash = match.group(1) assert secret_hash != "" # reset the password r = self.uc.post( useradm.URL_PASSWORD_RESET_COMPLETE, body={"secret_hash": secret_hash, "password": new_password}, ) assert r.status_code == 202 # try to login using the new password r = self.uc.call("POST", useradm.URL_LOGIN, auth=(email, new_password)) assert r.status_code == 200 assert bool(r.text) def test_password_reset_non_existent_email(self, clean_mongo): r = self.uc.post( useradm.URL_PASSWORD_RESET_START, body={"email": "*****@*****.**"}, ) assert r.status_code == 202 def test_password_reset_empty_email(self, clean_mongo): r = self.uc.post(useradm.URL_PASSWORD_RESET_START, body={"email": ""}) assert r.status_code == 400 def test_password_reset_invalid_body(self, clean_mongo): r = self.uc.post( useradm.URL_PASSWORD_RESET_START, body={"email": ["*****@*****.**"]} ) assert r.status_code == 400 def test_password_reset_complete_invalid_secret(self, clean_mongo): r = self.uc.post( useradm.URL_PASSWORD_RESET_COMPLETE, body={"secret_hash": "dummy", "password": "******"}, ) assert r.status_code == 400 def test_password_reset_complete_invalid_body(self, clean_mongo): r = self.uc.post( useradm.URL_PASSWORD_RESET_COMPLETE, body={"secret_hash": ["dummy"], "password": "******"}, ) assert r.status_code == 400
def migrate(self): if isK8S(): return self.container_manager.execute(self.cid, [self.path, "migrate"])
"test.mender.io-" + uuidv4, "some.user+" + uuidv4 + "@foo.com", "secretsecret", ) # Create tenant with two users tenant = create_org(tenant, username, password, "enterprise") tenant.users.append( create_user("some.other.user+" + uuidv4 + "@foo.com", password, tenant.id)) tenants.append(tenant) yield tenants @pytest.mark.skipif( isK8S(), reason="not testable in a staging or production environment") class Test2FAEnterprise: def _login(self, user, totp=None): body = {} if totp is not None: body = {"token2fa": totp} r = uadm.call("POST", useradm.URL_LOGIN, auth=(user.name, user.pwd), body=body) return r def _verify(self, utoken, totp): body = {"token2fa": totp}
class TestVerifyEmailEnterprise: uc = ApiClient(useradm.URL_MGMT) @pytest.mark.skipif( isK8S(), reason="not testable in a staging or production environment" ) def test_verify_email(self, clean_mongo, smtp_mock): uuidv4 = str(uuid.uuid4()) tenant, email, password = ( "test.mender.io-" + uuidv4, "some.user+" + uuidv4 + "@example.com", "secretsecret", ) create_org(tenant, email, password) # login and try to enable two factor authentication # it shouldn't be possible since the user email address # has not been verified r = self.uc.call("POST", useradm.URL_LOGIN, auth=(email, password)) assert r.status_code == 200 utoken = r.text r = self.uc.with_auth(utoken).call( "POST", useradm.URL_SETTINGS, body={"2fa": "enabled"} ) assert r.status_code == 403 # verify user email address r = self.uc.post(useradm.URL_VERIFY_EMAIL_START, body={"email": email}) assert r.status_code == 202 # wait for the verification email message = None for i in range(15): messages = smtp_mock.filtered_messages(email) if len(messages) > 0: message = messages[0] break time.sleep(1) # be sure we received the email assert message is not None assert message.data != "" # extract the secret hash from the link match = re.search( r"https://hosted.mender.io/ui/#/activate/([a-z0-9\-]+)", message.data.decode("utf-8"), ) secret_hash = match.group(1) assert secret_hash != "" # complete the email address r = self.uc.post( useradm.URL_VERIFY_EMAIL_COMPLETE, body={"secret_hash": secret_hash}, ) assert r.status_code == 204 # try to enable two factor authentication after email address verification # now it should be possible r = self.uc.with_auth(utoken).call( "POST", useradm.URL_SETTINGS, body={"2fa": "enabled"} ) assert r.status_code == 201 def test_verify_email_non_existent_email(self, clean_mongo): r = self.uc.post( useradm.URL_VERIFY_EMAIL_START, body={"email": "*****@*****.**"}, ) assert r.status_code == 202 def test_verify_email_empty_email(self, clean_mongo): r = self.uc.post(useradm.URL_VERIFY_EMAIL_START, body={"email": ""}) assert r.status_code == 400 def test_verify_email_invalid_body(self, clean_mongo): r = self.uc.post( useradm.URL_VERIFY_EMAIL_START, body={"email": ["*****@*****.**"]} ) assert r.status_code == 400 def test_verify_email_complete_invalid_secret(self, clean_mongo): r = self.uc.post( useradm.URL_VERIFY_EMAIL_COMPLETE, body={"secret_hash": "dummy"}, ) assert r.status_code == 400 def test_verify_email_complete_invalid_body(self, clean_mongo): r = self.uc.post( useradm.URL_VERIFY_EMAIL_COMPLETE, body={"secret_hash": ["dummy"]}, ) assert r.status_code == 400
{"artifact": "v1", "idx": 0}, {"artifact": "v1", "idx": 1}, {"artifact": "v1", "idx": 2}, ], ) # sleep a few seconds waiting for the data propagation to the reporting service # and the Elasticsearch indexing to complete time.sleep(reporting.REPORTING_DATA_PROPAGATION_SLEEP_TIME_SECS) return tenant_os @pytest.mark.skipif( useExistingTenant(), reason="not feasible to test with existing tenant", ) @pytest.mark.skipif( isK8S(), reason="reporting service not deployed to staging or production environment", ) class TestReportingSearchEnterprise: @property def logger(self): try: return self._logger except AttributeError: self._logger = logging.getLogger(self.__class__.__name__) return self._logger @pytest.mark.parametrize( "test_case", [ # test_case0
class TestContactSupportEnterprise: def test_contact_support_bad_request(self, clean_mongo): uuidv4 = str(uuid.uuid4()) tenant, email, password = ( "test.mender.io-" + uuidv4, "some.user-" + uuidv4 + "@example.com", "secretsecret", ) create_org(tenant, email, password) r = api_useradm.call("POST", useradm.URL_LOGIN, auth=(email, password)) assert r.status_code == 200 utoken = r.text r = api_tadm_v2.with_auth(utoken).call( "POST", tenantadm_v2.URL_CONTACT_SUPPORT, body={"foo": "bar"}) assert r.status_code == 400 @pytest.mark.skipif( isK8S(), reason="not testable in a staging or production environment") def test_contact_support(self, clean_mongo, smtp_mock): uuidv4 = str(uuid.uuid4()) tenant, email, password = ( "test.mender.io-" + uuidv4, "some.user-" + uuidv4 + "@example.com", "secretsecret", ) org = create_org(tenant, email, password) r = api_useradm.call("POST", useradm.URL_LOGIN, auth=(email, password)) assert r.status_code == 200 utoken = r.text r = api_tadm_v2.with_auth(utoken).call( "POST", tenantadm_v2.URL_CONTACT_SUPPORT, body={ "subject": "foo", "body": "bar" }, ) assert r.status_code == 202 # wait for the email message = None for i in range(15): messages = smtp_mock.filtered_messages("*****@*****.**") if len(messages) > 0: message = messages[0] break time.sleep(1) # be sure we received the email assert message is not None assert message.data != "" # and the email is properly formatted data = message.data.decode("utf-8") match = re.search( r"Subject: ([a-z0-9\-]+)", data, ) subject = match.group(1) assert re.search(r"Subject: foo", data) is not None assert re.search(r"From: [email protected]", data) is not None assert re.search(r"To: [email protected]", data) is not None assert re.search(r"Organization ID: " + org.id, data) is not None assert re.search(r"Organization name: " + tenant, data) is not None assert re.search(r"Plan name: os", data) is not None assert re.search(r"User ID: " + org.users[0].id, data) is not None assert re.search(r"User Email: " + email, data) is not None assert re.search(r"bar", data) is not None
class Authentication: auth_header = None org_name = "admin" username = "******" password = "******" multitenancy = isK8S() if isK8S(): plan = "enterprise" current_tenant = {} def __init__(self, name=org_name, username=username, password=password): """ :param org_name: Name of tenant organization :param username: Username - must be an email :param password: Password associated with tenant user """ self.reset() self.org_name = name self.org_create = True self.username = username self.password = password def reset(self): # Reset all temporary values. self.auth_header = Authentication.auth_header self.org_name = Authentication.org_name self.username = Authentication.username self.password = Authentication.password self.multitenancy = Authentication.multitenancy self.current_tenant = Authentication.current_tenant def set_tenant(self, org_name, username, password, plan="os"): self.new_tenant(org_name, username, password, plan) def new_tenant(self, org_name, username, password, plan="os"): self.multitenancy = True self.reset_auth_token() self.org_name = org_name self.org_create = True self.username = username self.password = password self.plan = plan self.get_auth_token() def get_auth_token(self, create_new_user=True): if self.auth_header is not None: return self.auth_header # try login - the user might be in a shared db # already (if not running xdist) r = self._do_login(self.username, self.password) logger.info("Getting authentication token for user %s@%s" % (self.username, self.org_name)) if create_new_user: if r.status_code != 200: if self.multitenancy and self.org_create: tenant_id = self._create_org(self.org_name, self.username, self.password, self.plan) tenant_id = tenant_id.strip() tenant_data = self._get_tenant_data(tenant_id) tenant_data_json = json.loads(tenant_data) self.current_tenant = { "tenant_id": tenant_id, "tenant_token": tenant_data_json["tenant_token"], "name": tenant_data_json["name"], } self.org_create = False else: self.create_user(self.username, self.password) # It might take some time for create_org to propagate the new user. # Retry login for a minute. for _ in range(60): r = self._do_login(self.username, self.password) if r.status_code == 200: break time.sleep(1) assert r.status_code == 200 return self.auth_header def create_user(self, username, password, tenant_id=""): namespace = get_container_manager().name cli = CliUseradm(containers_namespace=namespace) cli.create_user(username, password, tenant_id) def get_tenant_id(self): return self.current_tenant["tenant_id"] def reset_auth_token(self): self.auth_header = None def _do_login(self, username, password): r = requests_retry().post( "https://%s/api/management/%s/useradm/auth/login" % (get_container_manager().get_mender_gateway(), api_version), verify=False, auth=HTTPBasicAuth(username, password), ) assert r.status_code == 200 or r.status_code == 401 if r.status_code == 200: self.auth_header = {"Authorization": "Bearer " + str(r.text)} logger.info("Using Authorization headers: " + str(r.text)) return r def _create_org(self, name, username, password, plan="os"): namespace = get_container_manager().name cli = CliTenantadm(containers_namespace=namespace) tenant_id = cli.create_org(name, username, password, plan) return tenant_id def _get_tenant_data(self, tenant_id): namespace = get_container_manager().name cli = CliTenantadm(containers_namespace=namespace) tenant = cli.get_tenant(tenant_id) return tenant
def _make_trial_tenant(self, env): uuidv4 = str(uuid.uuid4()) tname = "test.mender.io-{}-{}".format(uuidv4, "trial") email = "some.user+{}@example.com".format(uuidv4) tadmm = ApiClient( host=get_container_manager().get_mender_gateway(), base_url=tenantadm_v2.URL_MGMT, ) args = { "organization": tname, "email": email, "password": "******", "name": "foo", "g-recaptcha-response": "dummy", "plan": "enterprise", } res = tadmm.call("POST", tenantadm_v2.URL_CREATE_ORG_TRIAL, body=args,) assert res.status_code == 202 # get tenant id tenantadm_host = ( tenantadm.HOST if isK8S() else get_container_manager().get_ip_of_service("mender-tenantadm")[0] + ":8080" ) tadmi = ApiClient( host=tenantadm_host, base_url=tenantadm.URL_INTERNAL, schema="http://", ) res = tadmi.call( "GET", tenantadm.URL_INTERNAL_TENANTS, qs_params={"username": email} ) assert res.status_code == 200 assert len(res.json()) == 1 apitenant = res.json()[0] cli = CliTenantadm(containers_namespace=env.name) tenant = cli.get_tenant(apitenant["id"]) tenant = json.loads(tenant) ttoken = tenant["tenant_token"] auth = Authentication(name=tname, username=email, password="******") auth.create_org = False auth.reset_auth_token() devauth = DeviceAuthV2(auth) new_tenant_client(env, "mender-client-trial", ttoken) devauth.accept_devices(1) devices = list( set([device["id"] for device in devauth.get_devices_status("accepted")]) ) assert 1 == len(devices) tenant = Tenant(tname, apitenant["id"], ttoken) u = User("", email, "correcthorse") tenant.users.append(u) tenant.device_id = devices[0] tenant.auth = auth tenant.devauth = devauth return tenant
) assert rsp.status_code == 201 rsp = api_deployments.call( "POST", deployments.URL_DEPLOYMENTS, body={ "artifact_name": f"{path.basename(tf.name)}", "devices": [device["id"] for device in devices], "name": "test-compat-deployment", }, ) assert rsp.status_code == 201 deployment_id = rsp.headers.get("Location").split("/")[-1] assert_successful_deployment(api_deployments, deployment_id) class TestClientCompatibilityOpenSource(TestClientCompatibilityBase): def test_compatibility(self, setup_os_compat): self.compatibility_test_impl(setup_os_compat) @pytest.mark.skipif( isK8S(), reason="not relevant in a staging or production environment" ) class TestClientCompatibilityEnterprise(TestClientCompatibilityBase): def test_enterprise_compatibility(self, setup_ent_compat): self.compatibility_test_impl(setup_ent_compat)
class TestMonitoringAlertsEnterprise(_TestMonitoringAlertsBase): useradm = ApiClient(useradm.URL_MGMT) devmonit = ApiClient(devicemonitor.URL_DEVICES) @pytest.mark.skipif( isK8S(), reason="no suitable smtp mock in staging environment", ) @pytest.mark.parametrize( argnames="test_case", argvalues=( { "alerts": [ { "name": "Something terrible may happen!", "timestamp": datetime.utcnow().isoformat() + "Z", "level": "CRITICAL", "subject": { "name": "mender-connect", "status": "killed", "type": "systemd.unit", "details": "Something terrible actually happened!", }, }, ], "email_regex": "(CRITICAL|OK)", }, { "alerts": [ { "name": "mender-client be like", "timestamp": datetime.utcnow().isoformat() + "Z", "level": "OK", "subject": { "name": "mender-connect", "status": "running", "type": "systemd.unit", "details": "It's all good", }, }, { "name": "sshd systemd unit", "timestamp": datetime.utcnow().isoformat() + "Z", "level": "CRITICAL", "subject": { "name": "sshd", "status": "killed", "type": "systemd.unit", "details": "Well, this is awkward", }, }, { "name": "Go please!", "timestamp": datetime.utcnow().isoformat() + "Z", "level": "CRITICAL", "subject": { "name": "gopls", "status": "killed", "type": "process", "details": "not again...", }, }, ], "email_regex": "(CRITICAL|OK)", }, ), ids=["CRITICAL alert", "CRITICAL and OK alerts"], ) def test_alerting_email(self, test_case, tenants_users_devices, smtp_mock): """ Checks that each alert a device issues to the backend triggers an email sent to the user. """ tenant = tenants_users_devices[0] user = tenant.users[0] devices = tenant.devices super().test_alerting_email(test_case, user, devices, smtp_mock)