influxdb/influxdb3/tests/server/configure.rs

1406 lines
42 KiB
Rust

use hyper::StatusCode;
use observability_deps::tracing::{debug, info};
use pretty_assertions::assert_eq;
use serde_json::{Value, json};
use test_helpers::assert_contains;
use crate::server::{ConfigProvider, TestServer};
#[tokio::test]
async fn api_v3_configure_distinct_cache_create() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/distinct_cache",
base = server.client_addr()
);
// Write some LP to the database to initialize the catalog
let db_name = "foo";
let tbl_name = "bar";
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=aa,t3=aaa f1=\"potato\",f2=true,f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
#[derive(Default)]
struct TestCase {
description: &'static str,
db: Option<&'static str>,
table: Option<&'static str>,
cache_name: Option<&'static str>,
max_cardinality: Option<usize>,
max_age: Option<u64>,
columns: &'static [&'static str],
expected: StatusCode,
}
let test_cases = [
TestCase {
description: "no parameters specified",
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "missing database name",
table: Some("bar"),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "invalid database name",
db: Some("carrot"),
table: Some("bar"),
expected: StatusCode::NOT_FOUND,
..Default::default()
},
TestCase {
description: "invalid table name",
db: Some("foo"),
table: Some("celery"),
expected: StatusCode::NOT_FOUND,
..Default::default()
},
TestCase {
description: "no columns specified",
db: Some("foo"),
table: Some("bar"),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "invalid column specified, that does not exist",
db: Some("foo"),
table: Some("bar"),
columns: &["leek"],
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "invalid column specified, that is not tag or string",
db: Some("foo"),
table: Some("bar"),
columns: &["f2"],
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "valid request, creates cache",
db: Some("foo"),
table: Some("bar"),
columns: &["t1", "t2"],
expected: StatusCode::CREATED,
..Default::default()
},
TestCase {
description: "identical to previous request, still success, but results in no 204 content",
db: Some("foo"),
table: Some("bar"),
columns: &["t1", "t2"],
expected: StatusCode::CONFLICT,
..Default::default()
},
TestCase {
description: "same as previous, but with incompatible configuratin, max cardinality",
db: Some("foo"),
table: Some("bar"),
columns: &["t1", "t2"],
max_cardinality: Some(1),
expected: StatusCode::CONFLICT,
..Default::default()
},
TestCase {
description: "same as previous, but with incompatible configuratin, max age",
db: Some("foo"),
table: Some("bar"),
columns: &["t1", "t2"],
max_age: Some(1),
expected: StatusCode::CONFLICT,
..Default::default()
},
TestCase {
description: "valid request, creates cache",
db: Some("foo"),
table: Some("bar"),
columns: &["t1"],
cache_name: Some("my_cache"),
expected: StatusCode::CREATED,
..Default::default()
},
TestCase {
description: "same as previous, but with incompatible configuratino, column set",
db: Some("foo"),
table: Some("bar"),
columns: &["t1", "t2"],
cache_name: Some("my_cache"),
expected: StatusCode::CONFLICT,
..Default::default()
},
];
for tc in test_cases {
let mut body = serde_json::json!({
"db": tc.db,
"table": tc.table,
"name": tc.cache_name,
"columns": tc.columns,
});
if let Some(mc) = tc.max_cardinality {
body["max_cardinality"] = mc.into();
}
if let Some(ma) = tc.max_age {
body["max_age"] = ma.into();
}
let resp = client
.post(&url)
.json(&body)
.send()
.await
.expect("send request to create distinct cache");
let status = resp.status();
assert_eq!(
tc.expected,
status,
"test case failed: {description}",
description = tc.description
);
}
}
#[tokio::test]
async fn api_v3_configure_distinct_cache_delete() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/distinct_cache",
base = server.client_addr()
);
let db_name = "foo";
let tbl_name = "bar";
let cache_name = "test_cache";
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=aa f1=true 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write lp to db");
struct TestCase {
description: &'static str,
request: Request,
expected: StatusCode,
}
enum Request {
Create(serde_json::Value),
Delete(DeleteRequest),
}
#[derive(Default)]
struct DeleteRequest {
db: Option<&'static str>,
table: Option<&'static str>,
name: Option<&'static str>,
}
use Request::*;
let mut test_cases = [
TestCase {
description: "create a distinct cache",
request: Create(serde_json::json!({
"db": db_name,
"table": tbl_name,
"name": cache_name,
"columns": ["t1", "t2"],
})),
expected: StatusCode::CREATED,
},
TestCase {
description: "delete with no parameters fails",
request: Delete(DeleteRequest {
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
TestCase {
description: "missing table, name params",
request: Delete(DeleteRequest {
db: Some(db_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
TestCase {
description: "missing name param",
request: Delete(DeleteRequest {
db: Some(db_name),
table: Some(tbl_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
TestCase {
description: "missing table param",
request: Delete(DeleteRequest {
db: Some(db_name),
name: Some(cache_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
TestCase {
description: "missing db param",
request: Delete(DeleteRequest {
table: Some(tbl_name),
name: Some(cache_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
TestCase {
description: "missing db, table param",
request: Delete(DeleteRequest {
name: Some(cache_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
TestCase {
description: "missing db, name param",
request: Delete(DeleteRequest {
table: Some(tbl_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
TestCase {
description: "valid request, will delete",
request: Delete(DeleteRequest {
db: Some(db_name),
table: Some(tbl_name),
name: Some(cache_name),
}),
expected: StatusCode::OK,
},
TestCase {
description: "same as previous, but gets 404 as the cache was already deleted",
request: Delete(DeleteRequest {
db: Some(db_name),
table: Some(tbl_name),
name: Some(cache_name),
}),
expected: StatusCode::NOT_FOUND,
},
];
// do a pass through the test cases deleting via JSON in the body:
for tc in &test_cases {
match &tc.request {
Create(body) => assert_eq!(
tc.expected,
client
.post(&url)
.json(&body)
.send()
.await
.expect("create request sends")
.status(),
"creation test case failed: {description}",
description = tc.description
),
Delete(DeleteRequest { db, table, name }) => {
let body = serde_json::json!({
"db": db,
"table": table,
"name": name,
});
assert_eq!(
tc.expected,
client
.delete(&url)
.json(&body)
.send()
.await
.expect("delete request send")
.status(),
"deletion test case using JSON body failed: {description}",
description = tc.description
);
}
}
}
// do another pass through the test cases, this time deleting via parameters in the URI:
// on this pass, need to switch the status code that does not provide any params to a 415
// UNSUPPORTED MEDIA TYPE error, as the handler sees no URI parameters, and attempts to parse
// the body as JSON, thus resulting in the 415.
test_cases[1].expected = StatusCode::UNSUPPORTED_MEDIA_TYPE;
for tc in &test_cases {
match &tc.request {
Create(body) => assert_eq!(
tc.expected,
client
.post(&url)
.json(&body)
.send()
.await
.expect("create request sends")
.status(),
"creation test case failed: {description}",
description = tc.description
),
Delete(DeleteRequest { db, table, name }) => {
let mut params = vec![];
if let Some(db) = db {
params.push(("db", db));
}
if let Some(table) = table {
params.push(("table", table));
}
if let Some(name) = name {
params.push(("name", name));
}
assert_eq!(
tc.expected,
client
.delete(&url)
.query(&params)
.send()
.await
.expect("delete request send")
.status(),
"deletion test case using URI parameters failed: {description}",
description = tc.description
);
}
}
}
}
#[tokio::test]
async fn api_v3_configure_last_cache_create() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/last_cache",
base = server.client_addr()
);
// Write some LP to the database to initialize the catalog:
let db_name = "db";
let tbl_name = "tbl";
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=b,t3=c f1=true,f2=\"hello\",f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
#[derive(Default)]
struct TestCase {
// These attributes all map to parameters of the request body:
description: &'static str,
db: Option<&'static str>,
table: Option<&'static str>,
cache_name: Option<&'static str>,
count: Option<usize>,
ttl: Option<usize>,
key_cols: Option<&'static [&'static str]>,
val_cols: Option<&'static [&'static str]>,
// This is the status code expected in the response:
expected: StatusCode,
}
let test_cases = [
TestCase {
description: "no parameters specified",
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "missing database name",
table: Some(tbl_name),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "missing table name",
db: Some(db_name),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "Good, will use defaults for everything omitted, and get back a 201",
db: Some(db_name),
table: Some(tbl_name),
expected: StatusCode::CREATED,
..Default::default()
},
TestCase {
description: "Same as before, will fail with 409",
db: Some(db_name),
table: Some(tbl_name),
expected: StatusCode::CONFLICT,
..Default::default()
},
// NOTE: this will only differ from the previous cache in name, should this actually
// be an error?
TestCase {
description: "Use a specific cache name, will succeed and create new cache",
db: Some(db_name),
table: Some(tbl_name),
cache_name: Some("my_cache"),
expected: StatusCode::CREATED,
..Default::default()
},
TestCase {
description: "Same as previous, but will get 409 because it conflicts",
db: Some(db_name),
table: Some(tbl_name),
cache_name: Some("my_cache"),
expected: StatusCode::CONFLICT,
..Default::default()
},
TestCase {
description: "Same as previous, but this time try to use different parameters, this \
will result in a conflict",
db: Some(db_name),
table: Some(tbl_name),
cache_name: Some("my_cache"),
// The default TTL that would have been used is 4 * 60 * 60 seconds (4 hours)
ttl: Some(666),
expected: StatusCode::CONFLICT,
..Default::default()
},
TestCase {
description: "Will create new cache, because key columns are unique, and so will be the name",
db: Some(db_name),
table: Some(tbl_name),
key_cols: Some(&["t1", "t2"]),
expected: StatusCode::CREATED,
..Default::default()
},
TestCase {
description: "Same as previous, but will get 409 because of conflict",
db: Some(db_name),
table: Some(tbl_name),
key_cols: Some(&["t1", "t2"]),
expected: StatusCode::CONFLICT,
..Default::default()
},
TestCase {
description: "Use an invalid key column (by name) is a not found",
db: Some(db_name),
table: Some(tbl_name),
key_cols: Some(&["not_a_key_column"]),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "Use an invalid key column (by type) is a bad request",
db: Some(db_name),
table: Some(tbl_name),
// f5 is a float, which is not supported as a key column:
key_cols: Some(&["f5"]),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "Use an invalid value column is a bad request",
db: Some(db_name),
table: Some(tbl_name),
val_cols: Some(&["not_a_value_column"]),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
TestCase {
description: "Use an invalid cache size is a bad request",
db: Some(db_name),
table: Some(tbl_name),
cache_name: Some("too_small"),
count: Some(0),
expected: StatusCode::BAD_REQUEST,
..Default::default()
},
];
for (i, t) in test_cases.into_iter().enumerate() {
let mut body = serde_json::json!({
"db": t.db,
"table": t.table,
"name": t.cache_name,
"key_columns": t.key_cols,
"value_columns": t.val_cols,
});
if let Some(c) = t.count {
body["count"] = c.into();
}
if let Some(t) = t.ttl {
body["ttl"] = t.into();
}
let resp = client
.post(&url)
.json(&body)
.send()
.await
.expect("send /api/v3/configure/last_cache request");
let status = resp.status();
assert_eq!(
t.expected,
status,
"test case ({i}) failed, {description}",
description = t.description
);
}
}
#[tokio::test]
async fn api_v3_configure_last_cache_delete() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/last_cache",
base = server.client_addr()
);
// Write some LP to the database to initialize the catalog:
let db_name = "db";
let tbl_name = "tbl";
let cache_name = "test_cache";
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=b,t3=c f1=true,f2=\"hello\",f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
struct TestCase {
request: Request,
// This is the status code expected in the response:
expected: StatusCode,
}
enum Request {
Create(serde_json::Value),
Delete(DeleteRequest),
}
#[derive(Default)]
struct DeleteRequest {
db: Option<&'static str>,
table: Option<&'static str>,
name: Option<&'static str>,
}
use Request::*;
let mut test_cases = [
// Create a cache:
TestCase {
request: Create(serde_json::json!({
"db": db_name,
"table": tbl_name,
"name": cache_name,
})),
expected: StatusCode::CREATED,
},
// Missing all params:
TestCase {
request: Delete(DeleteRequest {
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
// Partial params:
TestCase {
request: Delete(DeleteRequest {
db: Some(db_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
// Partial params:
TestCase {
request: Delete(DeleteRequest {
table: Some(tbl_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
// Partial params:
TestCase {
request: Delete(DeleteRequest {
name: Some(cache_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
// Partial params:
TestCase {
request: Delete(DeleteRequest {
db: Some(db_name),
table: Some(tbl_name),
..Default::default()
}),
expected: StatusCode::BAD_REQUEST,
},
// All params, good:
TestCase {
request: Delete(DeleteRequest {
db: Some(db_name),
table: Some(tbl_name),
name: Some(cache_name),
}),
expected: StatusCode::OK,
},
// Same as previous, with correct parameters provided, but gest 404, as its already deleted:
TestCase {
request: Delete(DeleteRequest {
db: Some(db_name),
table: Some(tbl_name),
name: Some(cache_name),
}),
expected: StatusCode::NOT_FOUND,
},
];
// Do one pass using the JSON body to delete:
for (i, t) in test_cases.iter().enumerate() {
match &t.request {
Create(body) => assert!(
client
.post(&url)
.json(&body)
.send()
.await
.expect("create request succeeds")
.status()
.is_success(),
"Creation test case ({i}) failed"
),
Delete(req) => {
let body = serde_json::json!({
"db": req.db,
"table": req.table,
"name": req.name,
});
let resp = client
.delete(&url)
.json(&body)
.send()
.await
.expect("send /api/v3/configure/last_cache request");
let status = resp.status();
assert_eq!(
t.expected, status,
"Deletion test case ({i}) using JSON body failed"
);
}
}
}
// Do another pass using the URI query string to delete:
// Note: this particular test exhibits different status code, because the empty query string
// as a result of there being no parameters provided makes the request handler attempt to
// parse the body as JSON - which gives a 415 error, because there is no body or content type
test_cases[1].expected = StatusCode::UNSUPPORTED_MEDIA_TYPE;
for (i, t) in test_cases.iter().enumerate() {
match &t.request {
Create(body) => assert!(
client
.post(&url)
.json(&body)
.send()
.await
.expect("create request succeeds")
.status()
.is_success(),
"Creation test case ({i}) failed"
),
Delete(req) => {
let mut params = vec![];
if let Some(db) = req.db {
params.push(("db", db));
}
if let Some(table) = req.table {
params.push(("table", table));
}
if let Some(name) = req.name {
params.push(("name", name));
}
let resp = client
.delete(&url)
.query(&params)
.send()
.await
.expect("send /api/v3/configure/last_cache request");
let status = resp.status();
assert_eq!(
t.expected, status,
"Deletion test case ({i}) using URI query string failed"
);
}
}
}
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_db_delete() {
let db_name = "foo";
let tbl_name = "tbl";
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/database?db={db_name}",
base = server.client_addr()
);
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=b,t3=c f1=true,f2=\"hello\",f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
// check foo db is present
let result = server
.api_v3_query_influxql(&[("q", "SHOW DATABASES"), ("format", "json")])
.await
.json::<Value>()
.await
.unwrap();
assert_eq!(
json!([{ "deleted": false, "iox::database": "_internal" }, { "deleted": false, "iox::database": "foo" } ]),
result
);
let resp = client
.delete(&url)
.send()
.await
.expect("delete database call succeed");
assert_eq!(200, resp.status());
// check foo db is now foo-YYYYMMDD..
let result = server
.api_v3_query_influxql(&[("q", "SHOW DATABASES"), ("format", "json")])
.await
.json::<Value>()
.await
.unwrap();
let array_result = result.as_array().unwrap();
assert_eq!(2, array_result.len());
let first_db = array_result.first().unwrap();
assert_contains!(
first_db
.as_object()
.unwrap()
.get("iox::database")
.unwrap()
.as_str()
.unwrap(),
"_internal"
);
let second_db = array_result.get(1).unwrap();
assert_contains!(
second_db
.as_object()
.unwrap()
.get("iox::database")
.unwrap()
.as_str()
.unwrap(),
"foo-"
);
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=b,t3=c f1=true,f2=\"hello\",f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
let result = server
.api_v3_query_influxql(&[("q", "SHOW DATABASES"), ("format", "json")])
.await
.json::<Value>()
.await
.unwrap();
let array_result = result.as_array().unwrap();
// check there are 2 dbs now, foo and foo-*
assert_eq!(3, array_result.len());
let first_db = array_result.first().unwrap();
let second_db = array_result.get(1).unwrap();
let third_db = array_result.get(2).unwrap();
assert_contains!(
first_db
.as_object()
.unwrap()
.get("iox::database")
.unwrap()
.as_str()
.unwrap(),
"_internal"
);
assert_eq!(
"foo",
second_db
.as_object()
.unwrap()
.get("iox::database")
.unwrap()
.as_str()
.unwrap(),
);
assert_contains!(
third_db
.as_object()
.unwrap()
.get("iox::database")
.unwrap()
.as_str()
.unwrap(),
"foo-"
);
}
#[tokio::test]
async fn api_v3_configure_db_delete_no_db() {
let db_name = "db";
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/database?db={db_name}",
base = server.client_addr()
);
let resp = client
.delete(&url)
.send()
.await
.expect("delete database call succeed");
assert_eq!(StatusCode::NOT_FOUND, resp.status());
}
#[tokio::test]
async fn api_v3_configure_db_delete_missing_query_param() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/database",
base = server.client_addr()
);
let resp = client
.delete(&url)
.send()
.await
.expect("delete database call succeed");
assert_eq!(StatusCode::BAD_REQUEST, resp.status());
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_db_create() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/database",
base = server.client_addr()
);
let resp = client
.post(&url)
.json(&json!({ "db": "foo" }))
.send()
.await
.expect("delete database call succeed");
assert_eq!(StatusCode::OK, resp.status());
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_db_create_db_with_same_name() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/database",
base = server.client_addr()
);
let resp = client
.post(&url)
.json(&json!({ "db": "foo" }))
.send()
.await
.expect("create database call did not succeed");
assert_eq!(StatusCode::OK, resp.status());
let resp = client
.post(&url)
.json(&json!({ "db": "foo" }))
.send()
.await
.expect("create database call should succeed");
assert_eq!(StatusCode::CONFLICT, resp.status());
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_db_create_db_hit_limit() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/database",
base = server.client_addr()
);
for i in 0..5 {
let resp = client
.post(&url)
.json(&json!({ "db": format!("foo{i}") }))
.send()
.await
.expect("create database call did not succeed");
assert_eq!(StatusCode::OK, resp.status());
}
let resp = client
.post(&url)
.json(&json!({ "db": "foo5" }))
.send()
.await
.expect("create database succeeded");
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, resp.status());
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_db_create_db_reuse_old_name() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/database",
base = server.client_addr()
);
let resp = client
.post(&url)
.json(&json!({ "db": "foo" }))
.send()
.await
.expect("create database call did not succeed");
assert_eq!(StatusCode::OK, resp.status());
let resp = client
.delete(format!("{url}?db=foo"))
.send()
.await
.expect("delete database call did not succeed");
assert_eq!(StatusCode::OK, resp.status());
let resp = client
.post(&url)
.json(&json!({ "db": "foo" }))
.send()
.await
.expect("create database call did not succeed");
assert_eq!(StatusCode::OK, resp.status());
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_table_create_then_write() {
let server = TestServer::spawn().await;
let client = server.http_client();
let db_url = format!(
"{base}/api/v3/configure/database",
base = server.client_addr()
);
let table_url = format!("{base}/api/v3/configure/table", base = server.client_addr());
let resp = client
.post(&db_url)
.json(&json!({ "db": "foo" }))
.send()
.await
.expect("create database call did not succeed");
assert_eq!(StatusCode::OK, resp.status());
let resp = client
.post(&table_url)
.json(&json!({
"db": "foo" ,
"table": "bar",
"tags": ["tag1", "tag2"],
"fields": [
{
"name": "field1",
"type": "uint64"
},
{
"name": "field2",
"type": "int64"
},
{
"name": "field3",
"type": "float64"
},
{
"name": "field4",
"type": "utf8"
},
{
"name": "field5",
"type": "bool"
}
]
}))
.send()
.await
.expect("create table call failed");
assert_eq!(StatusCode::OK, resp.status());
let result = server
.query_sql("foo")
.with_sql("SELECT * FROM bar")
.run()
.unwrap();
assert_eq!(result, json!([]));
server
.write_lp_to_db(
"foo",
"bar,tag1=1,tag2=2 field1=1u,field2=2i,field3=3,field4=\"4\",field5=true 2998574938",
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
let result = server
.query_sql("foo")
.with_sql("SELECT * FROM bar")
.run()
.unwrap();
assert_eq!(
result,
json!([{
"tag1": "1",
"tag2": "2",
"field1": 1,
"field2": 2,
"field3": 3.0,
"field4": "4",
"field5": true,
"time": "2065-01-07T17:28:58"
}])
);
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_table_create_no_fields() {
let server = TestServer::spawn().await;
let client = server.http_client();
let db_url = format!(
"{base}/api/v3/configure/database",
base = server.client_addr()
);
let table_url = format!("{base}/api/v3/configure/table", base = server.client_addr());
let resp = client
.post(&db_url)
.json(&json!({ "db": "foo" }))
.send()
.await
.expect("create database call did not succeed");
assert_eq!(StatusCode::OK, resp.status());
let resp = client
.post(&table_url)
.json(&json!({
"db": "foo" ,
"table": "bar",
"tags": ["one", "two"],
"fields": []
}))
.send()
.await
.expect("create table call failed");
assert_eq!(StatusCode::OK, resp.status());
let result = server
.query_sql("foo")
.with_sql("SELECT * FROM bar")
.run()
.unwrap();
assert_eq!(result, json!([]));
server
.write_lp_to_db(
"foo",
"bar,one=1,two=2 new_field=0 2998574938",
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
let result = server
.query_sql("foo")
.with_sql("SELECT * FROM bar")
.run()
.unwrap();
assert_eq!(
result,
json!([{
"one": "1",
"two": "2",
"new_field": 0.0,
"time": "2065-01-07T17:28:58"
}])
);
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_table_create_invalid_field_types() {
let server = TestServer::spawn().await;
let client = server.http_client();
let table_url = format!("{base}/api/v3/configure/table", base = server.client_addr());
let resp = client
.post(&table_url)
.json(&json!({
"db": "foo",
"table": "bar",
"tags": ["tag1"],
"fields": [
{
"name": "invalid",
"type": "foobar"
}
]
}))
.send()
.await
.expect("create table call failed");
assert!(resp.status().is_client_error());
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_table_delete() {
let db_name = "foo";
let tbl_name = "tbl";
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/table?db={db_name}&table={tbl_name}",
base = server.client_addr()
);
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=b,t3=c f1=true,f2=\"hello\",f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
let resp = client
.delete(&url)
.send()
.await
.expect("delete table call succeed");
assert_eq!(200, resp.status());
// check foo db has table with name in tbl-YYYYMMDD.. format
let result = server
.api_v3_query_influxql(&[("q", "SHOW MEASUREMENTS on foo"), ("format", "json")])
.await
.json::<Value>()
.await
.unwrap();
debug!(result = ?result, ">> RESULT");
let array_result = result.as_array().unwrap();
assert_eq!(1, array_result.len());
let first_db = array_result.first().unwrap();
assert_contains!(
first_db
.as_object()
.unwrap()
.get("name")
.unwrap()
.as_str()
.unwrap(),
"tbl-"
);
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=b,t3=c f1=true,f2=\"hello\",f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
let result = server
.api_v3_query_influxql(&[("q", "SHOW MEASUREMENTS on foo"), ("format", "json")])
.await
.json::<Value>()
.await
.unwrap();
debug!(result = ?result, ">> RESULT");
let array_result = result.as_array().unwrap();
// check there are 2 tables now, tbl and tbl-*
assert_eq!(2, array_result.len());
let first_db = array_result.first().unwrap();
let second_db = array_result.get(1).unwrap();
assert_eq!(
"tbl",
first_db
.as_object()
.unwrap()
.get("name")
.unwrap()
.as_str()
.unwrap(),
);
assert_contains!(
second_db
.as_object()
.unwrap()
.get("name")
.unwrap()
.as_str()
.unwrap(),
"tbl-"
);
}
#[tokio::test]
async fn api_v3_configure_table_delete_no_db() {
let db_name = "db";
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!(
"{base}/api/v3/configure/table?db={db_name}&table=foo",
base = server.client_addr()
);
let resp = client
.delete(&url)
.send()
.await
.expect("delete database call succeed");
assert_eq!(StatusCode::NOT_FOUND, resp.status());
}
#[tokio::test]
async fn api_v3_configure_table_delete_missing_query_param() {
let server = TestServer::spawn().await;
let client = server.http_client();
let url = format!("{base}/api/v3/configure/table", base = server.client_addr());
let resp = client
.delete(&url)
.send()
.await
.expect("delete table call succeed");
assert_eq!(StatusCode::BAD_REQUEST, resp.status());
}
#[tokio::test]
async fn try_deleting_table_after_db_is_deleted() {
let db_name = "db";
let tbl_name = "tbl";
let server = TestServer::spawn().await;
let client = server.http_client();
let delete_db_url = format!(
"{base}/api/v3/configure/database?db={db_name}",
base = server.client_addr()
);
let delete_table_url = format!(
"{base}/api/v3/configure/table?db={db_name}&table={tbl_name}",
base = server.client_addr()
);
server
.write_lp_to_db(
db_name,
format!("{tbl_name},t1=a,t2=b,t3=c f1=true,f2=\"hello\",f3=4i,f4=4u,f5=5 1000"),
influxdb3_client::Precision::Second,
)
.await
.expect("write to db");
// db call should succeed
let resp = client
.delete(&delete_db_url)
.send()
.await
.expect("delete database call succeed");
assert_eq!(StatusCode::OK, resp.status());
// but table delete call should fail with NOT_FOUND
let resp = client
.delete(&delete_table_url)
.send()
.await
.expect("delete table call succeed");
assert_eq!(StatusCode::NOT_FOUND, resp.status());
}
#[test_log::test(tokio::test)]
async fn api_v3_configure_token_delete() {
let token_name = "_admin";
let server = TestServer::configure().with_auth().spawn().await;
let client = server.http_client();
let create_url = format!(
"{base}/api/v3/configure/token/admin",
base = server.client_addr()
);
let delete_url = format!("{base}/api/v3/configure/token", base = server.client_addr());
let admin_token = server.token().expect("admin token to be present");
let delete_result = client
.delete(&delete_url)
.bearer_auth(admin_token)
.query(&[("token_name", token_name)])
.send()
.await
.unwrap();
info!(?delete_result, "test: result running the token delete");
assert_eq!(delete_result.status(), StatusCode::METHOD_NOT_ALLOWED);
// create admin token again - this will fail as operator token already exists
let result = client.post(&create_url).send().await.unwrap();
info!(?result, "test: result running the create token");
assert_eq!(result.status(), StatusCode::CONFLICT);
}
#[test_log::test(tokio::test)]
async fn test_token_paths_are_not_allowed_when_starting_without_auth() {
let server = TestServer::spawn().await;
let client = server.http_client();
let create_url = format!(
"{base}/api/v3/configure/token/admin",
base = server.client_addr()
);
let regenerate_url = format!(
"{base}/api/v3/configure/token/admin",
base = server.client_addr()
);
let delete_url = format!("{base}/api/v3/configure/token", base = server.client_addr());
for url in &[create_url, regenerate_url] {
let result = client.post(url).send().await.unwrap();
assert_eq!(result.status(), StatusCode::METHOD_NOT_ALLOWED);
}
let result = client.delete(delete_url).send().await.unwrap();
assert_eq!(result.status(), StatusCode::METHOD_NOT_ALLOWED);
}