Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ User-visible changes to `voltage`, newest first. Each pull request that changes

## Unreleased

- `webhooks test WEBHOOK_ID` without `--data` sends a test event, with a new `delivery_id` and a payload of type `test` and event `created`. Before, it failed with "This operation requires --data". `--data` still sends your own payload.
- A list that returns every item in one response, such as `wallets list`, accepts `--all` and ignores it, because there are no more pages. Before, it rejected `--all`. `--all` still pages through lists that take `--limit`.
- A command that acts on one wallet, such as `wallets get`, also takes the wallet as `--wallet ID`. If the argument and `--wallet` name different wallets, the command stops. Without either, the error names both ways to pass it. A profile's or `VOLTAGE_WALLET_ID`'s wallet is never used, so a delete always names its wallet.
- A request that gets no response says why: a timeout (for the auth service, its fixed 30-second limit, which `--timeout` does not change), a connection not made within the 10-second connect limit (which `--timeout` does not change), a failed TLS connection, a connection that could not be made, or one that closed early. A hint names the next step. A write that timed out or closed early may have been applied, so its hint says to look up the resource ID before running the command again, never to retry. This covers API, auth, price, and checkout stream requests, which before said only that the request failed.
Expand Down
8 changes: 7 additions & 1 deletion docs/guides/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ voltage webhooks update WEBHOOK_ID --profile prod \

## Test, pause, and resume

A test delivery takes a body with a new `delivery_id` and a sample event `payload`. The smallest is a `test.created` event, for example in `test-delivery.json`:
Without `--data`, `webhooks test` sends a `test.created` event with a new `delivery_id`:

```sh
voltage webhooks test WEBHOOK_ID --profile prod --execute
```

To send your own delivery, pass a body with a new `delivery_id` and a sample event `payload` with `--data`, for example in `test-delivery.json`:

```json
{
Expand Down
13 changes: 11 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@ pub enum FriendlyFlags {
Quote(QuoteFlags),
CreateWebhook(WebhookFlags),
UpdateWebhook(UpdateWebhookFlags),
/// `webhooks test` without `--data` sends the contract's test event.
TestWebhook,
}

impl FriendlyFlags {
Expand All @@ -556,7 +558,7 @@ impl FriendlyFlags {
Self::Payment(flags) => flags.id.is_none(),
Self::Quote(flags) => flags.id.is_none(),
Self::CreateWebhook(flags) => flags.id.is_none(),
Self::UpdateWallet(_) | Self::UpdateWebhook(_) => false,
Self::UpdateWallet(_) | Self::UpdateWebhook(_) | Self::TestWebhook => false,
}
}
}
Expand Down Expand Up @@ -832,6 +834,7 @@ fn friendly_flags(id: OperationId, m: &ArgMatches) -> Result<Option<FriendlyFlag
OperationId::RequestAQuote => FriendlyFlags::Quote(parse(m)?),
OperationId::CreateWebhook => FriendlyFlags::CreateWebhook(parse(m)?),
OperationId::UpdateWebhook => FriendlyFlags::UpdateWebhook(parse(m)?),
OperationId::TestWebhook => FriendlyFlags::TestWebhook,
_ => return Ok(None),
}))
}
Expand Down Expand Up @@ -1079,6 +1082,9 @@ fn friendly_requirements(id: OperationId, command_name: &str) -> Option<&'static
(OperationId::CreateWallet, "create") => Some(
"Select one environment with --env or --profile, then pass --name, --credit-line, --network, and --limit.",
),
(OperationId::TestWebhook, _) => Some(
"Without --data, sends a test event: a new delivery_id and a payload of type test with event created.",
),
_ => None,
}
}
Expand Down Expand Up @@ -1155,7 +1161,10 @@ fn endpoint_examples(id: OperationId, command_name: &str) -> Option<&'static str
voltage webhooks keys rotate WEBHOOK_ID --profile prod --output-file webhook-key.json --execute"
}
(OperationId::TestWebhook, _) => {
" # Send a test delivery with a delivery_id and a sample payload
" # Send the test event
voltage webhooks test WEBHOOK_ID --profile prod --execute

# Send your own delivery_id and payload
voltage webhooks test WEBHOOK_ID --profile prod --data @test-delivery.json --execute"
}
(OperationId::StartWebhookAsUser, _) => {
Expand Down
52 changes: 51 additions & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,41 @@ struct UpdateWebhookBody {
events: Vec<Map<String, Value>>,
}

/// The contract's test delivery: `payload` of type `test`, whose only event is `created`.
#[derive(Serialize)]
struct TestWebhookBody {
delivery_id: Uuid,
payload: TestPayload,
}

#[derive(Serialize)]
struct TestPayload {
#[serde(rename = "type")]
kind: &'static str,
detail: TestDetail,
}

#[derive(Serialize)]
struct TestDetail {
event: &'static str,
data: &'static str,
}

impl TestWebhookBody {
fn new() -> Self {
Self {
delivery_id: Uuid::new_v4(),
payload: TestPayload {
kind: "test",
detail: TestDetail {
event: "created",
data: "Test delivery from voltage webhooks test",
},
},
}
}
}

fn create_wallet(flags: &WalletFlags, scope: &Scope) -> Result<CreateWalletBody> {
Ok(CreateWalletBody {
id: new_id(flags.id),
Expand Down Expand Up @@ -592,6 +627,7 @@ fn friendly_body(
events: events(event)?,
})?
}
FriendlyFlags::TestWebhook => serde_json::to_value(TestWebhookBody::new())?,
})
}

Expand Down Expand Up @@ -1263,11 +1299,25 @@ mod tests {
assert!(hint.contains("--unit usd --to btc"));
}
assert_eq!(
request_error(&["webhooks", "test", WALLET]),
request_error(&["payments", "check"]),
"This operation requires --data @file or --data -"
);
}

#[test]
fn webhook_test_without_data_sends_the_contract_test_event() {
let first = build(&["webhooks", "test", WALLET]).unwrap();
let body = first.body.unwrap();
assert_eq!(
body["payload"],
json!({"type": "test", "detail": {"event": "created", "data": "Test delivery from voltage webhooks test"}})
);
let delivery: Uuid = serde_json::from_value(body["delivery_id"].clone()).unwrap();
// Each test gets its own delivery ID.
let second = build(&["webhooks", "test", WALLET]).unwrap().body.unwrap();
assert_ne!(second["delivery_id"], json!(delivery));
}

#[test]
fn raw_payloads_must_agree_with_scope_and_carry_payment_identity() {
let check = |id: OperationId, alias: Option<PaymentDirection>, body: Value| {
Expand Down
32 changes: 32 additions & 0 deletions tests/cli_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2557,6 +2557,38 @@ async fn a_described_change_sends_nothing_records_nothing_and_omits_private_valu
assert!(server.received_requests().await.unwrap().is_empty());
}

#[tokio::test]
async fn webhook_test_without_data_sends_the_test_event() {
let (server, dir) = fixture().await;
let test_path = format!("/organizations/{ORG}/environments/{ENV}/webhooks/{RESOURCE}/test");
respond_once(&server, route("POST", test_path), accepted()).await;
let sent = cli(dir.path(), &server)
.args([
"webhooks",
"test",
RESOURCE,
"--org",
ORG,
"--env",
ENV,
"--execute",
])
.assert()
.success();
assert_eq!(json_stdout(&sent)["http_status"], 202);
let requests = server.received_requests().await.unwrap();
let body: Value = serde_json::from_slice(&requests[0].body).unwrap();
assert_eq!(body["payload"]["type"], "test");
assert_eq!(body["payload"]["detail"]["event"], "created");
assert!(
body["delivery_id"]
.as_str()
.unwrap()
.parse::<uuid::Uuid>()
.is_ok()
);
}

#[test]
fn requests_without_a_response_say_why_and_name_the_next_step() {
let dir = private_tempdir();
Expand Down
Loading