├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Criteria.php ├── Criteria │ ├── Equals.php │ ├── GreaterThan.php │ ├── LessThan.php │ └── NotEquals.php ├── CriteriaSet.php ├── Data.php ├── Instance.php ├── Notify.php └── Notify │ ├── Callback.php │ ├── ErrorLog.php │ ├── Monolog.php │ ├── Pagerduty.php │ └── Slack.php └── tests ├── Criteria ├── EqualsTest.php ├── GreaterThanTest.php ├── LessThanTest.php └── NotEqualsTest.php ├── CriteriaSetTest.php ├── DataTest.php ├── InstanceTest.php └── Notify └── CallbackTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | before_script: 3 | - wget http://getcomposer.org/composer.phar 4 | - php composer.phar install --dev 5 | php: 6 | - 7.1 7 | sudo: false 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Canary: Input Detection and Response 2 | ================== 3 | 4 | [![Build Status](https://travis-ci.org/psecio/canary.svg?branch=master)](https://travis-ci.org/psecio/canary) 5 | 6 | The origin of the term "canary" (as a method of detection) was originally used by those that worked deep in mines and would take a canary 7 | (the bird) with them to detect gas or other reasons they needed to leave. If the bird started behaving oddly they knew something was amiss. 8 | This same concept is applied in the security world and is similarly called a "canary". 9 | 10 | Similarly, the `Canary` library allows you to define key/value combinations that can be used to detect when certain data is used and notify 11 | you using a variety of methods including the default PHP error log, log handling via `Monolog` and messages to `Slack` channels. 12 | 13 | For example, you may generate a special username that you want to use as a trigger. This username isn't actually a user in your system 14 | but you do want to be notified if a login attempt is made using it. `Canary` makes this simple by defining checks with an `if` method and, 15 | optionally, a handler using a `then` method. For example, say we generated the username of `canary1234@foo.com` and we want to detect when 16 | it's used. You can define this in a `Canary` expression like so: 17 | 18 | ```php 19 | 'canary1234@foo.com', 22 | 'password' => 'sup3rs3cr3t' 23 | ]; 24 | 25 | \Psecio\Canary\Instance::build()->if('username', 'canary1234@foo.com')->execute(); 26 | 27 | // Or you can set multiple match values to look for with an array 28 | $matches = [ 29 | 'username' => 'canary1234@foo.com', 30 | 'password' => 'sup3rs3cr3t' 31 | ]; 32 | \Psecio\Canary\Instance::build()->if($matches)->execute(); 33 | ?> 34 | ``` 35 | 36 | In this example we're looking at the current input and checking to see if there's a `username` value of `canary1234@foo.com`. In the case 37 | of our current `$_POST` values, there's a match. By default (if no `then` handler is defined) the information about the match is output to 38 | the error like (via the `Psecio\Canary\Notify\ErrorLog` handler). The JSON encoded result looks like this: 39 | 40 | ``` 41 | {"type":"equals","key":"username","value":"canary1234@foo.com"} 42 | ``` 43 | 44 | > **NOTE:** Canary automatically pulls in the `$_GET` and `$_POST` superglobal values for evaluation so you don't need to manually pass 45 | then in. 46 | 47 | ### Using an external data source 48 | 49 | `Canary` also allows you to use a (static) class method to provide the `if` portion of the evaluation with data. To use it, just pass in the class 50 | and static method name as a string: 51 | 52 | ``` 53 | if($classMethod)->execute(); 57 | ?> 58 | ``` 59 | 60 | The return from this method must be an array otherwise an exception will be thrown. 61 | 62 | ### Supported Notifier Methods 63 | 64 | Currently `Canary` supports the following notification methods: 65 | 66 | | Type | Class | Expected Input | 67 | | --------- | -------------------------------- | --------------------------- | 68 | | Error log | `\Psecio\Canary\Notify\ErrorLog` | None, uses default location | 69 | | Monolog | `\Psecio\Canary\Notify\Monolog` | `\Monolog\Logger` | 70 | | Callback | `\Psecio\Canary\Notify\Callback` | Callable function | 71 | | Slack | `\Psecio\Canary\Notify\Slack` | `\Maknz\Slack\Client` | 72 | | PagerDuty | `\Psecio\Canary\Notify\PagerDuty`| `\PagerDuty\Event` | 73 | 74 | ### Creating a Custom Handler (Callback) 75 | 76 | If you don't want your results to go to the error log, you can create your own handler via the `then` method. Currently the only custom 77 | handler supported is a callable method. So, say we wanted to output a message to the user of our special username and kill the script. We 78 | might use something like this: 79 | 80 | ```php 81 | 'canary1234@foo.com']; 83 | 84 | \Psecio\Canary\Instance::build()->if('username', 'canary1234@foo.com') 85 | ->then(function($criteria) { 86 | die("You shouldn't have done that!"); 87 | }) 88 | ->execute(); 89 | ?> 90 | ``` 91 | 92 | In this handler, when it detects that the username value matches our criteria, the callback is executed and the `die` call kills the script. 93 | 94 | ### Passing in custom data 95 | 96 | You can also provide your own data set if you don't want to auto-load the current `$_GET` and `$_POST` values. To pass the data in you can use the 97 | `data` value in the configuration and passing it in: 98 | 99 | ```php 100 | [ 102 | 'username' => 'foobar@baz.com' 103 | ]]; 104 | \Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->execute(); 105 | ?> 106 | ``` 107 | 108 | ### Using a default logger 109 | 110 | You can set it as the default logger for **all** `if` checks via the `notify` key in the `build()` configuration options: 111 | 112 | ```php 113 | pushHandler(new StreamHandler('/tmp/mylog.log', Logger::WARNING)); 118 | 119 | $config = [ 120 | 'notify' => $log 121 | ]; 122 | \Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->execute(); 123 | 124 | ?> 125 | ``` 126 | 127 | > **NOTE:** If you provide a default handler via the `notify` configuration it will override all other custom notification methods. 128 | 129 | 130 | ### Using Monolog 131 | 132 | The `Canary` tool also allows you to use the [Monolog](https://github.com/Seldaek/monolog) logging library to define a bit more customization to the structure of the data and how it's output. Like before, we create the `Canary` instance but for the input of the `then` method we provide a `Monolog\Logger` instance: 133 | 134 | ```php 135 | 'test']; 142 | 143 | // create a log channel 144 | $log = new Logger('name'); 145 | $log->pushHandler(new StreamHandler('/tmp/mylog.log', Logger::WARNING)); 146 | 147 | \Psecio\Canary\Instance::build() 148 | ->if('username', 'canary1234@foo.com') 149 | ->then($log) 150 | ->execute(); 151 | ?> 152 | ``` 153 | 154 | ### Using Slack 155 | 156 | You can also make use of the [Maknz\Slack](https://github.com/maknz/slack) library to send messages to Slack when a canary is triggered: 157 | 158 | ```php 159 | '#my-channel-name', 162 | 'link_names' => true 163 | ]; 164 | $slack = new Maknz\Slack\Client('https://hooks.slack.com/services/.....', $settings); 165 | 166 | \Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->then($slack); 167 | ?> 168 | ``` 169 | 170 | You'll need to [set up an incoming webhook](https://my.slack.com/services/new/incoming-webhook) and replace the URL value in the `Client` 171 | create with the custom URL you're given. The default name for the notifications is `Canary Agent` and the output includes the same JSON 172 | information as the other notification methods. 173 | 174 | ### Using PagerDuty 175 | 176 | `Canary` also allows you to send notifications to your account on the PagerDuty service using the [nmcquay/pagerduty](https://github.com/nmcquay/pagerduty) library: 177 | 178 | ```php 179 | setServiceKey('[.... your service ID ....]'); 182 | 183 | \Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->then($pager); 184 | ?> 185 | ``` 186 | 187 | You can find the service ID by going to your services page (`https://[your domain].pagerduty.com/services`) and clicking on the service you want to use. The ID is under the "Integrations" tab as the "Integration Key". 188 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"psecio/canary", 3 | "type":"library", 4 | "description":"An input detection and response library", 5 | "keywords":["input", "detect", "response"], 6 | "homepage":"https://github.com/psecio/canary.git", 7 | "license":"MIT", 8 | "authors":[ 9 | { 10 | "name":"Chris Cornutt", 11 | "email":"ccornutt@phpdeveloper.org" 12 | } 13 | ], 14 | "require-dev": { 15 | "phpunit/phpunit": "^7.0" 16 | }, 17 | "suggest": { 18 | "monolog/monolog": "Allows for the creation of Monolog-based notifiers", 19 | "maknz/slack": "Allows for the creation of a Slack notifier (sends to a channel)", 20 | "nmcquay/pagerduty": "Used to create PagerDuty notifications" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Psecio\\Canary\\": "src/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Criteria.php: -------------------------------------------------------------------------------- 1 | setNotify(new ErrorLog()); 19 | } 20 | 21 | public function setNotify($notify) 22 | { 23 | $this->notify = $notify; 24 | } 25 | 26 | public function getNotify() 27 | { 28 | return $this->notify; 29 | } 30 | 31 | public abstract function evaluate(Data $input); 32 | public abstract function jsonSerialize() : array; 33 | public abstract function toArray() : array; 34 | } 35 | -------------------------------------------------------------------------------- /src/Criteria/Equals.php: -------------------------------------------------------------------------------- 1 | key = $params[0]; 18 | $this->value = $params[1]; 19 | 20 | parent::__construct($params); 21 | } 22 | 23 | public function evaluate(Data $input) 24 | { 25 | $val = $input->resolve($this->key); 26 | 27 | if (is_array($this->value) && in_array($val, $this->value, false)) { 28 | return true; 29 | } 30 | 31 | return ($val == $this->value); 32 | } 33 | 34 | public function jsonSerialize() : array 35 | { 36 | return $this->toArray(); 37 | } 38 | 39 | public function toArray() : array 40 | { 41 | return [ 42 | 'type' => 'equals', 43 | 'key' => $this->key, 44 | 'value' => $this->value 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Criteria/GreaterThan.php: -------------------------------------------------------------------------------- 1 | resolve($this->key); 12 | 13 | if (is_array($this->value) || is_array($val)) { 14 | throw new \InvalidArgumentException('Greater Than cannot evaluate array'); 15 | } 16 | 17 | return ($val > $this->value); 18 | } 19 | 20 | public function toArray() : array 21 | { 22 | return [ 23 | 'type' => 'greater-than', 24 | 'key' => $this->key, 25 | 'value' => $this->value 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Criteria/LessThan.php: -------------------------------------------------------------------------------- 1 | resolve($this->key); 12 | 13 | if (is_array($this->value) || is_array($val)) { 14 | throw new \InvalidArgumentException('Less Than cannot evaluate array'); 15 | } 16 | 17 | return ($val < $this->value); 18 | } 19 | 20 | public function toArray() : array 21 | { 22 | return [ 23 | 'type' => 'less-than', 24 | 'key' => $this->key, 25 | 'value' => $this->value 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Criteria/NotEquals.php: -------------------------------------------------------------------------------- 1 | resolve($this->key); 12 | 13 | if (is_array($this->value) && in_array($val, $this->value, false)) { 14 | return false; 15 | } 16 | 17 | return ($val != $this->value); 18 | } 19 | 20 | public function toArray() : array 21 | { 22 | return [ 23 | 'type' => 'not-equals', 24 | 'key' => $this->key, 25 | 'value' => $this->value 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/CriteriaSet.php: -------------------------------------------------------------------------------- 1 | criteria = $criteria; 46 | $this->matchType = $matchType; 47 | } 48 | 49 | /** 50 | * Add a new criteria to the set 51 | * 52 | * @param \Psecio\Canary\Criteria $criteria Criteria instance 53 | */ 54 | public function add($criteria) 55 | { 56 | $this->criteria[] = $criteria; 57 | } 58 | 59 | /** 60 | * Check for offset (ArrayAccess) 61 | * 62 | * @param integer $offset Offset to check 63 | */ 64 | public function offsetExists($offset) 65 | { 66 | return isset($this->criteria[$offset]); 67 | } 68 | 69 | /** 70 | * Get an offset (ArrayAccess) 71 | * 72 | * @param integer $offset Offset to get 73 | */ 74 | public function offsetGet($offset) 75 | { 76 | return $this->criteria[$offset]; 77 | } 78 | 79 | /** 80 | * Set an offset 81 | * 82 | * @param integer $offset Offset location 83 | * @param mixed $value Value to set 84 | */ 85 | public function offsetSet($offset, $value) 86 | { 87 | $this->criteria[$offset] = $value; 88 | } 89 | 90 | /** 91 | * Remove an offset 92 | * 93 | * @param integer $offset Integer offset 94 | */ 95 | public function offsetUnset($offset) 96 | { 97 | if (isset($this->criteria[$offset])) { 98 | unset($this->criteria[$offset]); 99 | } 100 | } 101 | 102 | /** 103 | * Return the current value (Iterator) 104 | * 105 | * @return mixed Value at the current location 106 | */ 107 | public function current() 108 | { 109 | return $this->criteria[$this->index]; 110 | } 111 | 112 | /** 113 | * Rewind the internal index to the base value (Iterator) 114 | */ 115 | public function rewind() 116 | { 117 | $this->index = 0; 118 | } 119 | 120 | /** 121 | * Advance the internal index to the next value (Iterator) 122 | */ 123 | public function next() 124 | { 125 | ++$this->index; 126 | } 127 | 128 | /** 129 | * Check to see if the value at the current index exists (Iterator) 130 | * 131 | * @return boolean Index exists/doesn't exist 132 | */ 133 | public function valid() 134 | { 135 | return isset($this->criteria[$this->index]); 136 | } 137 | 138 | /** 139 | * Return the current index value (Iterator) 140 | * 141 | * @return integer Index value 142 | */ 143 | public function key() 144 | { 145 | return $this->index; 146 | } 147 | 148 | /** 149 | * Return the count of the current objects (Countable) 150 | * 151 | * @return integer Count of objects 152 | */ 153 | public function count() 154 | { 155 | return count($this->criteria); 156 | } 157 | 158 | /** 159 | * Get the match type value 160 | * 161 | * @return string Match type setting 162 | */ 163 | public function getMatchType() 164 | { 165 | return $this->matchType; 166 | } 167 | 168 | /** 169 | * Set the same notification type on all criteria 170 | * 171 | * @param \Psecio\Canary\Notify $notify Notify object instance 172 | */ 173 | public function setNotify($notify) 174 | { 175 | foreach ($this->criteria as $criteria) { 176 | $criteria->setNotify($notify); 177 | } 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/Data.php: -------------------------------------------------------------------------------- 1 | data = $data; 12 | } 13 | 14 | public function add($key, $value) 15 | { 16 | $this->data[$key] = $value; 17 | } 18 | 19 | public function resolve($key) 20 | { 21 | $found = null; 22 | $result = $this->find($key, $this->data, $found); 23 | return $found; 24 | } 25 | 26 | public function find($key, $data, &$found) 27 | { 28 | foreach ($data as $index => $value) { 29 | // If the key we're looking for matches, return 30 | if ($index == $key) { 31 | $found = $value; 32 | return true; 33 | 34 | } else { 35 | // If not, see if the value is an array so we can iterate 36 | if (is_array($value)) { 37 | $this->find($key, $value, $found); 38 | if ($found !== null) { 39 | return true; 40 | } 41 | } 42 | } 43 | } 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Instance.php: -------------------------------------------------------------------------------- 1 | null 25 | ]; 26 | 27 | /** 28 | * Current CriteraSet instance 29 | * @var Psecio\Canary\CriteriaSet 30 | */ 31 | private $criteria; 32 | 33 | /** 34 | * Initialize the object with the provided configuration 35 | * 36 | * @param array $config Configuration options [optional] 37 | */ 38 | public function __construct(array $config = []) 39 | { 40 | if (!empty($config)) { 41 | $this->setConfig($config); 42 | } 43 | $this->criteria = new CriteriaSet(); 44 | 45 | if (isset($config['data'])) { 46 | $this->data = new Data($config['data']); 47 | } else { 48 | $this->data = new Data($this->getDefaultData()); 49 | } 50 | } 51 | 52 | /** 53 | * Static method to create a new instance 54 | * 55 | * @param array $config Configuration options [optional] 56 | * @return \Psecio\Canary\Instance instance 57 | */ 58 | public static function build(array $config = []) : \Psecio\Canary\Instance 59 | { 60 | $instance = new \Psecio\Canary\Instance($config); 61 | return $instance; 62 | } 63 | 64 | /** 65 | * Loads the default data, pulling in $_GET, $_POST and some $_SERVER values 66 | * 67 | * @return array Set of default data 68 | */ 69 | protected function getDefaultData() 70 | { 71 | return [ 72 | 'get' => $_GET, 73 | 'post' => $_POST, 74 | 'request' => [ 75 | 'uri' => (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : null 76 | ] 77 | ]; 78 | } 79 | 80 | /** 81 | * Return the current Data instance 82 | * 83 | * @return \Psecio\Canary\Data instance 84 | */ 85 | public function getData() 86 | { 87 | return $this->data; 88 | } 89 | 90 | /** 91 | * Set the current configuration options 92 | * 93 | * @param array $config Configuration options 94 | */ 95 | public function setConfig(array $config) 96 | { 97 | $this->config = $config; 98 | } 99 | 100 | /** 101 | * Get the current configuration. If a key is provided and exists, return the value 102 | * 103 | * @param string $key Key to locate [optional] 104 | * @return mixed Returns either the value if found or all configuration options 105 | */ 106 | public function getConfig($key = null) 107 | { 108 | return ($key !== null && array_key_exists($key, $this->config)) ? $this->config[$key] : $this->config; 109 | } 110 | 111 | /** 112 | * Get the current set of Criteria 113 | * 114 | * @return \Psecio\Canary\CriteriaSet 115 | */ 116 | public function getCriteria() 117 | { 118 | return $this->criteria; 119 | } 120 | 121 | /** 122 | * Set up a new criteria based on the input. If a CriteriaSet object is 123 | * provided, set the current critera value to that. 124 | * 125 | * If the method is called with two paramaters, create a simple Equals criteria 126 | * 127 | * @param mixed $params Options to use for criteria 128 | * @return \Psecio\Canary\Instance instance 129 | */ 130 | public function if(...$params) : \Psecio\Canary\Instance 131 | { 132 | if (count($params) == 1 && $params[0] instanceof CriteriaSet) { 133 | $this->criteria = $params[0]; 134 | } elseif (count($params) == 1 && is_string($params[0])) { 135 | // Try to load it like a class 136 | list($classNs, $method) = explode('::', $params[0]); 137 | if (!class_exists($classNs)) { 138 | throw new \InvalidArgumentException('Invalid data source: '.$classNs); 139 | } 140 | $result = call_user_func($classNs.'::'.$method); 141 | 142 | if (!is_array($result)) { 143 | throw new \InvalidArgumentException('Method call must return an array'); 144 | } 145 | foreach ($result as $index => $value) { 146 | $equals = new Equals($index, $value); 147 | $this->criteria->add($equals); 148 | } 149 | 150 | } elseif (count($params) == 1 && $params[0] instanceof Criteria) { 151 | $this->criteria->add($params[0]); 152 | 153 | } elseif (count($params) >= 1) { 154 | if (is_array($params[0])) { 155 | $set = new CriteriaSet(); 156 | 157 | foreach ($params[0] as $index => $value) { 158 | $equals = new Equals($index, $value); 159 | $set->add($equals); 160 | } 161 | $this->criteria->add($set); 162 | } else { 163 | // Add it as a single "equals" criteria 164 | $equals = new Equals($params[0], $params[1]); 165 | $this->criteria->add($equals); 166 | } 167 | 168 | } 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Set up a notification handler. This notification will be assigned 175 | * to the last criteria added. 176 | * 177 | * @param mixed $callback Input, either a callable or an instance of a Notify object 178 | * 179 | * @return \Psecio\Canary\Instance instance 180 | */ 181 | public function then($callback) : \Psecio\Canary\Instance 182 | { 183 | // If they call this, get the last criteria on the stack and update the notify 184 | $last = count($this->criteria) - 1; 185 | $this->criteria[$last]->setNotify($callback); 186 | 187 | return $this; 188 | } 189 | 190 | /** 191 | * Resolve the notification if it's not already a Notify instance 192 | * 193 | * @param mixed $notify Notification method 194 | * @return \Psecio\Canary\Notify instance 195 | */ 196 | public function resolveNotify($notify) 197 | { 198 | if ($notify instanceof Notify) { 199 | return $notify; 200 | } 201 | 202 | if (is_callable($notify)) { 203 | $notify = new Notify\Callback(['callback' => $notify]); 204 | 205 | } elseif ($notify instanceof \Monolog\Logger) { 206 | $notify = new \Psecio\Canary\Notify\Monolog($notify); 207 | 208 | } elseif ($notify instanceof \Maknz\Slack\Client) { 209 | $notify = new \Psecio\Canary\Notify\Slack($notify); 210 | 211 | } elseif ($notify instanceof \PagerDuty\Event) { 212 | $notify = new \Psecio\Canary\Notify\Pagerduty($notify); 213 | 214 | } else { 215 | throw new \InvalidArgumentException('Invalid notification method: '.get_class($notify)); 216 | } 217 | return $notify; 218 | } 219 | 220 | /** 221 | * Execute the criteria on the current set of data 222 | * 223 | * @return bool Pass/fail status of evaluation 224 | */ 225 | public function execute() : bool 226 | { 227 | return $this->recurseCriteria($this->criteria); 228 | } 229 | 230 | /** 231 | * Recurse the criteria and evaluate it based on the current data 232 | * 233 | * @param \Psecio\Canary\Criteria|\Psecio\Canary\CriteriaSec $criteria Single criteria or a set 234 | * @return boolean Match/no match result 235 | */ 236 | public function recurseCriteria($criteria) : bool 237 | { 238 | $match = false; 239 | $defaultNotify = $this->getConfig('notify'); 240 | if (!$defaultNotify instanceof Notify && !is_callable($defaultNotify)) { 241 | $defaultNotify = null; 242 | } 243 | 244 | foreach ($criteria as $index => $instance) { 245 | if ($instance instanceof CriteriaSet) { 246 | $match = $this->recurseCriteria($instance); 247 | 248 | } else { 249 | if ($instance->evaluate($this->getData()) === true) { 250 | $matches[$index] = true; 251 | 252 | // If we've been given a default notify handler, always use that 253 | $notify = ($defaultNotify !== null) ? $defaultNotify : $instance->getNotify(); 254 | 255 | // If it's not a Notify already, resolve it 256 | if (!($notify instanceof Notify)) { 257 | $notify = $this->resolveNotify($notify); 258 | } 259 | 260 | $notify->execute($instance); 261 | $match = true; 262 | } 263 | } 264 | } 265 | 266 | return $match; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Notify.php: -------------------------------------------------------------------------------- 1 | config = $config; 12 | } 13 | 14 | public function getConfig($key = null) 15 | { 16 | return ($key !== null && isset($this->config[$key])) ? $this->config[$key] : $this->config; 17 | } 18 | 19 | public abstract function execute(Criteria $criteria); 20 | } 21 | -------------------------------------------------------------------------------- /src/Notify/Callback.php: -------------------------------------------------------------------------------- 1 | getConfig('callback'); 12 | return $callback($criteria); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Notify/ErrorLog.php: -------------------------------------------------------------------------------- 1 | $log 13 | ]); 14 | } 15 | 16 | public function execute(Criteria $criteria) 17 | { 18 | $this->getConfig('logger')->error(json_encode($criteria)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Notify/Pagerduty.php: -------------------------------------------------------------------------------- 1 | $event 13 | ]); 14 | } 15 | 16 | public function execute(Criteria $criteria) 17 | { 18 | $event = $this->getConfig('event'); 19 | $event->setDescription('Canary triggered') 20 | ->setDetails($criteria->toArray()); 21 | 22 | $resp = $event->trigger(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Notify/Slack.php: -------------------------------------------------------------------------------- 1 | $client 15 | ]); 16 | } 17 | 18 | public function execute(Criteria $criteria) 19 | { 20 | $client = $this->getConfig('client'); 21 | $client->from(self::NAME) 22 | ->enableMarkdown() 23 | ->send('*Canary triggered:* '.json_encode($criteria)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Criteria/EqualsTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 35 | 36 | $notify = function () { 37 | 38 | }; 39 | 40 | $this->assertNull($criteria->setNotify($notify)); 41 | 42 | $this->assertSame($notify, $criteria->getNotify()); 43 | } 44 | 45 | public function testGetNotify() 46 | { 47 | $criteria = new Equals(1234, 1234); 48 | 49 | $this->assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 50 | 51 | $notify = function () { 52 | 53 | }; 54 | 55 | $this->assertNull($criteria->setNotify($notify)); 56 | 57 | $this->assertSame($notify, $criteria->getNotify()); 58 | } 59 | 60 | /** 61 | * @dataProvider dataEvaluate 62 | * @param $data 63 | * @param $criteriaValue 64 | * @param $expected 65 | */ 66 | public function testEvaluate($data, $criteriaValue, $expected) 67 | { 68 | $criteria = new Equals('myField', $criteriaValue); 69 | 70 | $actual = $criteria->evaluate(new Data([ 71 | 'myField' => $data, 72 | ])); 73 | 74 | $this->assertSame($expected, $actual); 75 | } 76 | 77 | public function dataEvaluate() 78 | { 79 | return [ 80 | [false, false, true], 81 | [true, false, false], 82 | [12.34, 12.34, true], 83 | [12.34, 43.21, false], 84 | [1234, 1234, true], 85 | [1234, 4321, false], 86 | [new \stdClass, new \stdClass, true], 87 | [new \stdClass, new \Exception, false], 88 | ['foo', 'foo', true], 89 | ['foo', 'bar', false], 90 | [1234, '1234', true], 91 | [0, false, true], 92 | [0.0, false, true], 93 | ['', false, true], 94 | [[], [], true], 95 | ['foo', ['foo'], true], 96 | ['foo', ['bar'], false], 97 | [[], [[]], true], 98 | ]; 99 | } 100 | 101 | public function testJsonSerialize() 102 | { 103 | $criteria = new Equals('foo', 'bar'); 104 | 105 | $result = \json_decode(\json_encode($criteria), true); 106 | 107 | $this->assertCount(3, $result); 108 | $this->assertArraySubset( 109 | [ 110 | 'type' => 'equals', 111 | 'key' => 'foo', 112 | 'value' => 'bar', 113 | ], 114 | $result 115 | ); 116 | } 117 | 118 | public function testToArray() 119 | { 120 | $criteria = new Equals('foo', 'bar'); 121 | 122 | $result = $criteria->toArray(); 123 | 124 | $this->assertCount(3, $result); 125 | $this->assertArraySubset( 126 | [ 127 | 'type' => 'equals', 128 | 'key' => 'foo', 129 | 'value' => 'bar', 130 | ], 131 | $result 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Criteria/GreaterThanTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 35 | 36 | $notify = function () { 37 | 38 | }; 39 | 40 | $this->assertNull($criteria->setNotify($notify)); 41 | 42 | $this->assertSame($notify, $criteria->getNotify()); 43 | } 44 | 45 | public function testGetNotify() 46 | { 47 | $criteria = new GreaterThan(1234, 1235); 48 | 49 | $this->assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 50 | 51 | $notify = function () { 52 | 53 | }; 54 | 55 | $this->assertNull($criteria->setNotify($notify)); 56 | 57 | $this->assertSame($notify, $criteria->getNotify()); 58 | } 59 | 60 | /** 61 | * @dataProvider dataEvaluate 62 | * @param $data 63 | * @param $criteriaValue 64 | * @param $expected 65 | */ 66 | public function testEvaluate($data, $criteriaValue, $expected) 67 | { 68 | $criteria = new GreaterThan('myField', $criteriaValue); 69 | 70 | $actual = $criteria->evaluate(new Data([ 71 | 'myField' => $data, 72 | ])); 73 | 74 | $this->assertSame($expected, $actual); 75 | } 76 | 77 | public function dataEvaluate() 78 | { 79 | return [ 80 | [3, 2, true], 81 | [2, 3, false], 82 | [2, 2, false], 83 | ["3", "2", true], 84 | ["2", "3", false], 85 | ["2", "2", false], 86 | ]; 87 | } 88 | 89 | public function testJsonSerialize() 90 | { 91 | $criteria = new GreaterThan('foo', 'bar'); 92 | 93 | $result = \json_decode(\json_encode($criteria), true); 94 | 95 | $this->assertCount(3, $result); 96 | $this->assertArraySubset( 97 | [ 98 | 'type' => 'greater-than', 99 | 'key' => 'foo', 100 | 'value' => 'bar', 101 | ], 102 | $result 103 | ); 104 | } 105 | 106 | public function testToArray() 107 | { 108 | $criteria = new GreaterThan('foo', 'bar'); 109 | 110 | $result = $criteria->toArray(); 111 | 112 | $this->assertCount(3, $result); 113 | $this->assertArraySubset( 114 | [ 115 | 'type' => 'greater-than', 116 | 'key' => 'foo', 117 | 'value' => 'bar', 118 | ], 119 | $result 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/Criteria/LessThanTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 35 | 36 | $notify = function () { 37 | 38 | }; 39 | 40 | $this->assertNull($criteria->setNotify($notify)); 41 | 42 | $this->assertSame($notify, $criteria->getNotify()); 43 | } 44 | 45 | public function testGetNotify() 46 | { 47 | $criteria = new LessThan(1234, 1235); 48 | 49 | $this->assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 50 | 51 | $notify = function () { 52 | 53 | }; 54 | 55 | $this->assertNull($criteria->setNotify($notify)); 56 | 57 | $this->assertSame($notify, $criteria->getNotify()); 58 | } 59 | 60 | /** 61 | * @dataProvider dataEvaluate 62 | * @param $data 63 | * @param $criteriaValue 64 | * @param $expected 65 | */ 66 | public function testEvaluate($data, $criteriaValue, $expected) 67 | { 68 | $criteria = new LessThan('myField', $criteriaValue); 69 | 70 | $actual = $criteria->evaluate(new Data([ 71 | 'myField' => $data, 72 | ])); 73 | 74 | $this->assertSame($expected, $actual); 75 | } 76 | 77 | public function dataEvaluate() 78 | { 79 | return [ 80 | [3, 2, false], 81 | [2, 3, true], 82 | [2, 2, false], 83 | ["3", "2", false], 84 | ["2", "3", true], 85 | ["2", "2", false], 86 | ]; 87 | } 88 | 89 | public function testJsonSerialize() 90 | { 91 | $criteria = new LessThan('foo', 'bar'); 92 | 93 | $result = \json_decode(\json_encode($criteria), true); 94 | 95 | $this->assertCount(3, $result); 96 | $this->assertArraySubset( 97 | [ 98 | 'type' => 'less-than', 99 | 'key' => 'foo', 100 | 'value' => 'bar', 101 | ], 102 | $result 103 | ); 104 | } 105 | 106 | public function testToArray() 107 | { 108 | $criteria = new LessThan('foo', 'bar'); 109 | 110 | $result = $criteria->toArray(); 111 | 112 | $this->assertCount(3, $result); 113 | $this->assertArraySubset( 114 | [ 115 | 'type' => 'less-than', 116 | 'key' => 'foo', 117 | 'value' => 'bar', 118 | ], 119 | $result 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/Criteria/NotEqualsTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 35 | 36 | $notify = function () { 37 | 38 | }; 39 | 40 | $this->assertNull($criteria->setNotify($notify)); 41 | 42 | $this->assertSame($notify, $criteria->getNotify()); 43 | } 44 | 45 | public function testGetNotify() 46 | { 47 | $criteria = new NotEquals(1234, 1235); 48 | 49 | $this->assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 50 | 51 | $notify = function () { 52 | 53 | }; 54 | 55 | $this->assertNull($criteria->setNotify($notify)); 56 | 57 | $this->assertSame($notify, $criteria->getNotify()); 58 | } 59 | 60 | /** 61 | * @dataProvider dataEvaluate 62 | * @param $data 63 | * @param $criteriaValue 64 | * @param $expected 65 | */ 66 | public function testEvaluate($data, $criteriaValue, $expected) 67 | { 68 | $criteria = new NotEquals('myField', $criteriaValue); 69 | 70 | $actual = $criteria->evaluate(new Data([ 71 | 'myField' => $data, 72 | ])); 73 | 74 | $this->assertSame($expected, $actual); 75 | } 76 | 77 | public function dataEvaluate() 78 | { 79 | return [ 80 | [false, false, false], 81 | [true, false, true], 82 | [12.34, 12.34, false], 83 | [12.34, 43.21, true], 84 | [1234, 1234, false], 85 | [1234, 4321, true], 86 | [new \stdClass, new \stdClass, false], 87 | [new \stdClass, new \Exception, true], 88 | ['foo', 'foo', false], 89 | ['foo', 'bar', true], 90 | [1234, '1234', false], 91 | [0, false, false], 92 | [0.0, false, false], 93 | ['', false, false], 94 | [[], [], false], 95 | ['foo', ['foo'], false], 96 | ['foo', ['bar'], true], 97 | [[], [[]], false], 98 | ]; 99 | } 100 | 101 | public function testJsonSerialize() 102 | { 103 | $criteria = new NotEquals('foo', 'bar'); 104 | 105 | $result = \json_decode(\json_encode($criteria), true); 106 | 107 | $this->assertCount(3, $result); 108 | $this->assertArraySubset( 109 | [ 110 | 'type' => 'not-equals', 111 | 'key' => 'foo', 112 | 'value' => 'bar', 113 | ], 114 | $result 115 | ); 116 | } 117 | 118 | public function testToArray() 119 | { 120 | $criteria = new NotEquals('foo', 'bar'); 121 | 122 | $result = $criteria->toArray(); 123 | 124 | $this->assertCount(3, $result); 125 | $this->assertArraySubset( 126 | [ 127 | 'type' => 'not-equals', 128 | 'key' => 'foo', 129 | 'value' => 'bar', 130 | ], 131 | $result 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/CriteriaSetTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($criteriaSet->offsetExists(0)); 20 | $criteriaSet->offsetSet(0, $criteria); 21 | $this->assertTrue($criteriaSet->offsetExists(0)); 22 | $this->assertSame($criteria, $criteriaSet->offsetGet(0)); 23 | $criteriaSet->offsetUnset(0); 24 | $this->assertFalse($criteriaSet->offsetExists(0)); 25 | 26 | $this->assertFalse(isset($criteriaSet[1])); 27 | $criteriaSet[1] = $criteria; 28 | $this->assertTrue(isset($criteriaSet[1])); 29 | $this->assertSame($criteria, $criteriaSet[1]); 30 | unset($criteriaSet[1]); 31 | $this->assertFalse(isset($criteriaSet[1])); 32 | } 33 | 34 | public function testIterator() 35 | { 36 | $firstCriteria = new Equals('myField', 1234); 37 | $secondCriteria = new Equals('myField', 2345); 38 | $criteriaSet = new CriteriaSet([ 39 | $firstCriteria, 40 | $secondCriteria, 41 | ]); 42 | 43 | $this->assertTrue($criteriaSet->valid()); 44 | $this->assertSame(0, $criteriaSet->key()); 45 | $this->assertSame($firstCriteria, $criteriaSet->current()); 46 | 47 | $criteriaSet->next(); 48 | $this->assertTrue($criteriaSet->valid()); 49 | $this->assertSame(1, $criteriaSet->key()); 50 | $this->assertSame($secondCriteria, $criteriaSet->current()); 51 | 52 | $criteriaSet->next(); 53 | $this->assertFalse($criteriaSet->valid()); 54 | 55 | $criteriaSet->rewind(); 56 | $this->assertTrue($criteriaSet->valid()); 57 | $this->assertSame(0, $criteriaSet->key()); 58 | $this->assertSame($firstCriteria, $criteriaSet->current()); 59 | } 60 | 61 | public function testCountable() 62 | { 63 | $criteriaSet = new CriteriaSet(); 64 | 65 | $this->assertCount(0, $criteriaSet); 66 | 67 | $criteriaSet[] = new Equals('myField', 1234); 68 | 69 | $this->assertCount(1, $criteriaSet); 70 | } 71 | 72 | public function testAdd() 73 | { 74 | $criteria = new Equals('myField', 1234); 75 | 76 | $criteriaSet = new CriteriaSet(); 77 | $criteriaSet->add($criteria); 78 | 79 | $this->assertSame($criteria, $criteriaSet->current()); 80 | } 81 | 82 | public function testGetMatchType() 83 | { 84 | $this->assertSame( 85 | CriteriaSet::MATCH_ANY, 86 | (new CriteriaSet())->getMatchType() 87 | ); 88 | 89 | $this->assertSame( 90 | CriteriaSet::MATCH_ALL, 91 | (new CriteriaSet([], CriteriaSet::MATCH_ALL))->getMatchType() 92 | ); 93 | } 94 | 95 | public function testSetNotify() 96 | { 97 | $criteria = new Equals('myField', 1234); 98 | 99 | $this->assertInstanceOf(ErrorLog::class, $criteria->getNotify()); 100 | 101 | $criteriaSet = new CriteriaSet([$criteria]); 102 | 103 | $callback = new Callback([ 104 | 'callback' => function () { 105 | 106 | }, 107 | ]); 108 | 109 | $criteriaSet->setNotify($callback); 110 | 111 | $this->assertSame($callback, $criteria->getNotify()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/DataTest.php: -------------------------------------------------------------------------------- 1 | 'bar', 15 | ]); 16 | 17 | $result = $data->resolve('foo'); 18 | 19 | $this->assertSame('bar', $result); 20 | } 21 | 22 | public function testResolveAddedValue() 23 | { 24 | $data = new Data(); 25 | $data->add('foo', 'bar'); 26 | 27 | $result = $data->resolve('foo'); 28 | 29 | $this->assertSame('bar', $result); 30 | } 31 | 32 | public function testResolveNestedValue() 33 | { 34 | $data = new Data([ 35 | 'foo' => [ 36 | 'bar' => [ 37 | 'fiz' => 'buz', 38 | ], 39 | ], 40 | ]); 41 | 42 | $result = $data->resolve('fiz'); 43 | 44 | $this->assertSame('buz', $result); 45 | } 46 | 47 | public function testDepthFirstKeyResolution() 48 | { 49 | $data = new Data([ 50 | 'foo' => [ 51 | 'bar' => [ 52 | 'fiz' => 'buz', 53 | ], 54 | 'fiz' => 'quux', 55 | ], 56 | ]); 57 | 58 | $result = $data->resolve('fiz'); 59 | 60 | $this->assertSame('buz', $result); 61 | } 62 | 63 | public function testNotResolve() 64 | { 65 | $data = new Data(); 66 | 67 | $result = $data->resolve('foo'); 68 | 69 | $this->assertNull($result); 70 | } 71 | 72 | public function testFind() 73 | { 74 | $data = [ 75 | 'foo' => 'bar', 76 | ]; 77 | 78 | $found = null; 79 | 80 | $result = (new Data())->find('foo', $data, $found); 81 | 82 | $this->assertTrue($result); 83 | $this->assertSame('bar', $found); 84 | } 85 | 86 | public function testFindNestedValue() 87 | { 88 | $data = [ 89 | 'foo' => [ 90 | 'bar' => [ 91 | 'fiz' => 'buz', 92 | ], 93 | ], 94 | ]; 95 | 96 | $found = null; 97 | 98 | $result = (new Data())->find('fiz', $data, $found); 99 | 100 | $this->assertTrue($result); 101 | $this->assertSame('buz', $found); 102 | } 103 | 104 | public function testNotFind() 105 | { 106 | $found = null; 107 | 108 | $result = (new Data())->find('foo', [], $found); 109 | 110 | $this->assertFalse($result); 111 | $this->assertNull($found); 112 | } 113 | 114 | public function testDepthFirstKeyFinding() 115 | { 116 | $data = [ 117 | 'foo' => [ 118 | 'bar' => [ 119 | 'fiz' => 'buz', 120 | ], 121 | 'fiz' => 'quux', 122 | ], 123 | ]; 124 | 125 | $found = null; 126 | 127 | $result = (new Data())->find('fiz', $data, $found); 128 | 129 | $this->assertTrue($result); 130 | $this->assertSame('buz', $found); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/InstanceTest.php: -------------------------------------------------------------------------------- 1 | true]; 17 | 18 | $instance = new Instance(); 19 | $instance->setConfig($config); 20 | 21 | $this->assertEquals($config, $instance->getConfig()); 22 | } 23 | 24 | public function testSetConfigInit() 25 | { 26 | $config = ['test' => true]; 27 | $instance = new Instance($config); 28 | 29 | $this->assertEquals($config, $instance->getConfig()); 30 | } 31 | 32 | public function testGetSpecificConfig() 33 | { 34 | $config = ['test' => true]; 35 | $instance = new Instance($config); 36 | 37 | $this->assertEquals(true, $instance->getConfig('test')); 38 | } 39 | 40 | public function testGetSetData() 41 | { 42 | $value = ['test' => true]; 43 | 44 | $data = new Data($value); 45 | $instance = new Instance(['data' => $value]); 46 | 47 | $this->assertEquals($data, $instance->getData()); 48 | } 49 | 50 | public function testBuildStatic() 51 | { 52 | $config = [ 53 | 'data' => ['test' => true] 54 | ]; 55 | $i = Instance::build($config); 56 | 57 | $this->assertInstanceOf(Instance::class, $i); 58 | 59 | // The config should match 60 | $this->assertEquals($config, $i->getConfig()); 61 | } 62 | 63 | public function testAddCriteriaSimple() 64 | { 65 | $i = Instance::build(); 66 | $i->if('username', 'test'); 67 | $criteria = $i->getCriteria(); 68 | 69 | $this->assertInstanceOf(CriteriaSet::class, $criteria); 70 | $this->assertCount(1, $criteria); 71 | } 72 | 73 | public function testAddCriteriaSet() 74 | { 75 | $set = new CriteriaSet(); 76 | $cinstance = new \Psecio\Canary\Criteria\Equals('username', 'test'); 77 | $set->add($cinstance); 78 | 79 | $i = Instance::build(); 80 | $i->if($set); 81 | 82 | $this->assertEquals($set, $i->getCriteria()); 83 | } 84 | 85 | public function testAddSingleCriteria() 86 | { 87 | $criteria = new Equals('test', 'foo'); 88 | $i = Instance::build()->if($criteria); 89 | 90 | $set = $i->getCriteria(); 91 | $this->assertCount(1, $set); 92 | 93 | $c = $set[0]; 94 | $this->assertEquals($c, $criteria); 95 | } 96 | 97 | public function testAddMultipleKeyValue() 98 | { 99 | $criteria = [ 100 | 'foo' => 'bar', 101 | 'baz' => 'quux' 102 | ]; 103 | $i = Instance::build()->if($criteria); 104 | $set = $i->getCriteria(); 105 | 106 | // The values are stored in a set so this set is the single value 107 | $this->assertCount(1, $set); 108 | $this->assertCount(2, $set[0]); 109 | 110 | $set = $set[0]; 111 | // Ensure that our values are the same as what we gave 112 | $first = $set[0]->toArray(); 113 | $this->assertEquals('bar', $first['value']); 114 | 115 | $second = $set[1]->toArray(); 116 | $this->assertEquals('quux', $second['value']); 117 | } 118 | 119 | public function testAddMultipleKeyValueListsToCriteria() 120 | { 121 | $criteria = [ 122 | 'foo' => ['bar', 'fiz', 'buz'], 123 | 'baz' => ['quux', 'qux'] 124 | ]; 125 | $i = Instance::build()->if($criteria); 126 | $set = $i->getCriteria(); 127 | 128 | // The values are stored in a set so this set is the single value 129 | $this->assertCount(1, $set); 130 | $this->assertCount(2, $set[0]); 131 | 132 | $set = $set[0]; 133 | // Ensure that our values are the same as what we gave 134 | $first = $set[0]->toArray(); 135 | $this->assertEquals(['bar', 'fiz', 'buz'], $first['value']); 136 | 137 | $second = $set[1]->toArray(); 138 | $this->assertEquals(['quux', 'qux'], $second['value']); 139 | } 140 | 141 | public function testAddNotifyToLast() 142 | { 143 | $callback = function() { echo 'test'; }; 144 | $config = [ 'callback' => $callback ]; 145 | 146 | $notify = new \Psecio\Canary\Notify\Callback($config); 147 | $i = Instance::build()->if(['test' => 'foo'])->then($notify); 148 | 149 | $set = $i->getCriteria(); 150 | $criteria = $set[0][0]; 151 | 152 | $notify = $criteria->getNotify(); 153 | // print_r($notify); 154 | $this->assertInstanceOf(\Psecio\Canary\Notify\Callback::class, $notify); 155 | $this->assertEquals($notify->getConfig('callback'), $callback); 156 | } 157 | 158 | public function testResolveNotify() 159 | { 160 | $notify = new \Psecio\Canary\Notify\Callback(['callback' => function() {}]); 161 | $i = Instance::build(); 162 | 163 | $result = $i->resolveNotify($notify); 164 | $this->assertEquals($result, $notify); 165 | 166 | // Now try with a callback 167 | $result = $i->resolveNotify(function() { }); 168 | $this->assertInstanceOf(\Psecio\Canary\Notify\Callback::class, $result); 169 | } 170 | 171 | /** 172 | * Throws an exception on a bad notify type 173 | * 174 | * @expectedException \InvalidArgumentException 175 | */ 176 | public function testResolveNotifyInvalid() 177 | { 178 | $notify = (object)['foo' => 'badvalue']; 179 | 180 | $i = Instance::build(); 181 | $i->resolveNotify($notify); 182 | } 183 | 184 | /** 185 | * @expectedException \UnexpectedValueException 186 | * @expectedExceptionMessage Whoops! 187 | */ 188 | public function testMatchCriteriaValue() 189 | { 190 | $config = [ 191 | 'data' => [ 192 | 'foo' => 'bar', 193 | ], 194 | 'notify' => function () { 195 | throw new \UnexpectedValueException('Whoops!'); 196 | }, 197 | ]; 198 | 199 | $criteria = [ 200 | 'foo' => 'bar', 201 | 'baz' => 'quux', 202 | ]; 203 | 204 | Instance::build($config) 205 | ->if($criteria) 206 | ->execute(); 207 | } 208 | 209 | /** 210 | * @expectedException \UnexpectedValueException 211 | * @expectedExceptionMessage Whoops! 212 | */ 213 | public function testMatchCriteriaList() 214 | { 215 | $config = [ 216 | 'data' => [ 217 | 'foo' => 'bar', 218 | ], 219 | 'notify' => function () { 220 | throw new \UnexpectedValueException('Whoops!'); 221 | }, 222 | ]; 223 | 224 | $criteria = [ 225 | 'foo' => [ 226 | 'bar', 227 | 'fiz', 228 | 'buz', 229 | ], 230 | 'baz' => [ 231 | 'quux', 232 | 'qux', 233 | ], 234 | ]; 235 | 236 | Instance::build($config) 237 | ->if($criteria) 238 | ->execute(); 239 | } 240 | 241 | public function testNotMatchCriteriaList() 242 | { 243 | $config = [ 244 | 'data' => [ 245 | 'foo' => 'FizBuz', 246 | ], 247 | 'notify' => function () { 248 | throw new \UnexpectedValueException('Whoops!'); 249 | }, 250 | ]; 251 | 252 | $criteria = [ 253 | 'foo' => [ 254 | 'bar', 255 | 'fiz', 256 | 'buz', 257 | ], 258 | 'baz' => [ 259 | 'quux', 260 | 'qux', 261 | ], 262 | ]; 263 | 264 | $result = Instance::build($config) 265 | ->if($criteria) 266 | ->execute(); 267 | 268 | $this->assertFalse($result); 269 | } 270 | 271 | /** 272 | * @expectedException \UnexpectedValueException 273 | * @expectedExceptionMessage Whoops! 274 | */ 275 | public function testConfigWithoutNotify() 276 | { 277 | $config = [ 278 | 'data' => [ 279 | 'foo' => 'bar', 280 | ], 281 | ]; 282 | 283 | $criteria = [ 284 | 'foo' => 'bar', 285 | 'baz' => 'quux', 286 | ]; 287 | 288 | Instance::build($config) 289 | ->if($criteria) 290 | ->then(function () { 291 | throw new \UnexpectedValueException('Whoops!'); 292 | }) 293 | ->execute(); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /tests/Notify/CallbackTest.php: -------------------------------------------------------------------------------- 1 | 'bar', 17 | 'baz' => 'quux', 18 | ]); 19 | 20 | $config = $notify->getConfig(); 21 | 22 | $this->assertCount(2, $config); 23 | $this->assertArraySubset( 24 | [ 25 | 'foo' => 'bar', 26 | 'baz' => 'quux', 27 | ], 28 | $config 29 | ); 30 | } 31 | 32 | public function testGetConfigByKey() 33 | { 34 | $notify = new Callback([ 35 | 'foo' => 'bar', 36 | 'baz' => 'quux', 37 | ]); 38 | 39 | $config = $notify->getConfig('foo'); 40 | 41 | $this->assertEquals('bar', $config); 42 | } 43 | 44 | public function testGetFullConfigOnKeyMiss() 45 | { 46 | $notify = new Callback([ 47 | 'foo' => 'bar', 48 | 'baz' => 'quux', 49 | ]); 50 | 51 | $config = $notify->getConfig('FizBuz'); 52 | 53 | $this->assertCount(2, $config); 54 | $this->assertArraySubset( 55 | [ 56 | 'foo' => 'bar', 57 | 'baz' => 'quux', 58 | ], 59 | $config 60 | ); 61 | } 62 | 63 | public function testExecute() 64 | { 65 | $expected = new \stdClass(); 66 | 67 | $notify = new Callback([ 68 | 'callback' => function () use ($expected) { 69 | return $expected; 70 | }, 71 | ]); 72 | 73 | $actual = $notify->execute(new Equals('myField', 1234)); 74 | 75 | $this->assertSame($expected, $actual); 76 | } 77 | } 78 | --------------------------------------------------------------------------------