slumber_cli 5.2.3

Command line interface for Slumber. Not intended for external use.
Documentation
//! Test `slumber db`

mod common;

use crate::common::{collection_file, tests_dir};
use itertools::Itertools;
use predicates::{
    prelude::{PredicateBooleanExt, predicate},
    str::PredicateStrExt,
};
use rstest::rstest;
use slumber_core::{
    collection::{CollectionFile, ProfileId, RecipeId},
    database::Database,
    http::{Exchange, RequestId},
};
use slumber_util::Factory;
use std::{fs, path::Path};
use uuid::Uuid;

// Use static IDs for the recipes so we can refer to them in expectations
const RECIPE1_NO_PROFILE_ID: RequestId =
    id("00000000-0000-0000-0000-000000000000");
const RECIPE1_PROFILE1_ID: RequestId =
    id("00000000-0000-0000-0000-000000000001");
const RECIPE2_ID: RequestId = id("00000000-0000-0000-0000-000000000002");
const OTHER_COLLECTION_ID: RequestId =
    id("00000000-0000-0000-0000-000000000003");

/// `slumber db --path` prints the DB path
#[test]
fn test_print_path() {
    let (mut command, data_dir) = common::slumber();
    command.args(["db", "--path"]);
    let expected = data_dir.join("state.sqlite").display().to_string();
    command
        .assert()
        .success()
        .stdout(predicate::eq(expected).trim());
}

/// `slumber db collection list`
#[rstest]
fn test_collection_list() {
    let (mut command, data_dir) = common::slumber();
    init_db(&data_dir);

    command
        .args(["db", "collection", "list"])
        .assert()
        .success()
        .stdout(
            predicates::str::contains("slumber.yml")
                .and(predicates::str::contains("other.yml")),
        );
}

/// `slumber db collection migrate` with paths as  arguments
///
/// The actual merge logic is tested in the database so we're just trying to
/// test the arg handling and basic functionality
#[test]
fn test_collection_migrate_paths() {
    let (mut command, data_dir) = common::slumber();
    let database = init_db(&data_dir);

    // Verify we start with 2 collections
    let collections = database.get_collections().unwrap();
    assert_eq!(collections.len(), 2);
    // Grab the first collection so we can ensure it's the only one left later.
    // Do a sanity check to make sure this is the one we're migrating TO
    let first_collection = &collections[0];
    assert!(
        first_collection.path.ends_with("slumber.yml"),
        "Expected target collection to be first in list"
    );

    // Merge the collections
    command
        .args(["db", "collection", "migrate", "other.yml", "slumber.yml"])
        .assert()
        .success()
        .stdout("Migrated other.yml into slumber.yml\n");

    // 1 collection now, all the requests are under that collection
    let collections = database.get_collections().unwrap();
    assert_eq!(collections.len(), 1);
    assert_eq!(collections[0].id, first_collection.id);
    assert_eq!(database.get_all_requests().unwrap().len(), 4);
}

/// `slumber db collection migrate` with IDs as arguments
#[test]
fn test_collection_migrate_ids() {
    let (mut command, data_dir) = common::slumber();
    let database = init_db(&data_dir);

    // Verify we start with 2 collections
    let collections = database.get_collections().unwrap();
    assert_eq!(collections.len(), 2);
    let id1 = collections[0].id;
    let id2 = collections[1].id;

    // Merge the collections
    command
        .args([
            "db",
            "collection",
            "migrate",
            &id2.to_string(),
            &id1.to_string(),
        ])
        .assert()
        .success()
        .stdout(format!("Migrated {id2} into {id1}\n"));

    // 1 collection now, all the requests are under that collection
    let collections = database.get_collections().unwrap();
    assert_eq!(collections.len(), 1);
    assert_eq!(collections[0].id, id1);
    assert_eq!(database.get_all_requests().unwrap().len(), 4);
}

/// `slumber db collection delete`
#[test]
fn test_collection_delete() {
    let (mut command, data_dir) = common::slumber();
    let database = init_db(&data_dir);

    // Verify we start with 2 collections and 3 requests
    let collections = database.get_collections().unwrap();
    assert_eq!(collections.len(), 2);
    let id = collections[0].id;
    assert_eq!(database.get_all_requests().unwrap().len(), 4);

    // Delete!!
    command
        .args(["db", "collection", "delete", &id.to_string()])
        .assert()
        .success()
        .stdout(format!("Deleted collection {id}\n"));

    // 1 collection now. The requests of the deleted collection are gone
    let collections = database.get_collections().unwrap();
    assert_eq!(collections.len(), 1);
    assert_ne!(collections[0].id, id); // Deleted collection is NOT present
    assert_eq!(database.get_all_requests().unwrap().len(), 1);
}

/// Test collection deletion when the file is already gone. Should still work
#[test]
fn test_collection_delete_file_missing() {
    let (mut command, data_dir) = common::slumber();
    let database = Database::from_directory(&data_dir).unwrap();

    // Make a new collection file and add it to the DB. Pre-canonicalize it
    // because we won't be able to canonicalize after deletion. This is relevant
    // because some systems use symlinks in the tmp file system
    let collection_path = data_dir.join("slumber.yml");
    fs::write(&collection_path, "").unwrap();
    let collection_path = collection_path.canonicalize().unwrap();
    let collection_file =
        CollectionFile::new(Some(collection_path.clone())).unwrap();
    let id = database
        .clone()
        .into_collection(&collection_file)
        .unwrap()
        .collection_id();

    // Sanity check
    assert_eq!(
        database
            .get_collections()
            .unwrap()
            .into_iter()
            .map(|collection| collection.id)
            .collect::<Vec<_>>(),
        [id]
    );

    // Delete the file before deleting from the DB
    fs::remove_file(&collection_path).unwrap();
    command
        .args([
            "db",
            "collection",
            "delete",
            collection_path.to_str().unwrap(),
        ])
        .assert()
        .success();

    assert_eq!(database.get_collections().unwrap(), []);
}

/// Passing an unknown ID to `slumber db collection delete` gives an error
#[test]
fn test_collection_delete_bad_id() {
    let (mut command, _) = common::slumber();

    let id = Uuid::new_v4().to_string();
    command
        .args(["db", "collection", "delete", &id])
        .assert()
        .failure()
        .stderr(format!("Unknown collection `{id}`\n"));
}

/// Test `slumber db request list`
#[rstest]
#[case::recipe(
    &["db", "request", "list", "recipe1"],
    &[RECIPE1_NO_PROFILE_ID, RECIPE1_PROFILE1_ID],
)]
#[case::no_profile(
    &["db", "request", "list", "recipe1", "-p"], &[RECIPE1_NO_PROFILE_ID],
)]
#[case::profile(
    &["db", "request", "list", "recipe1", "-p", "profile1"], &[RECIPE1_PROFILE1_ID],
)]
#[case::collection(
    &["db", "request", "list"],
    &[RECIPE1_NO_PROFILE_ID, RECIPE1_PROFILE1_ID, RECIPE2_ID],
)]
#[case::different_collection(
    &["-f", "./other.yml", "db", "request", "list"],
    &[OTHER_COLLECTION_ID],
)]
#[case::all(
    &["db", "request", "list", "--all"],
    &[RECIPE1_NO_PROFILE_ID, RECIPE1_PROFILE1_ID, RECIPE2_ID, OTHER_COLLECTION_ID],
)]
fn test_request_list(
    #[case] arguments: &[&str],
    #[case] expected_requests: &[RequestId],
) {
    let (mut command, data_dir) = common::slumber();
    init_db(&data_dir);

    command.args(arguments).assert().success().stdout(
        predicates::function::function(|stdout: &str| {
            expected_requests
                .iter()
                .all(|expected_id| stdout.contains(&expected_id.to_string()))
        }),
    );
}

/// Test `slumber db request delete`
#[rstest]
fn test_request_delete() {
    let (mut command, data_dir) = common::slumber();
    let database = init_db(&data_dir);

    command
        .args([
            "db",
            "request",
            "delete",
            &RECIPE1_PROFILE1_ID.to_string(),
            &RECIPE1_NO_PROFILE_ID.to_string(),
        ])
        .assert()
        .success();
    let remaining = database
        .get_all_requests()
        .unwrap()
        .into_iter()
        .map(|exchange| exchange.id)
        .sorted()
        .collect_vec();
    assert_eq!(&remaining, &[RECIPE2_ID, OTHER_COLLECTION_ID]);
}

const fn id(s: &str) -> RequestId {
    let Ok(uuid) = Uuid::try_parse(s) else {
        panic!("Bad value") // unwrap() isn't const
    };
    RequestId(uuid)
}

fn init_db(data_dir: &Path) -> Database {
    let database = Database::from_directory(data_dir).unwrap();

    // Add multiple requests for the primary collection
    let collection1_db = database
        .clone()
        .into_collection(&collection_file())
        .unwrap();
    let profile_id: ProfileId = "profile1".into();
    let recipe_id: RecipeId = "recipe1".into();
    collection1_db
        .insert_exchange(&Exchange::factory((
            RECIPE1_NO_PROFILE_ID,
            None,
            recipe_id.clone(),
        )))
        .unwrap();
    collection1_db
        .insert_exchange(&Exchange::factory((
            RECIPE1_PROFILE1_ID,
            Some(profile_id),
            recipe_id,
        )))
        .unwrap();
    collection1_db
        .insert_exchange(&Exchange::factory((
            RECIPE2_ID,
            None,
            "recipe2".into(),
        )))
        .unwrap();

    // Add one under another collection
    let collection2_db = database
        .clone()
        .into_collection(
            &CollectionFile::new(Some(tests_dir().join("other.yml"))).unwrap(),
        )
        .unwrap();
    collection2_db
        .insert_exchange(&Exchange::factory(OTHER_COLLECTION_ID))
        .unwrap();

    database
}