Skip to main content

arminius_lib_message_bus/mock/
event.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Mock implementations of [`Publisher`] and
6//! [`Subscriber`](crate::Subscriber) for unit testing
7//! without a NATS server.
8
9use std::{
10	ops::Deref,
11	sync::{Arc, Mutex},
12};
13
14use futures::Stream;
15
16use crate::{Error, Event, Publisher, ReceivedEvent, subject_matches};
17
18/// In-memory [`Publisher`] for use in unit tests.
19///
20/// Captures all published events so tests can assert on what was published
21/// without needing a running NATS server.
22///
23/// `MockPublisher<E>` can only capture events of exactly type `E`. To capture
24/// multiple event types in one publisher, wrap them in an enum or use
25/// [`MockBus`](crate::mock::MockBus).
26///
27/// # Example
28///
29/// ```rust
30/// use arminius_lib_message_bus::{Event, Publisher, mock::MockPublisher};
31/// use serde::{Deserialize, Serialize};
32///
33/// #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
34/// struct RoomCreated {
35/// 	room_id: String,
36/// }
37///
38/// impl Event for RoomCreated {
39/// 	const SUBJECT: &str = "RoomCreated.v1";
40/// }
41///
42/// # async fn example() {
43/// let publisher: MockPublisher<RoomCreated> = MockPublisher::new();
44/// publisher.publish(RoomCreated { room_id: "!abc".into() }).await.unwrap();
45/// assert_eq!(publisher.published_messages().len(), 1);
46/// # }
47/// ```
48#[derive(Clone, Debug, Default)]
49pub struct MockPublisher<E> {
50	/// All events published so far, in order.
51	published: Arc<Mutex<Vec<E>>>,
52}
53
54impl<E: Clone> MockPublisher<E> {
55	/// Creates a new empty `MockPublisher`.
56	#[must_use]
57	pub fn new() -> Self {
58		Self { published: Arc::new(Mutex::new(Vec::new())) }
59	}
60
61	/// Returns a snapshot of all events published so far, in order.
62	#[expect(clippy::expect_used, reason = "panicking is acceptable in test-only code")]
63	#[must_use]
64	pub fn published_messages(&self) -> Vec<E> {
65		self.published
66			.lock()
67			.expect("lock should not be poisoned: no holder should panic while the mutex is held")
68			.to_vec()
69	}
70}
71
72impl<T> Publisher for MockPublisher<T>
73where
74	T: Event + Clone + 'static,
75{
76	#[expect(clippy::expect_used, reason = "panicking is acceptable in test-only code")]
77	async fn publish<E>(&self, message: E) -> Result<(), Error>
78	where
79		E: Event + Clone + 'static,
80	{
81		let any_message: &dyn std::any::Any = &message;
82		if let Some(mock_event) = any_message.downcast_ref::<T>() {
83			let subject = mock_event.subject();
84			// Assert misconfiguration of subscribe subject and publish subject
85			debug_assert!(
86				subject_matches(E::SUBJECT, &subject),
87				"concrete subject {subject:?} is not covered by the subject pattern {:?}",
88				E::SUBJECT,
89			);
90
91			self.published
92				.lock()
93				.expect(
94					"lock should not be poisoned: no holder should panic while the mutex is held",
95				)
96				.push(mock_event.clone());
97		} else {
98			return Err(Error::SendFailed {
99				error: format!(
100					"Type mismatch: MockPublisher<{}> cannot publish events of type {}.",
101					std::any::type_name::<T>(),
102					std::any::type_name::<E>(),
103				),
104			});
105		}
106
107		Ok(())
108	}
109}
110
111/// A received event wrapper for use in unit tests.
112///
113/// Tracks whether [`ack`](ReceivedEvent::ack) was called, so tests can assert
114/// that processing code acknowledges messages correctly.
115#[derive(Debug, Clone)]
116pub struct MockReceivedEvent<E> {
117	/// The wrapped event.
118	event: E,
119	/// Whether [`ack`](ReceivedEvent::ack) has been called.
120	acked: Arc<Mutex<bool>>,
121	/// The OTel context associated with this event.
122	context: opentelemetry::Context,
123}
124
125impl<E> MockReceivedEvent<E> {
126	/// Creates a new wrapper `MockReceivedEvent` around an event with new OTel
127	/// context and that is not yet acknowledged.
128	pub fn new(event: E) -> Self {
129		Self { event, acked: Arc::new(Mutex::new(false)), context: opentelemetry::Context::new() }
130	}
131}
132
133impl<E: PartialEq> PartialEq for MockReceivedEvent<E> {
134	#[expect(clippy::expect_used, reason = "panicking is acceptable in test-only code")]
135	fn eq(&self, other: &Self) -> bool {
136		let self_ack = *self.acked.lock().expect("lock poisoned");
137		let other_ack = *other.acked.lock().expect("lock poisoned");
138		self.event == other.event && self_ack == other_ack
139	}
140}
141
142impl<E: Event + Clone> From<&E> for MockReceivedEvent<E> {
143	fn from(value: &E) -> Self {
144		Self {
145			event: value.clone(),
146			acked: Arc::new(Mutex::new(false)),
147			context: opentelemetry::Context::new(),
148		}
149	}
150}
151
152impl<E: Event> From<E> for MockReceivedEvent<E> {
153	fn from(value: E) -> Self {
154		Self {
155			event: value,
156			acked: Arc::new(Mutex::new(false)),
157			context: opentelemetry::Context::new(),
158		}
159	}
160}
161
162impl<E: Event> Deref for MockReceivedEvent<E> {
163	type Target = E;
164
165	fn deref(&self) -> &Self::Target {
166		&self.event
167	}
168}
169
170impl<E> MockReceivedEvent<E> {
171	/// Returns `true` if [`ack`](ReceivedEvent::ack) has been called on this
172	/// event.
173	#[expect(clippy::expect_used, reason = "panicking is acceptable in test-only code")]
174	pub fn was_acked(&self) -> bool {
175		*self
176			.acked
177			.lock()
178			.expect("lock should not be poisoned: no holder should panic while the mutex is held")
179	}
180}
181
182impl<E: Event> ReceivedEvent<E> for MockReceivedEvent<E> {
183	#[expect(clippy::expect_used, reason = "panicking is acceptable in test-only code")]
184	async fn ack(&self) -> Result<(), Error> {
185		*self.acked.lock().expect(
186			"lock should not be poisoned: no holder should panic while the mutex is held",
187		) = true;
188		Ok(())
189	}
190
191	fn context(&self) -> &opentelemetry::Context {
192		&self.context
193	}
194}
195
196/// Helper for creating test event streams.
197///
198/// Wraps a list of events into a stream of [`MockReceivedEvent`] values and
199/// returns ack-checkers alongside so tests can verify acknowledgment.
200///
201/// ```rust
202/// use arminius_lib_message_bus::{
203/// 	Event, ReceivedEvent, mock::MockSubscriber,
204/// };
205/// use futures::StreamExt;
206/// use serde::{Deserialize, Serialize};
207///
208/// #[derive(Clone, Debug, Serialize, Deserialize)]
209/// struct Ping {
210/// 	id: u32,
211/// }
212/// impl Event for Ping {
213/// 	const SUBJECT: &str = "Ping.v1";
214/// }
215///
216/// # async fn example() {
217/// let (mut stream, checkers) =
218/// 	MockSubscriber::new().create_stream(vec![Ping { id: 1 }]);
219/// while let Some(Ok(event)) = stream.next().await {
220/// 	event.ack().await.unwrap();
221/// }
222/// assert!(checkers[0].was_acked());
223/// # }
224/// ```
225#[derive(Debug, Default)]
226pub struct MockSubscriber<E> {
227	/// Binds the subscriber to its event type `E`.
228	_phantom: std::marker::PhantomData<E>,
229}
230
231impl<E: Event + Clone> MockSubscriber<E> {
232	/// Creates a new `MockSubscriber`.
233	#[must_use]
234	pub fn new() -> Self {
235		Self { _phantom: std::marker::PhantomData }
236	}
237
238	/// Wraps `events` into a stream of [`MockReceivedEvent`] values.
239	///
240	/// Returns the stream and a parallel `Vec` of the same wrapped events so
241	/// callers can call [`MockReceivedEvent::was_acked`] after processing.
242	pub fn create_stream(
243		&self,
244		events: Vec<E>,
245	) -> (impl Stream<Item = Result<MockReceivedEvent<E>, Error>> + use<E>, Vec<MockReceivedEvent<E>>)
246	{
247		let mocks: Vec<MockReceivedEvent<E>> = events.into_iter().map(Into::into).collect();
248		let checkers = mocks.clone();
249		(futures::stream::iter(mocks.into_iter().map(Ok)), checkers)
250	}
251}