Skip to main content

reinhardt_admin/core/
model_admin.rs

1//! Model admin configuration and trait
2//!
3//! This module defines how models are displayed and managed in the admin interface.
4
5use crate::types::{AdminError, AdminResult};
6use async_trait::async_trait;
7use reinhardt_db::orm::Filter;
8
9/// Object-safe trait for admin permission checks.
10///
11/// This trait provides the minimum user information needed for admin
12/// permission decisions, without exposing generic type parameters
13/// from [`BaseUser`](reinhardt_auth::BaseUser) or [`FullUser`](reinhardt_auth::FullUser).
14///
15/// A blanket implementation is provided for all types implementing
16/// [`FullUser`](reinhardt_auth::FullUser), so any custom user model
17/// with `FullUser` will automatically satisfy this trait.
18///
19/// For simpler user models that only implement `BaseUser` (without `FullUser`),
20/// this trait can be implemented manually to enable admin authentication.
21pub trait AdminUser: Send + Sync {
22	/// Whether the user account is active
23	fn is_active(&self) -> bool;
24
25	/// Whether the user is a staff member (can access admin)
26	fn is_staff(&self) -> bool;
27
28	/// Whether the user is a superuser (all permissions granted)
29	fn is_superuser(&self) -> bool;
30
31	/// The username for audit logging
32	fn get_username(&self) -> &str;
33}
34
35/// Blanket implementation for all types implementing [`FullUser`](reinhardt_auth::FullUser).
36///
37/// This ensures that any custom user model with `FullUser` implementation
38/// automatically satisfies `AdminUser`.
39impl<T: reinhardt_auth::FullUser> AdminUser for T {
40	fn is_active(&self) -> bool {
41		reinhardt_auth::BaseUser::is_active(self)
42	}
43
44	fn is_staff(&self) -> bool {
45		reinhardt_auth::FullUser::is_staff(self)
46	}
47
48	fn is_superuser(&self) -> bool {
49		reinhardt_auth::FullUser::is_superuser(self)
50	}
51
52	fn get_username(&self) -> &str {
53		reinhardt_auth::FullUser::username(self)
54	}
55}
56
57/// Trait for configuring model administration
58///
59/// Implement this trait to customize how a model is displayed and edited in the admin.
60#[async_trait]
61pub trait ModelAdmin: Send + Sync {
62	/// Get the model name
63	fn model_name(&self) -> &str;
64
65	/// Get the database table name
66	///
67	/// By default, returns an empty string as a placeholder.
68	/// Implementors should override this to return the actual table name.
69	fn table_name(&self) -> &str {
70		// Default implementation returns empty string
71		// Override in implementations to return actual table name
72		""
73	}
74
75	/// Get the primary key field name
76	///
77	/// By default, returns "id".
78	fn pk_field(&self) -> &str {
79		"id"
80	}
81
82	/// Fields to display in list view
83	fn list_display(&self) -> Vec<&str> {
84		vec!["id"]
85	}
86
87	/// Fields that can be used for filtering
88	fn list_filter(&self) -> Vec<&str> {
89		vec![]
90	}
91
92	/// Fields that can be searched
93	fn search_fields(&self) -> Vec<&str> {
94		vec![]
95	}
96
97	/// Fields to display in forms (None = all fields)
98	fn fields(&self) -> Option<Vec<&str>> {
99		None
100	}
101
102	/// Read-only fields
103	fn readonly_fields(&self) -> Vec<&str> {
104		vec![]
105	}
106
107	/// Ordering for list view (prefix with "-" for descending)
108	fn ordering(&self) -> Vec<&str> {
109		vec!["-id"]
110	}
111
112	/// Number of items per page (None = use site default)
113	fn list_per_page(&self) -> Option<usize> {
114		None
115	}
116
117	/// Check if user has permission to view this model
118	///
119	/// Default implementation denies all access (deny-by-default).
120	/// Override this method to grant view permission based on user attributes.
121	///
122	/// # Migration from previous versions
123	///
124	/// Previously, this method accepted `&(dyn std::any::Any + Send + Sync)`.
125	/// It now accepts `&dyn AdminUser` for type-safe permission checks.
126	async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
127		false
128	}
129
130	/// Check if user has permission to add instances
131	///
132	/// Default implementation denies all access (deny-by-default).
133	/// Override this method to grant add permission based on user attributes.
134	async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
135		false
136	}
137
138	/// Check if user has permission to change instances
139	///
140	/// Default implementation denies all access (deny-by-default).
141	/// Override this method to grant change permission based on user attributes.
142	async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
143		false
144	}
145
146	/// Check if user has permission to delete instances
147	///
148	/// Default implementation denies all access (deny-by-default).
149	/// Override this method to grant delete permission based on user attributes.
150	async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
151		false
152	}
153
154	/// Restrict object-level reads and mutations for this user.
155	///
156	/// `None` denies object access. `Some(vec![])` explicitly allows every object,
157	/// while non-empty filters are combined with the requested primary key.
158	fn object_filters(&self, _user: &dyn AdminUser) -> Option<Vec<Filter>> {
159		None
160	}
161}
162
163/// Configuration-based model admin implementation
164///
165/// Provides a simple way to configure model admin without implementing the trait.
166///
167/// # Examples
168///
169/// ```
170/// use reinhardt_admin::core::{ModelAdminConfig, ModelAdmin};
171///
172/// let admin = ModelAdminConfig::builder()
173///     .model_name("User")
174///     .list_display(vec!["id", "username", "email"])
175///     .list_filter(vec!["is_active"])
176///     .search_fields(vec!["username", "email"])
177///     .allow_all(true)
178///     .build()
179///     .unwrap();
180///
181/// assert_eq!(admin.model_name(), "User");
182/// ```
183#[derive(Debug, Clone)]
184pub struct ModelAdminConfig {
185	model_name: String,
186	table_name: Option<String>,
187	pk_field: String,
188	list_display: Vec<String>,
189	list_filter: Vec<String>,
190	search_fields: Vec<String>,
191	fields: Option<Vec<String>>,
192	readonly_fields: Vec<String>,
193	ordering: Vec<String>,
194	list_per_page: Option<usize>,
195	allow_view: bool,
196	allow_add: bool,
197	allow_change: bool,
198	allow_delete: bool,
199}
200
201impl ModelAdminConfig {
202	/// Create a new model admin configuration
203	///
204	/// # Examples
205	///
206	/// ```
207	/// use reinhardt_admin::core::{ModelAdminConfig, ModelAdmin};
208	///
209	/// let admin = ModelAdminConfig::new("User");
210	/// assert_eq!(admin.model_name(), "User");
211	/// ```
212	pub fn new(model_name: impl Into<String>) -> Self {
213		Self {
214			model_name: model_name.into(),
215			table_name: None,
216			pk_field: "id".into(),
217			list_display: vec!["id".into()],
218			list_filter: vec![],
219			search_fields: vec![],
220			fields: None,
221			readonly_fields: vec![],
222			ordering: vec!["-id".into()],
223			list_per_page: None,
224			allow_view: false,
225			allow_add: false,
226			allow_change: false,
227			allow_delete: false,
228		}
229	}
230
231	/// Start building a model admin configuration
232	///
233	/// # Examples
234	///
235	/// ```
236	/// use reinhardt_admin::core::ModelAdminConfig;
237	///
238	/// let admin = ModelAdminConfig::builder()
239	///     .model_name("User")
240	///     .list_display(vec!["id", "username"])
241	///     .build()
242	///     .unwrap();
243	/// ```
244	pub fn builder() -> ModelAdminConfigBuilder {
245		ModelAdminConfigBuilder::default()
246	}
247
248	/// Set list display fields
249	pub fn with_list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
250		self.list_display = fields.into_iter().map(Into::into).collect();
251		self
252	}
253
254	/// Set list filter fields
255	pub fn with_list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
256		self.list_filter = fields.into_iter().map(Into::into).collect();
257		self
258	}
259
260	/// Set search fields
261	pub fn with_search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
262		self.search_fields = fields.into_iter().map(Into::into).collect();
263		self
264	}
265}
266
267#[async_trait]
268impl ModelAdmin for ModelAdminConfig {
269	fn model_name(&self) -> &str {
270		&self.model_name
271	}
272
273	fn table_name(&self) -> &str {
274		self.table_name
275			.as_deref()
276			.unwrap_or(self.model_name.as_str())
277	}
278
279	fn pk_field(&self) -> &str {
280		&self.pk_field
281	}
282
283	fn list_display(&self) -> Vec<&str> {
284		self.list_display.iter().map(|s| s.as_str()).collect()
285	}
286
287	fn list_filter(&self) -> Vec<&str> {
288		self.list_filter.iter().map(|s| s.as_str()).collect()
289	}
290
291	fn search_fields(&self) -> Vec<&str> {
292		self.search_fields.iter().map(|s| s.as_str()).collect()
293	}
294
295	fn fields(&self) -> Option<Vec<&str>> {
296		self.fields
297			.as_ref()
298			.map(|f| f.iter().map(|s| s.as_str()).collect())
299	}
300
301	fn readonly_fields(&self) -> Vec<&str> {
302		self.readonly_fields.iter().map(|s| s.as_str()).collect()
303	}
304
305	fn ordering(&self) -> Vec<&str> {
306		self.ordering.iter().map(|s| s.as_str()).collect()
307	}
308
309	fn list_per_page(&self) -> Option<usize> {
310		self.list_per_page
311	}
312
313	async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
314		self.allow_view
315	}
316
317	async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
318		self.allow_add
319	}
320
321	async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
322		self.allow_change
323	}
324
325	async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
326		self.allow_delete
327	}
328
329	fn object_filters(&self, _user: &dyn AdminUser) -> Option<Vec<Filter>> {
330		Some(Vec::new())
331	}
332}
333
334/// Builder for ModelAdminConfig
335#[derive(Debug, Default)]
336pub struct ModelAdminConfigBuilder {
337	model_name: Option<String>,
338	table_name: Option<String>,
339	pk_field: Option<String>,
340	list_display: Option<Vec<String>>,
341	list_filter: Option<Vec<String>>,
342	search_fields: Option<Vec<String>>,
343	fields: Option<Vec<String>>,
344	readonly_fields: Option<Vec<String>>,
345	ordering: Option<Vec<String>>,
346	list_per_page: Option<usize>,
347	allow_view: Option<bool>,
348	allow_add: Option<bool>,
349	allow_change: Option<bool>,
350	allow_delete: Option<bool>,
351}
352
353impl ModelAdminConfigBuilder {
354	/// Set the model name
355	pub fn model_name(mut self, name: impl Into<String>) -> Self {
356		self.model_name = Some(name.into());
357		self
358	}
359
360	/// Set the database table name
361	///
362	/// If not set, defaults to the model name.
363	pub fn table_name(mut self, name: impl Into<String>) -> Self {
364		self.table_name = Some(name.into());
365		self
366	}
367
368	/// Set the primary key field name
369	///
370	/// If not set, defaults to "id".
371	pub fn pk_field(mut self, field: impl Into<String>) -> Self {
372		self.pk_field = Some(field.into());
373		self
374	}
375
376	/// Set list display fields
377	pub fn list_display(mut self, fields: Vec<impl Into<String>>) -> Self {
378		self.list_display = Some(fields.into_iter().map(Into::into).collect());
379		self
380	}
381
382	/// Set list filter fields
383	pub fn list_filter(mut self, fields: Vec<impl Into<String>>) -> Self {
384		self.list_filter = Some(fields.into_iter().map(Into::into).collect());
385		self
386	}
387
388	/// Set search fields
389	pub fn search_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
390		self.search_fields = Some(fields.into_iter().map(Into::into).collect());
391		self
392	}
393
394	/// Set form fields
395	pub fn fields(mut self, fields: Vec<impl Into<String>>) -> Self {
396		self.fields = Some(fields.into_iter().map(Into::into).collect());
397		self
398	}
399
400	/// Set readonly fields
401	pub fn readonly_fields(mut self, fields: Vec<impl Into<String>>) -> Self {
402		self.readonly_fields = Some(fields.into_iter().map(Into::into).collect());
403		self
404	}
405
406	/// Set ordering
407	pub fn ordering(mut self, fields: Vec<impl Into<String>>) -> Self {
408		self.ordering = Some(fields.into_iter().map(Into::into).collect());
409		self
410	}
411
412	/// Set items per page
413	pub fn list_per_page(mut self, count: usize) -> Self {
414		self.list_per_page = Some(count);
415		self
416	}
417
418	/// Set view permission
419	///
420	/// If not set, defaults to `false` (deny-by-default).
421	pub fn allow_view(mut self, allow: bool) -> Self {
422		self.allow_view = Some(allow);
423		self
424	}
425
426	/// Set add permission
427	///
428	/// If not set, defaults to `false` (deny-by-default).
429	pub fn allow_add(mut self, allow: bool) -> Self {
430		self.allow_add = Some(allow);
431		self
432	}
433
434	/// Set change permission
435	///
436	/// If not set, defaults to `false` (deny-by-default).
437	pub fn allow_change(mut self, allow: bool) -> Self {
438		self.allow_change = Some(allow);
439		self
440	}
441
442	/// Set delete permission
443	///
444	/// If not set, defaults to `false` (deny-by-default).
445	pub fn allow_delete(mut self, allow: bool) -> Self {
446		self.allow_delete = Some(allow);
447		self
448	}
449
450	/// Set all permissions (view, add, change, delete) at once
451	///
452	/// Convenience method for granting or denying all operations.
453	///
454	/// # Examples
455	///
456	/// ```
457	/// use reinhardt_admin::core::ModelAdminConfig;
458	///
459	/// let admin = ModelAdminConfig::builder()
460	///     .model_name("User")
461	///     .allow_all(true)
462	///     .build()
463	///     .unwrap();
464	/// ```
465	pub fn allow_all(mut self, allow: bool) -> Self {
466		self.allow_view = Some(allow);
467		self.allow_add = Some(allow);
468		self.allow_change = Some(allow);
469		self.allow_delete = Some(allow);
470		self
471	}
472
473	/// Build the configuration
474	///
475	/// # Errors
476	///
477	/// Returns `AdminError::ValidationError` if `model_name` is not set.
478	pub fn build(self) -> AdminResult<ModelAdminConfig> {
479		let model_name = self
480			.model_name
481			.ok_or_else(|| AdminError::ValidationError("model_name is required".to_string()))?;
482
483		Ok(ModelAdminConfig {
484			model_name,
485			table_name: self.table_name,
486			pk_field: self.pk_field.unwrap_or_else(|| "id".into()),
487			list_display: self.list_display.unwrap_or_else(|| vec!["id".into()]),
488			list_filter: self.list_filter.unwrap_or_default(),
489			search_fields: self.search_fields.unwrap_or_default(),
490			fields: self.fields,
491			readonly_fields: self.readonly_fields.unwrap_or_default(),
492			ordering: self.ordering.unwrap_or_else(|| vec!["-id".into()]),
493			list_per_page: self.list_per_page,
494			allow_view: self.allow_view.unwrap_or(false),
495			allow_add: self.allow_add.unwrap_or(false),
496			allow_change: self.allow_change.unwrap_or(false),
497			allow_delete: self.allow_delete.unwrap_or(false),
498		})
499	}
500}
501
502#[cfg(all(test, server))]
503mod tests {
504	use super::*;
505	use rstest::rstest;
506
507	/// Dummy AdminUser for testing permission methods
508	struct TestAdminUser {
509		active: bool,
510		staff: bool,
511		superuser: bool,
512		username: String,
513	}
514
515	impl TestAdminUser {
516		fn new() -> Self {
517			Self {
518				active: true,
519				staff: true,
520				superuser: false,
521				username: "test_user".to_string(),
522			}
523		}
524	}
525
526	impl AdminUser for TestAdminUser {
527		fn is_active(&self) -> bool {
528			self.active
529		}
530
531		fn is_staff(&self) -> bool {
532			self.staff
533		}
534
535		fn is_superuser(&self) -> bool {
536			self.superuser
537		}
538
539		fn get_username(&self) -> &str {
540			&self.username
541		}
542	}
543
544	#[rstest]
545	fn test_model_admin_config_creation() {
546		let admin = ModelAdminConfig::new("User");
547		assert_eq!(admin.model_name(), "User");
548		assert_eq!(admin.list_display(), vec!["id"]);
549		assert_eq!(admin.list_filter(), Vec::<&str>::new());
550	}
551
552	#[rstest]
553	fn test_model_admin_config_builder() {
554		let admin = ModelAdminConfig::builder()
555			.model_name("User")
556			.list_display(vec!["id", "username", "email"])
557			.list_filter(vec!["is_active"])
558			.search_fields(vec!["username", "email"])
559			.list_per_page(50)
560			.build()
561			.unwrap();
562
563		assert_eq!(admin.model_name(), "User");
564		assert_eq!(admin.list_display(), vec!["id", "username", "email"]);
565		assert_eq!(admin.list_filter(), vec!["is_active"]);
566		assert_eq!(admin.search_fields(), vec!["username", "email"]);
567		assert_eq!(admin.list_per_page(), Some(50));
568	}
569
570	#[rstest]
571	fn test_with_methods() {
572		let admin = ModelAdminConfig::new("Post")
573			.with_list_display(vec!["id", "title", "author"])
574			.with_list_filter(vec!["status", "created_at"])
575			.with_search_fields(vec!["title", "content"]);
576
577		assert_eq!(admin.list_display(), vec!["id", "title", "author"]);
578		assert_eq!(admin.list_filter(), vec!["status", "created_at"]);
579		assert_eq!(admin.search_fields(), vec!["title", "content"]);
580	}
581
582	#[rstest]
583	fn test_builder_without_model_name_returns_error() {
584		// Arrange & Act
585		let result = ModelAdminConfig::builder().build();
586
587		// Assert
588		assert!(result.is_err());
589		let err = result.unwrap_err();
590		assert!(err.to_string().contains("model_name is required"));
591	}
592
593	/// Helper struct for testing default trait permission behavior
594	struct DefaultPermissionAdmin;
595
596	#[async_trait]
597	impl ModelAdmin for DefaultPermissionAdmin {
598		fn model_name(&self) -> &str {
599			"TestModel"
600		}
601	}
602
603	/// Helper struct for testing explicit permission grants
604	struct AllowAllPermissionAdmin;
605
606	#[async_trait]
607	impl ModelAdmin for AllowAllPermissionAdmin {
608		fn model_name(&self) -> &str {
609			"AllowedModel"
610		}
611
612		async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
613			true
614		}
615
616		async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
617			true
618		}
619
620		async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
621			true
622		}
623
624		async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
625			true
626		}
627	}
628
629	#[rstest]
630	#[tokio::test]
631	async fn test_default_permissions_deny_view() {
632		// Arrange
633		let admin = DefaultPermissionAdmin;
634		let user = TestAdminUser::new();
635
636		// Act
637		let result = admin.has_view_permission(&user as &dyn AdminUser).await;
638
639		// Assert
640		assert_eq!(result, false);
641	}
642
643	#[rstest]
644	#[tokio::test]
645	async fn test_default_permissions_deny_add() {
646		// Arrange
647		let admin = DefaultPermissionAdmin;
648		let user = TestAdminUser::new();
649
650		// Act
651		let result = admin.has_add_permission(&user as &dyn AdminUser).await;
652
653		// Assert
654		assert_eq!(result, false);
655	}
656
657	#[rstest]
658	#[tokio::test]
659	async fn test_default_permissions_deny_change() {
660		// Arrange
661		let admin = DefaultPermissionAdmin;
662		let user = TestAdminUser::new();
663
664		// Act
665		let result = admin.has_change_permission(&user as &dyn AdminUser).await;
666
667		// Assert
668		assert_eq!(result, false);
669	}
670
671	#[rstest]
672	#[tokio::test]
673	async fn test_default_permissions_deny_delete() {
674		// Arrange
675		let admin = DefaultPermissionAdmin;
676		let user = TestAdminUser::new();
677
678		// Act
679		let result = admin.has_delete_permission(&user as &dyn AdminUser).await;
680
681		// Assert
682		assert_eq!(result, false);
683	}
684
685	#[rstest]
686	#[tokio::test]
687	async fn test_explicit_override_grants_all_permissions() {
688		// Arrange
689		let admin = AllowAllPermissionAdmin;
690		let user = TestAdminUser::new();
691
692		// Act
693		let view = admin.has_view_permission(&user as &dyn AdminUser).await;
694		let add = admin.has_add_permission(&user as &dyn AdminUser).await;
695		let change = admin.has_change_permission(&user as &dyn AdminUser).await;
696		let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
697
698		// Assert
699		assert_eq!(view, true);
700		assert_eq!(add, true);
701		assert_eq!(change, true);
702		assert_eq!(delete, true);
703	}
704
705	#[rstest]
706	#[tokio::test]
707	async fn test_model_admin_config_inherits_deny_by_default() {
708		// Arrange
709		let admin = ModelAdminConfig::new("User");
710		let user = TestAdminUser::new();
711
712		// Act
713		let view = admin.has_view_permission(&user as &dyn AdminUser).await;
714		let add = admin.has_add_permission(&user as &dyn AdminUser).await;
715		let change = admin.has_change_permission(&user as &dyn AdminUser).await;
716		let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
717
718		// Assert
719		assert_eq!(view, false);
720		assert_eq!(add, false);
721		assert_eq!(change, false);
722		assert_eq!(delete, false);
723	}
724
725	// ==================== ModelAdminConfig field tests ====================
726
727	#[rstest]
728	fn test_model_admin_config_custom_pk_field() {
729		// Arrange
730		let admin = ModelAdminConfig::builder()
731			.model_name("User")
732			.pk_field("uuid")
733			.build()
734			.unwrap();
735
736		// Act
737		let pk = admin.pk_field();
738
739		// Assert
740		assert_eq!(pk, "uuid");
741	}
742
743	#[rstest]
744	fn test_model_admin_config_default_pk_field() {
745		// Arrange
746		let admin = ModelAdminConfig::builder()
747			.model_name("User")
748			.build()
749			.unwrap();
750
751		// Act
752		let pk = admin.pk_field();
753
754		// Assert
755		assert_eq!(pk, "id");
756	}
757
758	#[rstest]
759	fn test_model_admin_config_custom_table_name() {
760		// Arrange
761		let admin = ModelAdminConfig::builder()
762			.model_name("User")
763			.table_name("my_users")
764			.build()
765			.unwrap();
766
767		// Act
768		let table = admin.table_name();
769
770		// Assert
771		assert_eq!(table, "my_users");
772	}
773
774	#[rstest]
775	fn test_model_admin_config_table_name_defaults_to_model_name() {
776		// Arrange
777		let admin = ModelAdminConfig::builder()
778			.model_name("User")
779			.build()
780			.unwrap();
781
782		// Act
783		let table = admin.table_name();
784
785		// Assert
786		assert_eq!(table, "User");
787	}
788
789	#[rstest]
790	#[tokio::test]
791	async fn test_model_admin_config_builder_inherits_deny_by_default() {
792		// Arrange
793		let admin = ModelAdminConfig::builder()
794			.model_name("Post")
795			.list_display(vec!["id", "title"])
796			.build()
797			.unwrap();
798		let user = TestAdminUser::new();
799
800		// Act
801		let view = admin.has_view_permission(&user as &dyn AdminUser).await;
802		let add = admin.has_add_permission(&user as &dyn AdminUser).await;
803
804		// Assert
805		assert_eq!(view, false);
806		assert_eq!(add, false);
807	}
808
809	#[rstest]
810	#[tokio::test]
811	async fn test_builder_allow_view_grants_view_permission() {
812		// Arrange
813		let admin = ModelAdminConfig::builder()
814			.model_name("Post")
815			.allow_view(true)
816			.build()
817			.unwrap();
818		let user = TestAdminUser::new();
819
820		// Act
821		let view = admin.has_view_permission(&user as &dyn AdminUser).await;
822		let add = admin.has_add_permission(&user as &dyn AdminUser).await;
823
824		// Assert
825		assert_eq!(view, true);
826		assert_eq!(add, false);
827	}
828
829	#[rstest]
830	#[tokio::test]
831	async fn test_builder_allow_all_grants_all_permissions() {
832		// Arrange
833		let admin = ModelAdminConfig::builder()
834			.model_name("Post")
835			.allow_all(true)
836			.build()
837			.unwrap();
838		let user = TestAdminUser::new();
839
840		// Act
841		let view = admin.has_view_permission(&user as &dyn AdminUser).await;
842		let add = admin.has_add_permission(&user as &dyn AdminUser).await;
843		let change = admin.has_change_permission(&user as &dyn AdminUser).await;
844		let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
845
846		// Assert
847		assert_eq!(view, true);
848		assert_eq!(add, true);
849		assert_eq!(change, true);
850		assert_eq!(delete, true);
851	}
852
853	#[rstest]
854	#[tokio::test]
855	async fn test_builder_allow_all_false_denies_all() {
856		// Arrange
857		let admin = ModelAdminConfig::builder()
858			.model_name("Post")
859			.allow_all(false)
860			.build()
861			.unwrap();
862		let user = TestAdminUser::new();
863
864		// Act
865		let view = admin.has_view_permission(&user as &dyn AdminUser).await;
866		let add = admin.has_add_permission(&user as &dyn AdminUser).await;
867
868		// Assert
869		assert_eq!(view, false);
870		assert_eq!(add, false);
871	}
872
873	#[rstest]
874	#[tokio::test]
875	async fn test_builder_individual_permissions() {
876		// Arrange
877		let admin = ModelAdminConfig::builder()
878			.model_name("Post")
879			.allow_view(true)
880			.allow_add(true)
881			.allow_change(false)
882			.allow_delete(false)
883			.build()
884			.unwrap();
885		let user = TestAdminUser::new();
886
887		// Act
888		let view = admin.has_view_permission(&user as &dyn AdminUser).await;
889		let add = admin.has_add_permission(&user as &dyn AdminUser).await;
890		let change = admin.has_change_permission(&user as &dyn AdminUser).await;
891		let delete = admin.has_delete_permission(&user as &dyn AdminUser).await;
892
893		// Assert
894		assert_eq!(view, true);
895		assert_eq!(add, true);
896		assert_eq!(change, false);
897		assert_eq!(delete, false);
898	}
899
900	// ==================== Decision table: allow_all controls permissions ====================
901
902	#[rstest]
903	#[case::allow_all_true(true, true)]
904	#[case::allow_all_false(false, false)]
905	#[tokio::test]
906	async fn test_allow_all_controls_view_permission(
907		#[case] allow_all: bool,
908		#[case] expected: bool,
909	) {
910		// Arrange
911		let admin = ModelAdminConfig::builder()
912			.model_name("PermTest")
913			.allow_all(allow_all)
914			.build()
915			.unwrap();
916		let user = TestAdminUser::new();
917
918		// Act
919		let result = admin.has_view_permission(&user as &dyn AdminUser).await;
920
921		// Assert
922		assert_eq!(result, expected);
923	}
924
925	// ==================== Boundary value: list_per_page override ====================
926
927	#[rstest]
928	#[case::with_list_per_page(Some(50), Some(50))]
929	#[case::without_list_per_page(None, None)]
930	fn test_list_per_page_override(
931		#[case] override_value: Option<usize>,
932		#[case] expected: Option<usize>,
933	) {
934		// Arrange
935		let mut builder = ModelAdminConfig::builder().model_name("PageTest");
936		if let Some(v) = override_value {
937			builder = builder.list_per_page(v);
938		}
939		let admin = builder.build().unwrap();
940
941		// Act
942		let result = admin.list_per_page();
943
944		// Assert
945		assert_eq!(result, expected);
946	}
947
948	// ==================== Boundary value: builder model_name validation ====================
949
950	#[rstest]
951	#[case::missing_model_name(true)]
952	#[case::valid_model_name(false)]
953	fn test_builder_model_name_validation(#[case] should_error: bool) {
954		// Arrange
955		let builder = if should_error {
956			// Do not set model_name to trigger error
957			ModelAdminConfig::builder()
958		} else {
959			ModelAdminConfig::builder().model_name("User")
960		};
961
962		// Act
963		let result = builder.build();
964
965		// Assert
966		assert_eq!(
967			result.is_err(),
968			should_error,
969			"should_error={}, got {:?}",
970			should_error,
971			result
972		);
973	}
974}