use async_trait::async_trait;
use drasi_lib::identity::{CredentialContext, Credentials, IdentityProvider};
use drasi_source_dataverse::client::DataverseClient;
use drasi_source_dataverse::types::{parse_delta_changes, DataverseChange};
use serde_json::json;
use std::sync::Arc;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
struct StaticToken;
#[async_trait]
impl IdentityProvider for StaticToken {
async fn get_credentials(&self, _ctx: &CredentialContext) -> anyhow::Result<Credentials> {
Ok(Credentials::Token {
username: "test".to_string(),
token: "mock-access-token-12345".to_string(),
})
}
fn clone_box(&self) -> Box<dyn IdentityProvider> {
Box::new(StaticToken)
}
}
#[tokio::test]
async fn dataverse_client_full_change_lifecycle() {
let server = MockServer::start().await;
let uri = server.uri();
Mock::given(method("GET"))
.and(path("/api/data/v9.2/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@odata.context": format!("{uri}/api/data/v9.2/$metadata#accounts"),
"value": [],
"@odata.deltaLink":
format!("{uri}/api/data/v9.2/accounts?$deltatoken=after-initial")
})))
.mount(&server)
.await;
let client = DataverseClient::new(&uri, "v9.2", Arc::new(StaticToken));
let initial = client
.initial_change_tracking("accounts", None)
.await
.expect("initial change tracking should succeed");
assert!(initial.value.is_empty(), "no records on initial");
let initial_delta = initial
.delta_link
.clone()
.expect("initial delta link must be present");
server.reset().await;
Mock::given(method("GET"))
.and(path("/api/data/v9.2/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@odata.context": format!("{uri}/api/data/v9.2/$metadata#accounts"),
"value": [{
"@odata.etag": "W/\"1001\"",
"accountid": "aaaa-bbbb-cccc-0001",
"name": "Contoso Ltd",
"revenue": 5_000_000.0,
"statecode": 0
}],
"@odata.deltaLink":
format!("{uri}/api/data/v9.2/accounts?$deltatoken=after-insert")
})))
.mount(&server)
.await;
let after_insert = client
.follow_delta_link(&initial_delta)
.await
.expect("follow_delta_link should succeed");
let changes = parse_delta_changes(&after_insert, "account");
assert_eq!(changes.len(), 1, "exactly one INSERT change expected");
match &changes[0] {
DataverseChange::NewOrUpdated {
id,
entity_name,
attributes,
} => {
assert_eq!(id, "aaaa-bbbb-cccc-0001");
assert_eq!(entity_name, "account");
assert_eq!(
attributes.get("name").and_then(|v| v.as_str()),
Some("Contoso Ltd")
);
}
other => panic!("expected NewOrUpdated, got {other:?}"),
}
let insert_delta = after_insert
.delta_link
.clone()
.expect("delta link after insert");
server.reset().await;
Mock::given(method("GET"))
.and(path("/api/data/v9.2/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@odata.context": format!("{uri}/api/data/v9.2/$metadata#accounts"),
"value": [{
"@odata.etag": "W/\"1002\"",
"accountid": "aaaa-bbbb-cccc-0001",
"name": "Contoso Updated",
"revenue": 7_500_000.0,
"statecode": 0
}],
"@odata.deltaLink":
format!("{uri}/api/data/v9.2/accounts?$deltatoken=after-update")
})))
.mount(&server)
.await;
let after_update = client
.follow_delta_link(&insert_delta)
.await
.expect("follow_delta_link for update should succeed");
let changes = parse_delta_changes(&after_update, "account");
assert_eq!(changes.len(), 1);
match &changes[0] {
DataverseChange::NewOrUpdated { attributes, .. } => {
assert_eq!(
attributes.get("name").and_then(|v| v.as_str()),
Some("Contoso Updated")
);
}
other => panic!("expected NewOrUpdated for update, got {other:?}"),
}
let update_delta = after_update
.delta_link
.clone()
.expect("delta link after update");
server.reset().await;
Mock::given(method("GET"))
.and(path("/api/data/v9.2/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@odata.context": format!("{uri}/api/data/v9.2/$metadata#accounts"),
"value": [{
"@odata.context": format!(
"{uri}/api/data/v9.2/$metadata#accounts/$deletedEntity"
),
"id": "aaaa-bbbb-cccc-0001",
"reason": "deleted"
}],
"@odata.deltaLink":
format!("{uri}/api/data/v9.2/accounts?$deltatoken=after-delete")
})))
.mount(&server)
.await;
let after_delete = client
.follow_delta_link(&update_delta)
.await
.expect("follow_delta_link for delete should succeed");
let changes = parse_delta_changes(&after_delete, "account");
assert_eq!(changes.len(), 1, "exactly one DELETE change expected");
match &changes[0] {
DataverseChange::Deleted { id, entity_name } => {
assert_eq!(id, "aaaa-bbbb-cccc-0001");
assert_eq!(entity_name, "account");
}
other => panic!("expected Deleted, got {other:?}"),
}
let delete_delta = after_delete
.delta_link
.clone()
.expect("delta link after delete");
server.reset().await;
Mock::given(method("GET"))
.and(path("/api/data/v9.2/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@odata.context": format!("{uri}/api/data/v9.2/$metadata#accounts"),
"value": [],
"@odata.deltaLink":
format!("{uri}/api/data/v9.2/accounts?$deltatoken=after-delete")
})))
.mount(&server)
.await;
let stable = client
.follow_delta_link(&delete_delta)
.await
.expect("stabilizing poll should succeed");
let changes = parse_delta_changes(&stable, "account");
assert!(
changes.is_empty(),
"no changes expected on stabilization, got {} changes",
changes.len()
);
}
#[tokio::test]
async fn dataverse_client_paginates_initial_change_tracking() {
let server = MockServer::start().await;
let uri = server.uri();
Mock::given(method("GET"))
.and(path("/api/data/v9.2/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@odata.context": format!("{uri}/api/data/v9.2/$metadata#accounts"),
"value": [
{ "accountid": "id-1", "name": "One" },
{ "accountid": "id-2", "name": "Two" }
],
"@odata.nextLink": format!("{uri}/api/data/v9.2/accounts/page2")
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/api/data/v9.2/accounts/page2"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@odata.context": format!("{uri}/api/data/v9.2/$metadata#accounts"),
"value": [
{ "accountid": "id-3", "name": "Three" }
],
"@odata.deltaLink":
format!("{uri}/api/data/v9.2/accounts?$deltatoken=final")
})))
.mount(&server)
.await;
let client = DataverseClient::new(&uri, "v9.2", Arc::new(StaticToken));
let page1 = client
.initial_change_tracking("accounts", None)
.await
.expect("page 1 should succeed");
assert_eq!(page1.value.len(), 2);
assert!(page1.delta_link.is_none());
let next = page1.next_link.expect("page 1 must hand off via nextLink");
let page2 = client
.follow_next_link(&next)
.await
.expect("page 2 should succeed");
assert_eq!(page2.value.len(), 1);
assert!(page2.next_link.is_none());
let final_delta = page2.delta_link.expect("final delta link required");
assert!(final_delta.contains("$deltatoken=final"));
}