1use super::classifier::NntpCommand;
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum CommandAction {
12 InterceptAuth(AuthAction),
14 Reject(&'static str),
16 ForwardStateless,
18 ForwardHighThroughput,
20}
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum AuthAction {
25 RequestPassword,
27 AcceptAuth,
29}
30
31pub struct CommandHandler;
33
34impl CommandHandler {
35 pub fn handle_command(command: &str) -> CommandAction {
37 match NntpCommand::classify(command) {
38 NntpCommand::AuthUser => CommandAction::InterceptAuth(AuthAction::RequestPassword),
39 NntpCommand::AuthPass => CommandAction::InterceptAuth(AuthAction::AcceptAuth),
40 NntpCommand::Stateful => {
41 CommandAction::Reject("Command not supported by this proxy (stateless proxy mode)")
42 }
43 NntpCommand::NonRoutable => CommandAction::Reject(
44 "Command not supported by this proxy (per-command routing mode)",
45 ),
46 NntpCommand::ArticleByMessageId => CommandAction::ForwardHighThroughput,
47 NntpCommand::Stateless => CommandAction::ForwardStateless,
48 }
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn test_auth_user_command() {
58 let action = CommandHandler::handle_command("AUTHINFO USER test");
59 assert_eq!(
60 action,
61 CommandAction::InterceptAuth(AuthAction::RequestPassword)
62 );
63 }
64
65 #[test]
66 fn test_auth_pass_command() {
67 let action = CommandHandler::handle_command("AUTHINFO PASS secret");
68 assert_eq!(action, CommandAction::InterceptAuth(AuthAction::AcceptAuth));
69 }
70
71 #[test]
72 fn test_stateful_command_rejected() {
73 let action = CommandHandler::handle_command("GROUP alt.test");
74 assert!(
75 matches!(action, CommandAction::Reject(msg) if msg.contains("stateless")),
76 "Expected Reject with 'stateless' in message"
77 );
78 }
79
80 #[test]
81 fn test_article_by_message_id() {
82 let action = CommandHandler::handle_command("ARTICLE <test@example.com>");
83 assert_eq!(action, CommandAction::ForwardHighThroughput);
84 }
85
86 #[test]
87 fn test_stateless_command() {
88 let action = CommandHandler::handle_command("LIST");
89 assert_eq!(action, CommandAction::ForwardStateless);
90
91 let action = CommandHandler::handle_command("HELP");
92 assert_eq!(action, CommandAction::ForwardStateless);
93 }
94
95 #[test]
96 fn test_all_stateful_commands_rejected() {
97 let stateful_commands = vec![
99 "GROUP alt.test",
100 "NEXT",
101 "LAST",
102 "LISTGROUP alt.test",
103 "ARTICLE 123",
104 "HEAD 456",
105 "BODY 789",
106 "STAT",
107 "XOVER 1-100",
108 ];
109
110 for cmd in stateful_commands {
111 match CommandHandler::handle_command(cmd) {
112 CommandAction::Reject(msg) => {
113 assert!(msg.contains("stateless") || msg.contains("not supported"));
114 }
115 other => panic!("Expected Reject for '{}', got {:?}", cmd, other),
116 }
117 }
118 }
119
120 #[test]
121 fn test_all_article_by_msgid_as_high_throughput() {
122 let msgid_commands = vec![
124 "ARTICLE <test@example.com>",
125 "BODY <msg@server.org>",
126 "HEAD <id@host.net>",
127 "STAT <unique@domain.com>",
128 ];
129
130 for cmd in msgid_commands {
131 assert_eq!(
132 CommandHandler::handle_command(cmd),
133 CommandAction::ForwardHighThroughput,
134 "Command '{}' should be high-throughput",
135 cmd
136 );
137 }
138 }
139
140 #[test]
141 fn test_various_stateless_commands() {
142 let stateless_commands = vec![
143 "HELP",
144 "LIST",
145 "LIST ACTIVE",
146 "LIST NEWSGROUPS",
147 "DATE",
148 "CAPABILITIES",
149 "QUIT",
150 ];
151
152 for cmd in stateless_commands {
153 assert_eq!(
154 CommandHandler::handle_command(cmd),
155 CommandAction::ForwardStateless,
156 "Command '{}' should be stateless",
157 cmd
158 );
159 }
160 }
161
162 #[test]
163 fn test_case_insensitive_handling() {
164 assert_eq!(
166 CommandHandler::handle_command("list"),
167 CommandAction::ForwardStateless
168 );
169 assert_eq!(
170 CommandHandler::handle_command("LiSt"),
171 CommandAction::ForwardStateless
172 );
173 assert_eq!(
174 CommandHandler::handle_command("QUIT"),
175 CommandAction::ForwardStateless
176 );
177 assert_eq!(
178 CommandHandler::handle_command("quit"),
179 CommandAction::ForwardStateless
180 );
181 }
182
183 #[test]
184 fn test_empty_command() {
185 let action = CommandHandler::handle_command("");
187 assert_eq!(action, CommandAction::ForwardStateless);
188 }
189
190 #[test]
191 fn test_whitespace_handling() {
192 let action = CommandHandler::handle_command(" LIST ");
194 assert_eq!(action, CommandAction::ForwardStateless);
195
196 let action = CommandHandler::handle_command(" AUTHINFO USER test ");
198 assert_eq!(
199 action,
200 CommandAction::InterceptAuth(AuthAction::RequestPassword)
201 );
202 }
203
204 #[test]
205 fn test_malformed_auth_commands() {
206 let action = CommandHandler::handle_command("AUTHINFO");
208 assert_eq!(action, CommandAction::ForwardStateless);
209
210 let action = CommandHandler::handle_command("AUTHINFO INVALID");
212 assert_eq!(action, CommandAction::ForwardStateless);
213 }
214
215 #[test]
216 fn test_auth_commands_without_arguments() {
217 let action = CommandHandler::handle_command("AUTHINFO USER");
219 assert_eq!(
220 action,
221 CommandAction::InterceptAuth(AuthAction::RequestPassword)
222 );
223
224 let action = CommandHandler::handle_command("AUTHINFO PASS");
226 assert_eq!(action, CommandAction::InterceptAuth(AuthAction::AcceptAuth));
227 }
228
229 #[test]
230 fn test_article_commands_with_newlines() {
231 let action = CommandHandler::handle_command("ARTICLE <msg@test.com>\r\n");
233 assert_eq!(action, CommandAction::ForwardHighThroughput);
234
235 let action = CommandHandler::handle_command("LIST\n");
237 assert_eq!(action, CommandAction::ForwardStateless);
238 }
239
240 #[test]
241 fn test_very_long_commands() {
242 let long_cmd = format!("LIST {}", "A".repeat(10000));
244 let action = CommandHandler::handle_command(&long_cmd);
245 assert_eq!(action, CommandAction::ForwardStateless);
246
247 let long_group = format!("GROUP {}", "alt.".repeat(1000));
249 match CommandHandler::handle_command(&long_group) {
250 CommandAction::Reject(_) => {} other => panic!("Expected Reject for long GROUP, got {:?}", other),
252 }
253 }
254
255 #[test]
256 fn test_command_action_equality() {
257 assert_eq!(
259 CommandAction::ForwardStateless,
260 CommandAction::ForwardStateless
261 );
262 assert_eq!(
263 CommandAction::ForwardHighThroughput,
264 CommandAction::ForwardHighThroughput
265 );
266 assert_eq!(
267 CommandAction::InterceptAuth(AuthAction::RequestPassword),
268 CommandAction::InterceptAuth(AuthAction::RequestPassword)
269 );
270
271 assert_ne!(
273 CommandAction::ForwardStateless,
274 CommandAction::ForwardHighThroughput
275 );
276 assert_ne!(
277 CommandAction::InterceptAuth(AuthAction::RequestPassword),
278 CommandAction::InterceptAuth(AuthAction::AcceptAuth)
279 );
280 }
281
282 #[test]
283 fn test_reject_messages() {
284 assert!(
286 matches!(
287 CommandHandler::handle_command("GROUP alt.test"),
288 CommandAction::Reject(msg) if !msg.is_empty() && msg.len() > 10
289 ),
290 "Expected Reject with meaningful message"
291 );
292 }
293
294 #[test]
295 fn test_unknown_commands_forwarded() {
296 let unknown_commands = ["INVALIDCOMMAND", "XYZABC", "RANDOM DATA", "12345"];
299
300 assert!(
301 unknown_commands.iter().all(|cmd| {
302 CommandHandler::handle_command(cmd) == CommandAction::ForwardStateless
303 }),
304 "All unknown commands should be forwarded as stateless"
305 );
306 }
307
308 #[test]
309 fn test_non_routable_commands_rejected() {
310 assert!(
312 matches!(
313 CommandHandler::handle_command("POST"),
314 CommandAction::Reject(msg) if msg.contains("routing")
315 ),
316 "Expected Reject for POST"
317 );
318
319 assert!(
321 matches!(
322 CommandHandler::handle_command("IHAVE <test@example.com>"),
323 CommandAction::Reject(msg) if msg.contains("routing")
324 ),
325 "Expected Reject for IHAVE"
326 );
327
328 assert!(
330 matches!(
331 CommandHandler::handle_command("NEWGROUPS 20240101 000000 GMT"),
332 CommandAction::Reject(msg) if msg.contains("routing")
333 ),
334 "Expected Reject for NEWGROUPS"
335 );
336
337 assert!(
339 matches!(
340 CommandHandler::handle_command("NEWNEWS * 20240101 000000 GMT"),
341 CommandAction::Reject(msg) if msg.contains("routing")
342 ),
343 "Expected Reject for NEWNEWS"
344 );
345 }
346
347 #[test]
348 fn test_reject_message_content() {
349 let CommandAction::Reject(stateful_reject) =
351 CommandHandler::handle_command("GROUP alt.test")
352 else {
353 panic!("Expected Reject")
354 };
355
356 let CommandAction::Reject(routing_reject) = CommandHandler::handle_command("POST") else {
357 panic!("Expected Reject")
358 };
359
360 assert!(stateful_reject.contains("stateless"));
362 assert!(routing_reject.contains("routing"));
363 assert_ne!(stateful_reject, routing_reject);
364 }
365}