Files
notion-sql/tests/integration.rs

142 lines
5.5 KiB
Rust

// SPDX-License-Identifier: MIT
// Copyright (c) 2026 James Maes
//! Integration tests for the notion-sql crate.
//!
//! These tests require network access to the Notion API and are marked
//! with the `#[ignore]` attribute by default. Run them with:
//!
//! ```text
//! cargo test --ignored
//! ```
//!
//! # Purpose and approach
//!
//! This file exercises the crate's two public-facing entry points end to end,
//! treating the crate as an external consumer would (i.e. it only imports the
//! re-exported public API, never internal modules). It is deliberately
//! organized around two concerns:
//!
//! * Schema translation — turning a Notion database JSON payload (the shape the
//! Notion REST API returns) into the crate's [`DatabaseSchema`] type, and
//! confirming the discovered column names match the source properties.
//! * SQL parsing — confirming that each of the four supported statement kinds
//! (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) is accepted by
//! [`parse_statement`]. These tests assert only that parsing *succeeds*;
//! semantic validation against a live database belongs to the network-gated
//! tests described above and is intentionally out of scope here.
//!
//! Note that despite the module header's mention of network access, every test
//! currently in this file is hermetic — they run against in-memory JSON and SQL
//! strings and require no Notion credentials, which is why none carry
//! `#[ignore]`. The header documents the broader intent for this file as the
//! home for live integration coverage as it is added.
use notion_sql::schema::DatabaseSchema;
use notion_sql::sql::parse_statement;
use serde_json::json;
/// Verifies that a Notion database JSON payload survives translation into a
/// [`DatabaseSchema`] with all of its property names preserved as columns.
///
/// The "round trip" here is JSON properties in, column names out: it guards
/// against regressions where a property type fails to map to a column or its
/// name is dropped/altered during parsing. The two property types chosen
/// (`title` and `status`) are representative of the distinct shapes Notion
/// uses, so the test also implicitly covers handling more than one property
/// kind in a single schema.
///
/// # Panics
///
/// Panics (failing the test) if [`DatabaseSchema::from_notion_database`]
/// returns `Err` — via `unwrap` — or if either expected column is missing from
/// the discovered set.
#[test]
fn database_schema_round_trip() {
// Hand-build the minimal slice of Notion's database object that the parser
// cares about: the `properties` map keyed by property name. Each property
// carries a `type` discriminant plus a same-named config object — this
// mirrors Notion's actual API response shape, where the empty `{}` config
// is valid for these property kinds.
let json_db = json!({
"properties": {
"Name": { "type": "title", "title": {} },
"Status": { "type": "status", "status": {} }
}
});
// Unwrap is intentional: a parse failure on this well-formed input is a
// genuine regression and should fail the test loudly.
let schema = DatabaseSchema::from_notion_database(&json_db).unwrap();
let columns: Vec<String> = schema.available_columns();
// Column ordering is not guaranteed by the schema, so assert membership
// rather than positional equality.
assert!(columns.contains(&"Name".to_string()));
assert!(columns.contains(&"Status".to_string()));
}
/// Confirms the parser accepts a representative `SELECT` with a projection list
/// and a `WHERE` clause containing a string-literal comparison.
///
/// Asserts success only; the parsed AST is not inspected here.
///
/// # Panics
///
/// Panics (failing the test) if [`parse_statement`] returns `Err`.
#[test]
fn parse_select_statement() {
let sql = "SELECT Name, Status FROM database WHERE Status = 'Active'";
let parsed = parse_statement(sql);
assert!(parsed.is_ok(), "Failed to parse SELECT statement");
}
/// Confirms the parser accepts an `INSERT` with an explicit column list and a
/// matching `VALUES` tuple of string literals.
///
/// Asserts success only; the parsed AST is not inspected here.
///
/// # Panics
///
/// Panics (failing the test) if [`parse_statement`] returns `Err`.
#[test]
fn parse_insert_statement() {
let sql = "INSERT INTO database (Name, Status) VALUES ('Test', 'Active')";
let parsed = parse_statement(sql);
assert!(parsed.is_ok(), "Failed to parse INSERT statement");
}
/// Confirms the parser accepts an `UPDATE` with a `SET` assignment and a
/// `WHERE` clause that targets a single row by name.
///
/// Asserts success only; the parsed AST is not inspected here.
///
/// # Panics
///
/// Panics (failing the test) if [`parse_statement`] returns `Err`.
#[test]
fn parse_update_statement() {
let sql = "UPDATE database SET Status = 'Done' WHERE Name = 'Task 1'";
let parsed = parse_statement(sql);
assert!(parsed.is_ok(), "Failed to parse UPDATE statement");
}
/// Confirms the parser accepts a `DELETE` qualified by a `WHERE` clause.
///
/// Asserts success only; the parsed AST is not inspected here. The `WHERE`
/// clause is included deliberately — an unqualified `DELETE` would target every
/// row, which is not the shape this test means to cover.
///
/// # Panics
///
/// Panics (failing the test) if [`parse_statement`] returns `Err`.
#[test]
fn parse_delete_statement() {
let sql = "DELETE FROM database WHERE Status = 'Archived'";
let parsed = parse_statement(sql);
assert!(parsed.is_ok(), "Failed to parse DELETE statement");
}