mqtt5_protocol/packet/
puback.rs

1use super::ack_common::{define_ack_packet, is_valid_publish_ack_reason_code};
2use crate::error::{MqttError, Result};
3use crate::packet::{AckPacketHeader, PacketType};
4use crate::protocol::v5::properties::Properties;
5
6define_ack_packet! {
7    /// MQTT PUBACK packet (`QoS` 1 publish acknowledgment)
8    pub struct PubAckPacket;
9    packet_type = PacketType::PubAck;
10    validator = is_valid_publish_ack_reason_code;
11    error_prefix = "PUBACK";
12}
13
14impl PubAckPacket {
15    #[must_use]
16    pub fn create_header(&self) -> AckPacketHeader {
17        AckPacketHeader::create(self.packet_id, self.reason_code)
18    }
19
20    /// # Errors
21    /// Returns an error if the reason code in the header is invalid
22    pub fn from_header(header: AckPacketHeader, properties: Properties) -> Result<Self> {
23        let reason_code = header.get_reason_code().ok_or_else(|| {
24            MqttError::MalformedPacket(format!(
25                "Invalid PUBACK reason code: 0x{:02X}",
26                header.reason_code
27            ))
28        })?;
29
30        if !is_valid_publish_ack_reason_code(reason_code) {
31            return Err(MqttError::MalformedPacket(format!(
32                "Invalid PUBACK reason code: {reason_code:?}"
33            )));
34        }
35
36        Ok(Self {
37            packet_id: header.packet_id,
38            reason_code,
39            properties,
40        })
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use crate::packet::{FixedHeader, MqttPacket};
48    use crate::protocol::v5::properties::PropertyId;
49    use crate::types::ReasonCode;
50    use bytes::{BufMut, BytesMut};
51
52    #[cfg(test)]
53    mod bebytes_tests {
54        use super::*;
55        use bebytes::BeBytes;
56        use proptest::prelude::*;
57
58        #[test]
59        fn test_ack_header_creation() {
60            let header = AckPacketHeader::create(123, ReasonCode::Success);
61            assert_eq!(header.packet_id, 123);
62            assert_eq!(header.reason_code, 0x00);
63            assert_eq!(header.get_reason_code(), Some(ReasonCode::Success));
64        }
65
66        #[test]
67        fn test_ack_header_round_trip() {
68            let header = AckPacketHeader::create(456, ReasonCode::QuotaExceeded);
69            let bytes = header.to_be_bytes();
70            assert_eq!(bytes.len(), 3);
71
72            let (decoded, consumed) = AckPacketHeader::try_from_be_bytes(&bytes).unwrap();
73            assert_eq!(consumed, 3);
74            assert_eq!(decoded, header);
75            assert_eq!(decoded.packet_id, 456);
76            assert_eq!(decoded.get_reason_code(), Some(ReasonCode::QuotaExceeded));
77        }
78
79        #[test]
80        fn test_puback_from_header() {
81            let header = AckPacketHeader::create(789, ReasonCode::NoMatchingSubscribers);
82            let properties = Properties::default();
83
84            let packet = PubAckPacket::from_header(header, properties).unwrap();
85            assert_eq!(packet.packet_id, 789);
86            assert_eq!(packet.reason_code, ReasonCode::NoMatchingSubscribers);
87        }
88
89        proptest! {
90            #[test]
91            fn prop_ack_header_round_trip(
92                packet_id in any::<u16>(),
93                reason_code in 0u8..=255u8
94            ) {
95                let header = AckPacketHeader {
96                    packet_id,
97                    reason_code,
98                };
99
100                let bytes = header.to_be_bytes();
101                let (decoded, consumed) = AckPacketHeader::try_from_be_bytes(&bytes).unwrap();
102
103                prop_assert_eq!(consumed, 3);
104                prop_assert_eq!(decoded, header);
105                prop_assert_eq!(decoded.packet_id, packet_id);
106                prop_assert_eq!(decoded.reason_code, reason_code);
107            }
108        }
109    }
110
111    #[test]
112    fn test_puback_basic() {
113        let packet = PubAckPacket::new(123);
114
115        assert_eq!(packet.packet_id, 123);
116        assert_eq!(packet.reason_code, ReasonCode::Success);
117        assert!(packet.properties.is_empty());
118    }
119
120    #[test]
121    fn test_puback_with_reason() {
122        let packet = PubAckPacket::new_with_reason(456, ReasonCode::NoMatchingSubscribers)
123            .with_reason_string("No subscribers for topic".to_string());
124
125        assert_eq!(packet.packet_id, 456);
126        assert_eq!(packet.reason_code, ReasonCode::NoMatchingSubscribers);
127        assert!(packet.properties.contains(PropertyId::ReasonString));
128    }
129
130    #[test]
131    fn test_puback_encode_decode_minimal() {
132        let packet = PubAckPacket::new(789);
133
134        let mut buf = BytesMut::new();
135        packet.encode(&mut buf).unwrap();
136
137        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
138        assert_eq!(fixed_header.packet_type, PacketType::PubAck);
139
140        let decoded = PubAckPacket::decode_body(&mut buf, &fixed_header).unwrap();
141        assert_eq!(decoded.packet_id, 789);
142        assert_eq!(decoded.reason_code, ReasonCode::Success);
143    }
144
145    #[test]
146    fn test_puback_encode_decode_with_reason() {
147        let packet = PubAckPacket::new_with_reason(999, ReasonCode::QuotaExceeded)
148            .with_user_property("quota".to_string(), "exceeded".to_string());
149
150        let mut buf = BytesMut::new();
151        packet.encode(&mut buf).unwrap();
152
153        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
154        let decoded = PubAckPacket::decode_body(&mut buf, &fixed_header).unwrap();
155
156        assert_eq!(decoded.packet_id, 999);
157        assert_eq!(decoded.reason_code, ReasonCode::QuotaExceeded);
158        assert!(decoded.properties.contains(PropertyId::UserProperty));
159    }
160
161    #[test]
162    fn test_puback_v311_style() {
163        let mut buf = BytesMut::new();
164        buf.put_u16(1234);
165
166        let fixed_header = FixedHeader::new(PacketType::PubAck, 0, 2);
167        let decoded = PubAckPacket::decode_body(&mut buf, &fixed_header).unwrap();
168
169        assert_eq!(decoded.packet_id, 1234);
170        assert_eq!(decoded.reason_code, ReasonCode::Success);
171        assert!(decoded.properties.is_empty());
172    }
173
174    #[test]
175    fn test_puback_invalid_reason_code() {
176        let mut buf = BytesMut::new();
177        buf.put_u16(123);
178        buf.put_u8(0xFF);
179
180        let fixed_header = FixedHeader::new(PacketType::PubAck, 0, 3);
181        let result = PubAckPacket::decode_body(&mut buf, &fixed_header);
182        assert!(result.is_err());
183    }
184
185    #[test]
186    fn test_puback_missing_packet_id() {
187        let mut buf = BytesMut::new();
188        buf.put_u8(0);
189
190        let fixed_header = FixedHeader::new(PacketType::PubAck, 0, 1);
191        let result = PubAckPacket::decode_body(&mut buf, &fixed_header);
192        assert!(result.is_err());
193    }
194}