kintone 0.6.2

kintone REST API client
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! # Integration Tests for kintone-rs
//!
//! This module contains integration tests that test the complete workflow
//! of creating apps, adding fields, deploying apps, and managing records
//! using the real Kintone API.
//!
//! ## Setup
//!
//! To run these tests, you need to set the following environment variables:
//!
//! - `KINTONE_BASE_URL`: Your Kintone domain URL (e.g., `https://your-domain.cybozu.com`)
//! - `KINTONE_USERNAME`: Your username for Kintone
//! - `KINTONE_PASSWORD`: Your password for Kintone
//!
//! **Note**: The space operations test requires space creation and deletion permissions.
//! This may require administrator privileges in your Kintone environment.
//!
//! ## Running the Tests
//!
//! These tests are marked with `#[ignore]` because they require a real Kintone environment.
//! To run them, use:
//!
//! ```bash
//! export KINTONE_BASE_URL=https://your-domain.cybozu.com
//! export KINTONE_USERNAME=your-username
//! export KINTONE_PASSWORD=your-password
//! cargo test --test integration_test -- --ignored
//! ```
//!
//! ## Test Scenarios
//!
//! - `integration_test_full_workflow`: Complete app creation, field addition, deployment, record management, and querying
//! - `integration_test_record_operations`: Record CRUD operations (Create, Read, Update)
//! - `integration_test_space_operations`: Space and thread management operations

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() {
    // https://docs.rs/env_logger/latest/env_logger/#specifying-defaults-for-environment-variables
    // https://docs.rs/env_logger/latest/env_logger/#capturing-logs-in-tests
    let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
        .is_test(true)
        .try_init();
}

// Test configuration structure
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()
    }
}

/// Waits for app deployment to complete, polling the status at regular intervals.
///
/// # Arguments
/// * `client` - The Kintone client to use for API calls
/// * `app_id` - The ID of the app being deployed
/// * `max_attempts` - Maximum number of polling attempts before timeout
///
/// # Panics
/// Panics if deployment fails, is cancelled, times out, or no status is found.
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] // This test requires real Kintone environment setup
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();

    // 1. Create an app
    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));

    // 2. Add fields to the app
    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);

    // 3. Deploy the app and wait for completion
    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);

    // 4. Add some records to the app
    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);
    }

    // 5. Retrieve records by ID and verify they match expectations
    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;

        // Verify name field
        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}");
        }

        // Verify age field
        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})");
    }

    // 6. Retrieve records with filter conditions and verify results
    // Test filter: age >= 30
    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;

    // We expect Bob (30), Charlie (35) to match the filter
    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] // This test requires real Kintone environment setup
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();

    // Create a simple app for record operations
    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;

    // Add a simple text field
    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");

    // Deploy the app
    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);

    // Test record CRUD operations
    // Create record
    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}");

    // Read record
    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");
    }

    // Update record
    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);

    // Verify update
    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!");
}

/*
#[test]
#[ignore] // This test requires real Kintone environment setup
fn integration_test_space_operations() {
    setup_logger();

    let config =
        TestConfig::from_env().expect("Failed to load test configuration from environment");
    let client = config.create_client();

    // Test space lifecycle: create -> add thread -> add comment -> delete
    let space_name = format!("Test Space {}", chrono::Utc::now().timestamp());

    // 1. Create a new space
    let create_space_response =
        space::add_space(&space_name).send(&client).expect("Failed to create space");

    let space_id = create_space_response.id;
    println!("Created space '{space_name}' with ID: {space_id}");

    // 2. Create a thread in the space
    let thread_name = "Integration Test Thread";
    let create_thread_response = space::add_thread(space_id, thread_name)
        .send(&client)
        .expect("Failed to create thread");

    let thread_id = create_thread_response.id;
    println!("Created thread '{thread_name}' with ID: {thread_id}");

    // 3. Add comments to the thread
    let comments = [
        "This is the first comment in our integration test.",
        "This is a second comment to test multiple comments.",
        "Final comment to complete the test scenario.",
    ];

    let mut comment_ids = Vec::new();

    for (i, comment_text) in comments.iter().enumerate() {
        let comment = ThreadComment {
            text: comment_text.to_string(),
            mentions: vec![], // No mentions in this basic test
        };

        let add_comment_response = space::add_thread_comment(space_id, thread_id, comment)
            .send(&client)
            .unwrap_or_else(|_| panic!("Failed to add comment {}", i + 1));

        comment_ids.push(add_comment_response.id);
        println!("Added comment {} with ID: {}", i + 1, add_comment_response.id);
    }

    // 4. Test comment with mentions
    let mention_comment = ThreadComment {
        text: "This comment mentions a user @user".to_string(),
        mentions: vec![Entity {
            entity_type: EntityType::USER,
            code: "user".to_string(),
        }],
    };

    let mention_response = space::add_thread_comment(space_id, thread_id, mention_comment)
        .send(&client)
        .expect("Failed to add comment with mentions");

    println!("Added comment with mentions, ID: {}", mention_response.id);

    // 5. Clean up: Delete the space
    // Note: This will delete the space and all its content (threads, comments)
    space::delete_space(space_id).send(&client).expect("Failed to delete space");

    println!("Successfully deleted space {space_id}");
}
*/