Skip to content

Commit 0af06f7

Browse files
authored
Merge pull request #457 from AmbireTech/issue-417-campaign-close-route
Issue 417 campaign close route
2 parents 0baae9e + caeb05a commit 0af06f7

File tree

3 files changed

+150
-16
lines changed

3 files changed

+150
-16
lines changed

sentry/src/db/campaign.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,19 @@ mod campaign_remaining {
309309
.query_async(&mut self.redis.clone())
310310
.await
311311
}
312+
313+
/// Atomic `getset` [`redis`] operation
314+
/// Used to close a [`Campaign`] `POST /campaign/close`
315+
pub async fn getset_remaining_to_zero(
316+
&self,
317+
campaign: CampaignId,
318+
) -> Result<u64, RedisError> {
319+
redis::cmd("GETSET")
320+
.arg(&Self::get_key(campaign))
321+
.arg(0)
322+
.query_async(&mut self.redis.clone())
323+
.await
324+
}
312325
}
313326

314327
#[cfg(test)]

sentry/src/lib.rs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -212,23 +212,21 @@ async fn campaigns_router<A: Adapter + 'static>(
212212
let req = CampaignLoad.call(req, app).await?;
213213

214214
campaign::insert_events::handle_route(req, app).await
215-
} else if let (Some(_caps), &Method::POST) =
215+
} else if let (Some(caps), &Method::POST) =
216216
(CLOSE_CAMPAIGN_BY_CAMPAIGN_ID.captures(path), method)
217217
{
218-
// TODO AIP#61: Close campaign:
219-
// - only by creator
220-
// - sets redis remaining = 0 (`newBudget = totalSpent`, i.e. `newBudget = oldBudget - remaining`)
221-
222-
// let (is_creator, auth_uid) = match auth {
223-
// Some(auth) => (auth.uid == channel.creator, auth.uid.to_string()),
224-
// None => (false, Default::default()),
225-
// };
226-
// Closing a campaign is allowed only by the creator
227-
// if has_close_event && is_creator {
228-
// return Ok(());
229-
// }
218+
let param = RouteParams(vec![caps
219+
.get(1)
220+
.map_or("".to_string(), |m| m.as_str().to_string())]);
221+
req.extensions_mut().insert(param);
230222

231-
Err(ResponseError::NotFound)
223+
req = Chain::new()
224+
.chain(AuthRequired)
225+
.chain(CampaignLoad)
226+
.apply(req, app)
227+
.await?;
228+
229+
campaign::close_campaign(req, app).await
232230
} else if method == Method::POST && path == "/v5/campaign/list" {
233231
req = AuthRequired.call(req, app).await?;
234232

sentry/src/routes/campaign.rs

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use primitives::{
2121
sentry::{
2222
campaign::CampaignListQuery,
2323
campaign_create::{CreateCampaign, ModifyCampaign},
24+
SuccessResponse,
2425
},
2526
spender::Spendable,
2627
Address, Campaign, CampaignId, Channel, ChannelId, Deposit, UnifiedNum,
@@ -292,6 +293,49 @@ pub async fn campaign_list<A: Adapter>(
292293
Ok(success_response(serde_json::to_string(&list_response)?))
293294
}
294295

296+
/// Can only be called by creator
297+
/// to close a campaign, just set it's budget to what it's spent so far (so that remaining == 0)
298+
/// newBudget = totalSpent, i.e. newBudget = oldBudget - remaining
299+
pub async fn close_campaign<A: Adapter>(
300+
req: Request<Body>,
301+
app: &Application<A>,
302+
) -> Result<Response<Body>, ResponseError> {
303+
let auth = req
304+
.extensions()
305+
.get::<Auth>()
306+
.expect("Auth should be present");
307+
308+
let mut campaign = req
309+
.extensions()
310+
.get::<Campaign>()
311+
.expect("We must have a campaign in extensions")
312+
.to_owned();
313+
314+
if auth.uid.to_address() != campaign.creator {
315+
Err(ResponseError::Forbidden(
316+
"Request not sent by campaign creator".to_string(),
317+
))
318+
} else {
319+
let old_remaining = app
320+
.campaign_remaining
321+
.getset_remaining_to_zero(campaign.id)
322+
.await
323+
.map_err(|e| ResponseError::BadRequest(e.to_string()))?;
324+
325+
campaign.budget = campaign
326+
.budget
327+
.checked_sub(&UnifiedNum::from(old_remaining))
328+
.ok_or_else(|| {
329+
ResponseError::BadRequest("Campaign budget overflows/underflows".to_string())
330+
})?;
331+
update_campaign(&app.pool, &campaign).await?;
332+
333+
Ok(success_response(serde_json::to_string(&SuccessResponse {
334+
success: true,
335+
})?))
336+
}
337+
}
338+
295339
pub mod update_campaign {
296340
use primitives::Config;
297341

@@ -903,13 +947,15 @@ mod test {
903947
update_campaign::{get_delta_budget, modify_campaign},
904948
*,
905949
};
906-
use crate::db::redis_pool::TESTS_POOL;
950+
use crate::db::{fetch_campaign, redis_pool::TESTS_POOL};
907951
use crate::test_util::setup_dummy_app;
908952
use crate::update_campaign::DeltaBudget;
909953
use adapter::DummyAdapter;
910954
use hyper::StatusCode;
911955
use primitives::{
912-
adapter::Deposit, util::tests::prep_db::DUMMY_CAMPAIGN, BigNum, ChannelId, ValidatorId,
956+
adapter::Deposit,
957+
util::tests::prep_db::{DUMMY_CAMPAIGN, IDS},
958+
BigNum, ChannelId, ValidatorId,
913959
};
914960

915961
#[tokio::test]
@@ -1249,4 +1295,81 @@ mod test {
12491295
assert_eq!(delta_budget, Some(DeltaBudget::Decrease(decrease_by)));
12501296
}
12511297
}
1298+
1299+
#[tokio::test]
1300+
async fn campaign_is_closed_properly() {
1301+
let campaign = DUMMY_CAMPAIGN.clone();
1302+
1303+
let app = setup_dummy_app().await;
1304+
1305+
insert_channel(&app.pool, campaign.channel)
1306+
.await
1307+
.expect("Should insert dummy channel");
1308+
insert_campaign(&app.pool, &campaign)
1309+
.await
1310+
.expect("Should insert dummy campaign");
1311+
1312+
// Test if remaining is set to 0
1313+
{
1314+
app.campaign_remaining
1315+
.set_initial(campaign.id, campaign.budget)
1316+
.await
1317+
.expect("should set");
1318+
1319+
let auth = Auth {
1320+
era: 0,
1321+
uid: ValidatorId::from(campaign.creator),
1322+
};
1323+
1324+
let req = Request::builder()
1325+
.extension(auth)
1326+
.extension(campaign.clone())
1327+
.body(Body::empty())
1328+
.expect("Should build Request");
1329+
1330+
close_campaign(req, &app)
1331+
.await
1332+
.expect("Should close campaign");
1333+
1334+
let closed_campaign = fetch_campaign(app.pool.clone(), &campaign.id)
1335+
.await
1336+
.expect("Should fetch campaign")
1337+
.expect("Campaign should exist");
1338+
1339+
// remaining == campaign_budget therefore old_budget - remaining = 0
1340+
assert_eq!(closed_campaign.budget, UnifiedNum::from_u64(0));
1341+
1342+
let remaining = app
1343+
.campaign_remaining
1344+
.get_remaining_opt(campaign.id)
1345+
.await
1346+
.expect("Should get remaining from redis")
1347+
.expect("There should be value for the Campaign");
1348+
1349+
assert_eq!(remaining, 0);
1350+
}
1351+
1352+
// Test if an error is returned when request is not sent by creator
1353+
{
1354+
let auth = Auth {
1355+
era: 0,
1356+
uid: IDS["leader"],
1357+
};
1358+
1359+
let req = Request::builder()
1360+
.extension(auth)
1361+
.extension(campaign.clone())
1362+
.body(Body::empty())
1363+
.expect("Should build Request");
1364+
1365+
let res = close_campaign(req, &app)
1366+
.await
1367+
.expect_err("Should return error for Bad Campaign");
1368+
1369+
assert_eq!(
1370+
ResponseError::Forbidden("Request not sent by campaign creator".to_string()),
1371+
res
1372+
);
1373+
}
1374+
}
12521375
}

0 commit comments

Comments
 (0)