rustauth-plugins 0.2.0

Official RustAuth plugin modules.
Documentation
use std::sync::Arc;

use http::{Method, StatusCode};
use rustauth_core::db::MemoryAdapter;
use rustauth_plugins::organization::{OrganizationOptions, TeamOptions};
use serde_json::json;

#[tokio::test]
async fn team_routes_cover_default_team_members_and_active_team(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let options = OrganizationOptions::builder()
        .teams(TeamOptions {
            enabled: true,
            create_default_team: true,
            maximum_teams: Some(3),
            maximum_members_per_team: Some(3),
            allow_removing_all_teams: false,
            ..Default::default()
        })
        .build();
    let auth = super::test_router(adapter, options)?;

    let ada = super::sign_up(&auth, "Ada", "ada-team@example.com").await?;
    let org = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/create",
        json!({"name":"Acme Teams","slug":"acme-teams"}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(org.status, StatusCode::OK);

    let full = super::request_json(
        &auth,
        Method::GET,
        "/api/auth/organization/get-full-organization",
        json!({}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(full.status, StatusCode::OK);
    assert_eq!(full.body["teams"].as_array().map(Vec::len), Some(1));

    let team = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/create-team",
        json!({"name":"Engineering"}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(team.status, StatusCode::OK);
    let team_id = team.body["id"].as_str().ok_or("missing team id")?;

    let ben = super::sign_up(&auth, "Ben", "ben-team@example.com").await?;
    let member = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/add-member",
        json!({"userId": ben.user_id, "role": "member"}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(member.status, StatusCode::OK);

    let team_member = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/add-team-member",
        json!({"teamId": team_id, "userId": ben.user_id}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(team_member.status, StatusCode::OK);

    let listed = super::request_json(
        &auth,
        Method::GET,
        &format!("/api/auth/organization/list-team-members?teamId={team_id}"),
        json!({}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(listed.status, StatusCode::OK);
    assert_eq!(listed.body.as_array().map(Vec::len), Some(2));

    let active = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/set-active-team",
        json!({"teamId": team_id}),
        Some(&ben.cookie),
    )
    .await?;
    assert_eq!(active.status, StatusCode::OK);

    Ok(())
}

#[tokio::test]
async fn accepting_invitation_to_full_team_does_not_create_partial_membership(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let options = OrganizationOptions::builder()
        .teams(TeamOptions {
            enabled: true,
            create_default_team: false,
            maximum_teams: None,
            maximum_members_per_team: Some(2),
            allow_removing_all_teams: true,
            ..Default::default()
        })
        .build();
    let auth = super::test_router(adapter, options)?;

    let ada = super::sign_up(&auth, "Ada", "ada-team-limit@example.com").await?;
    let org = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/create",
        json!({"name":"Team Limit","slug":"team-limit"}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(org.status, StatusCode::OK);

    let team = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/create-team",
        json!({"name":"Engineering"}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(team.status, StatusCode::OK);
    let team_id = team.body["id"].as_str().ok_or("missing team id")?;

    let ben = super::sign_up(&auth, "Ben", "ben-team-limit@example.com").await?;
    let invite = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/invite-member",
        json!({"email":"ben-team-limit@example.com","role":"member","teamId":team_id}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(invite.status, StatusCode::OK);
    let invitation_id = invite.body["id"]
        .as_str()
        .ok_or("missing invitation id")?
        .to_owned();

    let carol = super::sign_up(&auth, "Carol", "carol-team-limit@example.com").await?;
    let member = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/add-member",
        json!({"userId": carol.user_id, "role": "member"}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(member.status, StatusCode::OK);
    let team_member = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/add-team-member",
        json!({"teamId": team_id, "userId": carol.user_id}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(team_member.status, StatusCode::OK);

    let accepted = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/accept-invitation",
        json!({"invitationId": invitation_id}),
        Some(&ben.cookie),
    )
    .await?;
    assert_eq!(accepted.status, StatusCode::FORBIDDEN);
    assert_eq!(accepted.body["code"], "TEAM_MEMBER_LIMIT_REACHED");

    let members = super::request_json(
        &auth,
        Method::GET,
        "/api/auth/organization/list-members",
        json!({}),
        Some(&ada.cookie),
    )
    .await?;
    assert_eq!(members.status, StatusCode::OK);
    assert_eq!(members.body["members"].as_array().map(Vec::len), Some(2));

    Ok(())
}

#[tokio::test]
async fn create_team_respects_explicit_organization_id_over_active_org(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let options = OrganizationOptions::builder()
        .teams(TeamOptions {
            enabled: true,
            create_default_team: false,
            allow_removing_all_teams: true,
            ..TeamOptions::default()
        })
        .build();
    let auth = super::test_router(adapter, options)?;
    let owner = super::sign_up(&auth, "Owner", "owner-team-explicit@example.com").await?;
    let first = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/create",
        json!({"name":"First Team Org","slug":"first-team-org"}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(first.status, StatusCode::OK);
    let first_id = first.body["id"].as_str().ok_or("missing first org id")?;
    let second = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/create",
        json!({"name":"Second Team Org","slug":"second-team-org"}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(second.status, StatusCode::OK);
    let second_id = second.body["id"].as_str().ok_or("missing second org id")?;

    let created = super::request_json(
        &auth,
        Method::POST,
        "/api/auth/organization/create-team",
        json!({"organizationId": first_id, "name":"Explicit First"}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(created.status, StatusCode::OK);
    assert_eq!(created.body["organizationId"], first_id);

    let first_full = super::request_json(
        &auth,
        Method::GET,
        &format!("/api/auth/organization/get-full-organization?organizationId={first_id}"),
        json!({}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(first_full.status, StatusCode::OK);
    assert_eq!(first_full.body["teams"].as_array().map(Vec::len), Some(1));
    let second_full = super::request_json(
        &auth,
        Method::GET,
        &format!("/api/auth/organization/get-full-organization?organizationId={second_id}"),
        json!({}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(second_full.status, StatusCode::OK);
    assert_eq!(
        second_full
            .body
            .get("teams")
            .and_then(serde_json::Value::as_array)
            .map_or(0, Vec::len),
        0
    );
    Ok(())
}

#[tokio::test]
async fn remove_team_blocks_last_team_unless_option_allows_it(
) -> Result<(), Box<dyn std::error::Error>> {
    let blocked_auth = super::test_router(
        Arc::new(MemoryAdapter::new()),
        OrganizationOptions::builder()
            .teams(TeamOptions {
                enabled: true,
                create_default_team: true,
                allow_removing_all_teams: false,
                ..TeamOptions::default()
            })
            .build(),
    )?;
    let owner = super::sign_up(
        &blocked_auth,
        "Owner",
        "owner-last-team-blocked@example.com",
    )
    .await?;
    let org = super::request_json(
        &blocked_auth,
        Method::POST,
        "/api/auth/organization/create",
        json!({"name":"Last Team Blocked","slug":"last-team-blocked"}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(org.status, StatusCode::OK);
    let team_id = org.body["teams"][0]["id"]
        .as_str()
        .ok_or("missing default team id")?;
    let denied = super::request_json(
        &blocked_auth,
        Method::POST,
        "/api/auth/organization/remove-team",
        json!({"teamId": team_id}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(denied.status, StatusCode::BAD_REQUEST);
    assert_eq!(denied.body["code"], "UNABLE_TO_REMOVE_LAST_TEAM");

    let allowed_auth = super::test_router(
        Arc::new(MemoryAdapter::new()),
        OrganizationOptions::builder()
            .teams(TeamOptions {
                enabled: true,
                create_default_team: true,
                allow_removing_all_teams: true,
                ..TeamOptions::default()
            })
            .build(),
    )?;
    let owner = super::sign_up(
        &allowed_auth,
        "Owner",
        "owner-last-team-allowed@example.com",
    )
    .await?;
    let org = super::request_json(
        &allowed_auth,
        Method::POST,
        "/api/auth/organization/create",
        json!({"name":"Last Team Allowed","slug":"last-team-allowed"}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(org.status, StatusCode::OK);
    let team_id = org.body["teams"][0]["id"]
        .as_str()
        .ok_or("missing default team id")?;
    let removed = super::request_json(
        &allowed_auth,
        Method::POST,
        "/api/auth/organization/remove-team",
        json!({"teamId": team_id}),
        Some(&owner.cookie),
    )
    .await?;
    assert_eq!(removed.status, StatusCode::OK);
    assert_eq!(removed.body["team"]["id"], team_id);
    Ok(())
}