observation-tools 0.0.13

Observation Tools Client Library
mod common;

use common::TestServer;
use observation_tools::server_client::types::PayloadOrPointerResponse;

#[test_log::test(tokio::test)]
async fn test_api_without_auth_when_no_key_configured() -> anyhow::Result<()> {
  let server = TestServer::new().await;
  let client = server.create_client()?;

  let execution_handle = client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await?;

  let execution_id = execution_handle.id();
  let api_client = server.create_api_client()?;
  let get_response = api_client
    .get_execution()
    .id(&execution_id.to_string())
    .send()
    .await?;

  assert_eq!(
    get_response.execution.id.to_string(),
    execution_id.to_string()
  );

  client.shutdown().await?;
  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_api_with_valid_key() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key = server.generate_api_key()?;
  let client = server.create_client_with_api_key(&api_key)?;

  let execution = client
    .begin_execution("test-execution-with-observation")?
    .wait_for_upload()
    .await?;

  let execution_id = execution.id();

  observation_tools::with_execution(execution, async {
    observation_tools::ObservationBuilder::new("test-observation")
      .label("test/label")
      .metadata("key1", "value1")
      .payload("test payload data")
      .build()
      .wait_for_upload()
      .await?;

    Ok::<_, anyhow::Error>(())
  })
  .await?;

  client.shutdown().await?;

  let observations = server.list_observations(&execution_id).await?;

  assert_eq!(observations.len(), 1);
  let obs = &observations[0];
  assert_eq!(obs.name, "test-observation");
  assert_eq!(obs.payload.as_str(), Some("test payload data"));

  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_api_without_auth_when_key_required() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let client = server.create_client()?;

  let result = client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await;

  assert!(
    result.is_err(),
    "Request without auth should fail when API key is required"
  );

  client.shutdown().await?;
  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_api_with_invalid_key() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let invalid_key = "obs_invalid_key_that_wont_work";
  let client = server.create_client_with_api_key(invalid_key)?;

  let result = client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await;

  assert!(result.is_err(), "Request with invalid API key should fail");

  client.shutdown().await?;
  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_api_with_malformed_key() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let malformed_key = "not-even-close-to-valid";
  let client = server.create_client_with_api_key(malformed_key)?;

  let result = client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await;

  assert!(
    result.is_err(),
    "Request with malformed API key should fail"
  );

  client.shutdown().await?;
  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_api_with_wrong_secret() -> anyhow::Result<()> {
  let server1 = TestServer::new_with_auth().await?;
  let api_key1 = server1.generate_api_key()?;

  let server2 = TestServer::new_with_auth().await?;
  let client = server2.create_client_with_api_key(&api_key1)?;

  let result = client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await;

  assert!(
    result.is_err(),
    "API key from different server should fail validation"
  );

  client.shutdown().await?;
  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_list_executions_with_auth() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key = server.generate_api_key()?;
  let client = server.create_client_with_api_key(&api_key)?;

  for i in 0..5 {
    let exec_name = format!("execution-{}", i);
    client
      .begin_execution(&exec_name)?
      .wait_for_upload()
      .await?;
  }

  let api_client = server.create_api_client_with_api_key(&api_key)?;
  let list_response = api_client.list_executions().send().await?;
  assert_eq!(list_response.executions.len(), 5);

  client.shutdown().await?;
  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_blob_upload_with_auth() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key = server.generate_api_key()?;
  let client = server.create_client_with_api_key(&api_key)?;

  let execution = client
    .begin_execution("test-execution-with-large-payload")?
    .wait_for_upload()
    .await?;

  let execution_id = execution.id();

  let large_data = "x".repeat(70_000);
  let large_payload = serde_json::json!({
    "data": large_data,
    "size": large_data.len(),
  });

  let observation_id = observation_tools::with_execution(execution, async {
    observation_tools::observe!(
      name = "large-observation",
      label = "test/large-payload",
      payload = large_payload
    )
    .wait_for_upload()
    .await
  })
  .await?;

  client.shutdown().await?;

  let observations = server.list_observations(&execution_id).await?;

  assert_eq!(observations.len(), 1);
  let obs = &observations[0];
  assert_eq!(obs.name, "large-observation");

  assert!(
    matches!(obs.payload, PayloadOrPointerResponse::Pointer { .. }),
    "Large payload data should be empty in metadata (stored as blob)"
  );

  let blob_url = format!(
    "{}/api/exe/{}/obs/{}/content",
    server.base_url(),
    execution_id,
    observation_id.id()
  );

  let response = reqwest::Client::new()
    .get(&blob_url)
    .header("Authorization", format!("Bearer {}", api_key))
    .send()
    .await?;

  assert!(
    response.status().is_success(),
    "Blob retrieval with auth should succeed"
  );

  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_multiple_valid_keys() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key1 = server.generate_api_key()?;
  let api_key2 = server.generate_api_key()?;

  let client1 = server.create_client_with_api_key(&api_key1)?;
  let client2 = server.create_client_with_api_key(&api_key2)?;

  let _execution1 = client1
    .begin_execution("execution-from-key1")?
    .wait_for_upload()
    .await?;

  let _execution2 = client2
    .begin_execution("execution-from-key2")?
    .wait_for_upload()
    .await?;

  client1.shutdown().await?;
  client2.shutdown().await?;

  let api_client1 = server.create_api_client_with_api_key(&api_key1)?;
  let list_response = api_client1.list_executions().send().await?;
  assert_eq!(list_response.executions.len(), 2);

  let api_client2 = server.create_api_client_with_api_key(&api_key2)?;
  let list_response = api_client2.list_executions().send().await?;
  assert_eq!(list_response.executions.len(), 2);

  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_observation_without_auth_after_execution_with_auth() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key = server.generate_api_key()?;
  let auth_client = server.create_client_with_api_key(&api_key)?;

  let execution = auth_client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await?;

  auth_client.shutdown().await?;

  let unauth_client = server.create_client()?;
  let result = observation_tools::with_execution(execution, async {
    observation_tools::observe!(name = "test-observation", payload = "test data")
      .wait_for_upload()
      .await
  })
  .await;

  assert!(
    result.is_err(),
    "Observation should fail when sent with unauthenticated client"
  );

  unauth_client.shutdown().await?;
  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_list_executions_without_auth_succeeds() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key = server.generate_api_key()?;
  let client = server.create_client_with_api_key(&api_key)?;

  client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await?;

  client.shutdown().await?;

  let api_client_no_auth = server.create_api_client()?;
  let result = api_client_no_auth.list_executions().send().await;

  assert!(
    result.is_ok(),
    "List executions should succeed without auth (read-only operation)"
  );

  let response = result?;
  assert_eq!(response.executions.len(), 1);

  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_get_execution_without_auth_succeeds() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key = server.generate_api_key()?;
  let client = server.create_client_with_api_key(&api_key)?;

  let execution = client
    .begin_execution("test-execution")?
    .wait_for_upload()
    .await?;

  let execution_id = execution.id();
  client.shutdown().await?;

  let api_client_no_auth = server.create_api_client()?;
  let result = api_client_no_auth
    .get_execution()
    .id(&execution_id.to_string())
    .send()
    .await;

  assert!(
    result.is_ok(),
    "Get execution should succeed without auth (read-only operation)"
  );

  let response = result?;
  assert_eq!(response.execution.id.to_string(), execution_id.to_string());

  Ok(())
}

#[test_log::test(tokio::test)]
async fn test_blob_retrieval_without_auth_succeeds() -> anyhow::Result<()> {
  let server = TestServer::new_with_auth().await?;
  let api_key = server.generate_api_key()?;
  let client = server.create_client_with_api_key(&api_key)?;

  let execution = client
    .begin_execution("test-execution-with-large-payload")?
    .wait_for_upload()
    .await?;

  let execution_id = execution.id();

  let large_data = "x".repeat(70_000);
  let large_payload = serde_json::json!({
    "data": large_data,
  });

  let observation_id = observation_tools::with_execution(execution, async {
    observation_tools::observe!(name = "large-observation", payload = large_payload)
      .wait_for_upload()
      .await
  })
  .await?;

  client.shutdown().await?;

  let blob_url = format!(
    "{}/api/exe/{}/obs/{}/content",
    server.base_url(),
    execution_id,
    observation_id.id()
  );

  let response = reqwest::get(&blob_url).await?;
  assert!(
    response.status().is_success(),
    "Blob retrieval should succeed without auth (read-only operation)"
  );

  Ok(())
}