Skip to main content

ferro_cli/templates/
mod.rs

1mod ai_boost;
2mod auth;
3pub mod ci_workflow;
4#[path = "do.rs"]
5pub mod do_;
6pub mod docker;
7mod entity;
8mod make;
9pub mod module;
10mod project;
11mod scaffold;
12
13pub use ai_boost::*;
14pub use auth::*;
15pub use docker::*;
16pub use entity::*;
17pub use make::*;
18pub use project::*;
19pub use scaffold::*;
20
21// ============================================================================
22// Tests
23// ============================================================================
24
25#[cfg(test)]
26mod tests {
27    use super::*;
28
29    // -------------------------------------------------------------------------
30    // Backend Template Tests
31    // -------------------------------------------------------------------------
32
33    #[test]
34    fn test_cargo_toml_substitution() {
35        let result = cargo_toml("my_app", "A test app", "Test Author <test@example.com>");
36        assert!(result.contains("name = \"my_app\""));
37        assert!(result.contains("description = \"A test app\""));
38        assert!(result.contains("authors = [\"Test Author <test@example.com>\"]"));
39    }
40
41    #[test]
42    fn test_cargo_toml_empty_author() {
43        let result = cargo_toml("my_app", "A test app", "");
44        assert!(result.contains("name = \"my_app\""));
45        assert!(!result.contains("authors = "));
46    }
47
48    #[test]
49    fn test_main_rs_substitution() {
50        let result = main_rs("my_app");
51        assert!(result.contains("my_app"));
52    }
53
54    #[test]
55    fn test_routes_rs_not_empty() {
56        assert!(!routes_rs().is_empty());
57        assert!(routes_rs().contains("routes"));
58    }
59
60    #[test]
61    fn test_controllers_mod_not_empty() {
62        assert!(!controllers_mod().is_empty());
63        assert!(controllers_mod().contains("auth"));
64        assert!(controllers_mod().contains("dashboard"));
65        assert!(controllers_mod().contains("profile"));
66        assert!(controllers_mod().contains("settings"));
67    }
68
69    #[test]
70    fn test_home_controller_not_empty() {
71        assert!(!home_controller().is_empty());
72        assert!(home_controller().contains("async fn index"));
73    }
74
75    #[test]
76    fn test_auth_controller_not_empty() {
77        assert!(!auth_controller().is_empty());
78        assert!(auth_controller().contains("login"));
79        assert!(auth_controller().contains("register"));
80    }
81
82    #[test]
83    fn test_dashboard_controller_not_empty() {
84        assert!(!dashboard_controller().is_empty());
85        assert!(dashboard_controller().contains("Dashboard"));
86    }
87
88    #[test]
89    fn test_profile_controller_not_empty() {
90        let content = profile_controller();
91        assert!(!content.is_empty());
92        assert!(content.contains("Profile"));
93        assert!(content.contains("async fn"));
94    }
95
96    #[test]
97    fn test_settings_controller_not_empty() {
98        let content = settings_controller();
99        assert!(!content.is_empty());
100        assert!(content.contains("Settings"));
101        assert!(content.contains("async fn"));
102    }
103
104    // -------------------------------------------------------------------------
105    // Middleware Template Tests
106    // -------------------------------------------------------------------------
107
108    #[test]
109    fn test_middleware_mod_not_empty() {
110        assert!(!middleware_mod().is_empty());
111        assert!(middleware_mod().contains("logging"));
112    }
113
114    #[test]
115    fn test_middleware_template_substitution() {
116        let result = middleware_template("auth", "AuthMiddleware");
117        assert!(result.contains("auth middleware"));
118        assert!(result.contains("pub struct AuthMiddleware"));
119        assert!(result.contains("impl Middleware for AuthMiddleware"));
120    }
121
122    #[test]
123    fn test_authenticate_middleware_not_empty() {
124        assert!(!authenticate_middleware().is_empty());
125        assert!(authenticate_middleware().contains("Middleware"));
126    }
127
128    // -------------------------------------------------------------------------
129    // Model Template Tests
130    // -------------------------------------------------------------------------
131
132    #[test]
133    fn test_models_mod_not_empty() {
134        let content = models_mod();
135        assert!(!content.is_empty());
136        assert!(content.contains("user"));
137        assert!(content.contains("password_reset_tokens"));
138    }
139
140    #[test]
141    fn test_user_model_not_empty() {
142        assert!(!user_model().is_empty());
143        assert!(user_model().contains("User"));
144    }
145
146    #[test]
147    fn test_password_reset_tokens_model_not_empty() {
148        let content = password_reset_tokens_model();
149        assert!(!content.is_empty());
150        assert!(content.contains("password_reset_tokens"));
151        assert!(content.contains("email"));
152        assert!(content.contains("token"));
153    }
154
155    // -------------------------------------------------------------------------
156    // Migration Template Tests
157    // -------------------------------------------------------------------------
158
159    #[test]
160    fn test_migrations_mod_not_empty() {
161        let content = migrations_mod();
162        assert!(!content.is_empty());
163        assert!(content.contains("create_users_table"));
164        assert!(content.contains("create_sessions_table"));
165        assert!(content.contains("create_password_reset_tokens_table"));
166    }
167
168    #[test]
169    fn test_create_users_migration_not_empty() {
170        assert!(!create_users_migration().is_empty());
171        assert!(create_users_migration().contains("Users"));
172    }
173
174    #[test]
175    fn test_create_sessions_migration_not_empty() {
176        assert!(!create_sessions_migration().is_empty());
177        assert!(create_sessions_migration().contains("sessions"));
178    }
179
180    #[test]
181    fn test_create_password_reset_tokens_migration_not_empty() {
182        let content = create_password_reset_tokens_migration();
183        assert!(!content.is_empty());
184        assert!(content.contains("password_reset_tokens"));
185    }
186
187    // -------------------------------------------------------------------------
188    // Config Template Tests
189    // -------------------------------------------------------------------------
190
191    #[test]
192    fn test_config_mod_not_empty() {
193        assert!(!config_mod().is_empty());
194        assert!(config_mod().contains("database"));
195    }
196
197    #[test]
198    fn test_config_database_not_empty() {
199        assert!(!config_database().is_empty());
200    }
201
202    #[test]
203    fn test_config_mail_not_empty() {
204        assert!(!config_mail().is_empty());
205    }
206
207    // -------------------------------------------------------------------------
208    // Frontend Template Tests
209    // -------------------------------------------------------------------------
210
211    #[test]
212    fn test_package_json_substitution() {
213        let result = package_json("my-project");
214        assert!(result.contains("\"name\": \"my-project-frontend\""));
215    }
216
217    #[test]
218    fn test_vite_config_not_empty() {
219        assert!(!vite_config().is_empty());
220        assert!(vite_config().contains("vite"));
221    }
222
223    #[test]
224    fn test_tsconfig_not_empty() {
225        assert!(!tsconfig().is_empty());
226        assert!(tsconfig().contains("compilerOptions"));
227    }
228
229    #[test]
230    fn test_index_html_substitution() {
231        let result = index_html("My App");
232        assert!(result.contains("<title>My App</title>"));
233    }
234
235    #[test]
236    fn test_main_tsx_not_empty() {
237        assert!(!main_tsx().is_empty());
238        assert!(main_tsx().contains("createInertiaApp"));
239    }
240
241    #[test]
242    fn test_home_page_not_empty() {
243        assert!(!home_page().is_empty());
244        assert!(home_page().contains("Home"));
245    }
246
247    #[test]
248    fn test_inertia_props_types_not_empty() {
249        let content = inertia_props_types();
250        assert!(!content.is_empty());
251        assert!(content.contains("User"));
252        assert!(content.contains("DashboardProps"));
253        assert!(content.contains("ProfileProps"));
254        assert!(content.contains("SettingsProps"));
255    }
256
257    // -------------------------------------------------------------------------
258    // Frontend Layout Template Tests
259    // -------------------------------------------------------------------------
260
261    #[test]
262    fn test_app_layout_not_empty() {
263        let content = app_layout();
264        assert!(!content.is_empty());
265        assert!(content.contains("AppLayout"));
266        assert!(content.contains("Sidebar"));
267    }
268
269    #[test]
270    fn test_auth_layout_not_empty() {
271        let content = auth_layout();
272        assert!(!content.is_empty());
273        assert!(content.contains("AuthLayout"));
274    }
275
276    #[test]
277    fn test_layouts_index_not_empty() {
278        let content = layouts_index();
279        assert!(!content.is_empty());
280        assert!(content.contains("AppLayout"));
281        assert!(content.contains("AuthLayout"));
282    }
283
284    #[test]
285    fn test_globals_css_not_empty() {
286        let content = globals_css();
287        assert!(!content.is_empty());
288        // Tailwind CSS v4 uses @import "tailwindcss" instead of @tailwind directives
289        assert!(content.contains("tailwindcss"));
290    }
291
292    // -------------------------------------------------------------------------
293    // Frontend Auth Page Template Tests
294    // -------------------------------------------------------------------------
295
296    #[test]
297    fn test_login_page_not_empty() {
298        let content = login_page();
299        assert!(!content.is_empty());
300        assert!(content.contains("Login"));
301        assert!(content.contains("AuthLayout"));
302    }
303
304    #[test]
305    fn test_register_page_not_empty() {
306        let content = register_page();
307        assert!(!content.is_empty());
308        assert!(content.contains("Register"));
309        assert!(content.contains("AuthLayout"));
310    }
311
312    #[test]
313    fn test_forgot_password_page_not_empty() {
314        let content = forgot_password_page();
315        assert!(!content.is_empty());
316        assert!(content.contains("ForgotPassword"));
317        assert!(content.contains("AuthLayout"));
318    }
319
320    #[test]
321    fn test_reset_password_page_not_empty() {
322        let content = reset_password_page();
323        assert!(!content.is_empty());
324        assert!(content.contains("ResetPassword"));
325        assert!(content.contains("AuthLayout"));
326    }
327
328    // -------------------------------------------------------------------------
329    // Frontend User Page Template Tests
330    // -------------------------------------------------------------------------
331
332    #[test]
333    fn test_dashboard_page_not_empty() {
334        let content = dashboard_page();
335        assert!(!content.is_empty());
336        assert!(content.contains("Dashboard"));
337        assert!(content.contains("AppLayout"));
338    }
339
340    #[test]
341    fn test_profile_page_not_empty() {
342        let content = profile_page();
343        assert!(!content.is_empty());
344        assert!(content.contains("Profile"));
345        assert!(content.contains("AppLayout"));
346    }
347
348    #[test]
349    fn test_settings_page_not_empty() {
350        let content = settings_page();
351        assert!(!content.is_empty());
352        assert!(content.contains("Settings"));
353        assert!(content.contains("AppLayout"));
354    }
355
356    // -------------------------------------------------------------------------
357    // Controller Template Generation Tests
358    // -------------------------------------------------------------------------
359
360    #[test]
361    fn test_controller_template_substitution() {
362        let result = controller_template("users");
363        assert!(result.contains("users controller"));
364        assert!(result.contains("#[handler]"));
365    }
366
367    // -------------------------------------------------------------------------
368    // Action Template Tests
369    // -------------------------------------------------------------------------
370
371    #[test]
372    fn test_action_template_substitution() {
373        let result = action_template("create_user", "CreateUser");
374        assert!(result.contains("create_user action"));
375        assert!(result.contains("pub struct CreateUser"));
376        assert!(result.contains("#[injectable]"));
377    }
378
379    #[test]
380    fn test_actions_mod_not_empty() {
381        assert!(!actions_mod().is_empty());
382    }
383
384    // -------------------------------------------------------------------------
385    // Error Template Tests
386    // -------------------------------------------------------------------------
387
388    #[test]
389    fn test_error_template_substitution() {
390        let result = error_template("UserNotFound");
391        assert!(result.contains("UserNotFound error"));
392        assert!(result.contains("pub struct UserNotFound"));
393        assert!(result.contains("#[domain_error"));
394    }
395
396    // -------------------------------------------------------------------------
397    // Inertia Page Template Tests
398    // -------------------------------------------------------------------------
399
400    #[test]
401    fn test_inertia_page_template_substitution() {
402        let result = inertia_page_template("Users");
403        assert!(result.contains("export default function Users()"));
404        assert!(result.contains("<h1"));
405    }
406
407    // -------------------------------------------------------------------------
408    // Event/Listener/Job Template Tests
409    // -------------------------------------------------------------------------
410
411    #[test]
412    fn test_event_template_substitution() {
413        let result = event_template("user_registered", "UserRegistered");
414        assert!(result.contains("UserRegistered"));
415        assert!(result.contains("impl Event for UserRegistered"));
416    }
417
418    #[test]
419    fn test_listener_template_substitution() {
420        let result = listener_template("send_welcome_email", "SendWelcomeEmail", "UserRegistered");
421        assert!(result.contains("SendWelcomeEmail"));
422        assert!(result.contains("impl Listener<UserRegistered>"));
423    }
424
425    #[test]
426    fn test_job_template_substitution() {
427        let result = job_template("send_email", "SendEmail");
428        assert!(result.contains("SendEmail"));
429        assert!(result.contains("impl Job for SendEmail"));
430    }
431
432    #[test]
433    fn test_events_mod_not_empty() {
434        assert!(!events_mod().is_empty());
435    }
436
437    #[test]
438    fn test_listeners_mod_not_empty() {
439        assert!(!listeners_mod().is_empty());
440    }
441
442    #[test]
443    fn test_jobs_mod_not_empty() {
444        assert!(!jobs_mod().is_empty());
445    }
446
447    // -------------------------------------------------------------------------
448    // Notification Template Tests
449    // -------------------------------------------------------------------------
450
451    #[test]
452    fn test_notification_template_substitution() {
453        let result = notification_template("order_shipped", "OrderShipped");
454        assert!(result.contains("OrderShipped"));
455        assert!(result.contains("impl Notification for OrderShipped"));
456    }
457
458    #[test]
459    fn test_notifications_mod_not_empty() {
460        assert!(!notifications_mod().is_empty());
461    }
462
463    // -------------------------------------------------------------------------
464    // Task Template Tests
465    // -------------------------------------------------------------------------
466
467    #[test]
468    fn test_task_template_substitution() {
469        let result = task_template("cleanup_old_sessions", "CleanupOldSessions");
470        assert!(result.contains("CleanupOldSessions"));
471        assert!(result.contains("impl Task for CleanupOldSessions"));
472    }
473
474    #[test]
475    fn test_tasks_mod_not_empty() {
476        assert!(!tasks_mod().is_empty());
477    }
478
479    #[test]
480    fn test_schedule_rs_not_empty() {
481        assert!(!schedule_rs().is_empty());
482    }
483
484    // -------------------------------------------------------------------------
485    // Seeder Template Tests
486    // -------------------------------------------------------------------------
487
488    #[test]
489    fn test_seeder_template_substitution() {
490        let result = seeder_template("users_seeder", "UsersSeeder");
491        assert!(result.contains("UsersSeeder"));
492        assert!(result.contains("impl Seeder for UsersSeeder"));
493    }
494
495    #[test]
496    fn test_seeders_mod_not_empty() {
497        assert!(!seeders_mod().is_empty());
498    }
499
500    // -------------------------------------------------------------------------
501    // Factory Template Tests
502    // -------------------------------------------------------------------------
503
504    #[test]
505    fn test_factory_template_substitution() {
506        let result = factory_template("user_factory", "UserFactory", "User");
507        assert!(result.contains("UserFactory"));
508        assert!(result.contains("impl Factory for UserFactory"));
509    }
510
511    #[test]
512    fn test_factories_mod_not_empty() {
513        assert!(!factories_mod().is_empty());
514    }
515
516    // -------------------------------------------------------------------------
517    // Policy Template Tests
518    // -------------------------------------------------------------------------
519
520    #[test]
521    fn test_policy_template_substitution() {
522        let result = policy_template("post_policy", "PostPolicy", "Post");
523        assert!(result.contains("PostPolicy"));
524        assert!(result.contains("impl Policy<Post>"));
525    }
526
527    #[test]
528    fn test_policies_mod_not_empty() {
529        assert!(!policies_mod().is_empty());
530    }
531
532    // -------------------------------------------------------------------------
533    // Docker Template Tests
534    // -------------------------------------------------------------------------
535
536    #[test]
537    fn test_dockerignore_template_not_empty() {
538        assert!(!dockerignore_template().is_empty());
539    }
540
541    #[test]
542    fn test_docker_compose_template_basic() {
543        let result = docker_compose_template("my_project", false, false);
544        assert!(result.contains("my_project"));
545        assert!(result.contains("postgres"));
546    }
547
548    #[test]
549    fn test_docker_compose_template_with_mailpit() {
550        let result = docker_compose_template("my_project", true, false);
551        assert!(result.contains("mailpit"));
552    }
553
554    #[test]
555    fn test_docker_compose_template_with_minio() {
556        let result = docker_compose_template("my_project", false, true);
557        assert!(result.contains("minio"));
558    }
559
560    // -------------------------------------------------------------------------
561    // Root File Template Tests
562    // -------------------------------------------------------------------------
563
564    #[test]
565    fn test_gitignore_not_empty() {
566        assert!(!gitignore().is_empty());
567        assert!(gitignore().contains("target"));
568    }
569
570    #[test]
571    fn test_env_substitution() {
572        let result = env("my_project");
573        assert!(result.contains("my_project"));
574    }
575
576    #[test]
577    fn test_env_example_not_empty() {
578        assert!(!env_example().is_empty());
579    }
580
581    #[test]
582    fn test_readme_substitution() {
583        let result = readme("my-app", "My App", "A test description");
584        assert!(result.contains("# My App"));
585        assert!(result.contains("A test description"));
586        assert!(result.contains("cd my-app"));
587        assert!(result.contains("ferro serve"));
588        assert!(result.contains("ferro db:migrate"));
589    }
590
591    #[test]
592    fn test_gitignore_does_not_ignore_cargo_lock() {
593        // Binary crates must commit Cargo.lock for reproducible builds.
594        let content = gitignore();
595        // No bare `Cargo.lock` line (comments mentioning it are fine).
596        assert!(
597            !content.lines().any(|l| l.trim() == "Cargo.lock"),
598            "gitignore template must not ignore Cargo.lock for binary crates"
599        );
600    }
601
602    // -------------------------------------------------------------------------
603    // AI Development Boost Template Tests
604    // -------------------------------------------------------------------------
605
606    #[test]
607    fn test_ferro_guidelines_template_not_empty() {
608        let content = ferro_guidelines_template();
609        assert!(!content.is_empty());
610        assert!(content.contains("Ferro Framework"));
611    }
612
613    #[test]
614    fn test_cursor_rules_template_not_empty() {
615        let content = cursor_rules_template();
616        assert!(!content.is_empty());
617        assert!(content.contains("Ferro"));
618    }
619
620    #[test]
621    fn test_claude_md_template_not_empty() {
622        let content = claude_md_template();
623        assert!(!content.is_empty());
624        assert!(content.contains("Ferro"));
625    }
626
627    #[test]
628    fn test_copilot_instructions_template_not_empty() {
629        let content = copilot_instructions_template();
630        assert!(!content.is_empty());
631        assert!(content.contains("Ferro"));
632    }
633
634    // -------------------------------------------------------------------------
635    // Entity Generation Helper Tests
636    // -------------------------------------------------------------------------
637
638    #[test]
639    fn test_entity_template_generates_valid_rust() {
640        let columns = vec![
641            ColumnInfo {
642                name: "id".to_string(),
643                col_type: "INTEGER".to_string(),
644                is_nullable: false,
645                is_primary_key: true,
646            },
647            ColumnInfo {
648                name: "name".to_string(),
649                col_type: "VARCHAR".to_string(),
650                is_nullable: false,
651                is_primary_key: false,
652            },
653            ColumnInfo {
654                name: "email".to_string(),
655                col_type: "VARCHAR".to_string(),
656                is_nullable: true,
657                is_primary_key: false,
658            },
659        ];
660
661        let result = entity_template("users", &columns);
662        assert!(result.contains("table_name = \"users\""));
663        assert!(result.contains("pub id: i32"));
664        assert!(result.contains("pub name: String"));
665        assert!(result.contains("pub email: Option<String>"));
666        assert!(result.contains("#[sea_orm(primary_key)]"));
667    }
668
669    #[test]
670    fn test_entity_template_handles_reserved_keywords() {
671        let columns = vec![
672            ColumnInfo {
673                name: "id".to_string(),
674                col_type: "INTEGER".to_string(),
675                is_nullable: false,
676                is_primary_key: true,
677            },
678            ColumnInfo {
679                name: "type".to_string(),
680                col_type: "VARCHAR".to_string(),
681                is_nullable: false,
682                is_primary_key: false,
683            },
684        ];
685
686        let result = entity_template("items", &columns);
687        assert!(result.contains("pub r#type: String"));
688        assert!(result.contains("column_name = \"type\""));
689    }
690
691    #[test]
692    fn test_user_model_template_generates_minimal_file() {
693        let columns = vec![
694            ColumnInfo {
695                name: "id".to_string(),
696                col_type: "INTEGER".to_string(),
697                is_nullable: false,
698                is_primary_key: true,
699            },
700            ColumnInfo {
701                name: "name".to_string(),
702                col_type: "VARCHAR".to_string(),
703                is_nullable: false,
704                is_primary_key: false,
705            },
706        ];
707
708        let result = user_model_template("users", "User", &columns);
709        // Type alias for convenient access
710        assert!(result.contains("pub type User = Model"));
711        // Re-exports entity module
712        assert!(result.contains("pub use super::entities::users::*"));
713        // Users table should have Authenticatable impl
714        assert!(result.contains("impl ferro::auth::Authenticatable for Model"));
715        // Should NOT contain manual method implementations (now generated by FerroModel macro)
716        assert!(!result.contains("pub fn query()"));
717        assert!(!result.contains("pub fn create()"));
718        assert!(!result.contains("pub struct UserBuilder"));
719    }
720
721    #[test]
722    fn test_entity_template_includes_ferro_model_derive() {
723        let columns = vec![ColumnInfo {
724            name: "id".to_string(),
725            col_type: "INTEGER".to_string(),
726            is_nullable: false,
727            is_primary_key: true,
728        }];
729
730        let result = entity_template("users", &columns);
731        // Should include FerroModel in derives
732        assert!(result.contains("FerroModel"));
733        assert!(result.contains("use ferro::FerroModel"));
734    }
735
736    #[test]
737    fn test_entities_mod_template() {
738        let tables = vec![
739            TableInfo {
740                name: "users".to_string(),
741                columns: vec![],
742            },
743            TableInfo {
744                name: "posts".to_string(),
745                columns: vec![],
746            },
747        ];
748
749        let result = entities_mod_template(&tables);
750        assert!(result.contains("pub mod users;"));
751        assert!(result.contains("pub mod posts;"));
752    }
753
754    // -------------------------------------------------------------------------
755    // SQL Type Conversion Tests (via entity_template)
756    // -------------------------------------------------------------------------
757
758    #[test]
759    fn test_sql_type_conversions() {
760        let test_cases = vec![
761            ("BIGINT", "i64"),
762            ("INT8", "i64"),
763            ("SMALLINT", "i16"),
764            ("INT2", "i16"),
765            ("INTEGER", "i32"),
766            ("INT", "i32"),
767            ("TEXT", "String"),
768            ("VARCHAR(255)", "String"),
769            ("CHAR(10)", "String"),
770            ("BOOLEAN", "bool"),
771            ("BOOL", "bool"),
772            ("REAL", "f32"),
773            ("FLOAT4", "f32"),
774            ("DOUBLE", "f64"),
775            ("FLOAT8", "f64"),
776            ("TIMESTAMP", "DateTimeUtc"),
777            ("DATETIME", "DateTimeUtc"),
778            ("DATE", "Date"),
779            ("TIME", "Time"),
780            ("UUID", "Uuid"),
781            ("JSON", "Json"),
782            ("JSONB", "Json"),
783            ("BYTEA", "Vec<u8>"),
784            ("BLOB", "Vec<u8>"),
785            ("DECIMAL", "Decimal"),
786            ("NUMERIC", "Decimal"),
787        ];
788
789        for (sql_type, expected_rust_type) in test_cases {
790            let columns = vec![ColumnInfo {
791                name: "test_col".to_string(),
792                col_type: sql_type.to_string(),
793                is_nullable: false,
794                is_primary_key: false,
795            }];
796
797            let result = entity_template("test_table", &columns);
798            assert!(
799                result.contains(&format!("pub test_col: {expected_rust_type}")),
800                "Failed for SQL type '{sql_type}': expected Rust type '{expected_rust_type}' not found in:\n{result}"
801            );
802        }
803    }
804
805    #[test]
806    fn test_nullable_types() {
807        let columns = vec![ColumnInfo {
808            name: "optional_name".to_string(),
809            col_type: "VARCHAR".to_string(),
810            is_nullable: true,
811            is_primary_key: false,
812        }];
813
814        let result = entity_template("test_table", &columns);
815        assert!(result.contains("pub optional_name: Option<String>"));
816    }
817
818    // -------------------------------------------------------------------------
819    // API Controller Template Tests
820    // -------------------------------------------------------------------------
821
822    #[test]
823    fn test_api_controller_template_substitution() {
824        let result = api_controller_template(
825            "Post",
826            "post",
827            "posts",
828            "    pub title: String,\n    pub body: String,",
829            "        .set_title(form.title.clone())\n        .set_body(form.body.clone())\n",
830            "        title: sea_orm::ActiveValue::Set(form.title.clone()),\n        body: sea_orm::ActiveValue::Set(form.body.clone()),",
831        );
832        assert!(result.contains("Post API controller"));
833        assert!(result.contains("pub async fn index"));
834        assert!(result.contains("pub async fn show"));
835        assert!(result.contains("pub async fn store"));
836        assert!(result.contains("pub async fn update"));
837        assert!(result.contains("pub async fn destroy"));
838        assert!(result.contains("json_response!"));
839        assert!(!result.contains("Inertia"));
840        // Verify builder pattern is used in update handler
841        assert!(result.contains(".update()"));
842        assert!(result.contains(".set_title("));
843        assert!(result.contains(".save()"));
844        // Verify old ActiveModel pattern is NOT used in update handler
845        assert!(!result.contains("let mut post: post::ActiveModel"));
846    }
847}