reinhardt_testkit/server_fn/
context.rs1#![cfg(native)]
28
29use std::collections::HashMap;
30use std::sync::Arc;
31
32use http::{HeaderMap, HeaderValue, StatusCode};
33use reinhardt_di::{InjectionContext, SingletonScope};
34use uuid::Uuid;
35
36use super::auth::{MockSession, TestUser};
37use super::mock_request::MockHttpRequest;
38
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
41pub enum TransactionMode {
42 #[default]
44 Rollback,
45 Commit,
47 None,
49}
50
51#[allow(clippy::type_complexity)]
71pub struct ServerFnTestContext {
72 singleton_scope: Arc<SingletonScope>,
73 overrides: Vec<Box<dyn FnOnce(&InjectionContext) + Send>>,
74 mock_request: Option<MockHttpRequest>,
75 mock_session: Option<MockSession>,
76 test_user: Option<TestUser>,
77 transaction_mode: TransactionMode,
78 request_headers: HeaderMap,
79 csrf_token: Option<String>,
80}
81
82impl ServerFnTestContext {
83 pub fn new(singleton_scope: Arc<SingletonScope>) -> Self {
89 Self {
90 singleton_scope,
91 overrides: Vec::new(),
92 mock_request: None,
93 mock_session: None,
94 test_user: None,
95 transaction_mode: TransactionMode::default(),
96 request_headers: HeaderMap::new(),
97 csrf_token: None,
98 }
99 }
100
101 pub fn with_database<P: Clone + Send + Sync + 'static>(mut self, pool: P) -> Self {
107 self.overrides.push(Box::new(move |ctx| {
108 ctx.set_singleton(pool);
109 }));
110 self
111 }
112
113 pub fn with_singleton<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
119 self.overrides.push(Box::new(move |ctx| {
120 ctx.set_singleton(value);
121 }));
122 self
123 }
124
125 #[deprecated(
142 since = "0.1.0-rc.16",
143 note = "use `.auth().session(&user).done()` instead"
144 )]
145 pub fn with_authenticated_user(mut self, user: TestUser) -> Self {
146 self.test_user = Some(user.clone());
147 self.mock_session = Some(MockSession::authenticated(user));
148 self
149 }
150
151 pub fn with_permissions<S: Into<String>>(mut self, permissions: Vec<S>) -> Self {
160 if let Some(ref mut user) = self.test_user {
161 for perm in permissions {
162 user.permissions.push(perm.into());
163 }
164 if let Some(ref mut session) = self.mock_session {
166 session.user = Some(user.clone());
167 }
168 } else {
169 let mut user = TestUser::authenticated("test-user");
170 for perm in permissions {
171 user.permissions.push(perm.into());
172 }
173 self.test_user = Some(user.clone());
174 self.mock_session = Some(MockSession::authenticated(user));
175 }
176 self
177 }
178
179 pub fn with_roles<S: Into<String>>(mut self, roles: Vec<S>) -> Self {
186 if let Some(ref mut user) = self.test_user {
187 for role in roles {
188 user.roles.push(role.into());
189 }
190 if let Some(ref mut session) = self.mock_session {
192 session.user = Some(user.clone());
193 }
194 } else {
195 let mut user = TestUser::authenticated("test-user");
196 for role in roles {
197 user.roles.push(role.into());
198 }
199 self.test_user = Some(user.clone());
200 self.mock_session = Some(MockSession::authenticated(user));
201 }
202 self
203 }
204
205 pub fn with_request(mut self, request: MockHttpRequest) -> Self {
214 self.mock_request = Some(request);
215 self
216 }
217
218 pub fn with_request_headers(mut self, headers: HeaderMap) -> Self {
224 self.request_headers = headers;
225 self
226 }
227
228 pub fn with_header(mut self, name: &str, value: &str) -> Self {
235 if let Ok(header_value) = HeaderValue::from_str(value)
236 && let Ok(header_name) = http::header::HeaderName::from_bytes(name.as_bytes())
237 {
238 self.request_headers.insert(header_name, header_value);
239 }
240 self
241 }
242
243 pub fn with_csrf_token(mut self, token: &str) -> Self {
251 self.csrf_token = Some(token.to_string());
252
253 if let Ok(header_value) = HeaderValue::from_str(token) {
255 self.request_headers
256 .insert("x-csrf-token", header_value.clone());
257 }
258
259 if let Some(ref mut session) = self.mock_session {
261 session.csrf_token = token.to_string();
262 }
263
264 self
265 }
266
267 pub fn with_transaction_mode(mut self, mode: TransactionMode) -> Self {
273 self.transaction_mode = mode;
274 self
275 }
276
277 pub fn with_transaction_rollback(self) -> Self {
281 self.with_transaction_mode(TransactionMode::Rollback)
282 }
283
284 pub fn with_session(mut self, session: MockSession) -> Self {
290 self.mock_session = Some(session);
291 self
292 }
293
294 #[cfg(native)]
310 pub fn auth(self) -> crate::auth::ServerFnAuthBuilder {
311 crate::auth::ServerFnAuthBuilder::new(self)
312 }
313
314 pub fn with_mock_session(mut self) -> Self {
316 if self.mock_session.is_none() {
317 self.mock_session = Some(MockSession::anonymous());
318 }
319 self
320 }
321
322 pub fn build(self) -> ServerFnTestEnv {
327 let ctx = InjectionContext::builder(self.singleton_scope.clone()).build();
328
329 for override_fn in self.overrides {
331 override_fn(&ctx);
332 }
333
334 if let Some(session) = self.mock_session.clone() {
336 ctx.set_singleton(session);
337 }
338
339 if let Some(user) = self.test_user.clone() {
341 ctx.set_singleton(user);
342 }
343
344 if let Some(request) = self.mock_request.clone() {
346 ctx.set_singleton(request);
347 }
348
349 ServerFnTestEnv {
350 injection_context: ctx,
351 mock_session: self.mock_session,
352 test_user: self.test_user,
353 mock_request: self.mock_request,
354 transaction_mode: self.transaction_mode,
355 request_headers: self.request_headers,
356 csrf_token: self.csrf_token,
357 }
358 }
359
360 pub fn build_context(self) -> InjectionContext {
364 self.build().injection_context
365 }
366}
367
368#[derive(Clone)]
370pub struct ServerFnTestEnv {
371 pub injection_context: InjectionContext,
373 pub mock_session: Option<MockSession>,
375 pub test_user: Option<TestUser>,
377 pub mock_request: Option<MockHttpRequest>,
379 pub transaction_mode: TransactionMode,
381 pub request_headers: HeaderMap,
383 pub csrf_token: Option<String>,
385}
386
387impl ServerFnTestEnv {
388 pub fn context(&self) -> &InjectionContext {
390 &self.injection_context
391 }
392
393 pub fn is_authenticated(&self) -> bool {
395 self.test_user.is_some() && self.mock_session.as_ref().is_some_and(|s| s.user.is_some())
396 }
397
398 pub fn user_id(&self) -> Option<Uuid> {
400 self.test_user.as_ref().map(|u| u.id)
401 }
402
403 pub fn has_permission(&self, permission: &str) -> bool {
406 self.test_user
407 .as_ref()
408 .is_some_and(|u| u.has_permission(permission))
409 }
410
411 pub fn has_role(&self, role: &str) -> bool {
413 self.test_user
414 .as_ref()
415 .is_some_and(|u| u.roles.iter().any(|r| r == role))
416 }
417
418 pub fn get_header(&self, name: &str) -> Option<&str> {
420 self.request_headers.get(name).and_then(|v| v.to_str().ok())
421 }
422}
423
424impl std::ops::Deref for ServerFnTestEnv {
425 type Target = InjectionContext;
426
427 fn deref(&self) -> &Self::Target {
428 &self.injection_context
429 }
430}
431
432#[derive(Debug, Clone)]
436pub struct ExpectedResult<T> {
437 pub value: Option<T>,
439 pub status: Option<StatusCode>,
441 pub headers: HashMap<String, String>,
443}
444
445impl<T> Default for ExpectedResult<T> {
446 fn default() -> Self {
447 Self {
448 value: None,
449 status: None,
450 headers: HashMap::new(),
451 }
452 }
453}
454
455impl<T> ExpectedResult<T> {
456 pub fn new() -> Self {
458 Self::default()
459 }
460
461 pub fn with_value(mut self, value: T) -> Self {
463 self.value = Some(value);
464 self
465 }
466
467 pub fn with_status(mut self, status: StatusCode) -> Self {
469 self.status = Some(status);
470 self
471 }
472
473 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
475 self.headers.insert(name.into(), value.into());
476 self
477 }
478
479 pub fn success(self) -> Self {
481 self.with_status(StatusCode::OK)
482 }
483
484 pub fn created(self) -> Self {
486 self.with_status(StatusCode::CREATED)
487 }
488
489 pub fn bad_request(self) -> Self {
491 self.with_status(StatusCode::BAD_REQUEST)
492 }
493
494 pub fn unauthorized(self) -> Self {
496 self.with_status(StatusCode::UNAUTHORIZED)
497 }
498
499 pub fn forbidden(self) -> Self {
501 self.with_status(StatusCode::FORBIDDEN)
502 }
503
504 pub fn not_found(self) -> Self {
506 self.with_status(StatusCode::NOT_FOUND)
507 }
508
509 pub fn conflict(self) -> Self {
511 self.with_status(StatusCode::CONFLICT)
512 }
513
514 pub fn internal_error(self) -> Self {
516 self.with_status(StatusCode::INTERNAL_SERVER_ERROR)
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn test_context_builder() {
526 let singleton = Arc::new(SingletonScope::new());
527 let ctx = ServerFnTestContext::new(singleton)
528 .with_mock_session()
529 .build();
530
531 assert!(ctx.mock_session.is_some());
532 }
533
534 #[test]
535 fn test_authenticated_user() {
536 let singleton = Arc::new(SingletonScope::new());
537 let user = TestUser::admin();
538 let ctx = ServerFnTestContext::new(singleton)
539 .with_authenticated_user(user)
540 .build();
541
542 assert!(ctx.is_authenticated());
543 assert!(ctx.test_user.is_some());
544 }
545
546 #[test]
547 fn test_permissions() {
548 let singleton = Arc::new(SingletonScope::new());
549 let ctx = ServerFnTestContext::new(singleton)
550 .with_authenticated_user(TestUser::authenticated("alice"))
551 .with_permissions(vec!["read", "write"])
552 .build();
553
554 assert!(ctx.has_permission("read"));
555 assert!(ctx.has_permission("write"));
556 assert!(!ctx.has_permission("admin"));
557 }
558
559 #[test]
560 fn test_csrf_token() {
561 let singleton = Arc::new(SingletonScope::new());
562 let ctx = ServerFnTestContext::new(singleton)
563 .with_mock_session()
564 .with_csrf_token("test-token")
565 .build();
566
567 assert_eq!(ctx.csrf_token.as_deref(), Some("test-token"));
568 assert_eq!(ctx.get_header("x-csrf-token"), Some("test-token"));
569 }
570
571 #[test]
572 fn test_transaction_mode() {
573 let singleton = Arc::new(SingletonScope::new());
574 let ctx = ServerFnTestContext::new(singleton)
575 .with_transaction_rollback()
576 .build();
577
578 assert_eq!(ctx.transaction_mode, TransactionMode::Rollback);
579 }
580}