|
1 | 1 | use std::{collections::HashMap, time::Duration};
|
2 | 2 |
|
3 | 3 | use chrono::{DateTime, Utc};
|
4 |
| -use futures::future::{join_all, TryFutureExt}; |
| 4 | +use futures::future::{join_all, try_join_all, TryFutureExt}; |
5 | 5 | use reqwest::{Client, Method};
|
6 | 6 | use slog::Logger;
|
7 | 7 |
|
8 | 8 | use primitives::{
|
9 | 9 | adapter::Adapter,
|
10 | 10 | balances::{CheckedState, UncheckedState},
|
11 | 11 | sentry::{
|
12 |
| - AccountingResponse, EventAggregateResponse, LastApprovedResponse, SuccessResponse, |
13 |
| - ValidatorMessageResponse, |
| 12 | + AccountingResponse, AllSpendersResponse, EventAggregateResponse, LastApprovedResponse, |
| 13 | + SuccessResponse, ValidatorMessageResponse, |
14 | 14 | },
|
15 | 15 | spender::Spender,
|
16 | 16 | util::ApiUrl,
|
@@ -161,28 +161,51 @@ impl<A: Adapter + 'static> SentryApi<A> {
|
161 | 161 | .map_err(Error::Request)
|
162 | 162 | }
|
163 | 163 |
|
164 |
| - // TODO: Pagination & use of `AllSpendersResponse` |
165 |
| - pub async fn get_all_spenders( |
| 164 | + /// page always starts from 0 |
| 165 | + pub async fn get_spenders_page( |
166 | 166 | &self,
|
167 |
| - channel: ChannelId, |
168 |
| - ) -> Result<HashMap<Address, Spender>, Error> { |
| 167 | + channel: &ChannelId, |
| 168 | + page: u64, |
| 169 | + ) -> Result<AllSpendersResponse, Error> { |
169 | 170 | let url = self
|
170 | 171 | .whoami
|
171 | 172 | .url
|
172 |
| - .join(&format!("v5/channel/{}/spender/all", channel)) |
| 173 | + .join(&format!("v5/channel/{}/spender/all?page={}", channel, page)) |
173 | 174 | .expect("Should not error when creating endpoint");
|
174 | 175 |
|
175 | 176 | self.client
|
176 | 177 | .get(url)
|
177 | 178 | .bearer_auth(&self.whoami.token)
|
178 | 179 | .send()
|
179 | 180 | .await?
|
180 |
| - // TODO: Should be `AllSpendersResponse` and should have pagination! |
181 | 181 | .json()
|
182 | 182 | .map_err(Error::Request)
|
183 | 183 | .await
|
184 | 184 | }
|
185 | 185 |
|
| 186 | + pub async fn get_all_spenders( |
| 187 | + &self, |
| 188 | + channel: ChannelId, |
| 189 | + ) -> Result<HashMap<Address, Spender>, Error> { |
| 190 | + let first_page = self.get_spenders_page(&channel, 0).await?; |
| 191 | + |
| 192 | + if first_page.pagination.total_pages < 2 { |
| 193 | + Ok(first_page.spenders) |
| 194 | + } else { |
| 195 | + let all: Vec<AllSpendersResponse> = try_join_all( |
| 196 | + (1..first_page.pagination.total_pages).map(|i| self.get_spenders_page(&channel, i)), |
| 197 | + ) |
| 198 | + .await?; |
| 199 | + |
| 200 | + let result_all: HashMap<Address, Spender> = std::iter::once(first_page) |
| 201 | + .chain(all.into_iter()) |
| 202 | + .flat_map(|p| p.spenders) |
| 203 | + .collect(); |
| 204 | + |
| 205 | + Ok(result_all) |
| 206 | + } |
| 207 | + } |
| 208 | + |
186 | 209 | /// Get the accounting from Sentry
|
187 | 210 | /// `Balances` should always be in `CheckedState`
|
188 | 211 | pub async fn get_accounting(
|
@@ -378,3 +401,179 @@ pub mod campaigns {
|
378 | 401 | client.get(endpoint).send().await?.json().await
|
379 | 402 | }
|
380 | 403 | }
|
| 404 | + |
| 405 | +#[cfg(test)] |
| 406 | +mod test { |
| 407 | + use super::*; |
| 408 | + use adapter::DummyAdapter; |
| 409 | + use primitives::{ |
| 410 | + adapter::DummyAdapterOptions, |
| 411 | + config::{configuration, Environment}, |
| 412 | + sentry::Pagination, |
| 413 | + util::tests::{ |
| 414 | + discard_logger, |
| 415 | + prep_db::{ |
| 416 | + ADDRESSES, DUMMY_CAMPAIGN, DUMMY_VALIDATOR_LEADER, IDS, |
| 417 | + }, |
| 418 | + }, |
| 419 | + UnifiedNum, |
| 420 | + }; |
| 421 | + use std::str::FromStr; |
| 422 | + use wiremock::{ |
| 423 | + matchers::{method, path, query_param}, |
| 424 | + Mock, MockServer, ResponseTemplate, |
| 425 | + }; |
| 426 | + |
| 427 | + #[tokio::test] |
| 428 | + async fn test_get_all_spenders() { |
| 429 | + let server = MockServer::start().await; |
| 430 | + let test_spender = Spender { |
| 431 | + total_deposited: UnifiedNum::from(100_000_000), |
| 432 | + spender_leaf: None, |
| 433 | + }; |
| 434 | + let mut all_spenders = HashMap::new(); |
| 435 | + all_spenders.insert(ADDRESSES["user"], test_spender.clone()); |
| 436 | + all_spenders.insert(ADDRESSES["publisher"], test_spender.clone()); |
| 437 | + all_spenders.insert(ADDRESSES["publisher2"], test_spender.clone()); |
| 438 | + all_spenders.insert(ADDRESSES["creator"], test_spender.clone()); |
| 439 | + all_spenders.insert(ADDRESSES["tester"], test_spender.clone()); |
| 440 | + |
| 441 | + let first_page_response = AllSpendersResponse { |
| 442 | + spenders: vec![ |
| 443 | + ( |
| 444 | + ADDRESSES["user"], |
| 445 | + all_spenders.get(&ADDRESSES["user"]).unwrap().to_owned(), |
| 446 | + ), |
| 447 | + ( |
| 448 | + ADDRESSES["publisher"], |
| 449 | + all_spenders |
| 450 | + .get(&ADDRESSES["publisher"]) |
| 451 | + .unwrap() |
| 452 | + .to_owned(), |
| 453 | + ), |
| 454 | + ] |
| 455 | + .into_iter() |
| 456 | + .collect(), |
| 457 | + pagination: Pagination { |
| 458 | + page: 0, |
| 459 | + total_pages: 3, |
| 460 | + }, |
| 461 | + }; |
| 462 | + |
| 463 | + let second_page_response = AllSpendersResponse { |
| 464 | + spenders: vec![ |
| 465 | + ( |
| 466 | + ADDRESSES["publisher2"], |
| 467 | + all_spenders |
| 468 | + .get(&ADDRESSES["publisher2"]) |
| 469 | + .unwrap() |
| 470 | + .to_owned(), |
| 471 | + ), |
| 472 | + ( |
| 473 | + ADDRESSES["creator"], |
| 474 | + all_spenders.get(&ADDRESSES["creator"]).unwrap().to_owned(), |
| 475 | + ), |
| 476 | + ] |
| 477 | + .into_iter() |
| 478 | + .collect(), |
| 479 | + pagination: Pagination { |
| 480 | + page: 1, |
| 481 | + total_pages: 3, |
| 482 | + }, |
| 483 | + }; |
| 484 | + |
| 485 | + let third_page_response = AllSpendersResponse { |
| 486 | + spenders: vec![( |
| 487 | + ADDRESSES["tester"], |
| 488 | + all_spenders.get(&ADDRESSES["tester"]).unwrap().to_owned(), |
| 489 | + )] |
| 490 | + .into_iter() |
| 491 | + .collect(), |
| 492 | + pagination: Pagination { |
| 493 | + page: 2, |
| 494 | + total_pages: 3, |
| 495 | + }, |
| 496 | + }; |
| 497 | + |
| 498 | + Mock::given(method("GET")) |
| 499 | + .and(path(format!( |
| 500 | + "/v5/channel/{}/spender/all", |
| 501 | + DUMMY_CAMPAIGN.channel.id() |
| 502 | + ))) |
| 503 | + .and(query_param("page", "0")) |
| 504 | + .respond_with(ResponseTemplate::new(200).set_body_json(&first_page_response)) |
| 505 | + .mount(&server) |
| 506 | + .await; |
| 507 | + |
| 508 | + Mock::given(method("GET")) |
| 509 | + .and(path(format!( |
| 510 | + "/v5/channel/{}/spender/all", |
| 511 | + DUMMY_CAMPAIGN.channel.id() |
| 512 | + ))) |
| 513 | + .and(query_param("page", "1")) |
| 514 | + .respond_with(ResponseTemplate::new(200).set_body_json(&second_page_response)) |
| 515 | + .mount(&server) |
| 516 | + .await; |
| 517 | + |
| 518 | + Mock::given(method("GET")) |
| 519 | + .and(path(format!( |
| 520 | + "/v5/channel/{}/spender/all", |
| 521 | + DUMMY_CAMPAIGN.channel.id() |
| 522 | + ))) |
| 523 | + .and(query_param("page", "2")) |
| 524 | + .respond_with(ResponseTemplate::new(200).set_body_json(&third_page_response)) |
| 525 | + .mount(&server) |
| 526 | + .await; |
| 527 | + |
| 528 | + let mut validators = Validators::new(); |
| 529 | + validators.insert( |
| 530 | + DUMMY_VALIDATOR_LEADER.id, |
| 531 | + Validator { |
| 532 | + url: ApiUrl::from_str(&server.uri()).expect("Should parse"), |
| 533 | + token: AuthToken::default(), |
| 534 | + }, |
| 535 | + ); |
| 536 | + let mut config = configuration(Environment::Development, None).expect("Should get Config"); |
| 537 | + config.spendable_find_limit = 2; |
| 538 | + |
| 539 | + let adapter = DummyAdapter::init( |
| 540 | + DummyAdapterOptions { |
| 541 | + dummy_identity: IDS["leader"], |
| 542 | + dummy_auth: Default::default(), |
| 543 | + dummy_auth_tokens: Default::default(), |
| 544 | + }, |
| 545 | + &config, |
| 546 | + ); |
| 547 | + let logger = discard_logger(); |
| 548 | + |
| 549 | + let sentry = |
| 550 | + SentryApi::init(adapter, logger, config, validators).expect("Should build sentry"); |
| 551 | + |
| 552 | + let mut res = sentry |
| 553 | + .get_all_spenders(DUMMY_CAMPAIGN.channel.id()) |
| 554 | + .await |
| 555 | + .expect("should get response"); |
| 556 | + |
| 557 | + // Checks for page 1 |
| 558 | + let res_user = res.remove(&ADDRESSES["user"]); |
| 559 | + let res_publisher = res.remove(&ADDRESSES["publisher"]); |
| 560 | + assert!(res_user.is_some() && res_publisher.is_some()); |
| 561 | + assert_eq!(res_user.unwrap(), test_spender); |
| 562 | + assert_eq!(res_publisher.unwrap(), test_spender); |
| 563 | + |
| 564 | + // Checks for page 2 |
| 565 | + let res_publisher2 = res.remove(&ADDRESSES["publisher2"]); |
| 566 | + let res_creator = res.remove(&ADDRESSES["creator"]); |
| 567 | + assert!(res_publisher2.is_some() && res_creator.is_some()); |
| 568 | + assert_eq!(res_publisher2.unwrap(), test_spender); |
| 569 | + assert_eq!(res_creator.unwrap(), test_spender); |
| 570 | + |
| 571 | + // Checks for page 3 |
| 572 | + let res_tester = res.remove(&ADDRESSES["tester"]); |
| 573 | + assert!(res_tester.is_some()); |
| 574 | + assert_eq!(res_tester.unwrap(), test_spender); |
| 575 | + |
| 576 | + // There should be no remaining elements |
| 577 | + assert_eq!(res.len(), 0) |
| 578 | + } |
| 579 | +} |
0 commit comments