diff --git a/.gitattributes b/.gitattributes index f7ed319..f74656e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ # Ignoring files for distribution archives spec/ export-ignore .cigar.json.example export-ignore +cigar.json.example export-ignore .coveralls.yml export-ignore .gitattributes export-ignore .gitignore export-ignore diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml new file mode 100644 index 0000000..dadb594 --- /dev/null +++ b/.github/workflows/analysis.yml @@ -0,0 +1,32 @@ +name: Code Analysis +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + run: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ['8.2'] + name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: curl #optional + coverage: xdebug + - name: Check PHP Version + run: php -v + - name: Install dependencies + run: composer install + - name: Run PHP_CS + run: vendor/bin/phpcs -p --colors --standard=phpcs.xml + - name: Run Psalm + run: vendor/bin/psalm --config=psalm.xml --show-info=true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c83dc2a..392bfb7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,9 +1,9 @@ name: Tests on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: run: @@ -11,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.1', '7.2', '7.3', '7.4'] + php-versions: ['8.1', '8.2'] name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} steps: - name: Checkout @@ -26,7 +26,7 @@ jobs: run: php -v - name: Install dependencies run: composer install - - name: Run Kahlan - run: vendor/bin/kahlan --reporter=verbose --clover=coverage.xml - - name: Coveralls - run: php vendor/bin/coveralls + - name: Run Tests + run: composer test + #- name: Coveralls + # run: php vendor/bin/php-coveralls diff --git a/.gitignore b/.gitignore index 85fa163..8c0f8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ /vendor /composer.lock +cigar.json .cigar.json .idea coverage.xml clover.xml +cache/ +build/ +tests/ConfigExamples/cigar.json +tests/ConfigExamples/alt.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df6a295..e44119b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ This Code of Conduct is adapted from the Contributor Covenant, version 1.1.0, av 1. Fork the repository 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Install dependencies (`composer install`) -4. Cigar uses [Kahlan](https://kahlan.github.io/docs/) as test runner. The integration tests reside within the `spec` directory. So, please make sure all test suite pass (`vendor/bin/kahlan --reporter=verbose`) +4. Cigar uses [PHPUnit](https://phpunit.de/) as test runner. The tests reside within the `tests` directory. So, please make sure all tests pass with `composer test` 5. Commit your changes (`git commit -am 'Add some feature'`) 6. Push to the branch (`git push origin my-new-feature`) 7. Create new Pull Request diff --git a/README.md b/README.md index 9c88cd1..08ba70f 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cigar -[![Build Status](https://travis-ci.org/Brunty/cigar.svg?branch=master)](https://travis-ci.org/Brunty/cigar) [![Coverage Status](https://coveralls.io/repos/github/Brunty/cigar/badge.svg?branch=master)](https://coveralls.io/github/Brunty/cigar?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f/mini.png)](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f) +[![Build Status](https://travis-ci.org/Brunty/cigar.svg?branch=main)](https://travis-ci.org/Brunty/cigar) [![Coverage Status](https://coveralls.io/repos/github/Brunty/cigar/badge.svg?branch=master)](https://coveralls.io/github/Brunty/cigar?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f/mini.png)](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f) A smoke testing tool inspired by [symm/vape](https://github.com/symm/vape) @@ -18,9 +18,17 @@ Pull via docker: `docker pull brunty/cigar` +## Upgrading from V1 - Breaking Changes + +- `.cigar.json` default config file name is now just `cigar.json` +- No longer testing against PHP 7.X and PHP 8.0 +- Command line option changes: + - `-c` is now `-f` for config file + - `-s` is now `-c` for connect-timeout + ## To use -Create a `.cigar.json` file that contains an array of json objects specifying the `url`, `status`, (optional) `content`, and (optional) `content-type` to check. +Create a `cigar.json` file that contains an array of json objects specifying the `url`, `status`, (optional) `content`, and (optional) `content-type` to check. ``` [ diff --git a/bin/cigar b/bin/cigar index 13e5a73..3d977a0 100755 --- a/bin/cigar +++ b/bin/cigar @@ -1,108 +1,128 @@ #!/usr/bin/env php now(); -if (isset($options['help'])) { - $content = << InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config file instead of the default cigar.json file'), -\033[33mUsage:\033[0m - cigar [options] + 'auth' => InputOption::create('auth', 'a', InputOption::VALUE_REQUIRED, 'Authorization header " "'), + 'header' => InputOption::create('header', 'h', InputOption::VALUE_REQUIRED, 'Custom header ": ", can be passed multiple times to send multiple headers'), + 'insecure' => InputOption::create('insecure', 'i', InputOption::VALUE_NONE, 'Allow invalid SSL certificates'), + 'url' => InputOption::create('url', 'u', InputOption::VALUE_REQUIRED, 'Base URL for checks, e.g. https://example.org/'), + + 'connect-timeout' => InputOption::create('connect-timeout', 'c', InputOption::VALUE_REQUIRED, 'Connect Timeout in seconds'), + 'timeout' => InputOption::create('timeout', 't', InputOption::VALUE_REQUIRED, 'Timeout in seconds'), + + 'quiet' => InputOption::create('quiet', 'q', InputOption::VALUE_NONE, 'Do not output any message'), + 'json' => InputOption::create('json', 'j', InputOption::VALUE_NONE, 'Output JSON'), + + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), + 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar'), +]); + +$input = new Input($inputOptions, getopt($inputOptions->shortCodes, $inputOptions->longCodes)); + +$output = new Output( + $input->getOption('quiet') + ? new QuietWriter() + : ($input->getOption('json') ? new JsonWriter() : new EchoWriter()), + $timer, +); + +$consoleCyan = ConsoleColours::cyan(); +$consoleYellow = ConsoleColours::yellow(); +$consoleGrey = ConsoleColours::grey(); +$consoleReset = ConsoleColours::reset(); +if ($input->getOption('help')) { + echo <<generateHelpOutputForOptions($inputOptions)} Created by Matt Brunt -E: matt@mfyu.co.uk -T: twitter.com/Brunty -G: github.com/brunty/cigar +{$consoleCyan}E:{$consoleReset} matt@brunty.me +{$consoleCyan}M:{$consoleReset} brunty.social/@brunty +{$consoleCyan}T:{$consoleReset} twitter.com/Brunty +{$consoleCyan}G:{$consoleReset} github.com/brunty/cigar HELP; - echo $content; - exit(0); + exit(ExitCode::SUCCESS->value); } -if (isset($options['version'])) { +if ($input->getOption('version')) { $version = CIGAR_VERSION; - $content = <<value); } -$writer = isset($options['j']) || isset($options['json']) ? new JsonWriter() : new EchoWriter(); -$outputter = new Outputter(isset($options['quiet']), $writer); - -$file = $options['c'] ?? ($options['config'] ?? '.cigar.json'); -$baseUrl = $options['u'] ?? ($options['url'] ?? null); +$configFile = (string) $input->getOption('config') ?: 'cigar.json'; +$baseUrl = (string) $input->getOption('url') ?: null; -if ( ! file_exists($file)) { - $outputter->writeErrorLine('Could not find configuration file: ' . $file); - exit(1); +if ( ! file_exists($configFile)) { + $output->writeErrorLine('Could not find configuration file: ' . $configFile); + exit(ExitCode::FAILURE->value); } -$secure = ! (isset($options['i']) || isset($options['insecure'])); -$authorization = $options['a'] ?? $options['auth'] ?? null; -$headers = (array) ($options['h'] ?? $options['header'] ?? []); -$connectTimeout = $options['s'] ?? $options['connect-timeout'] ?? null; -$timeout = $options['t'] ?? $options['timeout'] ?? null; - try { - $domains = (new Parser($baseUrl, $connectTimeout, $timeout))->parse($file); -} catch (\Throwable $e) { - $outputter->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); - exit(1); + $configParser = new ConfigParser( + $baseUrl, + (int) $input->getOption('connect-timeout') ?: null, + (int) $input->getOption('timeout') ?: null, + ); + $domains = $configParser->parse($configFile); +} catch (Throwable $e) { + $output->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); + exit(ExitCode::FAILURE->value); } -$results = (new AsyncChecker($secure, $authorization, $headers))->check($domains); -$passedResults = array_filter($results, function (Result $result) { - return $result->hasPassed(); -}); - -$outputter->outputResults($passedResults, $results, $start); +$results = (new AsyncChecker( + ! $input->getOption('insecure'), + (string) $input->getOption('auth') ?: null, + (array) $input->getOption('header') ?: [], +))->check($domains); -if (count($passedResults) !== count($results)) { - exit(1); -} +$output->outputResults($results, $start); -exit(0); +exit($results->hasPassed() ? ExitCode::SUCCESS->value : ExitCode::FAILURE->value); diff --git a/.cigar.json.example b/cigar.json.example similarity index 100% rename from .cigar.json.example rename to cigar.json.example diff --git a/composer.json b/composer.json index 722c27c..d6f77e6 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "authors": [ { "name": "Matt Brunt", - "email": "matt@mfyu.co.uk" + "email": "matt@brunty.me" } ], "autoload": { @@ -20,20 +20,39 @@ } }, "require": { - "php": ">=7.0", + "php": "8.1.*||8.2.*", "ext-curl": "*", "ext-json": "*" }, "require-dev": { - "symfony/process": "^3.3", "mikey179/vfsstream": "^1.6", - "satooshi/php-coveralls": "^1.0", - "kahlan/kahlan": "^4.0" + "php-coveralls/php-coveralls": "^2.6", + "symfony/process": "^6.3", + "vimeo/psalm": "^5.15", + "squizlabs/php_codesniffer": "^3.7", + "slevomat/coding-standard": "^8.13", + "phpunit/phpunit": "^10.3", + "infection/infection": "^0.27.2" }, "bin": [ "bin/cigar" ], "scripts": { - "test": "vendor/bin/kahlan --reporter=verbose" + "test": "XDEBUG_MODE=coverage php vendor/bin/phpunit --color=always --coverage-html=./build/coverage", + "test:unit": "@test --testsuite=unit", + "test:mutation": "vendor/bin/infection", + "psalm": "vendor/bin/psalm --config=psalm.xml --show-info=true --no-cache", + "cs": "vendor/bin/phpcs -p --colors --standard=phpcs.xml", + "cs:fix": "vendor/bin/phpcbf -p --colors --standard=phpcs.xml", + "check": [ + "@psalm", + "@cs" + ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "infection/extension-installer": true + } } } diff --git a/docker/2.0/Dockerfile b/docker/2.0/Dockerfile new file mode 100755 index 0000000..7653d75 --- /dev/null +++ b/docker/2.0/Dockerfile @@ -0,0 +1,24 @@ +# Cigar Docker Container +FROM composer AS composer + +FROM php:8.2-cli-alpine + +LABEL maintainer="matt@brunty.me" + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +# Goto temporary directory +WORKDIR /tmp + +# Run composer and cigar installation. +RUN composer require "brunty/cigar:^2.0" --optimize-autoloader --prefer-dist --no-scripts && \ + ln -s /tmp/vendor/bin/cigar /usr/local/bin/cigar + +# Set up the application directory. +VOLUME ["/app"] +WORKDIR /app + +# Set up the command arguments. +ENTRYPOINT ["/usr/local/bin/cigar"] + + diff --git a/docker/README.md b/docker/README.md index 2729845..3d6b80e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,7 +2,7 @@ Replace the versions as appropriate ``` -docker build . -t brunty/cigar:1.12.4 -f ./docker/1.12/Dockerfile +docker build . -t brunty/cigar:2.0.0 -f ./docker/2.0/Dockerfile docker push brunty/cigar --all-tags ``` diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..ab6d189 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,52 @@ +{ + "source": { + "directories": [ + "src" + ], + "excludes": [ + "AsyncChecker.php", + "tests", + ] + }, + "timeout": 10, + "logs": { + "text": "build/infection/infection.log", + "html": "build/infection/infection.html", + "summary": "build/infection/summary.log", + "json": "build/infection/infection-log.json", + "perMutator": "build/infection/per-mutator.md", + "github": true, + }, + "testFramework":"phpunit", + "testFrameworkOptions": "--testsuite=unit", + "mutators": { + "@default": true, + "CastArray": { + "ignore": [ + "Brunty\\Cigar\\ConfigParser::parse::31", + ], + }, + "CastString": { + "ignore": [ + "Brunty\\Cigar\\ConfigParser::parse::37", + ], + }, + "DecrementInteger": { + "ignore": [ + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::38", + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::39", + ] + }, + "IncrementInteger": { + "ignore": [ + "Brunty\\Cigar\\ConfigParser::parse::31" + ] + }, + "GreaterThan": { + "ignore": [ + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::51", + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::55", + ] + } + } +} diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..3939cbd --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,201 @@ + + + + + + src/App/Command/TwigLintCommand.php + + + + + + + + src/Migrations + tests/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/App/Validator/Rule/*Exception.php + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app/bootstrap.php + cron/index.php + + + + + + + + + + + + + + + + + tests/integration/* + tests/functional/* + tests/integration/* + tests/unit/* + + + + + + bin/cigar + src + tests + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..cb79cae --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + tests/Functional + + + tests/Integration + + + tests/Unit + + + + + + src + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..59ecbc6 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/spec/AsyncCheckerSpec.php b/spec/AsyncCheckerSpec.php deleted file mode 100644 index 3bc0060..0000000 --- a/spec/AsyncCheckerSpec.php +++ /dev/null @@ -1,81 +0,0 @@ -check($domains); - - $expected = [ - new Result($domain, 200, '', 'text/html; charset=utf-8'), - ]; - - expect($results)->toEqual($expected); - }); - - it('checks more than one domain', function () { - - $domains = [ - new Url('http://httpbin.org/status/200', 200), - new Url('http://httpbin.org/status/500', 500), - new Url('http://httpbin.org/status/404', 404), - ]; - - $results = (new AsyncChecker)->check($domains); - - $expected = array_map(function (Url $domain) { - return new Result($domain, $domain->getStatus(), '', 'text/html; charset=utf-8'); - }, $domains); - - expect($results)->toEqual($expected); - }); - - it('checks authorization header', function () { - $domain = new Url('http://httpbin.org/get', 200); - $expectedAuthHeader = 'Basic dXNyOnBzd2Q='; - - $results = (new AsyncChecker(false, $expectedAuthHeader))->check([$domain]); - - $decodedContent = json_decode($results[0]->getContents(), true); - $actualAuthHeader = $decodedContent['headers']['Authorization'] ?? null; - - expect($actualAuthHeader)->toEqual($expectedAuthHeader); - }); - - context('when SSL verification is disabled', function () { - it('checks a domain that has an invalid certificate', function () { - // Need to change for a better setup URL that doesn't default to a potentially unknown site - $domain = new Url('https://cigar-do-not-work.apps.brunty.me', 200); - $domains = [$domain]; - - $results = (new AsyncChecker(false))->check($domains); - - $expected = [ - new Result($domain, 200), - ]; - - expect($results[0]->getStatusCode())->toEqual($expected[0]->getStatusCode()); - }); - }); - - context('when timeouts are set', function () { - it('checks a URL that will timeout', function () { - // Need to change for a better setup URL that doesn't default to a potentially unknown site - $domain = new Url('https://httpbin.org/delay/3', 200, null, null, 1, 1); - $domains = [$domain]; - - $results = (new AsyncChecker())->check($domains); - - $expected = [ - new Result($domain, 0), - ]; - - expect($results[0]->getStatusCode())->toEqual($expected[0]->getStatusCode()); - }); - }); -}); diff --git a/spec/CigarCliSpec.php b/spec/CigarCliSpec.php deleted file mode 100755 index 97fe9b4..0000000 --- a/spec/CigarCliSpec.php +++ /dev/null @@ -1,97 +0,0 @@ -run(); - }); - - it('passes if given good URLs to check', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('is quiet if given the option', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar --quiet'); - $process->run(); - - expect($process->getOutput())->toBe(''); - expect($process->getExitCode())->toBe(0); - }); - - it('fails if given bad URLs to check', function () { - $process = new Process('cd spec && cp stubs/.cigar.fail.json .cigar.json && ../bin/cigar'); - $process->run(); - - expect($process->getExitCode())->toBe(1); - }); - - it('can be given an alternative configuration file to load with a short command line flag', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar -c .config.json'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be given an alternative configuration file to load with a long command line flag', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar --config .config.json'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('fails with insecure certs', function () { - $process = new Process('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar'); - $process->run(); - - expect($process->getExitCode())->toBe(1); - }); - - it('passes with insecure certs if the insecure flag is specified', function () { - $process = new Process('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar --insecure'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed a base URL as a short command argument', function () { - $process = new Process('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar -u http://httpbin.org'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed a base URL as a full command argument', function () { - $process = new Process('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar --url=http://httpbin.org'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed headers as full arguments', function () { - $process = new Process('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed headers as short arguments', function () { - $process = new Process('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed timeout arguments and it overwrites the configured value in .cigar.json', function () { - $process = new Process('cd spec && cp stubs/.cigar.timeouts.json .cigar.json && ../bin/cigar -t 1'); - $process->run(); - - expect($process->getExitCode())->toBe(1); - }); -}); diff --git a/spec/JsonWriterSpec.php b/spec/JsonWriterSpec.php deleted file mode 100644 index 8d1ef60..0000000 --- a/spec/JsonWriterSpec.php +++ /dev/null @@ -1,53 +0,0 @@ -domain = new \Brunty\Cigar\Url('url', 418, 'teapot', 'teapot'); - $this->results = [ - new \Brunty\Cigar\Result($this->domain, 418, 'teapot', 'teapot'), - new \Brunty\Cigar\Result($this->domain, 419), - ]; - }); - - it('outputs an error line', function () { - $fn = function () { - (new JsonWriter)->writeErrorLine('Error message'); - }; - - expect($fn)->toEcho('{"type":"error","message":"Error message"}' . PHP_EOL); - }); - - it('outputs results if all results have passed', function () { - $results = $this->results; - - $fn = function () use ($results) { - $writer = new JsonWriter(); - $writer->writeResults(2, 2, true, 1.5, ...$results); - }; - - $output = <<< OUTPUT -{"type":"results","time_taken":1.5,"passed":true,"results_count":2,"results_passed_count":2,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} - -OUTPUT; - - expect($fn)->toEcho($output); - }); - - it('outputs results if some URLs have failed', function () { - $results = $this->results; - - $fn = function () use ($results) { - $writer = new JsonWriter(); - $writer->writeResults(1, 2, false, 1.5, ...$results); - }; - - $output = <<< OUTPUT -{"type":"results","time_taken":1.5,"passed":false,"results_count":2,"results_passed_count":1,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} - -OUTPUT; - - expect($fn)->toEcho($output); - }); -}); diff --git a/spec/OutputterSpec.php b/spec/OutputterSpec.php deleted file mode 100644 index e019864..0000000 --- a/spec/OutputterSpec.php +++ /dev/null @@ -1,66 +0,0 @@ -domain = new \Brunty\Cigar\Url('url', 418, 'teapot', 'teapot'); - $this->results = [ - new \Brunty\Cigar\Result($this->domain, 418, 'teapot', 'teapot'), - new \Brunty\Cigar\Result($this->domain, 419), - ]; - $this->passedResults = array_filter($this->results, function (Result $result) { - return $result->hasPassed(); - }); - }); - - it('outputs an error line', function () { - $fn = function () { - (new Outputter)->writeErrorLine('Error message'); - }; - - expect($fn)->toEcho("\033[31mError message\033[0m\n"); - }); - - it('does not output an error line when run quietly', function () { - $fn = function () { - (new Outputter($quiet = true))->writeErrorLine('Error message'); - }; - - expect($fn)->toEcho(''); - }); - - it('outputs results if some results have passed', function () { - $results = $this->results; - $passedResults = $this->passedResults; - - allow('microtime')->toBeCalled()->andReturn(3); - - $fn = function () use ($passedResults, $results) { - (new Outputter)->outputResults($passedResults, $results, 1.5); - }; - - $output = <<< OUTPUT -\e[32m✓ url [418:418] [teapot:teapot] teapot\e[0m -\e[31m✘ url [418:419] [teapot:] teapot\e[0m - -[\e[31m1/2\e[0m] passed in 1.5s - - -OUTPUT; - - expect($fn)->toEcho($output); - }); - - it('outputs results quietly', function () { - $results = $this->results; - $passedResults = $this->passedResults; - - $fn = function () use ($passedResults, $results) { - (new Outputter(true))->outputResults($passedResults, $results, 1.5); - }; - - expect($fn)->toEcho(''); - }); -}); diff --git a/spec/ParserSpec.php b/spec/ParserSpec.php deleted file mode 100644 index b106b8d..0000000 --- a/spec/ParserSpec.php +++ /dev/null @@ -1,108 +0,0 @@ - '[ - { - "url": "http://httpbin.org/status/418", - "status": 418 - }, - { - "url": "http://httpbin.org/status/200", - "status": 200 - }, - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot", - "content-type": "kitchen/teapot", - "connect-timeout": 1, - "timeout": 2 - } -] -', - ]; - vfsStream::setup('root', null, $structure); - - $results = (new Parser)->parse('vfs://root/.cigar.json'); - - $expected = [ - new Url('http://httpbin.org/status/418', 418), - new Url('http://httpbin.org/status/200', 200), - new Url('http://httpbin.org/status/418', 418, 'teapot', 'kitchen/teapot', 1, 2), - ]; - - expect($results)->toEqual($expected); - }); - - it('lets errors be thrown on parsing a file', function () { - $structure = [ - '.cigar.json' => 'http://httpbin.org/status/418', - ]; - vfsStream::setup('root', null, $structure); - - $fn = function () { - (new Parser)->parse('vfs://root/.cigar.json'); - }; - - expect($fn)->toThrow(new ParseError('Could not parse vfs://root/.cigar.json')); - }); - - it('parses a file that contains both relative and absolute URLs', function () { - $structure = [ - '.cigar.json' => '[ - { - "url": "/status/418", - "status": 418 - }, - { - "url": "status/200", - "status": 200 - }, - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot" - } -] -', - ]; - vfsStream::setup('root', null, $structure); - - $results = (new Parser('http://httpbin.org'))->parse('vfs://root/.cigar.json'); - - $expected = [ - new Url('http://httpbin.org/status/418', 418), - new Url('http://httpbin.org/status/200', 200), - new Url('http://httpbin.org/status/418', 418, 'teapot'), - ]; - - expect($results)->toEqual($expected); - }); - - it('throws an exception if a URL cannot be parsed', function () { - $structure = [ - '.cigar.json' => '[ - { - "url": "http://:80", - "status": 418, - "content": "teapot" - } -] -', - ]; - vfsStream::setup('root', null, $structure); - - - $fn = function () { - (new Parser)->parse('vfs://root/.cigar.json'); - }; - - expect($fn)->toThrow(new ParseError('Could not parse URL: http://:80')); - }); -}); diff --git a/spec/ResultsSpec.php b/spec/ResultsSpec.php deleted file mode 100644 index 94ca34f..0000000 --- a/spec/ResultsSpec.php +++ /dev/null @@ -1,40 +0,0 @@ -hasPassed())->toBe(true); - }); - - it('fails if status codes do not match', function () { - $domain = new Url('http://httpbin.org/status/200', 200); - $result = new Result($domain, 201); - - expect($result->hasPassed())->toBe(false); - }); - - it('passes if the response contains matching content', function () { - $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); - $result = new Result($domain, 200, '

foobar

'); - expect($result->hasPassed())->toBe(true); - }); - - it('fails if the response does not contain matching content', function () { - $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); - $result = new Result($domain, 200, '

hi there

'); - expect($result->hasPassed())->toBe(false); - }); - - it('returns the domain and status code', function () { - $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); - $result = new Result($domain, 200, '

hi there

'); - - expect($result->getUrl())->toBe($domain); - expect($result->getStatusCode())->toBe(200); - }); -}); diff --git a/spec/stubs/.cigar.fail.json b/spec/stubs/.cigar.fail.json deleted file mode 100644 index 0f3d661..0000000 --- a/spec/stubs/.cigar.fail.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot" - }, - { - "url": "http://httpbin.org/status/200", - "status": 200, - "content-type": "text/html" - }, - { - "url": "http://httpbin.org/status/304", - "status": 304 - }, - { - "url": "http://httpbin.org/status/500", - "status": 501 - }, - { - "url": "http://httpbin.org/delay/3", - "status": 200, - "request-timeout": 1, - "response-timeout": 1 - }, - { - "url": "http://httpbin.org/drip?duration=3", - "status": 200, - "request-timeout": 1, - "response-timeout": 1 - } -] diff --git a/spec/stubs/.cigar.headers.json b/spec/stubs/.cigar.headers.json deleted file mode 100755 index 856017f..0000000 --- a/spec/stubs/.cigar.headers.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "url": "http://httpbin.org/headers", - "status": 200, - "content": "some-header-content-here" - }, - { - "url": "http://httpbin.org/headers", - "status": 200, - "content": "more-header-content-here" - } -] diff --git a/spec/stubs/.cigar.insecure.json b/spec/stubs/.cigar.insecure.json deleted file mode 100644 index 596ac3f..0000000 --- a/spec/stubs/.cigar.insecure.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "url": "https://cigar-do-not-work.apps.brunty.me", - "status": 200 - } -] diff --git a/spec/stubs/.cigar.pass.json b/spec/stubs/.cigar.pass.json deleted file mode 100644 index a422997..0000000 --- a/spec/stubs/.cigar.pass.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot" - }, - { - "url": "http://httpbin.org/status/200", - "status": 200, - "content-type": "text/html" - }, - { - "url": "http://httpbin.org/status/304", - "status": 304 - }, - { - "url": "http://httpbin.org/status/500", - "status": 500 - }, - { - "url": "http://httpbin.org/delay/3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - }, - { - "url": "http://httpbin.org/drip?duration=3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - } -] diff --git a/spec/stubs/.cigar.timeouts.json b/spec/stubs/.cigar.timeouts.json deleted file mode 100644 index f3b12cc..0000000 --- a/spec/stubs/.cigar.timeouts.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "url": "http://httpbin.org/delay/3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - }, - { - "url": "http://httpbin.org/drip?duration=3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - } -] diff --git a/spec/stubs/.cigar.without-base.json b/spec/stubs/.cigar.without-base.json deleted file mode 100644 index 57fb68e..0000000 --- a/spec/stubs/.cigar.without-base.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "url": "/status/418", - "status": 418, - "content": "teapot" - }, - { - "url": "status/200", - "status": 200, - "content-type": "text/html" - }, - { - "url": "http://httpbin.org/status/304", - "status": 304 - } -] diff --git a/src/AsyncChecker.php b/src/AsyncChecker.php index a95ac8c..4a50515 100644 --- a/src/AsyncChecker.php +++ b/src/AsyncChecker.php @@ -1,67 +1,52 @@ checkSsl = $checkSsl; - $this->authorizationHeader = $authorizationHeader; - $this->headers = $headers; + public function __construct( + private readonly bool $checkSsl = true, + private readonly ?string $authorizationHeader = null, + private array $headers = [] + ) { } /** * @param Url[] $urlsToCheck - * - * @return Result[] */ - public function check(array $urlsToCheck): array + public function check(array $urlsToCheck): Results { $mh = curl_multi_init(); $channels = []; foreach ($urlsToCheck as $urlToCheck) { $channel = curl_init(); - $url = $urlToCheck->getUrl(); + $url = $urlToCheck->url; curl_setopt($channel, CURLOPT_URL, $url); curl_setopt($channel, CURLOPT_HEADER, 0); curl_setopt($channel, CURLOPT_RETURNTRANSFER, 1); - if ( ! $this->checkSsl) { + if (!$this->checkSsl) { curl_setopt($channel, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($channel, CURLOPT_SSL_VERIFYHOST, 0); } if ($this->authorizationHeader) { - $this->headers[] = "Authorization: {$this->authorizationHeader}"; + $this->headers[] = "Authorization: $this->authorizationHeader"; } if (!empty($this->headers)) { curl_setopt($channel, CURLOPT_HTTPHEADER, $this->headers); } - if ($urlToCheck->getConnectTimeout() !== null && $urlToCheck->getConnectTimeout() > 0) { - curl_setopt($channel, CURLOPT_CONNECTTIMEOUT, $urlToCheck->getConnectTimeout()); + if ($urlToCheck->connectTimeout !== null && $urlToCheck->connectTimeout > 0) { + curl_setopt($channel, CURLOPT_CONNECTTIMEOUT, $urlToCheck->connectTimeout); } - if ($urlToCheck->getTimeout() !== null && $urlToCheck->getTimeout() > 0) { - curl_setopt($channel, CURLOPT_TIMEOUT, $urlToCheck->getTimeout()); + + if ($urlToCheck->timeout !== null && $urlToCheck->timeout > 0) { + curl_setopt($channel, CURLOPT_TIMEOUT, $urlToCheck->timeout); } curl_multi_add_handle($mh, $channel); @@ -69,27 +54,24 @@ public function check(array $urlsToCheck): array $channels[$url] = $channel; } - $active = null; - - $running = null; do { curl_multi_exec($mh, $running); } while ($running); - $return = []; + $results = []; foreach ($urlsToCheck as $urlToCheck) { - $key = $urlToCheck->getUrl(); + $key = $urlToCheck->url; $channel = $channels[$key]; $code = (int) curl_getinfo($channel, CURLINFO_HTTP_CODE); $content = curl_multi_getcontent($channel); - $contentType = curl_getinfo($channel, CURLINFO_CONTENT_TYPE) ?? null; + $contentType = (string) curl_getinfo($channel, CURLINFO_CONTENT_TYPE); - $return[] = new Result($urlToCheck, $code, $content, $contentType); + $results[] = new Result($urlToCheck, $code, $content, $contentType); curl_multi_remove_handle($mh, $channel); } curl_multi_close($mh); - return $return; + return new Results(...$results); } } diff --git a/src/ConfigParser.php b/src/ConfigParser.php new file mode 100644 index 0000000..2c90d93 --- /dev/null +++ b/src/ConfigParser.php @@ -0,0 +1,67 @@ +baseUrl = rtrim((string) $baseUrl, '/'); + } + + /** + * @return Url[] + * + * @throws ParseError + */ + public function parse(string $filename): array + { + try { + $urls = (array) json_decode(file_get_contents($filename), true, 3, JSON_THROW_ON_ERROR); + } catch (JsonException) { + throw new ParseError('Could not parse ' . $filename); + } + + return array_map(function (array $value) { + $url = $this->getUrl((string) $value['url']); + + return new Url( + $url, + (int) $value['status'], + array_key_exists('content', $value) ? (string) $value['content'] : '', + array_key_exists('content-type', $value) ? (string) $value['content-type'] : '', + array_key_exists('connect-timeout', $value) ? (int) $value['connect-timeout'] : $this->connectTimeout, + array_key_exists('timeout', $value) ? (int) $value['timeout'] : $this->timeout, + ); + }, $urls); + } + + /** + * @throws ParseError + */ + private function getUrl(string $url): string + { + $urlParts = parse_url($url); + + if ($urlParts === false) { + throw new ParseError("Could not parse URL: $url"); + } + + if ($this->baseUrl !== '' && !isset($urlParts['host'])) { + $url = $this->baseUrl . '/' . ltrim($url, '/'); + } + + return $url; + } +} diff --git a/src/ConsoleColours.php b/src/ConsoleColours.php new file mode 100644 index 0000000..b9941ee --- /dev/null +++ b/src/ConsoleColours.php @@ -0,0 +1,57 @@ +value . 'm'; + } +} diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 12b1ca6..73cb8ee 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -1,66 +1,82 @@ results as $result) { $this->writeLine($result); - ob_flush(); } ob_end_flush(); - $this->writeStats($numberOfPassedResults, $numberOfResults, $passed, $timeDiff); + $this->writeStats($results, $timeDiff); } - private function writeLine(Result $result) + private function writeLine(Result $result): void { $contentType = ''; - list ($colour, $status) = $this->getColourAndStatus($result); - if ($result->getUrl()->getContentType() !== null) { - $contentType = " [{$result->getUrl()->getContentType()}:{$result->getContentType()}]"; + [$colour, $status] = $this->getColourAndStatus($result); + if ($result->url->contentType !== '') { + $contentType = sprintf(' [%s:%s]', $result->url->contentType, $result->contentType); } - echo "{$colour}{$status} {$result->getUrl()->getUrl()} [{$result->getUrl()->getStatus()}:{$result->getStatusCode()}]{$contentType} {$result->getUrl()->getContent()}" . self::CONSOLE_RESET . PHP_EOL; + echo sprintf( + '%s%s %s [%s:%s]%s %s' . ConsoleColours::reset() . PHP_EOL, + $colour, + $status, + $result->url->url, + $result->url->status, + $result->statusCode, + $contentType, + $result->url->content + ); } - private function writeStats(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff) + private function writeStats(Results $results, float $timeDiff): void { - $color = self::CONSOLE_GREEN; - $reset = self::CONSOLE_RESET; + $color = ConsoleColours::green(); + $reset = ConsoleColours::reset(); - if ( ! $passed) { - $color = self::CONSOLE_RED; + if ($results->hasPassed() === false) { + $color = ConsoleColours::red(); } - echo PHP_EOL . "[{$color}{$numberOfPassedResults}/{$numberOfResults}{$reset}] passed in {$timeDiff}s" . PHP_EOL . PHP_EOL; + echo sprintf( + PHP_EOL . '[%s%s/%s%s] passed in %ss' . PHP_EOL . PHP_EOL, + $color, + $results->numberOfPassedResults(), + $results->totalNumberOfResults(), + $reset, + $timeDiff + ); } + /** + * @return string[] + */ private function getColourAndStatus(Result $result): array { $passed = $result->hasPassed(); - $colour = self::CONSOLE_GREEN; + $colour = ConsoleColours::green(); $status = self::SYMBOL_PASSED; - if ( ! $passed) { - $colour = self::CONSOLE_RED; + if ($passed === false) { + $colour = ConsoleColours::red(); $status = self::SYMBOL_FAILED; } diff --git a/src/ExitCode.php b/src/ExitCode.php new file mode 100644 index 0000000..4f9064d --- /dev/null +++ b/src/ExitCode.php @@ -0,0 +1,11 @@ +inputOptions->options) === false) { + throw new InvalidArgumentException("Could not find option with $optionName"); + } + + $option = $this->inputOptions->options[$optionName]; + + $longOptionWasSubmitted = array_key_exists($option->longCode, $this->submittedOptions); + $shortOptionWasSubmitted = array_key_exists($option->shortCode, $this->submittedOptions); + + if ($option->valueIsRequired()) { + if ($longOptionWasSubmitted) { + return $this->submittedOptions[$option->longCode] ?: $option->default; + } + + if ($shortOptionWasSubmitted) { + return $this->submittedOptions[$option->shortCode] ?: $option->default; + } + + return $option->default; + } + + return $longOptionWasSubmitted || $shortOptionWasSubmitted; + } +} diff --git a/src/InputOption.php b/src/InputOption.php new file mode 100644 index 0000000..42bacc9 --- /dev/null +++ b/src/InputOption.php @@ -0,0 +1,45 @@ +shortCode . ($this->valueMode === self::VALUE_REQUIRED && $this->shortCode !== '' ? ':' : ''); + } + + public function fullLongCode(): string + { + return $this->longCode . ($this->valueMode === self::VALUE_REQUIRED && $this->longCode !== '' ? ':' : ''); + } + + public function valueIsRequired(): bool + { + return $this->valueMode === self::VALUE_REQUIRED; + } +} diff --git a/src/InputOptions.php b/src/InputOptions.php new file mode 100644 index 0000000..6977d1f --- /dev/null +++ b/src/InputOptions.php @@ -0,0 +1,30 @@ + $options */ + public function __construct(public readonly array $options) + { + $shortCodes = ''; + $longCodes = []; + + ksort($options); + + foreach ($options as $inputOption) { + $shortCodes .= $inputOption->fullShortCode(); + $longCodes[] = $inputOption->fullLongCode(); + } + + $this->shortCodes = $shortCodes; + $this->longCodes = $longCodes; + } +} diff --git a/src/JsonWriter.php b/src/JsonWriter.php index cbe0f1d..f0e90b5 100644 --- a/src/JsonWriter.php +++ b/src/JsonWriter.php @@ -1,10 +1,12 @@ 'error', @@ -12,15 +14,17 @@ public function writeErrorLine(string $message) ]), PHP_EOL; } - public function writeResults(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff, Result ...$results) - { + public function writeResults( + Results $results, + float $timeDiff, + ): void { echo json_encode([ 'type' => 'results', 'time_taken' => $timeDiff, - 'passed' => $passed, - 'results_count' => $numberOfResults, - 'results_passed_count' => $numberOfPassedResults, - 'results' => array_map([$this, 'line'], $results), + 'passed' => $results->hasPassed(), + 'results_count' => $results->totalNumberOfResults(), + 'results_passed_count' => $results->numberOfPassedResults(), + 'results' => array_map([$this, 'line'], $results->results), ]), PHP_EOL; } @@ -28,12 +32,12 @@ private function line(Result $result): array { return [ 'passed' => $result->hasPassed(), - 'url' => $result->getUrl()->getUrl(), - 'status_code_expected' => $result->getUrl()->getStatus(), - 'status_code_actual' => $result->getStatusCode(), - 'content_type_expected' => $result->getUrl()->getContentType(), - 'content_type_actual' => $result->getContentType(), - 'content_expected' => $result->getUrl()->getContent(), + 'url' => $result->url->url, + 'status_code_expected' => $result->url->status, + 'status_code_actual' => $result->statusCode, + 'content_type_expected' => $result->url->contentType, + 'content_type_actual' => $result->contentType, + 'content_expected' => $result->url->content, ]; } } diff --git a/src/Output.php b/src/Output.php new file mode 100644 index 0000000..b07ba58 --- /dev/null +++ b/src/Output.php @@ -0,0 +1,105 @@ +writer->writeErrorLine($message); + } + + public function outputResults(Results $results, float $startedAt): void + { + $timeDiff = round($this->timer->now() - $startedAt, 3); + + $this->writer->writeResults($results, $timeDiff); + } + + public function generateHelpOutputForOptions(InputOptions $inputOptions): string + { + $consoleGreen = ConsoleColours::green(); + $consoleReset = ConsoleColours::reset(); + + $output = ''; + + if ($inputOptions->options !== []) { + $output = ConsoleColours::yellow() . 'Options:' . $consoleReset . PHP_EOL; + } + + $longestShortCodeLength = 0; + $longestLongCodeLength = 0; + $valuePlaceholderLength = strlen(self::VALUE_PLACEHOLDER); + + foreach ($inputOptions->options as $option) { + $shortCodeLength = 3; // at it's shortest it is "-c " + $longCodeLength = strlen($option->longCode) + 3; // +3 is for double dash before & the space or = after + + if ($option->valueIsRequired()) { + $shortCodeLength += $valuePlaceholderLength; + $longCodeLength += $valuePlaceholderLength; + } + + if ($shortCodeLength > $longestShortCodeLength) { + $longestShortCodeLength = $shortCodeLength; + } + + if ($longCodeLength > $longestLongCodeLength) { + $longestLongCodeLength = $longCodeLength; + } + } + + foreach ($inputOptions->options as $option) { + $shortCode = $this->getShortCodeOutput($option); + $longCode = $this->getLongCodeOutput($option); + + $shortCode = str_pad($shortCode, $longestShortCodeLength); + $longCode = str_pad($longCode, $longestLongCodeLength); + + $output .= sprintf( + ' %s%s %s%s %s' . PHP_EOL, + $consoleGreen, + $shortCode, + $longCode, + $consoleReset, + $option->description + ); + } + + return $output; + } + + private function getShortCodeOutput(InputOption $option): string + { + if ($option->shortCode === '') { + return ''; + } + + $shortCode = "-$option->shortCode"; + + if ($option->valueIsRequired()) { + $shortCode = "-$option->shortCode " . self::VALUE_PLACEHOLDER; + } + + return $shortCode; + } + + private function getLongCodeOutput(InputOption $option): string + { + $longCode = "--$option->longCode"; + + if ($option->valueIsRequired()) { + $longCode = "--$option->longCode=" . self::VALUE_PLACEHOLDER; + } + + return $longCode; + } +} diff --git a/src/Outputter.php b/src/Outputter.php deleted file mode 100644 index dfbd006..0000000 --- a/src/Outputter.php +++ /dev/null @@ -1,46 +0,0 @@ -isQuiet = $isQuiet; - $this->writer = $writer instanceof WriterInterface ? $writer : new EchoWriter(); - } - - public function writeErrorLine(string $message) - { - if ($this->isQuiet) { - return; - } - - $this->writer->writeErrorLine($message); - } - - public function outputResults(array $passedResults, array $results, float $startTime) - { - if ($this->isQuiet) { - return; - } - - $numberOfResults = count($results); - $numberOfPassedResults = count($passedResults); - $end = microtime(true); - $timeDiff = round($end - $startTime, 3); - $passed = $numberOfPassedResults === $numberOfResults; - - $this->writer->writeResults($numberOfPassedResults, $numberOfResults, $passed, $timeDiff, ...$results); - } -} diff --git a/src/Parser.php b/src/Parser.php deleted file mode 100644 index c89e4b0..0000000 --- a/src/Parser.php +++ /dev/null @@ -1,77 +0,0 @@ -baseUrl = rtrim($baseUrl, '/'); - $this->connectTimeout = $connectTimeout; - $this->timeout = $timeout; - } - - /** - * @param string $filename - * - * @return Url[] - * @throws \ParseError - */ - public function parse(string $filename): array - { - $urls = json_decode(file_get_contents($filename), true); - - if($urls === null) { - throw new \ParseError('Could not parse ' . $filename); - } - - return array_map(function($value) { - $url = $this->getUrl($value['url']); - - return new Url( - $url, - $value['status'], - $value['content'] ?? null, - $value['content-type'] ?? null, - $value['connect-timeout'] ?? $this->connectTimeout, - $value['timeout'] ?? $this->timeout - ); - }, $urls); - } - - /** - * @param string $url - * - * @return string - * @throws \ParseError - */ - private function getUrl(string $url): string - { - $urlParts = parse_url($url); - - if ($urlParts === false) { - throw new \ParseError("Could not parse URL: $url"); - } - - if ($this->baseUrl !== null && ! isset($urlParts['host'])) { - $url = $this->baseUrl . '/' . ltrim($url, '/'); - } - - return $url; - } -} diff --git a/src/QuietWriter.php b/src/QuietWriter.php new file mode 100644 index 0000000..10b8113 --- /dev/null +++ b/src/QuietWriter.php @@ -0,0 +1,16 @@ +url = $url; - $this->statusCode = $statusCode; - $this->contents = $contents; - $this->contentType = $contentType; + public function __construct( + public readonly Url $url, + public readonly int $statusCode, + public readonly string $contents = '', + public readonly string $contentType = '' + ) { } public function hasPassed(): bool @@ -37,36 +19,16 @@ public function hasPassed(): bool return $this->statusMatches() && $this->responseMatchesContentType() && $this->responseHasContent(); } - public function getContentType() - { - return $this->contentType; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } - - public function getUrl(): Url - { - return $this->url; - } - - public function getContents() - { - return $this->contents; - } - private function statusMatches(): bool { - return $this->statusCode === $this->url->getStatus(); + return $this->statusCode === $this->url->status; } private function responseMatchesContentType(): bool { - $expectedContentType = $this->url->getContentType(); + $expectedContentType = $this->url->contentType; - if ($expectedContentType === null || $this->contentType === null) { + if ($expectedContentType === '') { return true; // nothing to check } @@ -75,9 +37,9 @@ private function responseMatchesContentType(): bool private function responseHasContent(): bool { - $expectedContent = $this->url->getContent(); + $expectedContent = $this->url->content; - if ($expectedContent === null) { + if ($expectedContent === '') { return true; // nothing to check } diff --git a/src/Results.php b/src/Results.php new file mode 100644 index 0000000..275d153 --- /dev/null +++ b/src/Results.php @@ -0,0 +1,45 @@ +results = $results; + } + + public function numberOfPassedResults(): int + { + if ($this->passedResults !== null) { + return $this->passedResults; + } + + $this->passedResults = 0; + + foreach ($this->results as $result) { + if ($result->hasPassed()) { + $this->passedResults++; + } + } + + return $this->passedResults; + } + + public function totalNumberOfResults(): int + { + return count($this->results); + } + + public function hasPassed(): bool + { + return $this->totalNumberOfResults() === $this->numberOfPassedResults(); + } +} diff --git a/src/SystemTimer.php b/src/SystemTimer.php new file mode 100644 index 0000000..34881eb --- /dev/null +++ b/src/SystemTimer.php @@ -0,0 +1,13 @@ +url = $url; - $this->status = $status; - $this->content = $content; - $this->contentType = $contentType; - $this->connectTimeout = $connectTimeout; - $this->timeout = $timeout; - } - - public function getUrl(): string - { - return $this->url; - } - - public function getStatus(): int - { - return $this->status; - } - - public function getContent() - { - return $this->content; - } - - public function getContentType() - { - return $this->contentType; - } - - public function getConnectTimeout() - { - return $this->connectTimeout; - } - - public function getTimeout() - { - return $this->timeout; } } diff --git a/src/Writer.php b/src/Writer.php new file mode 100644 index 0000000..0d63b88 --- /dev/null +++ b/src/Writer.php @@ -0,0 +1,12 @@ +run(); + } + + #[Test] + public function it_displays_help_output(): void + { + $process = Process::fromShellCommandline('./bin/cigar --help'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + $this->assertStringContainsString('--connect-timeout=VALUE', $process->getOutput()); + } + + #[Test] + public function it_displays_version_output(): void + { + $process = Process::fromShellCommandline('./bin/cigar --version'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + $this->assertStringContainsString('Version', $process->getOutput()); + } + + #[Test] + public function it_passes_if_given_good_urls_to_check(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json cigar.json && ../../bin/cigar'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_is_quiet_if_the_option_is_passed(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json cigar.json && ../../bin/cigar --quiet'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + $this->assertSame('', $process->getOutput()); + } + + #[Test] + public function it_fails_if_given_bad_urls_to_check(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp fail.json cigar.json && ../../bin/cigar'); + $process->run(); + + $this->assertSame(1, $process->getExitCode()); + } + + #[Test] + public function it_can_be_given_an_alternative_config_file_by_a_short_command_line_flag(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json alt.json && ../../bin/cigar -f alt.json'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_given_an_alternative_config_file_by_a_long_command_line_flag(): void + { + + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json alt.json && ../../bin/cigar --config alt.json'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_fails_with_insecure_certs(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp insecure.json cigar.json && ../../bin/cigar'); + $process->run(); + + $this->assertSame(1, $process->getExitCode()); + } + + #[Test] + public function it_passes_with_insecure_certs_if_the_long_flag_is_given(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp insecure.json cigar.json && ../../bin/cigar --insecure'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_passes_with_insecure_certs_if_the_short_flag_is_given(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp insecure.json cigar.json && ../../bin/cigar -i'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_a_base_url_as_a_short_command_argument(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp without-base.json cigar.json && ../../bin/cigar -u http://httpbin.org'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_a_base_url_as_a_long_command_argument(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp without-base.json cigar.json && ../../bin/cigar --url http://httpbin.org'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_headers_as_long_options(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp headers.json cigar.json && ../../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_headers_as_short_options(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp headers.json cigar.json && ../../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_bt_passed_timeout_arguments_and_overwrites_the_configured_value_in_the_config_file(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp timeouts.json cigar.json && ../../bin/cigar -t 1'); + $process->run(); + + $this->assertSame(1, $process->getExitCode()); + } +} diff --git a/tests/Integration/AsyncCheckerTest.php b/tests/Integration/AsyncCheckerTest.php new file mode 100644 index 0000000..3b13945 --- /dev/null +++ b/tests/Integration/AsyncCheckerTest.php @@ -0,0 +1,95 @@ +check($urls); + + $this->assertFalse($results->hasPassed()); + $this->assertEquals(5, $results->totalNumberOfResults()); + $this->assertEquals(3, $results->numberOfPassedResults()); + } + + #[Test] + public function it_checks_urls_with_authorization(): void + { + $urls = [ + new Url('http://httpbin.org/headers', 200, 'some-random-string'), + ]; + + $checker = new AsyncChecker(true, 'some-random-string'); + + $results = $checker->check($urls); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_checks_urls_with_headers_set(): void + { + $urls = [ + new Url('http://httpbin.org/headers', 200, 'Header-Foo'), + ]; + + $checker = new AsyncChecker(true, null, ['Header-Foo: Content-Bar']); + + $results = $checker->check($urls); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_checks_urls_with_insecure_ssl(): void + { + $urls = [ + new Url('https://cigar-do-not-work.apps.brunty.me', 403, ''), + ]; + + $checker = new AsyncChecker(false); + + $results = $checker->check($urls); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_checks_urls_with_insecure_ssl_fail(): void + { + $urls = [ + new Url('https://cigar-do-not-work.apps.brunty.me', 403, ''), + ]; + + $checker = new AsyncChecker(); + + $results = $checker->check($urls); + + $this->assertFalse($results->hasPassed()); + } +} diff --git a/tests/Output/output-results-all-passed.json b/tests/Output/output-results-all-passed.json new file mode 100644 index 0000000..9012260 --- /dev/null +++ b/tests/Output/output-results-all-passed.json @@ -0,0 +1 @@ +{"type":"results","time_taken":1.5,"passed":true,"results_count":2,"results_passed_count":2,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} diff --git a/tests/Output/output-results-some-failed.json b/tests/Output/output-results-some-failed.json new file mode 100644 index 0000000..7449dc9 --- /dev/null +++ b/tests/Output/output-results-some-failed.json @@ -0,0 +1 @@ +{"type":"results","time_taken":1.5,"passed":false,"results_count":2,"results_passed_count":1,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} diff --git a/tests/Unit/ConfigParserTest.php b/tests/Unit/ConfigParserTest.php new file mode 100644 index 0000000..e39de4a --- /dev/null +++ b/tests/Unit/ConfigParserTest.php @@ -0,0 +1,127 @@ + '[ + { + "url": "http://httpbin.org/status/200", + "status": 200 + }, + { + "url": "http://httpbin.org/status/418", + "status": 418, + "content": "teapot", + "content-type": "kitchen/teapot", + "connect-timeout": 1, + "timeout": 2 + }, + { + "url": "http://httpbin.org/status/200", + "status": 200, + "content": 1, + "content-type": 2, + "connect-timeout": "3", + "timeout": "4" + } +] +', + ]; + vfsStream::setup('root', null, $structure); + + $results = (new ConfigParser())->parse('vfs://root/cigar.json'); + + $expected = [ + new Url('http://httpbin.org/status/200', 200), + new Url('http://httpbin.org/status/418', 418, 'teapot', 'kitchen/teapot', 1, 2), + new Url('http://httpbin.org/status/200', 200, '1', '2', 3, 4), + ]; + + $this->assertEquals($expected, $results); + } + + #[Test] + public function it_parses_a_file_that_contains_both_absolute_and_relative_urls(): void + { + $structure = [ + 'cigar.json' => '[ + { + "url": "/status/418", + "status": 418 + }, + { + "url": "status/200", + "status": "200" + }, + { + "url": "http://httpbin.org/status/418", + "status": 418, + "content": "teapot" + } +] +', + ]; + vfsStream::setup('root', null, $structure); + + $results = (new ConfigParser('http://httpbin.org/'))->parse('vfs://root/cigar.json'); + + $expected = [ + new Url('http://httpbin.org/status/418', 418), + new Url('http://httpbin.org/status/200', 200), + new Url('http://httpbin.org/status/418', 418, 'teapot'), + ]; + + $this->assertEquals($expected, $results); + } + + #[Test] + public function it_throws_an_exception_if_a_url_cannot_be_parsed(): void + { + $this->expectException(ParseError::class); + $this->expectExceptionMessage('Could not parse URL: http://:80'); + $structure = [ + 'cigar.json' => '[ + { + "url": "http://:80", + "status": 418, + "content": "teapot" + } +] +', + ]; + vfsStream::setup('root', null, $structure); + + (new ConfigParser())->parse('vfs://root/cigar.json'); + } + + #[Test] + public function it_lets_errors_be_thrown_on_parsing_a_file(): void + { + $this->expectException(ParseError::class); + $this->expectExceptionMessage('Could not parse vfs://root/cigar.json'); + $structure = [ + 'cigar.json' => 'http://httpbin.org/status/418', + ]; + vfsStream::setup('root', null, $structure); + + (new ConfigParser())->parse('vfs://root/cigar.json'); + } +} diff --git a/tests/Unit/ConsoleColoursTest.php b/tests/Unit/ConsoleColoursTest.php new file mode 100644 index 0000000..9396c42 --- /dev/null +++ b/tests/Unit/ConsoleColoursTest.php @@ -0,0 +1,35 @@ +assertSame("\e[{$code}m", ConsoleColours::$method()); + } +} diff --git a/tests/Unit/EchoWriterTest.php b/tests/Unit/EchoWriterTest.php new file mode 100644 index 0000000..cb3e200 --- /dev/null +++ b/tests/Unit/EchoWriterTest.php @@ -0,0 +1,71 @@ +writer = new EchoWriter(); + } + + #[Test] + public function it_writes_an_error_line(): void + { + $expected = <<writer->writeErrorLine('error'); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } + + #[Test] + public function it_writes_results(): void + { + $results = new Results( + new Result(new Url('url1', 201, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url2', 200, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url3', 200, ''), 200, '', ''), + ); + + $expected = <<writer->writeResults($results, 0.5); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } +} diff --git a/tests/Unit/InputOptionTest.php b/tests/Unit/InputOptionTest.php new file mode 100644 index 0000000..4a38dde --- /dev/null +++ b/tests/Unit/InputOptionTest.php @@ -0,0 +1,64 @@ +assertSame('l:', $inputOption->fullShortCode()); + } + + #[Test] + public function it_returns_the_full_long_code_when_a_value_is_required(): void + { + $inputOption = InputOption::create('long-code', 'l', InputOption::VALUE_REQUIRED); + + $this->assertSame('long-code:', $inputOption->fullLongCode()); + } + + #[Test] + public function it_returns_the_full_short_code_when_a_value_is_not_required(): void + { + $inputOption = InputOption::create('long-code', 'l'); + + $this->assertSame('l', $inputOption->fullShortCode()); + } + + #[Test] + public function it_returns_the_full_long_code_when_a_value_is_not_required(): void + { + $inputOption = InputOption::create('long-code', 'l'); + + $this->assertSame('long-code', $inputOption->fullLongCode()); + } + + #[Test] + #[DataProvider('value_required_or_not')] + public function it_returns_if_value_is_required(int $valueMode, bool $expectedReturn): void + { + $inputOption = InputOption::create('long-code', 'l', $valueMode); + $this->assertSame($expectedReturn, $inputOption->valueIsRequired()); + } + + public static function value_required_or_not(): array + { + return [ + [InputOption::VALUE_NONE, false], + [InputOption::VALUE_REQUIRED, true], + ]; + } +} diff --git a/tests/Unit/InputOptionsTest.php b/tests/Unit/InputOptionsTest.php new file mode 100644 index 0000000..ba4200b --- /dev/null +++ b/tests/Unit/InputOptionsTest.php @@ -0,0 +1,29 @@ + InputOption::create('help', 'h'), + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED), + ]); + + $this->assertSame('f:h', $inputOptions->shortCodes); + $this->assertSame(['file:', 'help'], $inputOptions->longCodes); + } +} diff --git a/tests/Unit/InputTest.php b/tests/Unit/InputTest.php new file mode 100644 index 0000000..2f5c599 --- /dev/null +++ b/tests/Unit/InputTest.php @@ -0,0 +1,140 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Could not find option with my-option'); + + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE), + ]), + [], + ); + + $input->getOption('my-option'); + } + + #[Test] + public function it_returns_false_if_a_no_value_option_was_not_submitted_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', 'h'), + ]), + [], + ); + + $this->assertFalse($input->getOption('help')); + } + + #[Test] + public function it_returns_true_if_a_no_value_option_was_submitted_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', 'h'), + ]), + ['h' => false], + ); + + $this->assertTrue($input->getOption('help')); + } + + #[Test] + public function it_returns_the_default_value_if_a_required_value_option_was_not_submitted_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + [], + ); + + $this->assertSame('default-value', $input->getOption('file')); + } + + #[Test] + public function it_returns_the_default_value_if_a_required_value_option_was_submitted_with_no_value_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['f' => false], // getopt() wouldn't even put this in the array, but this is just to be safe + ); + + $this->assertSame('default-value', $input->getOption('file')); + } + + #[Test] + public function it_returns_the_value_if_a_required_value_option_was_submitted_with_a_value_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['f' => 'bar'], + ); + + $this->assertSame('bar', $input->getOption('file')); + } + + #[Test] + public function it_returns_true_if_a_no_value_option_was_submitted_with_a_long_code(): void + { + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', 'h'), + ]), + ['help' => false], + ); + + $this->assertTrue($input->getOption('help')); + } + + #[Test] + public function it_returns_the_default_value_if_a_required_value_option_was_submitted_with_no_value_with_a_long_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['file' => false], // getopt() wouldn't even put this in the array, but this is just to be safe + ); + + $this->assertSame('default-value', $input->getOption('file')); + } + + #[Test] + public function it_returns_the_value_if_a_required_value_option_was_submitted_with_a_value_with_a_long_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['file' => 'bar'], + ); + + $this->assertSame('bar', $input->getOption('file')); + } +} diff --git a/tests/Unit/JsonWriterTest.php b/tests/Unit/JsonWriterTest.php new file mode 100644 index 0000000..72b7b03 --- /dev/null +++ b/tests/Unit/JsonWriterTest.php @@ -0,0 +1,65 @@ +writer = new JsonWriter(); + } + + #[Test] + public function it_writes_an_error_line(): void + { + $expected = <<writer->writeErrorLine('problem'); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } + + #[Test] + public function it_writes_results(): void + { + $results = new Results( + new Result(new Url('url', 201, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url', 200, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url', 200, 'c', 't'), 200, 'c', 't'), + ); + + $expected = <<writer->writeResults($results, 0.5); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } +} diff --git a/tests/Unit/OutputTest.php b/tests/Unit/OutputTest.php new file mode 100644 index 0000000..e05122e --- /dev/null +++ b/tests/Unit/OutputTest.php @@ -0,0 +1,116 @@ +errorMessage .= $message; + } + + public function writeResults(Results $results, float $timeDiff): void + { + } + }; + + $output = new Output($writer, new SystemTimer()); + $output->writeErrorLine('message-here'); + + $this->assertSame('message-here', $writer->errorMessage); + } + + #[Test] + public function it_outputs_results(): void + { + $writer = new class implements Writer { + public Results $results; + + public float $timeDiff = 0; + + public function writeErrorLine(string $message): void + { + } + + public function writeResults(Results $results, float $timeDiff): void + { + $this->results = $results; + $this->timeDiff = $timeDiff; + } + }; + + $timer = new class implements Timer { + public function now(): float + { + return 2.63456; + } + }; + + $output = new Output($writer, $timer); + $results = new Results(); + $output->outputResults($results, 0.5); + + $this->assertSame(2.135, $writer->timeDiff); + } + + #[Test] + public function it_generates_help_output(): void + { + $writer = new class implements Writer { + public function writeErrorLine(string $message): void + { + } + + public function writeResults(Results $results, float $timeDiff): void + { + } + }; + + $output = new Output($writer, new SystemTimer()); + + $inputOptions = new InputOptions([ + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), + 'version' => InputOption::create('version', 'v', InputOption::VALUE_NONE, 'Print the version of Cigar'), + 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config'), + 'url' => InputOption::create('url', '', InputOption::VALUE_REQUIRED, 'Something with a URL'), + ]); + $helpText = $output->generateHelpOutputForOptions($inputOptions); + + $expectedHelpText = <<assertSame($expectedHelpText, $helpText); + } +} diff --git a/tests/Unit/ResultTest.php b/tests/Unit/ResultTest.php new file mode 100644 index 0000000..5cad190 --- /dev/null +++ b/tests/Unit/ResultTest.php @@ -0,0 +1,93 @@ + [ + new Url('url', 200, 'content', 'text'), + 200, + 'content', + 'text', + true, + ], + 'Status mismatch, Content match, Content Type match' => [ + new Url('url', 201, 'content', 'text'), + 200, + 'content', + 'text', + false, + ], + 'Status match, Content match, Content Type mismatch' => [ + new Url('url', 200, 'content', 'text'), + 200, + 'content', + 'html', + false, + ], + 'Status match, Content mismatch, Content Type match' => [ + new Url('url', 200, 'content', 'text'), + 200, + 'some text', + 'text', + false, + ], + 'Status match, Content null, Content Type match' => [ + new Url('url', 200, '', 'TEXT'), + 200, + 'some text', + 'TExt', + true, + ], + 'Status match, Content empty, Content Type empty' => [ + new Url('url', 200, '', ''), + 200, + 'some text', + 'text', + true, + ], + 'Status match, Content mismatch, Content Type empty' => [ + new Url('url', 200, 'content', ''), + 200, + '', + 'text', + false, + ], + 'Status match, Content both empty, Content Type empty' => [ + new Url('url', 200, '', 't'), + 200, + '', + 'y', + false, + ], + ]; + } + + #[Test] + #[DataProvider('results')] + public function it_returns_if_it_has_passed( + Url $url, + int $statusCode, + ?string $contents, + ?string $contentType, + bool $hasPassed + ): void { + $result = new Result($url, $statusCode, $contents, $contentType); + + $this->assertSame($hasPassed, $result->hasPassed()); + } +} diff --git a/tests/Unit/ResultsTest.php b/tests/Unit/ResultsTest.php new file mode 100644 index 0000000..d57805c --- /dev/null +++ b/tests/Unit/ResultsTest.php @@ -0,0 +1,68 @@ +assertSame(1, $results->numberOfPassedResults()); + $this->assertSame(1, $results->numberOfPassedResults()); + } + + #[Test] + public function it_returns_the_total_number_of_results(): void + { + $results = new Results( + new Result(new Url('url', 200), 201), + new Result(new Url('url', 200), 201), + new Result(new Url('url', 200), 200), + ); + + $this->assertSame(3, $results->totalNumberOfResults()); + } + + #[Test] + public function it_returns_true_if_the_total_results_equals_passed_results(): void + { + $results = new Results( + new Result(new Url('url', 200), 200), + new Result(new Url('url', 200), 200), + new Result(new Url('url', 200), 200), + ); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_returns_false_if_the_total_results_does_not_equal_passed_results(): void + { + $results = new Results( + new Result(new Url('url', 200), 201), + new Result(new Url('url', 200), 200), + new Result(new Url('url', 200), 200), + ); + + $this->assertFalse($results->hasPassed()); + } +} diff --git a/tests/Unit/SystemTimerTest.php b/tests/Unit/SystemTimerTest.php new file mode 100644 index 0000000..59132b3 --- /dev/null +++ b/tests/Unit/SystemTimerTest.php @@ -0,0 +1,25 @@ +now(); + + $this->assertNotSame($now, $timer->now()); + } +}