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