use std::{
env,
thread::{self, sleep},
time::Duration,
};
use kintone::{
client::{Auth, KintoneClient, KintoneClientBuilder},
middleware,
model::{
app::field::{FieldProperty, NumberFieldProperty, SingleLineTextFieldProperty},
record::{FieldValue, Record},
},
v1::{app, record},
};
fn setup_logger() {
let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.is_test(true)
.try_init();
}
struct TestConfig {
base_url: String,
username: String,
password: String,
}
impl TestConfig {
fn from_env() -> Result<Self, String> {
let base_url = env::var("KINTONE_BASE_URL")
.map_err(|_| "KINTONE_BASE_URL environment variable is required")?;
let username = env::var("KINTONE_USERNAME")
.map_err(|_| "KINTONE_USERNAME environment variable is required")?;
let password = env::var("KINTONE_PASSWORD")
.map_err(|_| "KINTONE_PASSWORD environment variable is required")?;
Ok(TestConfig {
base_url,
username,
password,
})
}
fn create_client(&self) -> KintoneClient {
KintoneClientBuilder::new(
&self.base_url,
Auth::password(self.username.clone(), self.password.clone()),
)
.layer(middleware::RetryLayer::new())
.layer(middleware::LoggingLayer::new())
.build()
}
}
fn wait_for_deployment_completion(client: &KintoneClient, app_id: u64, max_attempts: u32) {
println!("Deployment started, waiting for completion...");
for attempt in 1..=max_attempts {
let status_response = app::settings::get_app_deploy_status()
.app(app_id)
.send(client)
.expect("Failed to check deployment status");
if let Some(app_status) = status_response.apps.first() {
match app_status.status {
app::settings::DeployStatus::Success => {
println!("Deployment completed successfully");
return;
}
app::settings::DeployStatus::Fail => {
panic!("Deployment failed");
}
app::settings::DeployStatus::Cancel => {
panic!("Deployment was cancelled");
}
app::settings::DeployStatus::Processing => {
if attempt == max_attempts {
panic!("Deployment did not complete within {max_attempts} attempts");
}
println!("Deployment still in progress (attempt {attempt}/{max_attempts})");
thread::sleep(Duration::from_secs(1));
}
}
} else {
panic!("No deployment status found for app {app_id}");
}
}
}
#[test]
#[ignore] fn integration_test_full_workflow() {
setup_logger();
let config =
TestConfig::from_env().expect("Failed to load test configuration from environment");
let client = config.create_client();
let app_name = format!("Test App {}", chrono::Utc::now().timestamp());
let create_response = app::add_app(&app_name).send(&client).expect("Failed to create app");
let app_id = create_response.app;
println!("Created app with ID: {app_id}");
sleep(Duration::from_secs(2));
let text_field = SingleLineTextFieldProperty {
code: "name".to_owned(),
label: "Name".to_owned(),
required: true,
max_length: Some(50),
..Default::default()
};
let number_field = NumberFieldProperty {
code: "age".to_owned(),
label: "Age".to_owned(),
required: false,
min_value: Some(0.into()),
max_value: Some(200.into()),
..Default::default()
};
let add_field_response = app::form::add_form_field(app_id)
.field(FieldProperty::SingleLineText(text_field))
.field(FieldProperty::Number(number_field))
.send(&client)
.expect("Failed to add fields");
println!("Added fields, new revision: {}", add_field_response.revision);
app::settings::deploy_app()
.app(app_id, Some(add_field_response.revision))
.send(&client)
.expect("Failed to start deployment");
wait_for_deployment_completion(&client, app_id, 30);
let test_records = vec![("Alice", 25), ("Bob", 30), ("Charlie", 35), ("Diana", 28)];
let mut record_ids = Vec::new();
for (name, age) in &test_records {
let record = Record::from([
("name", FieldValue::SingleLineText(name.to_string())),
("age", FieldValue::Number(Some(age.into()))),
]);
let add_record_response = record::add_record(app_id)
.record(record)
.send(&client)
.expect("Failed to add record");
record_ids.push(add_record_response.id);
println!("Added record for {} with ID: {}", name, add_record_response.id);
}
for (i, &record_id) in record_ids.iter().enumerate() {
let get_response = record::get_record(app_id, record_id)
.send(&client)
.expect("Failed to get record");
let retrieved_record = &get_response.record;
let expected_name = test_records[i].0;
let expected_age = test_records[i].1;
if let Some(FieldValue::SingleLineText(name)) = retrieved_record.get("name") {
assert_eq!(name, expected_name, "Name field mismatch for record {record_id}");
} else {
panic!("Name field not found or wrong type for record {record_id}");
}
if let Some(FieldValue::Number(Some(age_decimal))) = retrieved_record.get("age") {
let age: i32 = age_decimal.to_string().parse().expect("Failed to parse age");
assert_eq!(age, expected_age, "Age field mismatch for record {record_id}");
} else {
panic!("Age field not found or wrong type for record {record_id}");
}
println!("✓ Record {record_id} verified: {expected_name} (age {expected_age})");
}
let filter_response = record::get_records(app_id)
.query("age >= 30")
.fields(&["name", "age"])
.send(&client)
.expect("Failed to get records with filter");
let filtered_records = &filter_response.records;
assert_eq!(filtered_records.len(), 2, "Expected 2 records with age >= 30");
let mut found_names: Vec<_> = filtered_records
.iter()
.filter_map(|record| match record.get("name") {
Some(FieldValue::SingleLineText(name)) => Some(name.clone()),
_ => None,
})
.collect();
found_names.sort();
let mut expected_names = vec!["Bob".to_string(), "Charlie".to_string()];
expected_names.sort();
assert_eq!(found_names, expected_names, "Filtered records don't match expectations");
println!("✓ Filter test passed: Found {} records with age >= 30", filtered_records.len());
println!("🎉 All integration tests passed!");
}
#[test]
#[ignore] fn integration_test_record_operations() {
setup_logger();
let config =
TestConfig::from_env().expect("Failed to load test configuration from environment");
let client = config.create_client();
let app_name = format!("Record Test App {}", chrono::Utc::now().timestamp());
let create_response = app::add_app(&app_name).send(&client).expect("Failed to create app");
let app_id = create_response.app;
let text_field = SingleLineTextFieldProperty {
code: "title".to_owned(),
label: "Title".to_owned(),
required: true,
max_length: Some(200),
..Default::default()
};
let add_field_response = app::form::add_form_field(app_id)
.field(FieldProperty::SingleLineText(text_field))
.send(&client)
.expect("Failed to add field");
app::settings::deploy_app()
.app(app_id, Some(add_field_response.revision))
.send(&client)
.expect("Failed to start deployment");
wait_for_deployment_completion(&client, app_id, 20);
let record = Record::from([("title", FieldValue::SingleLineText("Test Record".to_owned()))]);
let add_response = record::add_record(app_id)
.record(record)
.send(&client)
.expect("Failed to add record");
let record_id = add_response.id;
println!("Created record with ID: {record_id}");
let get_response = record::get_record(app_id, record_id)
.send(&client)
.expect("Failed to get record");
if let Some(FieldValue::SingleLineText(title)) = get_response.record.get("title") {
assert_eq!(title, "Test Record");
println!("✓ Record read test passed");
} else {
panic!("Title field not found or wrong type");
}
let update_record =
Record::from([("title", FieldValue::SingleLineText("Updated Test Record".to_owned()))]);
let update_response = record::update_record(app_id)
.id(record_id)
.record(update_record)
.revision(get_response.record.revision().unwrap())
.send(&client)
.expect("Failed to update record");
println!("Updated record to revision: {}", update_response.revision);
let get_updated_response = record::get_record(app_id, record_id)
.send(&client)
.expect("Failed to get updated record");
if let Some(FieldValue::SingleLineText(title)) = get_updated_response.record.get("title") {
assert_eq!(title, "Updated Test Record");
println!("✓ Record update test passed");
} else {
panic!("Updated title field not found or wrong type");
}
println!("🎉 Record operations test passed!");
}