async def test_handshake_unknown_device(running_backend, alice, mallory): with pytest.raises(BackendConnectionRefused) as exc: async with backend_authenticated_cmds_factory( alice.organization_addr, mallory.device_id, mallory.signing_key ) as cmds: await cmds.ping() assert str(exc.value) == "Invalid handshake information"
async def test_backend_disconnect_during_handshake(tcp_stream_spy, alice, backend_addr): client_answered = False async def poorly_serve_client(stream): nonlocal client_answered transport = await Transport.init_for_server(stream) handshake = ServerHandshake() await transport.send(handshake.build_challenge_req()) await transport.recv() # Close connection during handshake await stream.aclose() client_answered = True async with trio.open_service_nursery() as nursery: async def connection_factory(*args, **kwargs): client_stream, server_stream = trio.testing.memory_stream_pair() nursery.start_soon(poorly_serve_client, server_stream) return client_stream with tcp_stream_spy.install_hook(backend_addr, connection_factory): with pytest.raises(BackendNotAvailable): async with backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key ) as cmds: await cmds.ping() nursery.cancel_scope.cancel() assert client_answered
async def test_backend_closed_cmds(running_backend, alice): async with backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key ) as cmds: pass with pytest.raises(trio.ClosedResourceError): await cmds.ping()
async def _list_invitations(config, device): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: rep = await cmds.invite_list() if rep["status"] != "ok": raise RuntimeError( f"Backend error while listing invitations: {rep}") display_statuses = { InvitationStatus.READY: click.style("ready", fg="green"), InvitationStatus.IDLE: click.style("idle", fg="yellow"), InvitationStatus.DELETED: click.style("deleted", fg="red"), } for invitation in rep["invitations"]: display_status = display_statuses[invitation["status"]] display_token = invitation["token"].hex if invitation["type"] == InvitationType.USER: display_type = f"user (email={invitation['claimer_email']})" else: # Device display_type = "device" click.echo(f"{display_token}\t{display_status}\t{display_type}") if not rep["invitations"]: click.echo("No invitations.")
async def _greet_invitation(config, device, token): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: async with spinner("Retrieving invitation info"): rep = await cmds.invite_list() if rep["status"] != "ok": raise RuntimeError(f"Backend error: {rep}") for invitation in rep["invitations"]: if invitation["token"] == token: break else: raise RuntimeError("Invitation not found") if invitation["type"] == InvitationType.USER: initial_ctx = UserGreetInitialCtx(cmds=cmds, token=token) do_greet = partial(_do_greet_user, device, initial_ctx) else: assert invitation["type"] == InvitationType.DEVICE initial_ctx = DeviceGreetInitialCtx(cmds=cmds, token=token) do_greet = partial(_do_greet_device, device, initial_ctx) while True: try: greet_done = await do_greet() if greet_done: break except InviteError as exc: click.secho(str(exc), fg="red") click.secho("Restarting the invitation process", fg="red")
async def test_organization_expired(running_backend, alice, expiredorg): with pytest.raises(BackendConnectionRefused) as exc: async with backend_authenticated_cmds_factory( expiredorg.addr, alice.device_id, alice.signing_key ) as cmds: await cmds.ping() assert str(exc.value) == "Trial organization has expired"
async def test_backend_switch_offline(running_backend, alice): async with backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key ) as cmds: await cmds.ping() with running_backend.offline(): with pytest.raises(BackendNotAvailable): await cmds.ping()
async def test_authenticated_cmds_has_right_methods(running_backend, alice): async with backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key ) as cmds: for method_name in AUTHENTICATED_CMDS: assert hasattr(cmds, method_name) for method_name in ALL_CMDS - AUTHENTICATED_CMDS: assert not hasattr(cmds, method_name)
async def test_handshake_rvk_mismatch(running_backend, alice, otherorg): bad_rvk_org_addr = BackendOrganizationAddr.build( backend_addr=alice.organization_addr, organization_id=alice.organization_id, root_verify_key=otherorg.root_verify_key, ) with pytest.raises(BackendConnectionRefused) as exc: async with backend_authenticated_cmds_factory( bad_rvk_org_addr, alice.device_id, alice.signing_key ) as cmds: await cmds.ping() assert str(exc.value) == "Root verify key for organization differs between client and server"
async def test_handshake_unknown_organization(running_backend, alice): unknown_org_addr = BackendOrganizationAddr.build( backend_addr=alice.organization_addr, organization_id="dummy", root_verify_key=alice.organization_addr.root_verify_key, ) with pytest.raises(BackendConnectionRefused) as exc: async with backend_authenticated_cmds_factory( unknown_org_addr, alice.device_id, alice.signing_key ) as cmds: await cmds.ping() assert str(exc.value) == "Invalid handshake information"
async def _user_fs_factory(device, event_bus=None, initialize_in_v0: bool = False): event_bus = event_bus or event_bus_factory() async with backend_authenticated_cmds_factory( device.organization_addr, device.device_id, device.signing_key ) as cmds: path = local_storage_path(device) rdm = RemoteDevicesManager(cmds, device.root_verify_key) async with UserFS.run( device, path, cmds, rdm, event_bus, get_pattern_filter() ) as user_fs: if not initialize_in_v0: await initialize_userfs_storage_v1(user_fs.storage) yield user_fs
async def _cancel_invitation(config, device, token): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: rep = await cmds.invite_delete( token=token, reason=InvitationDeletedReason.CANCELLED) if rep["status"] != "ok": raise RuntimeError( f"Backend error while cancelling invitation: {rep}") click.echo("Invitation deleted.")
async def test_handshake_revoked_device(running_backend, alice, bob): revoked_user_certificate = RevokedUserCertificateContent( author=alice.device_id, timestamp=pendulum.now(), user_id=bob.user_id ).dump_and_sign(alice.signing_key) await running_backend.backend.user.revoke_user( organization_id=alice.organization_id, user_id=bob.user_id, revoked_user_certificate=revoked_user_certificate, revoked_user_certifier=alice.device_id, ) with pytest.raises(BackendConnectionRefused) as exc: async with backend_authenticated_cmds_factory( bob.organization_addr, bob.device_id, bob.signing_key ) as cmds: await cmds.ping() assert str(exc.value) == "Device has been revoked"
async def _invite_device(config, device): async with spinner("Creating device invitation"): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: rep = await cmds.invite_new(type=InvitationType.DEVICE) if rep["status"] != "ok": raise RuntimeError( f"Backend refused to create device invitation: {rep}") action_addr = BackendInvitationAddr.build( backend_addr=device.organization_addr, organization_id=device.organization_id, invitation_type=InvitationType.DEVICE, token=rep["token"], ) action_addr_display = click.style(action_addr.to_url(), fg="yellow") click.echo(f"url: {action_addr_display}")
async def initialize_test_organization( config_dir: Path, backend_address: BackendAddr, password: str, administration_token: str, force: bool, additional_users_number: int, additional_devices_number: int, ) -> Tuple[LocalDevice, LocalDevice, LocalDevice]: configure_logging("WARNING") organization_id = OrganizationID("Org") # Create organization async with apiv1_backend_administration_cmds_factory( backend_address, administration_token) as administration_cmds: rep = await administration_cmds.organization_create(organization_id) assert rep["status"] == "ok" bootstrap_token = rep["bootstrap_token"] organization_bootstrap_addr = BackendOrganizationBootstrapAddr.build( backend_address, organization_id, bootstrap_token) # Bootstrap organization and Alice user and create device "laptop" for Alice async with apiv1_backend_anonymous_cmds_factory( organization_bootstrap_addr) as anonymous_cmds: alice_device = await bootstrap_organization( cmds=anonymous_cmds, human_handle=HumanHandle(label="Alice", email="*****@*****.**"), device_label="laptop", ) save_device_with_password(config_dir, alice_device, password, force=force) config = load_config(config_dir, debug="DEBUG" in os.environ) # Create context manager, alice_client will be needed for the rest of the script async with logged_client_factory(config, alice_device) as alice_client: async with backend_authenticated_cmds_factory( addr=alice_device.organization_addr, device_id=alice_device.device_id, signing_key=alice_device.signing_key, ) as alice_cmds: # Create new device "pc" for Alice other_alice_device = await _register_new_device( cmds=alice_cmds, author=alice_device, device_label="pc") save_device_with_password(config_dir, other_alice_device, password, force=force) # Add additional random device for alice if additional_devices_number > 0: print( f"Adding {additional_devices_number} devices in the test group" ) print(" ... please wait ...") await _add_random_device( cmds=alice_cmds, config_dir=config_dir, force=force, password=password, device=alice_device, additional_devices_number=additional_devices_number, ) print(" Done ") # Invite Bob in organization bob_device = await _register_new_user( cmds=alice_cmds, author=alice_device, device_label="laptop", human_handle=HumanHandle(email="*****@*****.**", label="Bob"), profile=UserProfile.STANDARD, ) save_device_with_password(config_dir, bob_device, password, force=force) # Create Alice workspace alice_ws_id = await alice_client.user_fs.workspace_create( "alice_workspace") # Create context manager async with logged_client_factory(config, bob_device) as bob_client: # Create Bob workspace bob_ws_id = await bob_client.user_fs.workspace_create( "bob_workspace") # Bob share workspace with Alice await bob_client.user_fs.workspace_share( bob_ws_id, alice_device.user_id, WorkspaceRole.MANAGER) # Alice share workspace with Bob await alice_client.user_fs.workspace_share( alice_ws_id, bob_device.user_id, WorkspaceRole.MANAGER) # Add additional random users if additional_users_number > 0: print( f"Adding {additional_users_number} users in the test group" ) print(" ... please wait ...") await _add_random_users( cmds=alice_cmds, author=alice_device, alice_client=alice_client, bob_client=bob_client, alice_ws_id=alice_ws_id, bob_ws_id=bob_ws_id, additional_users_number=additional_users_number, ) print(" Done ") # Synchronize every device for device in (alice_device, other_alice_device, bob_device): async with logged_client_factory(config, device) as client: await client.user_fs.process_last_messages() await client.user_fs.sync() return (alice_device, other_alice_device, bob_device)
def _cmds_factory(keepalive): return backend_authenticated_cmds_factory(alice.organization_addr, alice.device_id, alice.signing_key, keepalive=keepalive)
async def test_events_listen_wait_has_watchdog(monkeypatch, mock_clock, running_backend, alice): # Spy on the transport events to detect the Pings/Pongs # (Note we are talking about websocket ping, not our own higher-level ping api) transport_events_sender, transport_events_receiver = trio.open_memory_channel(100) _vanilla_next_ws_event = Transport._next_ws_event async def _mocked_next_ws_event(transport): event = await _vanilla_next_ws_event(transport) await transport_events_sender.send((transport, event)) return event monkeypatch.setattr(Transport, "_next_ws_event", _mocked_next_ws_event) async def next_ping_related_event(): while True: transport, event = await transport_events_receiver.receive() if isinstance(event, (Ping, Pong)): return (transport, event) # Highjack the backend api to control the wait time and the final # event that will be returned to the client backend_received_cmd = trio.Event() backend_client_ctx = None vanilla_api_events_listen = running_backend.backend.apis[HandshakeType.AUTHENTICATED][ "events_listen" ] async def _mocked_api_events_listen(client_ctx, msg): nonlocal backend_client_ctx backend_client_ctx = client_ctx backend_received_cmd.set() return await vanilla_api_events_listen(client_ctx, msg) running_backend.backend.apis[HandshakeType.AUTHENTICATED][ "events_listen" ] = _mocked_api_events_listen events_listen_rep = None async with backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key, keepalive=2 ) as cmds: mock_clock.rate = 1 async with trio.open_service_nursery() as nursery: async def _cmd(): nonlocal events_listen_rep events_listen_rep = await cmds.events_listen(wait=True) nursery.start_soon(_cmd) # Wait for the connection to be established with the backend with trio.fail_after(1): await backend_received_cmd.wait() # Now advance time until ping is requested await trio.testing.wait_all_tasks_blocked() mock_clock.jump(2) with trio.fail_after(2): backend_transport, event = await next_ping_related_event() assert isinstance(event, Ping) client_transport, event = await next_ping_related_event() assert isinstance(event, Pong) assert client_transport is not backend_transport # Wait for another ping, just to be sure... await trio.testing.wait_all_tasks_blocked() mock_clock.jump(2) with trio.fail_after(1): backend_transport2, event = await next_ping_related_event() assert isinstance(event, Ping) assert backend_transport is backend_transport2 client_transport2, event = await next_ping_related_event() assert isinstance(event, Pong) assert client_transport is client_transport2 await backend_client_ctx.send_events_channel.send( {"event": APIEvent.PINGED, "ping": "foo"} ) assert events_listen_rep == {"status": "ok", "event": APIEvent.PINGED, "ping": "foo"}
async def test_ping(running_backend, alice): async with backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key ) as cmds: rep = await cmds.ping("Hello World !") assert rep == {"status": "ok", "pong": "Hello World !"}
async def _remote_devices_manager_factory(device): async with backend_authenticated_cmds_factory( device.organization_addr, device.device_id, device.signing_key ) as cmds: yield RemoteDevicesManager(cmds, device.root_verify_key)
async def bob_backend_cmds(running_backend, bob): async with backend_authenticated_cmds_factory( bob.organization_addr, bob.device_id, bob.signing_key ) as cmds: yield cmds
async def alice2_backend_cmds(running_backend, alice2): async with backend_authenticated_cmds_factory( alice2.organization_addr, alice2.device_id, alice2.signing_key ) as cmds: yield cmds