|
| 1 | +//! `connection().connect(auth)` (`connect({auth})` in |
| 2 | +//! `packages/zero-client/src/client/connection.ts`): a token a client |
| 3 | +//! without a user can't take is an error before anything happens, and |
| 4 | +//! clearing the token really drops it. |
| 5 | +
|
| 6 | +use std::rc::Rc; |
| 7 | +use std::sync::Arc; |
| 8 | +use zero_client::client::{ |
| 9 | + ClientErrorKind, ConnectAuth, ConnectionError, ConnectionStatus, Error, Zero, ZeroOptions, |
| 10 | +}; |
| 11 | +use zero_client::json::{self, Value}; |
| 12 | +use zero_client::protocol::sec_protocols; |
| 13 | +use zero_client::schema::Schema; |
| 14 | +use zero_client::testing::{ManualScheduler, MockSocket, MockSocketFactory, RecordingLogSink}; |
| 15 | + |
| 16 | +struct Harness { |
| 17 | + zero: Zero, |
| 18 | + scheduler: Rc<ManualScheduler>, |
| 19 | + factory: Rc<MockSocketFactory>, |
| 20 | +} |
| 21 | + |
| 22 | +impl Harness { |
| 23 | + fn new(user_id: Option<&str>, auth: Option<&str>) -> Self { |
| 24 | + let schema = Schema::from_json( |
| 25 | + &json::parse( |
| 26 | + r#"{"tables": {"issue": {"name": "issue", "primaryKey": ["id"], |
| 27 | + "columns": {"id": {"type": "string"}}}}}"#, |
| 28 | + ) |
| 29 | + .unwrap(), |
| 30 | + ) |
| 31 | + .unwrap(); |
| 32 | + let scheduler = ManualScheduler::new(1_700_000_000_000.0); |
| 33 | + let factory = MockSocketFactory::new(); |
| 34 | + let options = ZeroOptions { |
| 35 | + cache_url: Some("http://zero.test".into()), |
| 36 | + user_id: user_id.map(str::to_owned), |
| 37 | + auth: auth.map(str::to_owned), |
| 38 | + log_sink: Some(Rc::new(RecordingLogSink::default())), |
| 39 | + ..ZeroOptions::default() |
| 40 | + }; |
| 41 | + let zero = Zero::new( |
| 42 | + Arc::new(schema), |
| 43 | + (), |
| 44 | + vec![], |
| 45 | + options, |
| 46 | + scheduler.clone(), |
| 47 | + Some(factory.clone()), |
| 48 | + ) |
| 49 | + .unwrap(); |
| 50 | + let h = Harness { |
| 51 | + zero, |
| 52 | + scheduler, |
| 53 | + factory, |
| 54 | + }; |
| 55 | + h.settle(); |
| 56 | + h |
| 57 | + } |
| 58 | + |
| 59 | + fn settle(&self) { |
| 60 | + self.zero.dispatch(|| {}); |
| 61 | + self.scheduler.run_due_timers(); |
| 62 | + } |
| 63 | + |
| 64 | + fn socket(&self) -> Rc<MockSocket> { |
| 65 | + self.factory.sockets().last().cloned().expect("a socket") |
| 66 | + } |
| 67 | + |
| 68 | + fn connected(&self) { |
| 69 | + self.socket().receive_text(r#"["connected",{"wsid":"w"}]"#); |
| 70 | + self.settle(); |
| 71 | + assert_eq!(self.status(), ConnectionStatus::Connected); |
| 72 | + } |
| 73 | + |
| 74 | + fn unauthorized(&self) { |
| 75 | + self.socket().receive_text( |
| 76 | + r#"["error",{"kind":"Unauthorized","message":"no","origin":"zeroCache"}]"#, |
| 77 | + ); |
| 78 | + self.settle(); |
| 79 | + assert_eq!(self.status(), ConnectionStatus::NeedsAuth); |
| 80 | + } |
| 81 | + |
| 82 | + fn status(&self) -> ConnectionStatus { |
| 83 | + self.zero.connection().state().status() |
| 84 | + } |
| 85 | + |
| 86 | + fn update_auths(&self) -> Vec<String> { |
| 87 | + self.socket() |
| 88 | + .messages() |
| 89 | + .into_iter() |
| 90 | + .filter(|m| m.starts_with(r#"["updateAuth""#)) |
| 91 | + .collect() |
| 92 | + } |
| 93 | + |
| 94 | + /// The `authToken` the last socket opened with. |
| 95 | + fn auth_token(&self) -> Option<String> { |
| 96 | + let protocol = |
| 97 | + sec_protocols::decode(self.socket().sec_protocol.as_deref().unwrap()).unwrap(); |
| 98 | + protocol |
| 99 | + .get("authToken") |
| 100 | + .and_then(Value::as_str) |
| 101 | + .map(str::to_owned) |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +fn rejection(r: Result<(), Error>) -> ConnectionError { |
| 106 | + match r { |
| 107 | + Err(Error::Connection(e)) => e, |
| 108 | + r => panic!("not a connection error: {r:?}"), |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +#[test] |
| 113 | +fn an_invalid_pair_is_an_error_and_clear_drops_the_token() { |
| 114 | + // A client without a user can't take a token: JS's `#setAuth` throws, |
| 115 | + // so `connect` rejects before it does anything. |
| 116 | + let h = Harness::new(None, None); |
| 117 | + h.connected(); |
| 118 | + let expected = ConnectionError::client( |
| 119 | + ClientErrorKind::Internal, |
| 120 | + "ZeroOptions.userID is required when auth is set.", |
| 121 | + ); |
| 122 | + assert_eq!( |
| 123 | + rejection(h.zero.connection().connect(ConnectAuth::Set("t".into()))), |
| 124 | + expected |
| 125 | + ); |
| 126 | + h.settle(); |
| 127 | + assert!(h.update_auths().is_empty(), "no updateAuth while connected"); |
| 128 | + h.unauthorized(); |
| 129 | + assert_eq!( |
| 130 | + rejection(h.zero.connection().connect(ConnectAuth::Set("t".into()))), |
| 131 | + expected |
| 132 | + ); |
| 133 | + h.settle(); |
| 134 | + assert_eq!(h.factory.sockets().len(), 1, "no reconnect"); |
| 135 | + assert_eq!(h.status(), ConnectionStatus::NeedsAuth); |
| 136 | + // An empty token is no token, so it is valid. |
| 137 | + h.zero |
| 138 | + .connection() |
| 139 | + .connect(ConnectAuth::Set(String::new())) |
| 140 | + .unwrap(); |
| 141 | + h.settle(); |
| 142 | + assert_eq!(h.factory.sockets().len(), 2); |
| 143 | + assert_eq!(h.auth_token(), None); |
| 144 | + |
| 145 | + // `auth: null` clears the token: it sends no `updateAuth`, and the |
| 146 | + // next connection carries none. |
| 147 | + let h = Harness::new(Some("u"), Some("t1")); |
| 148 | + assert_eq!(h.auth_token().as_deref(), Some("t1")); |
| 149 | + h.connected(); |
| 150 | + h.zero |
| 151 | + .connection() |
| 152 | + .connect(ConnectAuth::Set("t2".into())) |
| 153 | + .unwrap(); |
| 154 | + h.zero.connection().connect(ConnectAuth::Clear).unwrap(); |
| 155 | + h.settle(); |
| 156 | + assert_eq!(h.update_auths(), [r#"["updateAuth",{"auth":"t2"}]"#]); |
| 157 | + h.unauthorized(); |
| 158 | + h.zero.connection().connect(ConnectAuth::Keep).unwrap(); |
| 159 | + h.settle(); |
| 160 | + assert_eq!(h.factory.sockets().len(), 2); |
| 161 | + assert_eq!(h.auth_token(), None); |
| 162 | +} |
0 commit comments