tlq_client/client.rs
1use crate::{
2 config::{Config, ConfigBuilder},
3 error::{Result, TlqError},
4 message::*,
5 retry::RetryStrategy,
6};
7use serde::{de::DeserializeOwned, Serialize};
8use std::time::Duration;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpStream;
11use tokio::time::timeout;
12use uuid::Uuid;
13
14const MAX_MESSAGE_SIZE: usize = 65536;
15
16/// The main client for interacting with TLQ (Tiny Little Queue) servers.
17///
18/// `TlqClient` provides an async, type-safe interface for all TLQ operations including
19/// adding messages, retrieving messages, and managing queue state. The client handles
20/// automatic retry with exponential backoff for transient failures.
21///
22/// # Examples
23///
24/// Basic usage:
25/// ```no_run
26/// use tlq_client::TlqClient;
27///
28/// #[tokio::main]
29/// async fn main() -> Result<(), tlq_client::TlqError> {
30/// let client = TlqClient::new("localhost", 1337)?;
31///
32/// // Add a message
33/// let message = client.add_message("Hello, World!").await?;
34/// println!("Added message: {}", message.id);
35///
36/// // Get messages
37/// let messages = client.get_messages(1).await?;
38/// if let Some(msg) = messages.first() {
39/// println!("Retrieved: {}", msg.body);
40/// }
41///
42/// Ok(())
43/// }
44/// ```
45pub struct TlqClient {
46 config: Config,
47 base_url: String,
48}
49
50impl TlqClient {
51 /// Creates a new TLQ client with default configuration.
52 ///
53 /// This is the simplest way to create a client, using default values for
54 /// timeout (30s), max retries (3), and retry delay (100ms).
55 ///
56 /// # Arguments
57 ///
58 /// * `host` - The hostname or IP address of the TLQ server
59 /// * `port` - The port number of the TLQ server
60 ///
61 /// # Examples
62 ///
63 /// ```no_run
64 /// use tlq_client::TlqClient;
65 ///
66 /// # fn example() -> Result<(), tlq_client::TlqError> {
67 /// let client = TlqClient::new("localhost", 1337)?;
68 /// # Ok(())
69 /// # }
70 /// ```
71 ///
72 /// # Errors
73 ///
74 /// Currently this method always returns `Ok`, but the `Result` is preserved
75 /// for future compatibility.
76 pub fn new(host: impl Into<String>, port: u16) -> Result<Self> {
77 let config = ConfigBuilder::new().host(host).port(port).build();
78
79 Ok(Self::with_config(config))
80 }
81
82 /// Creates a new TLQ client with custom configuration.
83 ///
84 /// Use this method when you need to customize timeout, retry behavior,
85 /// or other client settings.
86 ///
87 /// # Arguments
88 ///
89 /// * `config` - A [`Config`] instance with your desired settings
90 ///
91 /// # Examples
92 ///
93 /// ```no_run
94 /// use tlq_client::{TlqClient, ConfigBuilder};
95 /// use std::time::Duration;
96 ///
97 /// # fn example() {
98 /// let config = ConfigBuilder::new()
99 /// .host("queue.example.com")
100 /// .port(8080)
101 /// .timeout(Duration::from_secs(5))
102 /// .max_retries(2)
103 /// .build();
104 ///
105 /// let client = TlqClient::with_config(config);
106 /// # }
107 /// ```
108 pub fn with_config(config: Config) -> Self {
109 let base_url = format!("{}:{}", config.host, config.port);
110 Self { config, base_url }
111 }
112
113 /// Returns a [`ConfigBuilder`] for creating custom configurations.
114 ///
115 /// This is a convenience method that's equivalent to [`ConfigBuilder::new()`].
116 ///
117 /// # Examples
118 ///
119 /// ```no_run
120 /// use tlq_client::TlqClient;
121 /// use std::time::Duration;
122 ///
123 /// # fn example() {
124 /// let client = TlqClient::with_config(
125 /// TlqClient::builder()
126 /// .host("localhost")
127 /// .port(1337)
128 /// .timeout(Duration::from_secs(10))
129 /// .build()
130 /// );
131 /// # }
132 /// ```
133 pub fn builder() -> ConfigBuilder {
134 ConfigBuilder::new()
135 }
136
137 async fn request<T, R>(&self, endpoint: &str, body: &T) -> Result<R>
138 where
139 T: Serialize,
140 R: DeserializeOwned,
141 {
142 let retry_strategy = RetryStrategy::new(self.config.max_retries, self.config.retry_delay);
143
144 retry_strategy
145 .execute(|| async { self.single_request(endpoint, body).await })
146 .await
147 }
148
149 async fn single_request<T, R>(&self, endpoint: &str, body: &T) -> Result<R>
150 where
151 T: Serialize,
152 R: DeserializeOwned,
153 {
154 let json_body = serde_json::to_vec(body)?;
155
156 let request = format!(
157 "POST {} HTTP/1.1\r\n\
158 Host: {}\r\n\
159 Content-Type: application/json\r\n\
160 Content-Length: {}\r\n\
161 Connection: close\r\n\
162 \r\n",
163 endpoint,
164 self.base_url,
165 json_body.len()
166 );
167
168 let mut stream = timeout(self.config.timeout, TcpStream::connect(&self.base_url))
169 .await
170 .map_err(|_| TlqError::Timeout(self.config.timeout.as_millis() as u64))?
171 .map_err(|e| TlqError::Connection(e.to_string()))?;
172
173 stream.write_all(request.as_bytes()).await?;
174 stream.write_all(&json_body).await?;
175 stream.flush().await?;
176
177 let mut response = Vec::new();
178 stream.read_to_end(&mut response).await?;
179
180 let response_str = String::from_utf8_lossy(&response);
181 let body = Self::parse_http_response(&response_str)?;
182 serde_json::from_str(body).map_err(Into::into)
183 }
184
185 /// Performs a health check against the TLQ server.
186 ///
187 /// This method sends a GET request to the `/hello` endpoint to verify
188 /// that the server is responding. It uses a fixed 5-second timeout
189 /// regardless of the client's configured timeout.
190 ///
191 /// # Returns
192 ///
193 /// * `Ok(true)` if the server responds with HTTP 200 OK
194 /// * `Ok(false)` if the server responds but not with 200 OK
195 /// * `Err` if there's a connection error or timeout
196 ///
197 /// # Examples
198 ///
199 /// ```no_run
200 /// use tlq_client::TlqClient;
201 ///
202 /// #[tokio::main]
203 /// async fn main() -> Result<(), tlq_client::TlqError> {
204 /// let client = TlqClient::new("localhost", 1337)?;
205 ///
206 /// if client.health_check().await? {
207 /// println!("Server is healthy");
208 /// } else {
209 /// println!("Server is not responding correctly");
210 /// }
211 ///
212 /// Ok(())
213 /// }
214 /// ```
215 ///
216 /// # Errors
217 ///
218 /// Returns [`TlqError::Connection`] for network issues, or [`TlqError::Timeout`]
219 /// if the server doesn't respond within 5 seconds.
220 pub async fn health_check(&self) -> Result<bool> {
221 let mut stream = timeout(Duration::from_secs(5), TcpStream::connect(&self.base_url))
222 .await
223 .map_err(|_| TlqError::Timeout(5000))?
224 .map_err(|e| TlqError::Connection(e.to_string()))?;
225
226 let request = format!(
227 "GET /hello HTTP/1.1\r\n\
228 Host: {}\r\n\
229 Connection: close\r\n\
230 \r\n",
231 self.base_url
232 );
233
234 stream.write_all(request.as_bytes()).await?;
235 stream.flush().await?;
236
237 let mut response = Vec::new();
238 stream.read_to_end(&mut response).await?;
239
240 let response_str = String::from_utf8_lossy(&response);
241 Ok(response_str.contains("200 OK"))
242 }
243
244 /// Adds a new message to the TLQ server.
245 ///
246 /// The message will be assigned a UUID v7 identifier and placed in the queue
247 /// with state [`MessageState::Ready`]. Messages have a maximum size limit of 64KB.
248 ///
249 /// # Arguments
250 ///
251 /// * `body` - The message content (any type that can be converted to String)
252 ///
253 /// # Returns
254 ///
255 /// Returns the created [`Message`] with its assigned ID and metadata.
256 ///
257 /// # Examples
258 ///
259 /// ```no_run
260 /// use tlq_client::TlqClient;
261 ///
262 /// #[tokio::main]
263 /// async fn main() -> Result<(), tlq_client::TlqError> {
264 /// let client = TlqClient::new("localhost", 1337)?;
265 ///
266 /// // Add a simple string message
267 /// let message = client.add_message("Hello, World!").await?;
268 /// println!("Created message {} with body: {}", message.id, message.body);
269 ///
270 /// // Add a formatted message
271 /// let user_data = "important data";
272 /// let message = client.add_message(format!("Processing: {}", user_data)).await?;
273 ///
274 /// Ok(())
275 /// }
276 /// ```
277 ///
278 /// # Errors
279 ///
280 /// * [`TlqError::MessageTooLarge`] if the message exceeds 64KB (65,536 bytes)
281 /// * [`TlqError::Connection`] for network connectivity issues
282 /// * [`TlqError::Timeout`] if the request times out
283 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
284 pub async fn add_message(&self, body: impl Into<String>) -> Result<Message> {
285 let body = body.into();
286
287 if body.len() > MAX_MESSAGE_SIZE {
288 return Err(TlqError::MessageTooLarge { size: body.len() });
289 }
290
291 let request = AddMessageRequest { body };
292 let message: Message = self.request("/add", &request).await?;
293 Ok(message)
294 }
295
296 /// Retrieves multiple messages from the TLQ server.
297 ///
298 /// This method fetches up to `count` messages from the queue. Messages are returned
299 /// in the order they were added and their state is changed to [`MessageState::Processing`].
300 /// The server may return fewer messages than requested if there are not enough
301 /// messages in the queue.
302 ///
303 /// # Arguments
304 ///
305 /// * `count` - Maximum number of messages to retrieve (must be greater than 0)
306 ///
307 /// # Returns
308 ///
309 /// Returns a vector of [`Message`] objects. The vector may be empty if no messages
310 /// are available in the queue.
311 ///
312 /// # Examples
313 ///
314 /// ```no_run
315 /// use tlq_client::TlqClient;
316 ///
317 /// #[tokio::main]
318 /// async fn main() -> Result<(), tlq_client::TlqError> {
319 /// let client = TlqClient::new("localhost", 1337)?;
320 ///
321 /// // Get up to 5 messages from the queue
322 /// let messages = client.get_messages(5).await?;
323 ///
324 /// for message in messages {
325 /// println!("Processing message {}: {}", message.id, message.body);
326 ///
327 /// // Process the message...
328 ///
329 /// // Delete when done
330 /// client.delete_message(message.id).await?;
331 /// }
332 ///
333 /// Ok(())
334 /// }
335 /// ```
336 ///
337 /// # Errors
338 ///
339 /// * [`TlqError::Validation`] if count is 0
340 /// * [`TlqError::Connection`] for network connectivity issues
341 /// * [`TlqError::Timeout`] if the request times out
342 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
343 pub async fn get_messages(&self, count: u32) -> Result<Vec<Message>> {
344 if count == 0 {
345 return Err(TlqError::Validation(
346 "Count must be greater than 0".to_string(),
347 ));
348 }
349
350 let request = GetMessagesRequest { count };
351 let messages: Vec<Message> = self.request("/get", &request).await?;
352 Ok(messages)
353 }
354
355 /// Retrieves a single message from the TLQ server.
356 ///
357 /// This is a convenience method equivalent to calling [`get_messages(1)`](Self::get_messages)
358 /// and taking the first result. If no messages are available, returns `None`.
359 ///
360 /// # Returns
361 ///
362 /// * `Ok(Some(message))` if a message was retrieved
363 /// * `Ok(None)` if no messages are available in the queue
364 /// * `Err` for connection or server errors
365 ///
366 /// # Examples
367 ///
368 /// ```no_run
369 /// use tlq_client::TlqClient;
370 ///
371 /// #[tokio::main]
372 /// async fn main() -> Result<(), tlq_client::TlqError> {
373 /// let client = TlqClient::new("localhost", 1337)?;
374 ///
375 /// // Get a single message
376 /// match client.get_message().await? {
377 /// Some(message) => {
378 /// println!("Got message: {}", message.body);
379 /// client.delete_message(message.id).await?;
380 /// }
381 /// None => println!("No messages available"),
382 /// }
383 ///
384 /// Ok(())
385 /// }
386 /// ```
387 ///
388 /// # Errors
389 ///
390 /// * [`TlqError::Connection`] for network connectivity issues
391 /// * [`TlqError::Timeout`] if the request times out
392 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
393 pub async fn get_message(&self) -> Result<Option<Message>> {
394 let messages = self.get_messages(1).await?;
395 Ok(messages.into_iter().next())
396 }
397
398 /// Deletes a single message from the TLQ server.
399 ///
400 /// This is a convenience method that calls [`delete_messages`](Self::delete_messages)
401 /// with a single message ID.
402 ///
403 /// # Arguments
404 ///
405 /// * `id` - The UUID of the message to delete
406 ///
407 /// # Returns
408 ///
409 /// Returns a string indicating the result of the operation (typically "Success" or a count).
410 ///
411 /// # Examples
412 ///
413 /// ```no_run
414 /// use tlq_client::TlqClient;
415 ///
416 /// #[tokio::main]
417 /// async fn main() -> Result<(), tlq_client::TlqError> {
418 /// let client = TlqClient::new("localhost", 1337)?;
419 ///
420 /// if let Some(message) = client.get_message().await? {
421 /// let result = client.delete_message(message.id).await?;
422 /// println!("Delete result: {}", result);
423 /// }
424 ///
425 /// Ok(())
426 /// }
427 /// ```
428 ///
429 /// # Errors
430 ///
431 /// * [`TlqError::Connection`] for network connectivity issues
432 /// * [`TlqError::Timeout`] if the request times out
433 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
434 pub async fn delete_message(&self, id: Uuid) -> Result<String> {
435 self.delete_messages(&[id]).await
436 }
437
438 /// Deletes multiple messages from the TLQ server.
439 ///
440 /// This method removes the specified messages from the queue permanently.
441 /// Messages can be in any state when deleted.
442 ///
443 /// # Arguments
444 ///
445 /// * `ids` - A slice of message UUIDs to delete (must not be empty)
446 ///
447 /// # Returns
448 ///
449 /// Returns a string indicating the number of messages deleted or "Success".
450 ///
451 /// # Examples
452 ///
453 /// ```no_run
454 /// use tlq_client::TlqClient;
455 ///
456 /// #[tokio::main]
457 /// async fn main() -> Result<(), tlq_client::TlqError> {
458 /// let client = TlqClient::new("localhost", 1337)?;
459 ///
460 /// let messages = client.get_messages(3).await?;
461 /// if !messages.is_empty() {
462 /// let ids: Vec<_> = messages.iter().map(|m| m.id).collect();
463 /// let result = client.delete_messages(&ids).await?;
464 /// println!("Deleted {} messages", result);
465 /// }
466 ///
467 /// Ok(())
468 /// }
469 /// ```
470 ///
471 /// # Errors
472 ///
473 /// * [`TlqError::Validation`] if the `ids` slice is empty
474 /// * [`TlqError::Connection`] for network connectivity issues
475 /// * [`TlqError::Timeout`] if the request times out
476 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
477 pub async fn delete_messages(&self, ids: &[Uuid]) -> Result<String> {
478 if ids.is_empty() {
479 return Err(TlqError::Validation("No message IDs provided".to_string()));
480 }
481
482 let request = DeleteMessagesRequest { ids: ids.to_vec() };
483 let response: String = self.request("/delete", &request).await?;
484 Ok(response)
485 }
486
487 /// Retries a single failed message on the TLQ server.
488 ///
489 /// This is a convenience method that calls [`retry_messages`](Self::retry_messages)
490 /// with a single message ID. The message state will be changed from
491 /// [`MessageState::Failed`] back to [`MessageState::Ready`].
492 ///
493 /// # Arguments
494 ///
495 /// * `id` - The UUID of the message to retry
496 ///
497 /// # Returns
498 ///
499 /// Returns a string indicating the result of the operation (typically "Success" or a count).
500 ///
501 /// # Examples
502 ///
503 /// ```no_run
504 /// use tlq_client::{TlqClient, MessageState};
505 ///
506 /// #[tokio::main]
507 /// async fn main() -> Result<(), tlq_client::TlqError> {
508 /// let client = TlqClient::new("localhost", 1337)?;
509 ///
510 /// // Find failed messages and retry them
511 /// let messages = client.get_messages(10).await?;
512 /// for message in messages {
513 /// if message.state == MessageState::Failed {
514 /// let result = client.retry_message(message.id).await?;
515 /// println!("Retry result: {}", result);
516 /// }
517 /// }
518 ///
519 /// Ok(())
520 /// }
521 /// ```
522 ///
523 /// # Errors
524 ///
525 /// * [`TlqError::Connection`] for network connectivity issues
526 /// * [`TlqError::Timeout`] if the request times out
527 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
528 pub async fn retry_message(&self, id: Uuid) -> Result<String> {
529 self.retry_messages(&[id]).await
530 }
531
532 /// Retries multiple failed messages on the TLQ server.
533 ///
534 /// This method changes the state of the specified messages from [`MessageState::Failed`]
535 /// back to [`MessageState::Ready`], making them available for processing again.
536 /// The retry count for each message will be incremented.
537 ///
538 /// # Arguments
539 ///
540 /// * `ids` - A slice of message UUIDs to retry (must not be empty)
541 ///
542 /// # Returns
543 ///
544 /// Returns a string indicating the number of messages retried or "Success".
545 ///
546 /// # Examples
547 ///
548 /// ```no_run
549 /// use tlq_client::{TlqClient, MessageState};
550 ///
551 /// #[tokio::main]
552 /// async fn main() -> Result<(), tlq_client::TlqError> {
553 /// let client = TlqClient::new("localhost", 1337)?;
554 ///
555 /// // Get all messages and retry the failed ones
556 /// let messages = client.get_messages(100).await?;
557 /// let failed_ids: Vec<_> = messages
558 /// .iter()
559 /// .filter(|m| m.state == MessageState::Failed)
560 /// .map(|m| m.id)
561 /// .collect();
562 ///
563 /// if !failed_ids.is_empty() {
564 /// let result = client.retry_messages(&failed_ids).await?;
565 /// println!("Retried {} failed messages", result);
566 /// }
567 ///
568 /// Ok(())
569 /// }
570 /// ```
571 ///
572 /// # Errors
573 ///
574 /// * [`TlqError::Validation`] if the `ids` slice is empty
575 /// * [`TlqError::Connection`] for network connectivity issues
576 /// * [`TlqError::Timeout`] if the request times out
577 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
578 pub async fn retry_messages(&self, ids: &[Uuid]) -> Result<String> {
579 if ids.is_empty() {
580 return Err(TlqError::Validation("No message IDs provided".to_string()));
581 }
582
583 let request = RetryMessagesRequest { ids: ids.to_vec() };
584 let response: String = self.request("/retry", &request).await?;
585 Ok(response)
586 }
587
588 /// Removes all messages from the TLQ server queue.
589 ///
590 /// This method permanently deletes all messages in the queue regardless of their state.
591 /// Use with caution as this operation cannot be undone.
592 ///
593 /// # Returns
594 ///
595 /// Returns a string indicating the result of the operation (typically "Success").
596 ///
597 /// # Examples
598 ///
599 /// ```no_run
600 /// use tlq_client::TlqClient;
601 ///
602 /// #[tokio::main]
603 /// async fn main() -> Result<(), tlq_client::TlqError> {
604 /// let client = TlqClient::new("localhost", 1337)?;
605 ///
606 /// // Clear all messages from the queue
607 /// let result = client.purge_queue().await?;
608 /// println!("Purge result: {}", result);
609 ///
610 /// Ok(())
611 /// }
612 /// ```
613 ///
614 /// # Errors
615 ///
616 /// * [`TlqError::Connection`] for network connectivity issues
617 /// * [`TlqError::Timeout`] if the request times out
618 /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
619 pub async fn purge_queue(&self) -> Result<String> {
620 let response: String = self.request("/purge", &serde_json::json!({})).await?;
621 Ok(response)
622 }
623
624 // Helper function to parse HTTP response - extracted for testing
625 fn parse_http_response(response: &str) -> Result<&str> {
626 if let Some(body_start) = response.find("\r\n\r\n") {
627 let headers = &response[..body_start];
628 let body = &response[body_start + 4..];
629
630 if let Some(status_line) = headers.lines().next() {
631 let parts: Vec<&str> = status_line.split_whitespace().collect();
632 if parts.len() >= 2 {
633 if let Ok(status_code) = parts[1].parse::<u16>() {
634 if status_code >= 400 {
635 return Err(TlqError::Server {
636 status: status_code,
637 message: body.to_string(),
638 });
639 }
640 }
641 }
642 }
643
644 Ok(body)
645 } else {
646 Err(TlqError::Connection("Invalid HTTP response".to_string()))
647 }
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
656 fn test_parse_http_response_success() {
657 let response =
658 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"message\":\"success\"}";
659
660 let result = TlqClient::parse_http_response(response);
661 assert!(result.is_ok());
662 assert_eq!(result.unwrap(), "{\"message\":\"success\"}");
663 }
664
665 #[test]
666 fn test_parse_http_response_server_error() {
667 let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n\r\nInternal server error occurred";
668
669 let result = TlqClient::parse_http_response(response);
670 match result {
671 Err(TlqError::Server { status, message }) => {
672 assert_eq!(status, 500);
673 assert_eq!(message, "Internal server error occurred");
674 }
675 _ => panic!("Expected server error"),
676 }
677 }
678
679 #[test]
680 fn test_parse_http_response_client_error() {
681 let response = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nBad request";
682
683 let result = TlqClient::parse_http_response(response);
684 match result {
685 Err(TlqError::Server { status, message }) => {
686 assert_eq!(status, 400);
687 assert_eq!(message, "Bad request");
688 }
689 _ => panic!("Expected client error"),
690 }
691 }
692
693 #[test]
694 fn test_parse_http_response_no_headers_separator() {
695 let response =
696 "HTTP/1.1 200 OK\nContent-Type: application/json\n{\"incomplete\":\"response\"}";
697
698 let result = TlqClient::parse_http_response(response);
699 match result {
700 Err(TlqError::Connection(msg)) => {
701 assert_eq!(msg, "Invalid HTTP response");
702 }
703 _ => panic!("Expected connection error"),
704 }
705 }
706
707 #[test]
708 fn test_parse_http_response_malformed_status_line() {
709 let response = "INVALID_STATUS_LINE\r\n\r\n{\"data\":\"test\"}";
710
711 let result = TlqClient::parse_http_response(response);
712 // Should still succeed because we only check if parts.len() >= 2 and parse fails gracefully
713 assert!(result.is_ok());
714 assert_eq!(result.unwrap(), "{\"data\":\"test\"}");
715 }
716
717 #[test]
718 fn test_parse_http_response_empty_body() {
719 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
720
721 let result = TlqClient::parse_http_response(response);
722 assert!(result.is_ok());
723 assert_eq!(result.unwrap(), "");
724 }
725
726 #[test]
727 fn test_parse_http_response_with_extra_headers() {
728 let response = "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nServer: TLQ/1.0\r\nConnection: close\r\n\r\n{\"id\":\"123\",\"status\":\"created\"}";
729
730 let result = TlqClient::parse_http_response(response);
731 assert!(result.is_ok());
732 assert_eq!(result.unwrap(), "{\"id\":\"123\",\"status\":\"created\"}");
733 }
734
735 #[test]
736 fn test_parse_http_response_status_code_edge_cases() {
737 // Test various status codes around the 400 boundary
738
739 // 399 should be success (< 400)
740 let response_399 = "HTTP/1.1 399 Custom Success\r\n\r\n{\"ok\":true}";
741 let result = TlqClient::parse_http_response(response_399);
742 assert!(result.is_ok());
743
744 // 400 should be error (>= 400)
745 let response_400 = "HTTP/1.1 400 Bad Request\r\n\r\nBad request";
746 let result = TlqClient::parse_http_response(response_400);
747 assert!(matches!(result, Err(TlqError::Server { status: 400, .. })));
748
749 // 599 should be error
750 let response_599 = "HTTP/1.1 599 Custom Error\r\n\r\nCustom error";
751 let result = TlqClient::parse_http_response(response_599);
752 assert!(matches!(result, Err(TlqError::Server { status: 599, .. })));
753 }
754
755 #[test]
756 fn test_max_message_size_constant() {
757 assert_eq!(MAX_MESSAGE_SIZE, 65536);
758 }
759
760 #[test]
761 fn test_client_creation() {
762 let client = TlqClient::new("test-host", 9999);
763 assert!(client.is_ok());
764
765 let client = client.unwrap();
766 assert_eq!(client.base_url, "test-host:9999");
767 }
768
769 #[test]
770 fn test_client_with_config() {
771 let config = Config {
772 host: "custom-host".to_string(),
773 port: 8080,
774 timeout: Duration::from_secs(10),
775 max_retries: 5,
776 retry_delay: Duration::from_millis(200),
777 };
778
779 let client = TlqClient::with_config(config);
780 assert_eq!(client.base_url, "custom-host:8080");
781 assert_eq!(client.config.max_retries, 5);
782 assert_eq!(client.config.timeout, Duration::from_secs(10));
783 }
784
785 #[test]
786 fn test_message_size_validation() {
787 let _client = TlqClient::new("localhost", 1337).unwrap();
788
789 // Test exact limit
790 let message_at_limit = "x".repeat(MAX_MESSAGE_SIZE);
791 let result = std::panic::catch_unwind(|| {
792 // We can't actually test async methods in sync tests without tokio,
793 // but we can verify the constant is correct
794 assert_eq!(message_at_limit.len(), MAX_MESSAGE_SIZE);
795 });
796 assert!(result.is_ok());
797
798 // Test over limit
799 let message_over_limit = "x".repeat(MAX_MESSAGE_SIZE + 1);
800 assert_eq!(message_over_limit.len(), MAX_MESSAGE_SIZE + 1);
801 }
802
803 #[tokio::test]
804 async fn test_add_message_size_validation() {
805 let client = TlqClient::new("localhost", 1337).unwrap();
806
807 // Test message at exact size limit (should be rejected because it's over the limit)
808 let large_message = "x".repeat(MAX_MESSAGE_SIZE + 1);
809 let result = client.add_message(large_message).await;
810
811 match result {
812 Err(TlqError::MessageTooLarge { size }) => {
813 assert_eq!(size, MAX_MESSAGE_SIZE + 1);
814 }
815 _ => panic!("Expected MessageTooLarge error"),
816 }
817
818 // Test empty message (should be valid)
819 let empty_message = "";
820 // We can't actually test without a server, but we can verify it passes size validation
821 assert!(empty_message.len() <= MAX_MESSAGE_SIZE);
822
823 // Test message exactly at limit (should be valid)
824 let max_message = "x".repeat(MAX_MESSAGE_SIZE);
825 // Size check should pass
826 assert_eq!(max_message.len(), MAX_MESSAGE_SIZE);
827 }
828
829 #[tokio::test]
830 async fn test_get_messages_validation() {
831 let client = TlqClient::new("localhost", 1337).unwrap();
832
833 // Test zero count (should be rejected)
834 let result = client.get_messages(0).await;
835 match result {
836 Err(TlqError::Validation(msg)) => {
837 assert_eq!(msg, "Count must be greater than 0");
838 }
839 _ => panic!("Expected validation error for zero count"),
840 }
841
842 // Test valid counts - these should pass without validation errors
843 let _ = client.get_messages(1).await; // Should be valid
844 let _ = client.get_messages(100).await; // Should be valid
845 let _ = client.get_messages(u32::MAX).await; // Should be valid
846 }
847
848 #[tokio::test]
849 async fn test_delete_messages_validation() {
850 let client = TlqClient::new("localhost", 1337).unwrap();
851
852 // Test empty IDs array
853 let result = client.delete_messages(&[]).await;
854 match result {
855 Err(TlqError::Validation(msg)) => {
856 assert_eq!(msg, "No message IDs provided");
857 }
858 _ => panic!("Expected validation error for empty IDs"),
859 }
860
861 // Test delete_message (single ID) - should not have validation issue
862 use uuid::Uuid;
863 let test_id = Uuid::now_v7();
864 // We can't test the actual call without a server, but we can verify
865 // it would call delete_messages with a single-item array
866 assert!(!vec![test_id].is_empty());
867 }
868
869 #[tokio::test]
870 async fn test_retry_messages_validation() {
871 let client = TlqClient::new("localhost", 1337).unwrap();
872
873 // Test empty IDs array
874 let result = client.retry_messages(&[]).await;
875 match result {
876 Err(TlqError::Validation(msg)) => {
877 assert_eq!(msg, "No message IDs provided");
878 }
879 _ => panic!("Expected validation error for empty IDs"),
880 }
881
882 // Test retry_message (single ID) - should not have validation issue
883 use uuid::Uuid;
884 let test_id = Uuid::now_v7();
885 // We can't test the actual call without a server, but we can verify
886 // it would call retry_messages with a single-item array
887 assert!(!vec![test_id].is_empty());
888 }
889
890 #[test]
891 fn test_client_builder_edge_cases() {
892 // Test builder with minimum values
893 let config = TlqClient::builder()
894 .host("")
895 .port(0)
896 .timeout_ms(0)
897 .max_retries(0)
898 .retry_delay_ms(0)
899 .build();
900
901 let client = TlqClient::with_config(config);
902 assert_eq!(client.base_url, ":0");
903 assert_eq!(client.config.max_retries, 0);
904 assert_eq!(client.config.timeout, Duration::from_millis(0));
905
906 // Test builder with maximum reasonable values
907 let config = TlqClient::builder()
908 .host("very-long-hostname-that-might-be-used-in-some-environments")
909 .port(65535)
910 .timeout_ms(600000) // 10 minutes
911 .max_retries(100)
912 .retry_delay_ms(10000) // 10 seconds
913 .build();
914
915 let client = TlqClient::with_config(config);
916 assert!(client.base_url.contains("very-long-hostname"));
917 assert_eq!(client.config.max_retries, 100);
918 assert_eq!(client.config.timeout, Duration::from_secs(600));
919 }
920
921 #[test]
922 fn test_config_validation() {
923 use crate::config::ConfigBuilder;
924 use std::time::Duration;
925
926 // Test various duration configurations
927 let config1 = ConfigBuilder::new()
928 .timeout(Duration::from_nanos(1))
929 .build();
930 assert_eq!(config1.timeout, Duration::from_nanos(1));
931
932 let config2 = ConfigBuilder::new()
933 .retry_delay(Duration::from_secs(3600)) // 1 hour
934 .build();
935 assert_eq!(config2.retry_delay, Duration::from_secs(3600));
936
937 // Test edge case ports
938 let config3 = ConfigBuilder::new().port(1).build();
939 assert_eq!(config3.port, 1);
940
941 let config4 = ConfigBuilder::new().port(65535).build();
942 assert_eq!(config4.port, 65535);
943
944 // Test very high retry counts
945 let config5 = ConfigBuilder::new().max_retries(1000).build();
946 assert_eq!(config5.max_retries, 1000);
947 }
948}