nostralink 0.2.1

Linked data library for nostr
Documentation
use std::env;
use std::path::PathBuf;

use crate::prelude::*;
use nostr::{EventBuilder, Metadata};
use rand_09::{distr::Alphanumeric, Rng};
use rstest::*;

struct TestEnv {
    keys: Keys,
    store_m: RdfEventsStore,
}

#[fixture]
fn test_meta() -> Metadata {
    Metadata::new()
        .about("About")
        .name("Test")
        .display_name("Test")
}

#[fixture]
fn test_env() -> TestEnv {
    let rng = rand_09::rng();
    let temp_id: String = rng
        .sample_iter(&Alphanumeric)
        .take(128)
        .map(char::from)
        .collect();

    let mut store_path = PathBuf::from(env::temp_dir());
    store_path.push(temp_id);

    let store = Store::open(store_path).expect("Cannot open store");
    let store_m = RdfEventsStore::new(store).unwrap();
    let keys = Keys::generate();

    TestEnv { keys, store_m }
}

#[fixture]
fn test_env_2() -> TestEnv {
    let rng = rand_09::rng();
    let temp_id: String = rng
        .sample_iter(&Alphanumeric)
        .take(128)
        .map(char::from)
        .collect();

    let mut store_path = PathBuf::from(env::temp_dir());
    store_path.push(temp_id);

    let store_m = RdfEventsStore::new(Store::new().unwrap())
        .unwrap()
        .with_dump_path(Some(PathBuf::from("src/tests/store.nq")));

    let keys = Keys::generate();

    TestEnv { keys, store_m }
}

#[fixture]
fn hello_note() -> Note {
    NoteBuilder::default()
        .language(Language::EnGb)
        .headline(Some("test".to_string()))
        .body(Some("Hello".to_string()))
        .build()
        .unwrap()
}

#[rstest]
#[tokio::test]
async fn test_note_inject(
    test_env: TestEnv,
    test_meta: Metadata,
    mut hello_note: Note,
) -> Result<(), RdfStoreError> {
    // Inject metadata
    let event = EventBuilder::metadata(&test_meta)
        .sign(&test_env.keys)
        .await
        .unwrap();

    test_env.store_m.insert_event(&event)?;

    let event = hello_note.event_builder()?.sign(&test_env.keys).await?;
    test_env.store_m.insert_event(&event)?;

    let subs = [
        subl(
            "created_at_since",
            Literal::from((Timestamp::now() - 3600).as_u64()),
        )?,
        subl("select_nostra", Literal::from(1))?,
        subl("only_followees", Literal::from(0))?,
        subl("filter_e_tag", Literal::from(0))?,
        subl("lang_tag_filter", Literal::from("en-GB"))?,
    ];

    let set =
        test_env
            .store_m
            .run_query(&nrq_get("content_union")?, subs, None)?;
    assert_eq!(set.count(), 1);
    let row = set.first().unwrap();

    let hl = row.get("headline").unwrap().to_string();

    assert_eq!(hl, "test");

    Ok(())
}

#[rstest]
#[tokio::test]
async fn test_reactions(
    test_env: TestEnv,
    test_meta: Metadata,
    mut hello_note: Note,
) -> Result<(), RdfStoreError> {
    // Inject metadata
    let event_meta = EventBuilder::metadata(&test_meta)
        .sign(&test_env.keys)
        .await
        .unwrap();
    test_env.store_m.insert_event(&event_meta)?;

    let note_event = hello_note.event_builder()?.sign(&test_env.keys).await?;
    test_env.store_m.insert_event(&note_event)?;

    let mut reaction = EventReactionBuilder::default()
        .event_id(note_event.id)
        .reaction_id(ReactionType::Like)
        .build()
        .unwrap();

    let mut reaction2 = EventReactionBuilder::default()
        .event_id(note_event.id)
        .reaction_id(ReactionType::Sweet)
        .build()
        .unwrap();

    let r_event = reaction.event_builder()?.sign(&test_env.keys).await?;
    test_env.store_m.insert_event(&r_event)?;

    let r2_event = reaction2.event_builder()?.sign(&test_env.keys).await?;
    test_env.store_m.insert_event(&r2_event)?;

    let set =
        test_env
            .store_m
            .run_query(&nrq_get("reactions_count")?, [], None)?;
    assert_eq!(set.count(), 2);

    let subs = [
        subl(
            "created_at_since",
            Literal::from((Timestamp::now() - 3600).as_u64()),
        )?,
        subl("select_nostra", Literal::from(1))?,
        subl("only_followees", Literal::from(0))?,
        subl("filter_e_tag", Literal::from(0))?,
        subl("lang_tag_filter", Literal::from("en-GB"))?,
    ];

    let set =
        test_env
            .store_m
            .run_query(&nrq_get("content_union")?, subs, None)?;

    assert_eq!(set.count(), 1);
    for row in &set.rows {
        println!("{:?}", row.get("count_likes"));
        println!("{:?}", row.get("reaction_id"));
    }

    Ok(())
}

#[rstest]
fn test_query_union(test_env_2: TestEnv) -> Result<(), RdfStoreError> {
    if let Err(e) = test_env_2.store_m.load_from_dump() {
        println!("ERR loading: {e}");
    }

    let subs = [
        subl("filter_e_tag", Literal::from(0))?,
        subl("lang_tag_filter", Literal::from(""))?,
    ];

    match test_env_2
        .store_m
        .run_query(&nrq_get("content_union")?, subs, None)
    {
        Ok(set) => {
            println!("Results count: {}", set.count());
            for row in &set.rows {
                println!("{row:?}");
            }
        }
        Err(e) => println!("{e:?}"),
    }

    Ok(())
}

#[rstest]
fn explain_query_union(test_env_2: TestEnv) -> Result<(), RdfStoreError> {
    use serde_json_path::JsonPath;

    if let Err(e) = test_env_2.store_m.load_from_dump() {
        eprintln!("Load error: {e}");
    }

    let subs = [
        subl("filter_e_tag", Literal::from(0))?,
        subl("lang_tag_filter", Literal::from(""))?,
    ];

    match test_env_2
        .store_m
        .explain_with_subs(&nrq_get("content_union_nostr")?, subs)
    {
        Ok(expl) => {
            let path = JsonPath::parse("$..[?(@.name)]")
                .map_err(|_| RdfStoreError::QueryError)?;

            let nodes = path.query(&expl).all();

            for node in &nodes {
                let name = node.get("name").unwrap();
                let rcount = node.get("number of results").unwrap();

                if let Some(dur) = node.get("duration in seconds") {
                    println!(
                        "{}: Duration: {} Count: {}",
                        name.as_str().unwrap(),
                        dur.as_f64().unwrap(),
                        rcount.as_i64().unwrap(),
                    );
                }
            }

            Ok(())
        }
        Err(e) => Err(e),
    }
}

/// Test delete_previous_events()
#[rstest]
#[tokio::test]
async fn test_delete_previous(
    test_env: TestEnv,
    test_meta: Metadata,
) -> Result<(), RdfStoreError> {
    for _x in 0..3 {
        let meta = Metadata::new().about(_x.to_string()).name("Test");

        let event_meta = EventBuilder::metadata(&meta)
            .sign(&test_env.keys)
            .await
            .unwrap();

        test_env.store_m.insert_event(&event_meta)?;

        std::thread::sleep(std::time::Duration::from_secs(2));
    }

    let subs = [subl("query_followees", Literal::from(0))?];

    let set = test_env.store_m.run_query(
        &nrq_get("user_metadata")?,
        subs.clone(),
        None,
    )?;

    assert_eq!(set.count(), 3);

    let event = EventBuilder::metadata(&test_meta)
        .sign(&test_env.keys)
        .await
        .unwrap();

    let _ = test_env.store_m.delete_previous_events(&event);

    let set =
        test_env
            .store_m
            .run_query(&nrq_get("user_metadata")?, subs, None)?;

    assert_eq!(set.count(), 0);

    Ok(())
}

#[rstest]
#[tokio::test]
async fn test_callbacks(test_env: TestEnv) -> Result<(), RdfStoreError> {
    test_env
        .store_m
        .register_event_callback(Kind::TextNote, |_event, quads| {
            println!("{quads:?}");
        })
        .expect("Cannot register event callback");

    let note_event = EventBuilder::text_note("test")
        .sign(&test_env.keys)
        .await
        .unwrap();

    test_env.store_m.insert_event(&note_event)?;
    Ok(())
}

/// Store an event, pull it out via the database API, and verify its integrity
#[rstest]
#[tokio::test]
async fn test_database(test_env: TestEnv) -> Result<(), RdfStoreError> {
    let note_event = EventBuilder::text_note("test")
        .sign(&test_env.keys)
        .await
        .unwrap();

    let _quadc = store_event(&note_event, &test_env.store_m.store).unwrap();

    let event = test_env.store_m.event_by_id(&note_event.id).await?.unwrap();
    assert_eq!(event.id, note_event.id);

    event.verify().expect("Invalid event");

    Ok(())
}

/// Test delete_events_older_than()
#[rstest]
#[tokio::test]
async fn test_delete_events_older_than(
    test_env: TestEnv,
) -> Result<(), RdfStoreError> {
    let note_event = EventBuilder::text_note("test")
        .sign(&test_env.keys)
        .await
        .unwrap();

    test_env.store_m.insert_event(&note_event)?;

    let long_note_event = EventBuilder::long_form_text_note("long")
        .sign(&test_env.keys)
        .await
        .unwrap();

    test_env.store_m.insert_event(&long_note_event)?;

    std::thread::sleep(std::time::Duration::from_secs(2));

    test_env.store_m.delete_events_older_than(
        Timestamp::now(),
        Some(vec![Kind::TextNote, Kind::LongFormTextNote]),
        None,
    )?;

    let kp = NamedNode::new_unchecked("https://w3id.org/nostr#kind");

    let results = test_env
        .store_m
        .store
        .quads_for_pattern(None, Some((&kp).into()), None, None)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|_| RdfStoreError::QueryError)?;

    assert_eq!(results.len(), 0);

    Ok(())
}