bloop_server_framework/
test_utils.rs

1use crate::achievement::AchievementContext;
2use crate::bloop::{Bloop, BloopProvider};
3use crate::evaluator::registration_number::RegistrationNumberProvider;
4use crate::nfc_uid::NfcUid;
5use crate::player::{PlayerInfo, PlayerMutator};
6use crate::trigger::TriggerRegistry;
7use chrono::DateTime;
8use std::collections::HashMap;
9use std::fmt::Debug;
10use std::sync::{Arc, OnceLock, RwLock};
11use std::time::Duration;
12use tokio::time::Instant;
13use uuid::Uuid;
14
15/// A utility struct providing a custom UTC clock based on a fixed base time
16/// and a monotonic timer to ensure consistent time progression.
17pub struct Utc;
18
19static BASE_INSTANT: OnceLock<Instant> = OnceLock::new();
20static BASE_UTC: OnceLock<DateTime<chrono::Utc>> = OnceLock::new();
21
22/// Returns the current time as `DateTime<Utc>`, computed from a fixed base UTC
23/// time plus the elapsed monotonic duration since program start.
24impl Utc {
25    pub fn now() -> DateTime<chrono::Utc> {
26        let base_instant = *BASE_INSTANT.get_or_init(Instant::now);
27        let base_utc = *BASE_UTC.get_or_init(|| DateTime::from_timestamp(0, 0).unwrap());
28
29        let elapsed = Instant::now()
30            .checked_duration_since(base_instant)
31            .unwrap_or(Duration::ZERO);
32
33        base_utc + chrono::Duration::from_std(elapsed).unwrap()
34    }
35}
36
37/// Builder for creating test/mock players with configurable properties.
38#[derive(Default, Debug)]
39pub struct MockPlayerBuilder {
40    nfc_uid: NfcUid,
41    bloops_count: usize,
42    registration_number: usize,
43    name: String,
44}
45
46impl MockPlayerBuilder {
47    /// Creates a new builder with default values.
48    pub fn new() -> Self {
49        Self {
50            nfc_uid: NfcUid::default(),
51            bloops_count: 0,
52            registration_number: 0,
53            name: "test".to_string(),
54        }
55    }
56
57    /// Sets the NFC UID of the mock player.
58    pub fn nfc_uid(mut self, nfc_uid: NfcUid) -> Self {
59        self.nfc_uid = nfc_uid;
60        self
61    }
62
63    /// Sets the initial bloops count for the mock player.
64    pub fn bloops_count(mut self, bloops_count: usize) -> Self {
65        self.bloops_count = bloops_count;
66        self
67    }
68
69    /// Sets the registration number of the mock player.
70    pub fn registration_number(mut self, registration_number: usize) -> Self {
71        self.registration_number = registration_number;
72        self
73    }
74
75    /// Sets the name of the mock player.
76    pub fn name(mut self, name: impl Into<String>) -> Self {
77        self.name = name.into();
78        self
79    }
80
81    /// Builds the [`MockPlayer`] wrapped in `Arc<RwLock>` along with its UUID.
82    pub fn build(self) -> (Arc<RwLock<MockPlayer>>, Uuid) {
83        let id = Uuid::new_v4();
84
85        (
86            Arc::new(RwLock::new(MockPlayer {
87                id,
88                nfc_uid: self.nfc_uid,
89                name: self.name,
90                bloops_count: self.bloops_count,
91                awarded: HashMap::new(),
92                registration_number: self.registration_number,
93            })),
94            id,
95        )
96    }
97}
98
99/// Represents a mock implementation of a player.
100///
101/// Holds player details and state useful for testing achievement logic.
102#[derive(Debug)]
103pub struct MockPlayer {
104    pub id: Uuid,
105    pub nfc_uid: NfcUid,
106    pub name: String,
107    pub bloops_count: usize,
108    pub awarded: HashMap<Uuid, DateTime<chrono::Utc>>,
109    pub registration_number: usize,
110}
111
112impl MockPlayer {
113    /// Returns a new [`MockPlayerBuilder`] to construct a mock player instance.
114    pub fn builder() -> MockPlayerBuilder {
115        MockPlayerBuilder::new()
116    }
117}
118
119impl PlayerInfo for MockPlayer {
120    fn id(&self) -> Uuid {
121        self.id
122    }
123
124    fn nfc_uid(&self) -> NfcUid {
125        self.nfc_uid
126    }
127
128    fn total_bloops(&self) -> usize {
129        self.bloops_count
130    }
131
132    fn awarded_achievements(&self) -> &HashMap<Uuid, DateTime<chrono::Utc>> {
133        &self.awarded
134    }
135}
136
137impl PlayerMutator for MockPlayer {
138    fn increment_bloops(&mut self) {
139        self.bloops_count += 1;
140    }
141
142    fn add_awarded_achievement(&mut self, achievement_id: Uuid, awarded_at: DateTime<chrono::Utc>) {
143        self.awarded.insert(achievement_id, awarded_at);
144    }
145}
146
147impl RegistrationNumberProvider for MockPlayer {
148    fn registration_number(&self) -> usize {
149        self.registration_number
150    }
151}
152
153/// Builder pattern struct for assembling an achievement test context.
154///
155/// Supports configuration of current bloop, player state, bloop provider,
156/// and trigger registry for testing achievement logic.
157#[derive(Debug)]
158pub struct TestCtxBuilder<Player, State = (), Trigger = ()> {
159    pub current_bloop: Bloop<Player>,
160    pub state: State,
161    pub bloop_provider: BloopProvider<Player>,
162    pub trigger_registry: TriggerRegistry<Trigger>,
163}
164
165impl<Player> TestCtxBuilder<Player, (), ()> {
166    /// Creates a new test context builder with a current bloop and default state.
167    pub fn new(current_bloop: Bloop<Player>) -> Self {
168        Self {
169            current_bloop,
170            state: (),
171            trigger_registry: TriggerRegistry::new(HashMap::new()),
172            bloop_provider: BloopProvider::new(Duration::from_secs(1800)),
173        }
174    }
175}
176
177impl<Player, State, Trigger> TestCtxBuilder<Player, State, Trigger> {
178    /// Sets the bloops to be used in the context's bloop provider.
179    pub fn bloops(mut self, bloops: Vec<Bloop<Player>>) -> Self {
180        self.bloop_provider = BloopProvider::with_bloops(Duration::from_secs(1800), bloops);
181        self
182    }
183
184    /// Sets the custom state for the context.
185    pub fn state<T: Default>(self, state: T) -> TestCtxBuilder<Player, T, Trigger> {
186        TestCtxBuilder {
187            current_bloop: self.current_bloop,
188            state,
189            bloop_provider: self.bloop_provider,
190            trigger_registry: self.trigger_registry,
191        }
192    }
193
194    /// Sets the trigger registry for the context.
195    pub fn trigger_registry<T>(
196        self,
197        trigger_registry: TriggerRegistry<T>,
198    ) -> TestCtxBuilder<Player, State, T> {
199        TestCtxBuilder {
200            current_bloop: self.current_bloop,
201            state: self.state,
202            bloop_provider: self.bloop_provider,
203            trigger_registry,
204        }
205    }
206
207    /// Builds the achievement context from the configured components.
208    pub fn build(&mut self) -> AchievementContext<'_, Player, State, Trigger> {
209        AchievementContext::new(
210            &self.current_bloop,
211            &self.bloop_provider,
212            &self.state,
213            &mut self.trigger_registry,
214        )
215    }
216}