//! Changing a network port after it is made: the name other machines see, its UDP port and who //! may join (FR-015, R-078). #![allow( clippy::expect_used, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used )] mod common; use midi_harbor_core::endpoint::{EndpointKind, InvitationPolicy, NetworkSession}; use midi_harbor_core::failure::FailureReason; use midi_harbor_core::ids::EndpointId; use midi_harbor_core::state::ConnectionPhase; use midi_harbor_daemon::{Daemon, DaemonError, NetworkPortChange}; use midi_harbor_platform::fake::FakeMidiPlatform; use midi_harbor_platform::midi::MidiPlatform; use std::net::{SocketAddr, UdpSocket}; use std::sync::Arc; use std::time::Duration; /// Starts a daemon standing in for one machine, over a scratch directory of its own. async fn machine(label: &str) -> Arc { let root = common::scratch("midi-harbor-network-ports") .join(format!("{label}-{}", uuid::Uuid::new_v4())); Daemon::start( common::quiet(root), Arc::new(FakeMidiPlatform::new()) as Arc, ) .await .expect("the daemon starts over a scratch directory") } /// Returns a network port's stored settings. async fn settings(daemon: &Arc, id: EndpointId) -> NetworkSession { daemon .read( |config, _| match config.endpoint(id).map(|e| e.kind.clone()) { Some(EndpointKind::NetworkSession(session)) => session, _ => panic!("not a network port"), }, ) .await } /// Returns an even UDP port that is free, with the one above it free too for the data port. /// /// Chosen at random below the ports systems hand out for port zero, which start at 32768 on Linux /// and 49152 on Windows. Windows hands those out in order, so a pair the system had just chosen /// and released here was often the next one given to a daemon starting in another test, and the /// move to it was refused. fn free_pair() -> u16 { for _ in 0..200 { // Even ports from 20000 to 29998. let port = 20_000 + 2 * rand::random_range(0..5_000u16); let free = |port: u16| { UdpSocket::bind(("0.0.0.0", port)).is_ok() && UdpSocket::bind(("::", port)).is_ok() }; if free(port) && free(port + 1) { return port; } } panic!("no free pair of UDP ports"); } /// Waits for a network port to be connected, reporting whether it was. async fn connected(daemon: &Arc, id: EndpointId) -> bool { for _ in 0..100 { if daemon .session_status(id) .await .is_some_and(|status| status.state.phase() == ConnectionPhase::Connected) { return true; } tokio::time::sleep(Duration::from_millis(50)).await; } false } /// Builds Stage on a near machine connected to Front of House on a far one. async fn joined() -> (Arc, EndpointId, Arc, EndpointId) { let far = machine("far").await; let front = far .create_network_session("Front of House", 0, InvitationPolicy::AcceptAll) .await .expect("the far network port is created"); let port = far .session_status(front.id) .await .expect("the far network port is listening") .control_port; let near = machine("near").await; let stage = near .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the near network port is created"); near.connect_peer(stage.id, SocketAddr::from(([127, 0, 0, 1], port))) .await .expect("the near network port invites the far one"); assert!( connected(&near, stage.id).await, "a far network port accepting everyone lets the near one in" ); (near, stage.id, far, front.id) } /// Proves that a new Bonjour name is applied to a running network port without dropping the /// machine connected to it, that a change naming only the Bonjour name leaves every other setting /// as it was, and that a machine joining afterwards is told the new name. /// /// The far port accepts everyone, so a change that reset the unnamed settings to their defaults /// would show as the invitation policy falling back to asking first. #[tokio::test] async fn a_new_bonjour_name_leaves_the_connected_machine_connected() { let (near, stage, far, front) = joined().await; let peer = far .session_status(front) .await .expect("the far network port is running") .peer_address; let before = settings(&far, front).await; far.update_network_port( front, NetworkPortChange { local_name: Some("Main Stage".to_owned()), ..NetworkPortChange::default() }, ) .await .expect("the far network port takes the new Bonjour name"); let after = settings(&far, front).await; assert_eq!( after.local_name.as_str(), "Main Stage", "the new Bonjour name is stored" ); assert_eq!( after.invitation_policy, InvitationPolicy::AcceptAll, "a change that says nothing about the invitation policy leaves it alone" ); assert_eq!( after.control_port, before.control_port, "a change that says nothing about the UDP port leaves it alone" ); assert_eq!( after.automatic_port, before.automatic_port, "a change that says nothing about the automatic port leaves it alone" ); tokio::time::sleep(Duration::from_millis(300)).await; let status = far .session_status(front) .await .expect("the far network port is still running"); assert_eq!( status.state.phase(), ConnectionPhase::Connected, "a new Bonjour name does not restart the network port" ); assert_eq!(status.peer_address, peer, "the machine was dropped"); assert!( connected(&near, stage).await, "the near machine stays connected across the far port's rename" ); // A machine joining now is told the new name. let port = status.control_port; let late = machine("late").await; let booth = late .create_network_session("Booth", 0, InvitationPolicy::Prompt) .await .expect("a third network port is created"); late.connect_peer(booth.id, SocketAddr::from(([127, 0, 0, 1], port))) .await .expect("the third network port invites the far one"); let mut told = None; for _ in 0..100 { told = late .session_status(booth.id) .await .and_then(|status| status.peer_name); if told.is_some() { break; } tokio::time::sleep(Duration::from_millis(50)).await; } assert_eq!( told.as_deref(), Some("Main Stage"), "a machine joining after the rename is told the new Bonjour name" ); } /// Proves that a new UDP port moves the listener, releases the old port and connects again to /// the machine the network port had connected to, so moving a port does not cost the connection. #[tokio::test] async fn a_new_udp_port_moves_the_listener_and_reconnects_its_machine() { let (near, stage, _far, _front) = joined().await; let old = near .session_status(stage) .await .expect("the near network port is running") .control_port; let new = free_pair(); near.update_network_port( stage, NetworkPortChange { control_port: Some(new), ..NetworkPortChange::default() }, ) .await .expect("the near network port moves to a free UDP port"); assert_eq!( settings(&near, stage).await.control_port, new, "the new UDP port is stored so a restart comes back on it" ); let status = near .session_status(stage) .await .expect("the near network port is running"); assert_eq!( status.control_port, new, "the running network port listens on the new UDP port" ); assert!( UdpSocket::bind(("0.0.0.0", old)).is_ok(), "the old UDP port is still held" ); assert!( connected(&near, stage).await, "the machine it had connected to was not connected to again" ); } /// Proves that a change whose UDP port cannot be bound is refused whole, before the running /// network port is touched. /// /// Stopping it and starting it again on the old port would leave a window in which a daemon /// starting beside it could take that port (R-082), so the listener's state must not even have /// restarted. #[tokio::test] async fn a_udp_port_that_cannot_be_bound_leaves_the_network_port_where_it_was() { let daemon = machine("taken").await; let stage = daemon .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the network port is created"); let old = settings(&daemon, stage.id).await.control_port; let listening_since = daemon .session_status(stage.id) .await .expect("the network port is running") .state .since(); let taken = free_pair(); let _holder = ( UdpSocket::bind(("0.0.0.0", taken)).expect("the test holds the UDP port it will ask for"), UdpSocket::bind(("::", taken)).ok(), ); let refused = daemon .update_network_port( stage.id, NetworkPortChange { control_port: Some(taken), local_name: Some("Main Stage".to_owned()), ..NetworkPortChange::default() }, ) .await; assert!(refused.is_err(), "a UDP port in use was accepted"); let kept = settings(&daemon, stage.id).await; assert_eq!( kept.control_port, old, "the refused UDP port was stored anyway" ); assert_eq!( kept.local_name.as_str(), "Stage", "the refused change was half applied" ); assert_eq!( daemon .session_status(stage.id) .await .expect("the network port is still running") .control_port, old, "the network port was left down" ); assert_eq!( daemon .session_status(stage.id) .await .expect("the network port is still running") .state .since(), listening_since, "the network port was stopped and started again" ); } /// What is tried against Stage, on a daemon where Booth is another network port. #[derive(Debug)] enum Attempt { /// Moves Stage to the UDP port Booth listens on. MoveToBoothsPort, /// Moves Stage to an even UDP port nothing holds. MoveToFreePort, /// Changes Stage as given. Change(NetworkPortChange), /// Makes another network port. Create { name: &'static str, local_name: Option<&'static str>, control_port: u16, }, } /// Proves that a network port cannot take a Bonjour name or UDP port another network port holds, /// nor an odd UDP port, while a name or port nobody holds is taken. /// /// Two network ports advertising one name look like one machine to every peer, and two on one UDP /// port cannot both listen. RTP-MIDI's data port is the one above the control port, so an odd /// control port would be moved up one without saying; it is refused instead, naming the even /// ports either side. #[tokio::test] async fn a_name_or_udp_port_another_network_port_holds_is_refused() { struct Case { name: &'static str, attempt: Attempt, refused: bool, /// Text the refusal must contain, so the user can tell what to change. naming: Option<&'static str>, } let cases = [ Case { name: "a UDP port another network port listens on", attempt: Attempt::MoveToBoothsPort, refused: true, naming: Some("Booth"), }, Case { name: "a Bonjour name another network port advertises", attempt: Attempt::Change(NetworkPortChange { local_name: Some("Booth".to_owned()), ..NetworkPortChange::default() }), refused: true, naming: None, }, Case { name: "an odd UDP port", attempt: Attempt::Change(NetworkPortChange { control_port: Some(5005), ..NetworkPortChange::default() }), refused: true, naming: None, }, Case { name: "a new network port on an odd UDP port", attempt: Attempt::Create { name: "Odd", local_name: None, control_port: 5005, }, refused: true, naming: None, }, Case { name: "a new network port advertising Stage", attempt: Attempt::Create { name: "Studio", local_name: Some("Stage"), control_port: 0, }, refused: true, naming: None, }, Case { name: "a Bonjour name nobody advertises", attempt: Attempt::Change(NetworkPortChange { local_name: Some("Main Stage".to_owned()), ..NetworkPortChange::default() }), refused: false, naming: None, }, Case { name: "an even UDP port nothing holds", attempt: Attempt::MoveToFreePort, refused: false, naming: None, }, Case { name: "a new network port advertising a name of its own", attempt: Attempt::Create { name: "Studio", local_name: Some("Studio"), control_port: 0, }, refused: false, naming: None, }, ]; for case in cases { let daemon = machine("held").await; let stage = daemon .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("Stage is created"); let booth = daemon .create_network_session("Booth", 0, InvitationPolicy::Prompt) .await .expect("Booth is created beside Stage"); let before = settings(&daemon, stage.id).await; let result = match case.attempt { Attempt::MoveToBoothsPort => { let port = settings(&daemon, booth.id).await.control_port; let change = NetworkPortChange { control_port: Some(port), ..NetworkPortChange::default() }; daemon.update_network_port(stage.id, change).await.map(drop) } Attempt::MoveToFreePort => { let change = NetworkPortChange { control_port: Some(free_pair()), ..NetworkPortChange::default() }; daemon.update_network_port(stage.id, change).await.map(drop) } Attempt::Change(change) => daemon.update_network_port(stage.id, change).await.map(drop), Attempt::Create { name, local_name, control_port, } => daemon .create_network_port( name, local_name, control_port, InvitationPolicy::Prompt, true, ) .await .map(drop), }; assert_eq!( result.is_err(), case.refused, "{}: refused should be {}, got {result:?}", case.name, case.refused ); if let (Some(text), Err(error)) = (case.naming, &result) { assert!( error.to_string().contains(text), "{}: the refusal does not say which network port holds it: {error}", case.name ); } if case.refused { let after = settings(&daemon, stage.id).await; assert_eq!( after.local_name, before.local_name, "{}: a refused change still renamed Stage", case.name ); assert_eq!( after.control_port, before.control_port, "{}: a refused change still moved Stage", case.name ); } } } /// Proves that a new invitation policy reaches the running network port: a machine held waiting /// under "ask first" is let in once the network port accepts anyone, without restarting it. #[tokio::test] async fn a_new_invitation_policy_applies_to_the_running_network_port() { let far = machine("policy-far").await; let front = far .create_network_session("Front of House", 0, InvitationPolicy::Prompt) .await .expect("the far network port is created"); let port = far .session_status(front.id) .await .expect("the far network port is listening") .control_port; let near = machine("policy-near").await; let stage = near .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the near network port is created"); near.connect_peer(stage.id, SocketAddr::from(([127, 0, 0, 1], port))) .await .expect("the near network port invites the far one"); tokio::time::sleep(Duration::from_millis(500)).await; assert_ne!( far.session_status(front.id) .await .expect("the far network port is running") .state .phase(), ConnectionPhase::Connected, "let in without being asked" ); far.set_invitation_policy(front.id, InvitationPolicy::AcceptAll) .await .expect("the far network port's policy changes to accepting everyone"); // The near side invites again on its own backoff, so this waits out a few of its tries. let mut let_in = false; for _ in 0..3 { if connected(&near, stage.id).await { let_in = true; break; } } assert!( let_in, "the new policy did not reach the running network port" ); } /// Waits for a network port's machines to satisfy a condition, returning the last seen. async fn machines_until( daemon: &Arc, id: EndpointId, wanted: impl Fn(&[midi_harbor_daemon::session::Machine]) -> bool, ) -> Vec { let mut seen = Vec::new(); for _ in 0..200 { seen = daemon .session_status(id) .await .map(|status| status.machines) .unwrap_or_default(); if wanted(&seen) { break; } tokio::time::sleep(Duration::from_millis(50)).await; } seen } /// Proves that a second machine invited beside the peer is carried and remembered beside it, /// that disconnecting the peer leaves the second machine joined and remembered as the peer in its /// place, and that a machine no longer taking part cannot be disconnected again. /// /// The peer disconnected by the user is forgotten, so it is not connected to again on the next /// start. #[tokio::test] async fn a_second_machine_joins_beside_the_first_and_each_can_be_disconnected_alone() { let (near, stage, far, front) = joined().await; let far_port = far .session_status(front) .await .expect("the far network port is running") .control_port; let booth_machine = machine("booth").await; let booth = booth_machine .create_network_session("Booth", 0, InvitationPolicy::AcceptAll) .await .expect("a third network port is created"); let booth_port = booth_machine .session_status(booth.id) .await .expect("the third network port is listening") .control_port; let (to_front, to_booth) = ( SocketAddr::from(([127, 0, 0, 1], far_port)), SocketAddr::from(([127, 0, 0, 1], booth_port)), ); assert!( settings(&near, stage).await.peer.is_some(), "the machine connected first is remembered as the peer" ); near.invite_machine(stage, to_booth) .await .expect("the near network port invites a second machine"); let both = machines_until(&near, stage, |machines| { machines.len() == 2 && machines.iter().all(|machine| machine.joined) }) .await; assert_eq!( both.iter() .map(|machine| machine.address) .collect::>(), vec![to_front, to_booth], "both machines are joined, the peer first: {both:?}" ); let remembered = settings(&near, stage).await; assert_eq!( remembered.other_peers.len(), 1, "the second machine is not remembered beside the first" ); let second = remembered.other_peers[0]; // The peer goes, and is forgotten. The second machine stays, remembered as the peer. near.disconnect_machine(stage, to_front) .await .expect("the near network port disconnects the first machine"); let remembered = settings(&near, stage).await; assert_eq!( remembered.peer, Some(second), "the machine that took the peer's place is not remembered as the peer" ); assert!( remembered.other_peers.is_empty(), "the machine promoted to peer is still remembered beside it: {remembered:?}" ); let left = machines_until(&near, stage, |machines| { machines.len() == 1 && machines[0].joined }) .await; assert_eq!( left.len(), 1, "only the second machine is left joined: {left:?}" ); assert_eq!( left[0].address, to_booth, "disconnecting the first machine left the second one carried" ); assert!( near.disconnect_machine(stage, to_front).await.is_err(), "a machine no longer taking part was disconnected again" ); } /// Starts a far machine with a network port accepting everyone, returning it, its network port /// and the address to reach that network port at. async fn accepting_machine(label: &str, name: &str) -> (Arc, EndpointId, SocketAddr) { let daemon = machine(label).await; let port = daemon .create_network_session(name, 0, InvitationPolicy::AcceptAll) .await .expect("the far network port is created"); let control_port = daemon .session_status(port.id) .await .expect("the far network port is listening") .control_port; ( daemon, port.id, SocketAddr::from(([127, 0, 0, 1], control_port)), ) } /// Reports whether every machine listed is carrying MIDI and they are exactly `expected`. fn all_joined(machines: &[midi_harbor_daemon::session::Machine], expected: &[SocketAddr]) -> bool { machines.iter().all(|machine| machine.joined) && machines .iter() .map(|machine| machine.address) .collect::>() == expected } /// Proves that a machine invited straight after the peer is connected is remembered beside the /// peer rather than in its place. /// /// Regression: the session's status was read to tell whether the machine became the peer, before /// the session had acted on the connect ahead of it. The machine was remembered as the peer, the /// peer was forgotten, and after a restart only the machine came back. On one thread the session /// cannot act between the two calls, so the old reading fails every time. #[tokio::test] async fn a_machine_invited_straight_after_connecting_is_remembered_beside_the_peer() { let (_front_machine, _, to_front) = accepting_machine("straight-front", "Front of House").await; let (_booth_machine, _, to_booth) = accepting_machine("straight-booth", "Booth").await; let near = machine("straight-near").await; let stage = near .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the near network port is created") .id; near.connect_peer(stage, to_front) .await .expect("the near network port connects to Front of House"); near.invite_machine(stage, to_booth) .await .expect("the near network port invites Booth beside it"); let remembered = settings(&near, stage).await; let peer = remembered .peer .expect("the machine connected first is remembered as the peer"); assert_eq!( addresses_of(&near, peer).await, vec![to_front.to_string()], "the machine invited took the peer's place: {remembered:?}" ); assert_eq!( remembered.other_peers.len(), 1, "the invited machine is remembered beside the peer: {remembered:?}" ); assert_eq!( addresses_of(&near, remembered.other_peers[0]).await, vec![to_booth.to_string()], "the machine remembered beside the peer is the one invited" ); } /// Returns the addresses a remembered machine is reached at. async fn addresses_of(daemon: &Arc, peer: midi_harbor_core::ids::PeerId) -> Vec { daemon .read(move |config, _| { config .peers .iter() .find(|known| known.id == peer) .map(|known| known.addresses.clone()) .unwrap_or_default() }) .await } /// Proves that every machine connected comes back after a restart, and that one the user /// disconnected does not (FR-015i). /// /// Regression: only the first machine was remembered, so a second one connected beside it had to /// be connected again by hand after every restart. #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn every_machine_connected_comes_back_after_a_restart_until_it_is_disconnected() { let (_front_machine, _, to_front) = accepting_machine("restart-front", "Front of House").await; let (_booth_machine, _, to_booth) = accepting_machine("restart-booth", "Booth").await; let root = common::scratch("midi-harbor-network-ports") .join(format!("restart-near-{}", uuid::Uuid::new_v4())); // Quieted once: `quiet` rewrites the configuration file, which a restart must find as it // was left. let paths = common::quiet(root); let start = || { Daemon::start( paths.clone(), Arc::new(FakeMidiPlatform::new()) as Arc, ) }; let first = start() .await .expect("the near daemon starts over a scratch directory"); let stage = first .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the near network port is created") .id; first .connect_peer(stage, to_front) .await .expect("the near network port connects to Front of House"); first .invite_machine(stage, to_booth) .await .expect("the near network port invites Booth beside it"); let both = machines_until(&first, stage, |machines| { all_joined(machines, &[to_front, to_booth]) }) .await; assert!( all_joined(&both, &[to_front, to_booth]), "both machines join before the restart: {both:?}" ); // Both come back after a restart. first.end_sessions().await; let second = start() .await .expect("the near daemon starts again over the same directory"); let back = machines_until(&second, stage, |machines| { all_joined(machines, &[to_front, to_booth]) }) .await; assert!( all_joined(&back, &[to_front, to_booth]), "only some machines came back: {back:?}" ); // The second machine, disconnected, is forgotten; the first is not. second .disconnect_machine(stage, to_booth) .await .expect("the near network port disconnects Booth"); let remembered = settings(&second, stage).await; assert!( remembered.peer.is_some(), "disconnecting the second machine forgot the peer too: {remembered:?}" ); assert!( remembered.other_peers.is_empty(), "the machine the user disconnected is still remembered: {remembered:?}" ); second.end_sessions().await; let third = start() .await .expect("the near daemon starts a third time over the same directory"); assert!( connected(&third, stage).await, "the first did not come back" ); tokio::time::sleep(Duration::from_secs(1)).await; let machines = machines_until(&third, stage, |machines| machines.len() == 1).await; assert_eq!( machines .iter() .map(|machine| machine.address) .collect::>(), vec![to_front], "a machine the user disconnected came back after a restart" ); } /// Proves that a machine connected beside the peer, whose network port goes away, stays listed /// and waiting without disturbing the peer, and is connected again without being asked when it /// returns on the same UDP port. #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn a_machine_connected_beside_the_first_comes_back_when_its_link_returns() { let (_front_machine, _, to_front) = accepting_machine("drop-front", "Front of House").await; let (booth_machine, booth, to_booth) = accepting_machine("drop-booth", "Booth").await; let near = machine("drop-near").await; let stage = near .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the near network port is created") .id; near.connect_peer(stage, to_front) .await .expect("the near network port connects to Front of House"); near.invite_machine(stage, to_booth) .await .expect("the near network port invites Booth beside it"); let both = machines_until(&near, stage, |machines| { all_joined(machines, &[to_front, to_booth]) }) .await; assert!( all_joined(&both, &[to_front, to_booth]), "both machines join before Booth goes away: {both:?}" ); // The second machine's network port goes away. booth_machine .set_enabled(booth, false) .await .expect("Booth's network port is switched off"); let waiting = machines_until(&near, stage, |machines| { machines .iter() .any(|machine| machine.address == to_booth && !machine.joined) }) .await; assert!( waiting .iter() .any(|machine| machine.address == to_booth && !machine.joined), "the second machine was let go: {waiting:?}" ); assert!( connected(&near, stage).await, "the first machine was dropped" ); // It returns on the same UDP port. booth_machine .set_enabled(booth, true) .await .expect("Booth's network port is switched on again"); let back = machines_until(&near, stage, |machines| { all_joined(machines, &[to_front, to_booth]) }) .await; assert!( all_joined(&back, &[to_front, to_booth]), "the second machine did not come back: {back:?}" ); } /// Proves that a machine invited while nothing is connected becomes the remembered peer, so it /// is connected to again after a restart like a machine connected to directly. #[tokio::test] async fn inviting_a_machine_with_nothing_connected_makes_it_the_remembered_peer() { let (_far, _, to_far) = accepting_machine("alone-far", "Front of House").await; let near = machine("alone-near").await; let stage = near .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the near network port is created"); near.invite_machine(stage.id, to_far) .await .expect("the near network port invites Front of House"); assert!( connected(&near, stage.id).await, "a machine accepting everyone lets the invited one in" ); assert!( settings(&near, stage.id).await.peer.is_some(), "the only machine is not connected again after a restart" ); } /// Builds a far machine asking about invitations and a near one inviting it, returning the far /// daemon, its network port, and the near side's network port with the far port to invite. async fn asking(label: &str) -> (Arc, EndpointId, Arc, EndpointId, SocketAddr) { let far = machine(&format!("{label}-far")).await; let front = far .create_network_session("Front of House", 0, InvitationPolicy::Prompt) .await .expect("the far network port is created"); let port = far .session_status(front.id) .await .expect("the far network port is listening") .control_port; let near = machine(&format!("{label}-near")).await; let stage = near .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the near network port is created"); ( far, front.id, near, stage.id, SocketAddr::from(([127, 0, 0, 1], port)), ) } /// Waits for an invitation to be waiting on the user, returning its identifier. async fn asked(daemon: &Arc) -> Option { for _ in 0..100 { if let Some(invitation) = daemon.pending_invitations().await.first() { return Some(invitation.id.clone()); } tokio::time::sleep(Duration::from_millis(50)).await; } None } /// How the far machine came to let the near one in. #[derive(Clone, Copy, Debug)] enum Grant { /// The user accepted its invitation once, for this run. AnsweredOnce, /// It was added as a known machine and trusted. Trusted, /// The user accepted its invitation once, and then it was added as a trusted machine named /// "Stage machine". AnsweredOnceAndTrusted, } /// How the far machine took that permission away. #[derive(Clone, Copy, Debug)] enum Withdraw { /// Trust was switched off for its address. TrustSwitchedOff, /// It was added again as a known machine named "Stage machine", untrusted. AddedAgainUntrusted, /// The known machine "Stage machine" was forgotten. Forgotten, } /// Proves that once permission to join is withdrawn, however it was given and however it was /// withdrawn, the machine's next invitation is asked about rather than let in. /// /// Each row connects, disconnects with `disconnect_peer`, withdraws permission and connects /// again, so it also proves that a session disconnected and connected again sends a fresh /// invitation, and that a machine answered once is connected. Regression: an invitation accepted /// once let the machine in until the daemon restarted, whatever trust later said, and a machine /// added again untrusted kept the trust it had. #[tokio::test] async fn once_trust_is_withdrawn_the_next_invitation_is_asked_about() { struct Case { name: &'static str, grant: Grant, withdraw: Withdraw, } let cases = [ Case { name: "answered once, then added untrusted", grant: Grant::AnsweredOnce, withdraw: Withdraw::AddedAgainUntrusted, }, Case { name: "answered once and trusted, then trust switched off", grant: Grant::AnsweredOnceAndTrusted, withdraw: Withdraw::TrustSwitchedOff, }, Case { name: "answered once and trusted, then forgotten", grant: Grant::AnsweredOnceAndTrusted, withdraw: Withdraw::Forgotten, }, Case { name: "trusted, then trust switched off", grant: Grant::Trusted, withdraw: Withdraw::TrustSwitchedOff, }, Case { name: "trusted, then added again untrusted", grant: Grant::Trusted, withdraw: Withdraw::AddedAgainUntrusted, }, ]; for case in cases { let (far, front, near, stage, to_far) = asking("trust").await; // Grant permission, and connect under it. match case.grant { Grant::AnsweredOnce | Grant::AnsweredOnceAndTrusted => { near.connect_peer(stage, to_far) .await .expect("the near network port invites the far one"); let invitation = asked(&far).await.unwrap_or_else(|| { panic!("{}: an unknown machine was not asked about", case.name) }); far.respond_to_invitation(&invitation, true, false) .await .expect("the far machine accepts the invitation once"); if matches!(case.grant, Grant::AnsweredOnceAndTrusted) { far.add_known_machine("127.0.0.1", Some("Stage machine".to_owned()), true) .await .expect("the far machine remembers the near one as trusted"); } } Grant::Trusted => { far.add_known_machine("127.0.0.1", None, true) .await .expect("the far machine trusts the near one"); } } near.connect_peer(stage, to_far) .await .expect("the near network port invites the far one again"); assert!( connected(&near, stage).await, "{}: a machine given permission was not let in", case.name ); // Withdraw it, and invite again. near.disconnect_peer(stage) .await .expect("the near network port leaves the far one"); match case.withdraw { Withdraw::TrustSwitchedOff => { let known = far .set_peer_trusted("127.0.0.1", false) .await .expect("the far machine switches trust off"); assert!( !known.trusted, "{}: switching trust off reports the machine as still trusted", case.name ); } Withdraw::AddedAgainUntrusted => { let again = far .add_known_machine("127.0.0.1", Some("Stage machine".to_owned()), false) .await .expect("the far machine adds the near one again, untrusted"); assert!( !again.trusted, "{}: the second add left it trusted", case.name ); assert_eq!( again.name, "Stage machine", "{}: adding a known machine again takes the name given", case.name ); } Withdraw::Forgotten => { far.remove_peer("Stage machine") .await .expect("the far machine forgets the near one"); } } near.connect_peer(stage, to_far) .await .expect("the near network port invites the far one a third time"); assert!( asked(&far).await.is_some(), "{}: a machine whose permission was withdrawn was let in without asking", case.name ); assert_ne!( far.session_status(front) .await .expect("the far network port is running") .state .phase(), ConnectionPhase::Connected, "{}: the far network port connected while the invitation waited on the user", case.name ); } } /// Proves that deleting a network port drops the invitations waiting on it, so the user is not /// asked about joining something that no longer exists. #[tokio::test] async fn deleting_a_network_port_drops_the_invitations_waiting_on_it() { let (far, front, near, stage, to_far) = asking("deleted").await; near.connect_peer(stage, to_far) .await .expect("the near network port invites the far one"); assert!( asked(&far).await.is_some(), "a far network port that asks first holds the invitation for the user" ); far.delete_network_port(front) .await .expect("the far network port is deleted"); assert!( far.pending_invitations().await.is_empty(), "an invitation to a deleted network port is still waiting" ); } /// Returns how many endpoints hold a name. async fn holding(daemon: &Arc, name: &str) -> usize { daemon .read(|config, _| { config .endpoints .iter() .filter(|e| e.name.as_str() == name) .count() }) .await } /// A name claimed on a daemon holding the port Keys, the network port Stage, and the network port /// Booth without its automatic port. #[derive(Debug)] enum Claim { /// Makes a port. Port(&'static str), /// Makes a network port, with or without its automatic port. NetworkPort { name: &'static str, automatic_port: bool, }, /// Renames an endpoint. Rename { from: &'static str, to: &'static str, }, /// Imports a configuration, merged with the one held. Import(&'static str), } /// What a claim should come to. #[derive(Debug)] enum Outcome { /// It is taken. Accepted, /// It is refused as a name already in use, naming that name. Taken(&'static str), /// It is refused with a message containing this text. Reported(&'static str), } /// Proves that ports and network ports share one namespace, whichever is made, renamed or /// imported second, while an endpoint keeping its own name is not a clash with itself. /// /// Other applications see a network port as a port of its name, so the two would show as /// identical ports. A network port without its automatic port holds its name too, because /// switching the automatic port on later would bring the clash back. A merging import matches /// endpoints by kind and name, so it would add the network port beside the port of its name. A /// rename that keeps the name is what an edit saved unchanged sends. #[tokio::test] async fn a_port_and_a_network_port_cannot_share_a_name() { struct Case { name: &'static str, claim: Claim, want: Outcome, } let cases = [ Case { name: "a port under a network port's name", claim: Claim::Port("Stage"), want: Outcome::Taken("Stage"), }, Case { name: "a port under a network port's name with spaces around it", claim: Claim::Port(" Stage "), want: Outcome::Taken("Stage"), }, Case { name: "a port under the name of a network port without its automatic port", claim: Claim::Port("Booth"), want: Outcome::Taken("Booth"), }, Case { name: "a network port under a port's name", claim: Claim::NetworkPort { name: "Keys", automatic_port: true, }, want: Outcome::Taken("Keys"), }, Case { name: "a network port without its automatic port under a port's name", claim: Claim::NetworkPort { name: "Keys", automatic_port: false, }, want: Outcome::Taken("Keys"), }, Case { name: "a port renamed to a network port's name", claim: Claim::Rename { from: "Keys", to: "Stage", }, want: Outcome::Taken("Stage"), }, Case { name: "a network port renamed to a port's name", claim: Claim::Rename { from: "Stage", to: "Keys", }, want: Outcome::Taken("Keys"), }, Case { name: "a network port imported under a port's name", claim: Claim::Import( "endpoints:\n\ - name: Keys\n\ \x20 kind: network_port\n\ \x20 control_port: 0\n", ), want: Outcome::Reported("both named 'Keys'"), }, Case { name: "a port renamed to its own name", claim: Claim::Rename { from: "Keys", to: "Keys", }, want: Outcome::Accepted, }, Case { name: "a network port renamed to its own name", claim: Claim::Rename { from: "Stage", to: "Stage", }, want: Outcome::Accepted, }, ]; for case in cases { let daemon = machine("names").await; daemon .create_virtual_port("Keys", 1, 1) .await .expect("the port Keys is created"); daemon .create_network_session("Stage", 0, InvitationPolicy::Prompt) .await .expect("the network port Stage is created"); daemon .create_network_port("Booth", None, 0, InvitationPolicy::Prompt, false) .await .expect("the network port Booth is created without its automatic port"); let result = match case.claim { Claim::Port(name) => daemon.create_virtual_port(name, 1, 1).await.map(drop), Claim::NetworkPort { name, automatic_port, } => daemon .create_network_port(name, None, 0, InvitationPolicy::Prompt, automatic_port) .await .map(drop), Claim::Rename { from, to } => { let id = daemon .resolve(from) .await .expect("the endpoint to rename exists"); daemon.rename_endpoint(id, to, true).await.map(drop) } Claim::Import(text) => daemon.import_configuration(text, false).await.map(drop), }; let as_wanted = match (&case.want, &result) { (Outcome::Accepted, Ok(())) => true, ( Outcome::Taken(name), Err(DaemonError::Failure(FailureReason::NameConflict { name: taken })), ) => taken == name, (Outcome::Reported(text), Err(error)) => error.to_string().contains(text), _ => false, }; assert!( as_wanted, "{}: expected {:?}, got {result:?}", case.name, case.want ); for held in ["Keys", "Stage", "Booth"] { assert_eq!( holding(&daemon, held).await, 1, "{}: {held} is no longer held by exactly one endpoint", case.name ); } } } /// Proves that a hand-written configuration holding a port and a network port of one name keeps /// both and records the clash in the history as an error. /// /// Dropping either would lose part of the user's setup, and saying nothing would leave two /// identical ports with no explanation. #[tokio::test] async fn a_hand_written_port_and_network_port_of_one_name_are_kept_and_reported() { let root = common::scratch("midi-harbor-network-ports") .join(format!("hand-written-{}", uuid::Uuid::new_v4())); let paths = midi_harbor_core::paths::Paths::rooted_at(root); std::fs::create_dir_all(paths.config_dir()).expect("the configuration directory is created"); std::fs::write( paths.config_file(), "preferences:\n\ \x20 advertise_sessions: false\n\ endpoints:\n\ - name: Stage\n\ \x20 kind: virtual_port\n\ - name: Stage\n\ \x20 kind: network_port\n\ \x20 control_port: 0\n", ) .expect("the hand-written configuration is written"); let daemon = Daemon::start( paths, Arc::new(FakeMidiPlatform::new()) as Arc, ) .await .expect("the daemon starts over the hand-written configuration"); assert_eq!( holding(&daemon, "Stage").await, 2, "both endpoints named Stage are kept" ); let reported = daemon.events(None, 100).await.into_iter().any(|event| { event.severity == midi_harbor_core::events::Severity::Error && event.detail.contains("both named 'Stage'") }); assert!(reported, "the clash was not recorded in the history"); }