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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 4.0.0

- Add SignatureValidationException

## 3.0.0

- Deploy to Google Cloud workflow
Expand Down
20 changes: 14 additions & 6 deletions lib/src/impl/blockfrost_webhook_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,21 @@ class BlockfrostWebhookHandler implements WebhookHandler {
}

// Validate signature
if (!validator.validate(
signatureHeader: signatureHeader,
requestPayload: requestPayload,
secretAuthToken: secretToken,
)) {
return Response(400, body: 'Signature validation failed!');
try {
validator.validate(
requestPayload: requestPayload,
signatureHeader: signatureHeader,
secretAuthToken: secretToken);
} on SignatureValidationException catch (e) {
return Response(
400,
// Or whatever single status code you prefer for all validation failures
body: 'Signature validation failed! ${e.toString()}',
);
} catch (e) {
return Response.internalServerError(body: 'Signature validation failed!');
}

// Process payload
try {
processor.process(requestPayload);
Expand Down
2 changes: 1 addition & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ packages:
description:
path: "."
ref: "feature/blockfrost_secure_webhooks"
resolved-ref: d0af10d45c0158656da5798ee4d7e5cc26254ada
resolved-ref: eb1f853fbd56ea4f67bff2e47f3f48d911c8f7a9
url: "https://github.com/andro-devs/blockfrost-dart.git"
source: git
version: "1.0.0"
Expand Down
78 changes: 67 additions & 11 deletions test/blockfrost_webhook_handler_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ void main() {
)).thenReturn(value);
}

// Define the common stubbing logic
void stubValidatorToThrowSignatureValidationException() {
when(() => mockValidator.validate(
signatureHeader: any(named: 'signatureHeader'),
requestPayload: any(named: 'requestPayload'),
secretAuthToken: testSecret,
))
.thenThrow(
SignatureValidationException('Invalid', header: "", payload: ""));
}

void stubValidatorToThrowException() {
when(() => mockValidator.validate(
signatureHeader: any(named: 'signatureHeader'),
requestPayload: any(named: 'requestPayload'),
secretAuthToken: testSecret,
)).thenThrow(Exception());
}

// Define the common stubbing logic
void stubProcessorToThrowException() {
when(() => mockProcessor.process(any())).thenThrow(Exception());
Expand Down Expand Up @@ -95,21 +114,58 @@ void main() {
// --- Test Case 2: Validation Failure ---
test('Should return 400 when validation fails (Rejection scenario)',
() async {
// STUB: Inject a validator that ALWAYS returns FALSE (simulates invalid signature)
stubValidatorToReturn(false);
// Use the revised stub that throws a concrete exception
stubValidatorToThrowSignatureValidationException();

final request = createMockRequest(
body: testBodyBlock,
headers: {'blockfrost-signature': validSignatureHeader},
);

// Call the handler, which will catch the thrown exception and return a Response
final response = await handler.handleWebhook(
request: request,
secretToken: testSecret,
validator: mockValidator,
processor: mockProcessor);
request: request,
secretToken: testSecret,
validator: mockValidator,
processor: mockProcessor,
);
expect(response.statusCode, 400);
expect(await response.readAsString(), 'Signature validation failed!');
final responseBody = await response.readAsString();
expect(responseBody, startsWith('Signature validation failed!'));
expect(responseBody, contains('SignatureValidationException: Invalid'));
expect(responseBody, contains('Header:'));
expect(responseBody, contains('Payload:'));

// Verify that the validator was called once
verify(() => mockValidator.validate(
signatureHeader: validSignatureHeader,
requestPayload: testBodyBlock,
secretAuthToken: testSecret,
)).called(1);

// Verify that the processor was never reached
verifyZeroInteractions(mockProcessor);
});

// --- Test Case 3: Validation Failure ---
test('Should return 500 when validation fails with internal error',
() async {
// Use the revised stub that throws a concrete exception
stubValidatorToThrowException();

final request = createMockRequest(
body: testBodyBlock,
headers: {'blockfrost-signature': validSignatureHeader},
);

// Call the handler, which will catch the thrown exception and return a Response
final response = await handler.handleWebhook(
request: request,
secretToken: testSecret,
validator: mockValidator,
processor: mockProcessor,
);
expect(response.statusCode, 500);

// Verify the mock was called to ensure we tested the correct path
verify(() => mockValidator.validate(
Expand All @@ -122,7 +178,7 @@ void main() {
verifyZeroInteractions(mockProcessor);
});

// --- Test Case 3: Empty payload ---
// --- Test Case 4: Empty payload ---
test('Should return 400 if the payload is empty', () async {
// STUB: Inject a validator that ALWAYS returns TRUE (simulates valid signature)
stubValidatorToReturn(true);
Expand All @@ -141,7 +197,7 @@ void main() {
verifyZeroInteractions(mockProcessor);
});

// --- Test Case 4: Successful Reception of an transaction webhook ---
// --- Test Case 5: Successful Reception of an transaction webhook ---
test('Should return 200 when validation succeeds and type is transaction',
() async {
stubValidatorToReturn(
Expand All @@ -164,7 +220,7 @@ void main() {
verify(() => mockProcessor.process(testBodyTx)).called(1);
});

// --- Test Case 5: Successful Reception of an transaction webhook ---
// --- Test Case 6: Successful Reception of an transaction webhook ---
test('Should return 200 when validation succeeds and type is transaction',
() async {
// STUB: Inject a validator that ALWAYS returns TRUE (simulates valid signature)
Expand All @@ -187,7 +243,7 @@ void main() {
verify(() => mockProcessor.process(testBodyBlock)).called(1);
});

// --- Test Case 6: Invalid JSON After Validation ---
// --- Test Case 7: Invalid JSON After Validation ---
test('Should return 500 if the validated payload is not valid JSON',
() async {
stubProcessorToThrowException();
Expand Down