autumn-web 0.5.0

An opinionated, convention-over-configuration web framework for Rust
Documentation
//! Integration tests for the factory builder generated by `#[model]`.
//!
//! Verifies that the generated `XxxFactory` type:
//! - builds in-memory `NewXxx` instances with zero required arguments
//! - accepts per-field overrides without touching other fields
//! - persists through the pool and returns the fully-populated record

#[cfg(feature = "db")]
mod factory_tests {
    #[cfg(feature = "test-support")]
    use diesel::prelude::*;
    #[cfg(feature = "test-support")]
    use diesel_async::AsyncPgConnection;
    #[cfg(feature = "test-support")]
    use diesel_async::RunQueryDsl;
    #[cfg(feature = "test-support")]
    use diesel_async::pooled_connection::deadpool::Pool;

    // ── Schema ─────────────────────────────────────────────────
    diesel::table! {
        factory_items (id) {
            id -> Int8,
            name -> Text,
            score -> Int4,
            active -> Bool,
            tag -> Nullable<Text>,
        }
    }

    // ── Model ──────────────────────────────────────────────────

    #[autumn_web::model(table = "factory_items")]
    pub struct FactoryItem {
        #[id]
        pub id: i64,
        pub name: String,
        pub score: i32,
        pub active: bool,
        pub tag: Option<String>,
    }

    // ── Build tests (no DB needed) ──────────────────────────────

    #[test]
    fn factory_build_with_defaults() {
        let item = FactoryItem::factory().build();
        assert_eq!(item.name, "");
        assert_eq!(item.score, 0);
        assert!(!item.active);
        assert_eq!(item.tag, None);
    }

    #[test]
    fn factory_build_override_single_field() {
        let item = FactoryItem::factory().name("Widget").build();
        assert_eq!(item.name, "Widget");
        // Other fields keep defaults
        assert_eq!(item.score, 0);
        assert!(!item.active);
    }

    #[test]
    fn factory_build_override_multiple_fields() {
        let item = FactoryItem::factory()
            .name("Gadget")
            .score(99)
            .active(true)
            .tag(Some("featured".to_string()))
            .build();
        assert_eq!(item.name, "Gadget");
        assert_eq!(item.score, 99);
        assert!(item.active);
        assert_eq!(item.tag, Some("featured".to_string()));
    }

    #[test]
    fn factory_build_is_independent() {
        // Two factory builds are independent; changing one doesn't affect the other
        let a = FactoryItem::factory().name("Alpha").build();
        let b = FactoryItem::factory().name("Beta").build();
        assert_eq!(a.name, "Alpha");
        assert_eq!(b.name, "Beta");
    }

    #[test]
    fn factory_default_impl() {
        let f1 = FactoryItemFactory::default();
        let f2 = FactoryItemFactory::default();
        let i1 = f1.build();
        let i2 = f2.build();
        assert_eq!(i1.name, i2.name);
        assert_eq!(i1.score, i2.score);
    }

    #[test]
    fn factory_build_returns_new_model() {
        // Verify build() returns NewFactoryItem (the Insertable type)
        let _: NewFactoryItem = FactoryItem::factory().build();
    }

    #[test]
    fn factory_string_setter_accepts_str_slice() {
        // impl Into<String> lets callers pass &str without .to_string()
        let item = FactoryItem::factory().name("direct slice").build();
        assert_eq!(item.name, "direct slice");
    }

    // ── DB tests ───────────────────────────────────────────────

    #[cfg(feature = "test-support")]
    async fn setup_table(pool: &Pool<AsyncPgConnection>) {
        let mut conn = pool.get().await.unwrap();
        diesel::sql_query(
            "CREATE TABLE IF NOT EXISTS factory_items (
                id      BIGSERIAL PRIMARY KEY,
                name    TEXT NOT NULL DEFAULT '',
                score   INT  NOT NULL DEFAULT 0,
                active  BOOL NOT NULL DEFAULT FALSE,
                tag     TEXT
            )",
        )
        .execute(&mut *conn)
        .await
        .unwrap();
        diesel::sql_query("TRUNCATE factory_items RESTART IDENTITY")
            .execute(&mut *conn)
            .await
            .unwrap();
    }

    #[cfg(feature = "test-support")]
    #[tokio::test]
    #[ignore = "requires Docker (testcontainers)"]
    async fn factory_create_persists_and_returns_record() {
        let db: &autumn_web::test::TestDb = autumn_web::test::TestDb::shared().await;
        setup_table(&db.pool()).await;

        let item = FactoryItem::factory()
            .name("Persisted")
            .score(7)
            .active(true)
            .create(&db.pool())
            .await;

        assert!(item.id > 0, "id should be populated by the database");
        assert_eq!(item.name, "Persisted");
        assert_eq!(item.score, 7);
        assert!(item.active);
    }

    #[cfg(feature = "test-support")]
    #[tokio::test]
    #[ignore = "requires Docker (testcontainers)"]
    async fn factory_create_default_then_query() {
        let db: &autumn_web::test::TestDb = autumn_web::test::TestDb::shared().await;
        setup_table(&db.pool()).await;

        let created = FactoryItem::factory()
            .name("QueryMe")
            .create(&db.pool())
            .await;

        // Verify we can query it back
        let mut conn = db.pool().get().await.unwrap();
        let fetched: FactoryItem = factory_items::table
            .find(created.id)
            .select(FactoryItem::as_select())
            .first(&mut *conn)
            .await
            .unwrap();

        assert_eq!(fetched.name, "QueryMe");
        assert_eq!(fetched.id, created.id);
    }

    #[cfg(feature = "test-support")]
    #[tokio::test]
    #[ignore = "requires Docker (testcontainers)"]
    async fn factory_create_multiple_independent_records() {
        let db: &autumn_web::test::TestDb = autumn_web::test::TestDb::shared().await;
        setup_table(&db.pool()).await;

        let a = FactoryItem::factory()
            .name("Alpha")
            .score(1)
            .create(&db.pool())
            .await;
        let b = FactoryItem::factory()
            .name("Beta")
            .score(2)
            .create(&db.pool())
            .await;

        assert_ne!(a.id, b.id, "each create() should produce a distinct record");
        assert_eq!(a.name, "Alpha");
        assert_eq!(b.name, "Beta");
    }

    /// Demonstrates the factory composition pattern:
    /// one model's factory references another's id to set up associations.
    #[test]
    fn factory_composition_via_field_override() {
        // In a multi-model scenario, you build a parent first, then pass its id.
        // This test shows the field-override pattern works for foreign keys.
        let item_for_user_42 = FactoryItem::factory()
            .score(42) // use score as a stand-in for a FK in this single-model test
            .build();
        assert_eq!(item_for_user_42.score, 42);
    }
}

// ── Factory composition tests ────────────────────────────────────────────────

#[cfg(feature = "db")]
mod composition_tests {
    #[cfg(feature = "test-support")]
    use diesel_async::RunQueryDsl;

    // ── Schema ──────────────────────────────────────────────────
    diesel::table! {
        comp_users (id) {
            id -> Int8,
            name -> Text,
        }
    }

    diesel::table! {
        comp_posts (id) {
            id -> Int8,
            title -> Text,
            user_id -> Int8,
        }
    }

    // ── Models with factory_assoc ────────────────────────────────

    #[autumn_web::model(table = "comp_users")]
    pub struct CompUser {
        #[id]
        pub id: i64,
        pub name: String,
    }

    #[autumn_web::model(table = "comp_posts")]
    pub struct CompPost {
        #[id]
        pub id: i64,
        pub title: String,
        #[factory_assoc(CompUser)]
        pub user_id: i64,
    }

    // ── In-memory tests ─────────────────────────────────────────

    #[test]
    fn assoc_field_defaults_to_zero_in_build() {
        let p = CompPost::factory().build();
        assert_eq!(p.user_id, 0); // None => unwrap_or_default()
    }

    #[test]
    fn assoc_field_explicit_id_setter() {
        let p = CompPost::factory().user_id(99_i64).build();
        assert_eq!(p.user_id, 99);
    }

    #[test]
    fn assoc_field_pre_built_instance_setter() {
        let user = CompUser {
            id: 42,
            name: "Alice".into(),
        };
        let p = CompPost::factory().user(&user).build();
        assert_eq!(p.user_id, 42);
        // user is still usable after calling .user(&user)
        assert_eq!(user.id, 42);
    }

    // ── DB tests (Docker required) ───────────────────────────────

    #[cfg(feature = "test-support")]
    async fn setup(
        pool: &diesel_async::pooled_connection::deadpool::Pool<diesel_async::AsyncPgConnection>,
    ) {
        let mut conn = pool.get().await.unwrap();
        diesel::sql_query(
            "CREATE TABLE IF NOT EXISTS comp_users (
                id   BIGSERIAL PRIMARY KEY,
                name TEXT NOT NULL DEFAULT ''
            )",
        )
        .execute(&mut *conn)
        .await
        .unwrap();
        diesel::sql_query(
            "CREATE TABLE IF NOT EXISTS comp_posts (
                id      BIGSERIAL PRIMARY KEY,
                title   TEXT NOT NULL DEFAULT '',
                user_id BIGINT NOT NULL DEFAULT 0
            )",
        )
        .execute(&mut *conn)
        .await
        .unwrap();
        diesel::sql_query("TRUNCATE comp_posts, comp_users RESTART IDENTITY CASCADE")
            .execute(&mut *conn)
            .await
            .unwrap();
    }

    #[cfg(feature = "test-support")]
    #[tokio::test]
    #[ignore = "requires Docker (testcontainers)"]
    async fn factory_auto_creates_associated_model() {
        let db: &autumn_web::test::TestDb = autumn_web::test::TestDb::shared().await;
        setup(&db.pool()).await;

        // No user_id override — factory should auto-create a CompUser
        let post = CompPost::factory()
            .title("Auto-composed post")
            .create(&db.pool())
            .await;

        assert!(post.id > 0);
        assert!(post.user_id > 0, "factory should have auto-created a user");
        assert_eq!(post.title, "Auto-composed post");
    }

    #[cfg(feature = "test-support")]
    #[tokio::test]
    #[ignore = "requires Docker (testcontainers)"]
    async fn factory_uses_pre_built_user_when_supplied() {
        let db: &autumn_web::test::TestDb = autumn_web::test::TestDb::shared().await;
        setup(&db.pool()).await;

        // Create a specific user first
        let user = CompUser::factory().name("Alice").create(&db.pool()).await;

        // Pass the pre-built user so no extra user is created
        let post = CompPost::factory()
            .title("Alice's post")
            .user(&user)
            .create(&db.pool())
            .await;

        assert_eq!(post.user_id, user.id);
    }

    #[cfg(feature = "test-support")]
    #[tokio::test]
    #[ignore = "requires Docker (testcontainers)"]
    async fn factory_explicit_user_id_skips_auto_create() {
        let db: &autumn_web::test::TestDb = autumn_web::test::TestDb::shared().await;
        setup(&db.pool()).await;

        // Create a real user to get a valid FK
        let user = CompUser::factory().name("Bob").create(&db.pool()).await;

        // Supply id directly — no auto-creation of another user
        let post = CompPost::factory()
            .title("Bob's post")
            .user_id(user.id)
            .create(&db.pool())
            .await;

        assert_eq!(post.user_id, user.id);
    }
}