reinhardt_testkit/server_fn/
context.rs1#![cfg(native)]
30
31use std::collections::HashMap;
32use std::sync::Arc;
33
34use http::{HeaderMap, HeaderValue, StatusCode};
35use reinhardt_di::{InjectionContext, SingletonScope};
36use uuid::Uuid;
37
38use super::auth::{MockSession, TestUser};
39use super::mock_request::MockHttpRequest;
40
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub enum TransactionMode {
44 #[default]
46 Rollback,
47 Commit,
49 None,
51}
52
53#[allow(clippy::type_complexity)]
75pub struct ServerFnTestContext {
76 singleton_scope: Arc<SingletonScope>,
77 overrides: Vec<Box<dyn FnOnce(&InjectionContext) + Send>>,
78 mock_request: Option<MockHttpRequest>,
79 mock_session: Option<MockSession>,
80 test_user: Option<TestUser>,
81 transaction_mode: TransactionMode,
82 request_headers: HeaderMap,
83 csrf_token: Option<String>,
84}
85
86impl ServerFnTestContext {
87 pub fn new(singleton_scope: Arc<SingletonScope>) -> Self {
93 Self {
94 singleton_scope,
95 overrides: Vec::new(),
96 mock_request: None,
97 mock_session: None,
98 test_user: None,
99 transaction_mode: TransactionMode::default(),
100 request_headers: HeaderMap::new(),
101 csrf_token: None,
102 }
103 }
104
105 pub fn with_database<P: Clone + Send + Sync + 'static>(mut self, pool: P) -> Self {
111 self.overrides.push(Box::new(move |ctx| {
112 ctx.set_singleton(pool);
113 }));
114 self
115 }
116
117 pub fn with_singleton<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
123 self.overrides.push(Box::new(move |ctx| {
124 ctx.set_singleton(value);
125 }));
126 self
127 }
128
129 pub fn with_permissions<S: Into<String>>(mut self, permissions: Vec<S>) -> Self {
138 if let Some(ref mut user) = self.test_user {
139 for perm in permissions {
140 user.permissions.push(perm.into());
141 }
142 if let Some(ref mut session) = self.mock_session {
144 session.user = Some(user.clone());
145 }
146 } else {
147 let mut user = TestUser::authenticated("test-user");
148 for perm in permissions {
149 user.permissions.push(perm.into());
150 }
151 self.test_user = Some(user.clone());
152 self.mock_session = Some(MockSession::authenticated(user));
153 }
154 self
155 }
156
157 pub fn with_roles<S: Into<String>>(mut self, roles: Vec<S>) -> Self {
164 if let Some(ref mut user) = self.test_user {
165 for role in roles {
166 user.roles.push(role.into());
167 }
168 if let Some(ref mut session) = self.mock_session {
170 session.user = Some(user.clone());
171 }
172 } else {
173 let mut user = TestUser::authenticated("test-user");
174 for role in roles {
175 user.roles.push(role.into());
176 }
177 self.test_user = Some(user.clone());
178 self.mock_session = Some(MockSession::authenticated(user));
179 }
180 self
181 }
182
183 pub fn with_request(mut self, request: MockHttpRequest) -> Self {
192 self.mock_request = Some(request);
193 self
194 }
195
196 pub fn with_request_headers(mut self, headers: HeaderMap) -> Self {
202 self.request_headers = headers;
203 self
204 }
205
206 pub fn with_header(mut self, name: &str, value: &str) -> Self {
213 if let Ok(header_value) = HeaderValue::from_str(value)
214 && let Ok(header_name) = http::header::HeaderName::from_bytes(name.as_bytes())
215 {
216 self.request_headers.insert(header_name, header_value);
217 }
218 self
219 }
220
221 pub fn with_csrf_token(mut self, token: &str) -> Self {
229 self.csrf_token = Some(token.to_string());
230
231 if let Ok(header_value) = HeaderValue::from_str(token) {
233 self.request_headers
234 .insert("x-csrf-token", header_value.clone());
235 }
236
237 if let Some(ref mut session) = self.mock_session {
239 session.csrf_token = token.to_string();
240 }
241
242 self
243 }
244
245 pub fn with_transaction_mode(mut self, mode: TransactionMode) -> Self {
251 self.transaction_mode = mode;
252 self
253 }
254
255 pub fn with_transaction_rollback(self) -> Self {
259 self.with_transaction_mode(TransactionMode::Rollback)
260 }
261
262 pub fn with_session(mut self, session: MockSession) -> Self {
268 self.mock_session = Some(session);
269 self
270 }
271
272 #[cfg(native)]
288 pub fn auth(self) -> crate::auth::ServerFnAuthBuilder {
289 crate::auth::ServerFnAuthBuilder::new(self)
290 }
291
292 pub fn with_mock_session(mut self) -> Self {
294 if self.mock_session.is_none() {
295 self.mock_session = Some(MockSession::anonymous());
296 }
297 self
298 }
299
300 pub fn build(self) -> ServerFnTestEnv {
305 let ctx = InjectionContext::builder(self.singleton_scope.clone()).build();
306
307 for override_fn in self.overrides {
309 override_fn(&ctx);
310 }
311
312 if let Some(session) = self.mock_session.clone() {
314 ctx.set_singleton(session);
315 }
316
317 if let Some(user) = self.test_user.clone() {
319 ctx.set_singleton(user);
320 }
321
322 if let Some(request) = self.mock_request.clone() {
324 ctx.set_singleton(request);
325 }
326
327 ServerFnTestEnv {
328 injection_context: ctx,
329 mock_session: self.mock_session,
330 test_user: self.test_user,
331 mock_request: self.mock_request,
332 transaction_mode: self.transaction_mode,
333 request_headers: self.request_headers,
334 csrf_token: self.csrf_token,
335 }
336 }
337
338 pub fn build_context(self) -> InjectionContext {
342 self.build().injection_context
343 }
344}
345
346#[derive(Clone)]
348pub struct ServerFnTestEnv {
349 pub injection_context: InjectionContext,
351 pub mock_session: Option<MockSession>,
353 pub test_user: Option<TestUser>,
355 pub mock_request: Option<MockHttpRequest>,
357 pub transaction_mode: TransactionMode,
359 pub request_headers: HeaderMap,
361 pub csrf_token: Option<String>,
363}
364
365impl ServerFnTestEnv {
366 pub fn context(&self) -> &InjectionContext {
368 &self.injection_context
369 }
370
371 pub fn is_authenticated(&self) -> bool {
373 self.test_user.is_some() && self.mock_session.as_ref().is_some_and(|s| s.user.is_some())
374 }
375
376 pub fn user_id(&self) -> Option<Uuid> {
378 self.test_user.as_ref().map(|u| u.id)
379 }
380
381 pub fn has_permission(&self, permission: &str) -> bool {
384 self.test_user
385 .as_ref()
386 .is_some_and(|u| u.has_permission(permission))
387 }
388
389 pub fn has_role(&self, role: &str) -> bool {
391 self.test_user
392 .as_ref()
393 .is_some_and(|u| u.roles.iter().any(|r| r == role))
394 }
395
396 pub fn get_header(&self, name: &str) -> Option<&str> {
398 self.request_headers.get(name).and_then(|v| v.to_str().ok())
399 }
400}
401
402impl std::ops::Deref for ServerFnTestEnv {
403 type Target = InjectionContext;
404
405 fn deref(&self) -> &Self::Target {
406 &self.injection_context
407 }
408}
409
410#[derive(Debug, Clone)]
414pub struct ExpectedResult<T> {
415 pub value: Option<T>,
417 pub status: Option<StatusCode>,
419 pub headers: HashMap<String, String>,
421}
422
423impl<T> Default for ExpectedResult<T> {
424 fn default() -> Self {
425 Self {
426 value: None,
427 status: None,
428 headers: HashMap::new(),
429 }
430 }
431}
432
433impl<T> ExpectedResult<T> {
434 pub fn new() -> Self {
436 Self::default()
437 }
438
439 pub fn with_value(mut self, value: T) -> Self {
441 self.value = Some(value);
442 self
443 }
444
445 pub fn with_status(mut self, status: StatusCode) -> Self {
447 self.status = Some(status);
448 self
449 }
450
451 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
453 self.headers.insert(name.into(), value.into());
454 self
455 }
456
457 pub fn success(self) -> Self {
459 self.with_status(StatusCode::OK)
460 }
461
462 pub fn created(self) -> Self {
464 self.with_status(StatusCode::CREATED)
465 }
466
467 pub fn bad_request(self) -> Self {
469 self.with_status(StatusCode::BAD_REQUEST)
470 }
471
472 pub fn unauthorized(self) -> Self {
474 self.with_status(StatusCode::UNAUTHORIZED)
475 }
476
477 pub fn forbidden(self) -> Self {
479 self.with_status(StatusCode::FORBIDDEN)
480 }
481
482 pub fn not_found(self) -> Self {
484 self.with_status(StatusCode::NOT_FOUND)
485 }
486
487 pub fn conflict(self) -> Self {
489 self.with_status(StatusCode::CONFLICT)
490 }
491
492 pub fn internal_error(self) -> Self {
494 self.with_status(StatusCode::INTERNAL_SERVER_ERROR)
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn test_context_builder() {
504 let singleton = Arc::new(SingletonScope::new());
505 let ctx = ServerFnTestContext::new(singleton)
506 .with_mock_session()
507 .build();
508
509 assert!(ctx.mock_session.is_some());
510 }
511
512 #[test]
513 fn test_authenticated_user() {
514 let singleton = Arc::new(SingletonScope::new());
515 let user = TestUser::admin();
516 let ctx = ServerFnTestContext::new(singleton)
517 .with_session(MockSession::authenticated(user))
518 .build();
519
520 assert!(ctx.mock_session.as_ref().is_some_and(|s| s.user.is_some()));
521 }
522
523 #[test]
524 fn test_permissions() {
525 let singleton = Arc::new(SingletonScope::new());
526 let ctx = ServerFnTestContext::new(singleton)
527 .with_permissions(vec!["read", "write"])
528 .build();
529
530 assert!(ctx.has_permission("read"));
531 assert!(ctx.has_permission("write"));
532 assert!(!ctx.has_permission("admin"));
533 }
534
535 #[test]
536 fn test_csrf_token() {
537 let singleton = Arc::new(SingletonScope::new());
538 let ctx = ServerFnTestContext::new(singleton)
539 .with_mock_session()
540 .with_csrf_token("test-token")
541 .build();
542
543 assert_eq!(ctx.csrf_token.as_deref(), Some("test-token"));
544 assert_eq!(ctx.get_header("x-csrf-token"), Some("test-token"));
545 }
546
547 #[test]
548 fn test_transaction_mode() {
549 let singleton = Arc::new(SingletonScope::new());
550 let ctx = ServerFnTestContext::new(singleton)
551 .with_transaction_rollback()
552 .build();
553
554 assert_eq!(ctx.transaction_mode, TransactionMode::Rollback);
555 }
556}