rfountain's avatar

Looking for Help Testing Amazon SNS Incoming Webhooks

I wrote a simple package to validate and handle incoming Amazon SNS Webhooks. The package utilizes the AWS PHP SDK, specifically the SNS Classes. First I grab the incoming HTTP POST Payload and then validate it.

 $message = Message::fromRawPostData();

        $validator = new MessageValidator();

        try {
            $validator->validate($message);
        } catch (InvalidSnsMessageException $e) {
            return response('SNS Message Validation Error: '.$e->getMessage(), 404);
        }

After validation, the package will fire an event which I can listen for an perform actions.

I can't seem to figure out a good way to Mock the Message::fromRawPostData() and MessageValidator so I can assert that my events were dispatched.

Looking for advice on a best path forward

0 likes
2 replies
rfountain's avatar
rfountain
OP
Best Answer
Level 16

Ok I actually managed to figure this out myself and I figure I would reply here in the event that someone else is trying to figure the same thing out.

The drawback to my approach is that I did have to add a bunch of classes/interfaces etc in order to successfully mock the AWS SNS calls to test my own code.

First I created 2 interfaces, and 2 classes for the Message and Validator

<?php

namespace OneThirtyOne\Sns\Concerns;

interface SnsMessageInterface
{
    /**
     * Get an instance of an SNS Message.
     *
     * @return \Aws\Sns\Message
     */
    public function get();

    /**
     * Get a message Property.
     *
     * @param $property
     *
     * @return mixed
     */
    public function __get($property);
}

<?php

namespace OneThirtyOne\Sns;

use OneThirtyOne\Sns\Concerns\SnsMessageInterface;

/**
 * Class Message.
 */
class Message implements SnsMessageInterface
{
    /**
     * @var \Aws\Sns\Message
     */
    protected $message;

    /**
     * Message constructor.
     */
    public function __construct()
    {
        $this->message = \Aws\Sns\Message::fromRawPostData();
    }

    /**
     * {@inheritdoc}
     */
    public function get()
    {
        return $this->message;
    }

    /**
     * {@inheritdoc}
     */
    public function __get($property)
    {
        return $this->message[$property];
    }
}
<?php

namespace OneThirtyOne\Sns\Concerns;

interface SnsValidatorInterface
{
    /**
     * Validate the incoming SNS Message.
     *
     * @param \Aws\Sns\Message $message
     *
     * @return mixed
     */
    public function validate($message);
}
<?php

namespace OneThirtyOne\Sns;

use Aws\Sns\MessageValidator;
use OneThirtyOne\Sns\Concerns\SnsValidatorInterface;

/**
 * Class Validator.
 */
class Validator implements SnsValidatorInterface
{
    /**
     * @var \Aws\Sns\MessageValidator
     */
    protected $validator;

    /**
     * Validator constructor.
     */
    public function __construct()
    {
        $this->validator = new MessageValidator();
    }

    /**
     * {@inheritdoc}
     */
    public function validate($message)
    {
        $this->validator->validate($message);
    }
}

Then in my SnsServiceProvider I bound those interfaces to their classes

 /**
     * Register the Service Provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(SnsMessageInterface::class, Message::class);
        $this->app->bind(SnsValidatorInterface::class, Validator::class);
    }

Now in my SnsController I type hinted to those interfaces

<?php

namespace OneThirtyOne\Sns\Controllers;

use Aws\Sns\Exception\InvalidSnsMessageException;
use Illuminate\Support\Facades\Http;
use OneThirtyOne\Sns\Concerns\SnsMessageInterface;
use OneThirtyOne\Sns\Concerns\SnsValidatorInterface;
use OneThirtyOne\Sns\Events\SnsEvent;
use OneThirtyOne\Sns\Events\SnsSubscriptionConfirmation;

/**
 * Class SnsController.
 */
class SnsController
{
    /**
     * @var \OneThirtyOne\Sns\Concerns\SnsMessageInterface
     */
    protected $message;

    /**
     * @var \OneThirtyOne\Sns\Concerns\SnsValidatorInterface
     */
    protected $validator;

    /**
     * @var array
     */
    protected $confirmation = [
        'SubscriptionConfirmation',
    ];

    /**
     * @var array
     */
    protected $notification = [
        'Notification',
    ];

    /**
     * SnsController constructor.
     *
     * @param \OneThirtyOne\Sns\Concerns\SnsMessageInterface   $message
     * @param \OneThirtyOne\Sns\Concerns\SnsValidatorInterface $validator
     */
    public function __construct(SnsMessageInterface $message, SnsValidatorInterface $validator)
    {
        $this->message = $message;
        $this->validator = $validator;
    }

    /**
     * Handle the HTTP Request from SNS Service.
     *
     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
     */
    public function handle()
    {
        try {
            $this->validator->validate($this->message->get());
        } catch (InvalidSnsMessageException $e) {
            return response('SNS Message Validation Error: '.$e->getMessage(), 404);
        }

        if (in_array($this->message->Type, $this->confirmation)) {
            $response = Http::get($this->message->SubscribeURL);

            if ($response->ok()) {
                SnsSubscriptionConfirmation::dispatch($this->message);
            }

            return response('OK', 200);
        }

        if (in_array($this->message->Type, $this->notification)) {
            SnsEvent::dispatch($this->message);
        }

        return response('OK', 200);
    }
}

From this point I seem to have successfully decoupled my app from the aws-sdk and now I can mock my interfaces in my tets

Hope this helps someone down the road.

1 like
enderandpeter@yahoo.com's avatar

Yes, I'm in a similar situation and you are right that there's no way out of this other than the Laravel Way ;-) I think I just need to swap out the MessageValidator class using the service container and use a mock for another class that has an SNSClient property.

Please or to participate in this conversation.