1use tokio::sync::mpsc::Sender;
2
3pub trait Phone: Send {
4 fn register(&self, aor: &str, regint: u32);
5 fn dial(&self, number: &str);
6 fn hangup(&self);
7 fn hangup_all(&self);
8 fn accept(&self);
9 fn hold(&self);
10 fn resume(&self);
11 fn mute(&self);
12 fn silence_alert(&self) {}
17 fn send_dtmf(&self, digit: char);
18 fn switch_line(&self, line: usize);
19 fn select_call(&self, call_id: &str) {
23 let _ = call_id;
24 }
25 fn transfer(&self, uri: &str);
26 fn attended_transfer_start(&self, uri: &str);
27 fn attended_transfer_exec(&self);
28 fn attended_transfer_abort(&self);
29 fn add_header(&self, key: &str, value: &str);
30 fn rm_header(&self, key: &str);
31 fn set_audio_source(&self, spec: &str);
35
36 fn media_stats(&self) -> Option<crate::event::MediaStats> {
40 None
41 }
42
43 fn audio_codec(&self) -> Option<crate::event::CodecInfo> {
46 None
47 }
48
49 fn received_dtmf(&self) -> String {
52 String::new()
53 }
54
55 fn inbound_headers(&self, call_id: &str) -> Vec<(String, String)> {
59 let _ = call_id;
60 Vec::new()
61 }
62
63 fn arm_invite_response(&self, scode: u16, reason: &str, headers: Vec<String>);
69
70 fn disarm_invite_response(&self);
72
73 fn deflect_incoming(&self, contact: &str, diversion: Option<&str>) {
77 let mut headers = vec![format!("Contact: <{contact}>")];
78 if let Some(div) = diversion {
79 headers.push(format!("Diversion: <{div}>"));
80 }
81 self.arm_invite_response(302, "Moved Temporarily", headers);
82 }
83}
84
85pub struct MockPhone {
93 cmd_tx: Sender<(String, String)>,
94}
95
96impl MockPhone {
97 pub fn new(cmd_tx: Sender<(String, String)>) -> Self {
98 Self { cmd_tx }
99 }
100
101 fn send(&self, cmd: &str, params: &str) {
102 if let Err(e) = self.cmd_tx.try_send((cmd.to_string(), params.to_string())) {
103 crate::rlog!(Warn, "cmd dropped: {} ({})", cmd, e);
104 }
105 }
106}
107
108impl Phone for MockPhone {
109 fn register(&self, _aor: &str, regint: u32) {
110 self.send("uareg", &format!("{} 0", regint));
111 }
112 fn dial(&self, number: &str) {
113 self.send("dial", number);
114 }
115 fn hangup(&self) {
116 self.send("hangup", "");
117 }
118 fn hangup_all(&self) {
119 self.send("hangupall", "");
120 }
121 fn accept(&self) {
122 self.send("accept", "");
123 }
124 fn hold(&self) {
125 self.send("hold", "");
126 }
127 fn resume(&self) {
128 self.send("resume", "");
129 }
130 fn mute(&self) {
131 self.send("mute", "");
132 }
133 fn silence_alert(&self) {
134 self.send("silence", "");
135 }
136 fn send_dtmf(&self, digit: char) {
137 self.send("sndcode", &digit.to_string());
138 }
139 fn switch_line(&self, line: usize) {
140 self.send("line", &line.to_string());
141 }
142 fn transfer(&self, uri: &str) {
143 self.send("transfer", uri);
144 }
145 fn attended_transfer_start(&self, uri: &str) {
146 self.send("atransferstart", uri);
147 }
148 fn attended_transfer_exec(&self) {
149 self.send("atransferexec", "");
150 }
151 fn attended_transfer_abort(&self) {
152 self.send("atransferabort", "");
153 }
154 fn add_header(&self, key: &str, value: &str) {
155 self.send(
156 "uaaddheader",
157 &format!("{}={} 0", key, uri_header_escape(value)),
158 );
159 }
160 fn rm_header(&self, key: &str) {
161 self.send("uarmheader", &format!("{} 0", key));
162 }
163 fn set_audio_source(&self, spec: &str) {
164 self.send("ausrc", spec);
165 }
166 fn arm_invite_response(&self, scode: u16, reason: &str, headers: Vec<String>) {
167 self.send(
168 "armresponse",
169 &format!("{scode} {reason} [{}]", headers.join("; ")),
170 );
171 }
172 fn disarm_invite_response(&self) {
173 self.send("disarmresponse", "");
174 }
175}
176
177fn uri_header_escape(s: &str) -> String {
185 let mut out = String::with_capacity(s.len());
186 for &b in s.as_bytes() {
187 if is_hvalue(b) {
188 out.push(b as char);
189 } else {
190 out.push_str(&format!("%{:02X}", b));
191 }
192 }
193 out
194}
195
196fn is_hvalue(b: u8) -> bool {
197 b.is_ascii_alphanumeric()
198 || matches!(
199 b,
200 b'-' | b'_'
201 | b'.'
202 | b'!'
203 | b'~'
204 | b'*'
205 | b'\''
206 | b'('
207 | b')'
208 | b'['
209 | b']'
210 | b'/'
211 | b'?'
212 | b':'
213 | b'+'
214 | b'$'
215 )
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn passes_through_hvalue_chars() {
224 assert_eq!(uri_header_escape("Foo-Bar_1.0"), "Foo-Bar_1.0");
225 assert_eq!(uri_header_escape("a[b]/c?d:e+f$"), "a[b]/c?d:e+f$");
226 }
227
228 #[test]
229 fn escapes_spaces_and_specials() {
230 assert_eq!(uri_header_escape("Foo Bar"), "Foo%20Bar");
231 assert_eq!(uri_header_escape("a=b;c,d"), "a%3Db%3Bc%2Cd");
232 assert_eq!(uri_header_escape("100%"), "100%25");
233 }
234
235 #[test]
236 fn escapes_non_ascii() {
237 assert_eq!(uri_header_escape("ä"), "%C3%A4");
238 }
239
240 fn make_phone() -> (MockPhone, tokio::sync::mpsc::Receiver<(String, String)>) {
241 let (tx, rx) = tokio::sync::mpsc::channel(8);
242 (MockPhone::new(tx), rx)
243 }
244
245 #[test]
246 fn add_header_emits_uaaddheader_with_ua_index_zero() {
247 let (phone, mut rx) = make_phone();
248 phone.add_header("X-Foo", "bar");
249 let (cmd, params) = rx.try_recv().expect("one message");
250 assert_eq!(cmd, "uaaddheader");
251 assert_eq!(params, "X-Foo=bar 0");
252 }
253
254 #[test]
255 fn add_header_encodes_unsafe_chars_in_value() {
256 let (phone, mut rx) = make_phone();
257 phone.add_header("History-Info", "<sip:1@x.com>;index=1");
258 let (_, params) = rx.try_recv().expect("one message");
259 assert_eq!(params, "History-Info=%3Csip:1%40x.com%3E%3Bindex%3D1 0");
260 }
261
262 #[test]
263 fn add_header_preserves_order_across_multiple_calls() {
264 let (phone, mut rx) = make_phone();
265 phone.add_header("History-Info", "<sip:1@x.com>;index=1");
266 phone.add_header("History-Info", "<sip:2@x.com>;index=2");
267 phone.add_header("X-Other", "hi");
268
269 let msgs: Vec<_> = std::iter::from_fn(|| rx.try_recv().ok()).collect();
270 assert_eq!(msgs.len(), 3);
271 assert_eq!(msgs[0].1, "History-Info=%3Csip:1%40x.com%3E%3Bindex%3D1 0");
272 assert_eq!(msgs[1].1, "History-Info=%3Csip:2%40x.com%3E%3Bindex%3D2 0");
273 assert_eq!(msgs[2].1, "X-Other=hi 0");
274 }
275}