mqtt5_protocol/packet/
puback.rs1use 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 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 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}