diff --git a/.gitignore b/.gitignore index a088589..2a9dcf0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor/ tmp examples/laravel/vendor/ .phpcomplete_extended/ +node_modules diff --git a/.travis.yml b/.travis.yml index d8c848c..c233d93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: php before_install: - nvm install node && nvm use node - npm install -g dredd --no-optional - - bundle install + - npm install - composer install php: - "5.4" @@ -15,4 +15,4 @@ php: script: - vendor/bin/phpcs --standard=psr2 -n src/ - vendor/bin/phpunit - - bundle exec cucumber + - npx cucumber-js features --require features/support --tags "not @skip" diff --git a/Dockerfile b/Dockerfile index a17c8ba..01d16eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,10 @@ FROM php RUN apt-get update \ && apt-get install -y \ wget \ + nodejs \ + npm \ git \ + net-tools \ && wget https://getcomposer.org/installer \ && chmod +x installer \ - && php installer --install-dir=/usr/local/bin/ --filename=composer + && php installer --install-dir=/usr/local/bin/ --filename=composer \ diff --git a/Gemfile b/Gemfile deleted file mode 100644 index f355ae3..0000000 --- a/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# A sample Gemfile -source "https://rubygems.org" - -gem "aruba" -gem "sinatra" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 7b778d0..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,46 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - aruba (0.8.0) - childprocess (~> 0.5.6) - contracts (~> 0.9) - cucumber (>= 1.3.19) - rspec-expectations (>= 2.99) - builder (3.2.2) - childprocess (0.5.6) - ffi (~> 1.0, >= 1.0.11) - contracts (0.10) - cucumber (2.0.1) - builder (>= 2.1.2) - cucumber-core (~> 1.2.0) - diff-lcs (>= 1.1.3) - gherkin (~> 2.12) - multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.1.2) - cucumber-core (1.2.0) - gherkin (~> 2.12.0) - diff-lcs (1.2.5) - ffi (1.9.10) - gherkin (2.12.2) - multi_json (~> 1.3) - multi_json (1.11.2) - multi_test (0.1.2) - rack (1.6.4) - rack-protection (1.5.3) - rack - rspec-expectations (3.3.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-support (3.3.0) - sinatra (1.4.6) - rack (~> 1.4) - rack-protection (~> 1.4) - tilt (>= 1.3, < 3) - tilt (2.0.1) - -PLATFORMS - ruby - -DEPENDENCIES - aruba - sinatra diff --git a/Makefile b/Makefile index b247064..283db2d 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,22 @@ .PHONY: lint test -test: lint - vendor/bin/phpunit - bundle exec cucumber + +IMAGE := dredd-hooks-php +DOCKER_RUN := docker run -it --init -v $$(pwd):/src -w /src $(IMAGE) + +build: + docker build -t $(IMAGE) . + +test: build lint node_modules + $(DOCKER_RUN) bash -c \ + "vendor/bin/phpunit && \ + npx cucumber-js features --require features/support/ --tags 'not @skip'" lint: vendor - vendor/bin/phpcs --standard=psr2 -n src/ + $(DOCKER_RUN) vendor/bin/phpcs --standard=psr2 -n src/ vendor: - composer install + $(DOCKER_RUN) composer install + +node_modules: + $(DOCKER_RUN) npm install diff --git a/README.md b/README.md index 05ea90b..c6e516f 100644 --- a/README.md +++ b/README.md @@ -101,24 +101,17 @@ This would execute for any transactions "nested" underneath 'Admin'. For exampl ## Tests -When making a contribution it is very important to not break existing functionality. This project uses PHPUnit for unit testing and -uses ruby based aruba. +When making a contribution it is very important to not break existing functionality. This project uses PHPUnit for unit testing and uses nodejs for Cucumber tests. + +### Dependencies +- Docker. This takes care of the php and nodejs dependencies. The test suite can be run by following these steps: -1. Install PHPUnit and cucumber locally. From the project root directory run - - `composer install` - - `bundle install` - -2. Execute PHPUnit tests - - `vendor/bin/phpunit` - -3. Run aruba/cucumber tests - - `bundle exec cucumber` +1. Run the phpunit and Cucumber tests +```bash +make test +``` More details about the integration test can be found in the [dredd-hooks-template repo](https://github.com/apiaryio/dredd-hooks-template) diff --git a/apiary.apib b/apiary.apib deleted file mode 100644 index f13a8ed..0000000 --- a/apiary.apib +++ /dev/null @@ -1,4 +0,0 @@ -# My Api -## GET /message -+ Response 200 (text/html;charset=utf-8) - Hello World! diff --git a/features/execution_order.feature b/features/execution_order.feature index f9383b2..ed9da40 100644 --- a/features/execution_order.feature +++ b/features/execution_order.feature @@ -1,27 +1,19 @@ Feature: Execution order Background: - Given I have "dredd-hooks-php" command installed - And I have "dredd" command installed - And a file named "server.rb" with: - """ - require 'sinatra' - get '/message' do - "Hello World!\n\n" - end - """ - + Given I have dredd-hooks-php installed + And I have Dredd installed + And a file "server.js" with a server responding on "http://localhost:4567/message" with "Hello World!" And a file named "apiary.apib" with: """ # My Api ## GET /message - + Response 200 (text/html;charset=utf-8) - Hello World! + + Response 200 (text/html) """ @announce Scenario: - Given a file named "hooks/execution_order_hookfile.php" with: + Given a file named "execution_order_hookfile.php" with: """ fail = true; echo "Yay! Failed!"; + flush(); }); """ - When I run `dredd ./apiary.apib http://localhost:4567 --server "ruby server.rb" --language "dredd-hooks-php" --hookfiles hooks/failedhook.php` + When I run `dredd ./apiary.apib http://localhost:4567 --server "nodejs server.js" --language php --hookfiles failedhook.php --loglevel debug` Then the exit status should be 1 And the output should contain: """ diff --git a/features/hook_handlers.feature b/features/hook_handlers.feature index 109d348..635fc03 100644 --- a/features/hook_handlers.feature +++ b/features/hook_handlers.feature @@ -1,23 +1,15 @@ Feature: Hook handlers Background: - Given I have "dredd-hooks-php" command installed - And I have "dredd" command installed - And a file named "server.rb" with: - """ - require 'sinatra' - get '/message' do - "Hello World!\n\n" - end - """ - + Given I have dredd-hooks-php installed + Given I have Dredd installed And a file named "apiary.apib" with: """ # My Api ## GET /message - + Response 200 (text/html;charset=utf-8) - Hello World! + + Response 200 (text/html) """ + And a file "server.js" with a server responding on "http://localhost:4567/message" with "Hello World!" @announce Scenario: @@ -30,12 +22,14 @@ Feature: Hook handlers Hooks::before('/message > GET', function(&$transaction) use ($key) { - var_dump("before hook handled"); + fprintf(STDERR, "before hook handled"); + flush(); }); Hooks::after('/message > GET', function(&$transaction) use ($key) { - echo "after hook handled"; + fprintf(STDERR, "after hook handled"); + flush(); }); Hooks::beforeValidation('/message > GET', function(&$transaction) use ($key) { @@ -69,7 +63,7 @@ Feature: Hook handlers }); """ - When I run `dredd ./apiary.apib http://localhost:4567 --server "ruby server.rb" --language dredd-hooks-php --hookfiles ./hookfile.php` + When I run `dredd ./apiary.apib http://localhost:4567 --server "node server.js" --language php --hookfiles ./hookfile.php --loglevel debug` Then the exit status should be 0 Then the output should contain: """ diff --git a/features/multiple_hookfiles.feature b/features/multiple_hookfiles.feature index 6eec68f..0f65518 100644 --- a/features/multiple_hookfiles.feature +++ b/features/multiple_hookfiles.feature @@ -1,27 +1,19 @@ Feature: Multiple hook files with a glob Background: - Given I have "dredd-hooks-php" command installed - And I have "dredd" command installed - And a file named "server.rb" with: - """ - require 'sinatra' - get '/message' do - "Hello World!\n\n" - end - """ - + Given I have dredd-hooks-php installed + Given I have Dredd installed And a file named "apiary.apib" with: """ # My Api ## GET /message - + Response 200 (text/html;charset=utf-8) - Hello World! + + Response 200 (text/html) """ + And a file "server.js" with a server responding on "http://localhost:4567/message" with "Hello World!" @announce Scenario: - Given a file named "hooks/hookfile1.php" with: + Given a file named "hookfile1.php" with: """ /dev/null 2>&1") - else - is_present = system("which #{command} > /dev/null 2>&1") - raise "Command #{command} is not present in the system" if not is_present - end -end - -Given(/^server under test is running$/) do -end - -Then(/^It should start listening on localhost port "([^"]*)"$/) do |port| - @client = TCPSocket.new 'localhost', port - @client.close -end - -Given(/^I connect to the server$/) do - @client = TCPSocket.new 'localhost', 61321 -end - -When(/^I send a JSON message to the socket:$/) do |string| - @data_sent = string - @client.send @data_sent, 0 -end - -When(/^I send a newline character as a message delimiter to the socket$/) do - @client.send "\n", 0 -end - -Then(/^I should receive same response$/) do - sleep 1 - data_received = @client.readline "\n" - if JSON.parse(data_received) != JSON.parse(@data_sent) - @client.close! - raise "Data received:\n#{data_received}\nDoesn't match data sent: #{@data_sent}\n" - end -end - -Then(/^I should be able to gracefully disconnect$/) do - @client.close -end diff --git a/features/support/env.rb b/features/support/env.rb deleted file mode 100644 index 70cc77d..0000000 --- a/features/support/env.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'aruba/cucumber' -require "sinatra/base" - -Before do - puts "Killing server..." - system "for i in `ps axu | grep 'server.rb' | awk '{print $2}'`; do kill -9 $i; done > /dev/null 2>&1" - puts "Killing handler..." - system "for i in `ps axu | grep 'dredd-hooks'| awk '{print $2}'`; do kill -9 $i; done > /dev/null 2>&1" - sleep 3 - - @aruba_timeout_seconds = 10 -end \ No newline at end of file diff --git a/features/support/server.rb b/features/support/server.rb deleted file mode 100644 index 76f511e..0000000 --- a/features/support/server.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'sinatra' - -get '/message' do - "Hello World!" -end diff --git a/features/support/steps.js b/features/support/steps.js new file mode 100644 index 0000000..3585df7 --- /dev/null +++ b/features/support/steps.js @@ -0,0 +1,196 @@ +const os = require('os'); +const path = require('path'); +const util = require('util'); +const childProcess = require('child_process'); +const { expect } = require('chai'); +const fs = require('fs-extra'); +const net = require('net'); +const url = require('url'); +const which = require('which'); +const pidtree = require('pidtree'); +const { + Given, + When, + Then, + Before, + After, + AfterAll, +} = require('cucumber'); + + +Before(function hook() { + this.dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dredd-hooks-template-')); + this.dreddBin = path.join(process.cwd(), 'node_modules', '.bin', 'dredd'); + process.env.PATH = `${process.env.PATH}:${process.cwd()}/bin`; + this.env = { ...process.env }; + this.dataSent = ''; +}); + +After(async function hook() { + // kill all running processes + const pids = []; + try { + pids.push(...await pidtree(process.pid)); + } catch (error) { + // the process doesn't exist anymore + } + pids.forEach((pid) => { + try { + process.kill(pid, 'SIGKILL'); + } catch (error) { + // re-throw except in case it is 'ESRCH' (process cannot be found) + if (error.code !== 'ESRCH') throw error; + } + }); + // remove the temporary directory + return fs.remove(this.dir); +}); + + +Given('I have Dredd installed', function step() { + which.sync(this.dreddBin); // throws if not found +}); + +Given('I have dredd-hooks-php installed', function step() { + which.sync("dredd-hooks-php"); // throws if not found +}); + +Given('a file {string} with a server responding on {string} to wildcard hooks', function step(filename, fullURL) { + const urlParts = url.parse(fullURL); + const content = `require('http') + .createServer((req, res) => { + if (req.method === 'GET') { + res.writeHead(200); + } else if (req.method === 'POST') { + res.writeHead(201); + } else if (req.method === 'DELETE') { + res.writeHead(204); + } else { + res.writeHead(500); + } + res.end(''); + }) + .listen(${urlParts.port}); +`; + fs.writeFileSync(path.join(this.dir, filename), content); +}); + +Given('a file {string} with a server responding on {string} with {string}', function step(filename, fullURL, body) { + const urlParts = url.parse(fullURL); + const content = ` +require('http') + .createServer((req, res) => { + if (req.url === '${urlParts.path}') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(''); + } else { + res.writeHead(500); + res.end(); + } + }) + .listen(${urlParts.port}); +`; + fs.writeFileSync(path.join(this.dir, filename), content); +}); + +Given('a file named {string} with:', function step(filename, content) { + fs.writeFileSync(path.join(this.dir, filename), content); +}); + +Given('I set the environment variables to:', function step(env) { + this.env = { ...this.env, ...env.rowsHash() }; +}); + + +When(/^I run `dredd ([^`]+)`$/, function step(args) { + this.proc = childProcess.spawnSync(`${this.dreddBin} ${args}`, [], { + shell: true, + cwd: this.dir, + env: this.env, + }); +}); + +When('I run {string} interactively, I wait for output to contain {string}', function step(command, output, callback) { + proc = childProcess.spawn(command, [], { + shell: true, + cwd: this.dir, + env: this.env, + }); + function read(data) { + if (data.toString().includes(output)) { + proc.stdout.removeListener('data', read); + proc.stderr.removeListener('data', read); + callback(); + } + } + + proc.stdout.on('data', read); + proc.stderr.on('data', read); +}); + + +When('I wait for output to contain {string}', function step(output, callback) { + const { proc } = this; + + function read(data) { + throw new Error(data.toString()); + if (data.toString().includes(output)) { + proc.stdout.removeListener('data', read); + proc.stderr.removeListener('data', read); + setTimeout(callback, 4500); + } + } + + proc.stdout.on('data', read); + proc.stderr.on('data', read); +}); + +When('I connect to the server', async function step() { + this.socket = new net.Socket(); + const connect = util.promisify(this.socket.connect.bind(this.socket)); + await connect(61321, '127.0.0.1'); +}); + +When('It should start listening on localhost port {int}', async function step(port) { + this.socket = new net.Socket(); + const connect = util.promisify(this.socket.connect.bind(this.socket)); + await connect(port, '127.0.0.1'); +}); + +When('I send a JSON message to the socket:', function step(message) { + this.socket.write(message); + this.dataSent += message; +}); + +When('I send a newline character as a message delimiter to the socket', function step() { + this.socket.write('\n'); +}); + + +Then('the exit status should be {int}', function step(status) { + expect(this.proc.status).to.equal(status); +}); + +Then('the output should contain:', function step(output) { + expect(this.proc.stdout.toString() + this.proc.stderr.toString()).to.contain(output); +}); + +Then('it should start listening on localhost port {int}', async function step(port) { + this.socket = new net.Socket(); + const connect = util.promisify(this.socket.connect.bind(this.socket)); + await connect(port, '127.0.0.1'); // throws if there's an issue + this.socket.end(); +}); + +Then('I should receive the same response', function step(callback) { + this.socket.on('data', (data) => { + const dataReceived = JSON.parse(data.toString()); + const dataSent = JSON.parse(this.dataSent); + expect(dataReceived).to.deep.equal(dataSent); + callback(); + }); +}); + +Then('I should be able to gracefully disconnect', function step() { + this.socket.end(); +}); diff --git a/features/tcp_server.feature b/features/tcp_server.feature index 8ac90d7..2a92963 100644 --- a/features/tcp_server.feature +++ b/features/tcp_server.feature @@ -1,66 +1,67 @@ +@skip Feature: TCP server and messages Scenario: TCP server - When I run `dredd-hooks-php` interactively - And I wait for output to contain "Starting" - Then It should start listening on localhost port "61321" + Given I have dredd-hooks-php installed + When I run "dredd-hooks-php" interactively, I wait for output to contain "Starting" + Then it should start listening on localhost port 61321 Scenario: Message exchange for event beforeEach - Given I run `dredd-hooks-php` interactively - When I wait for output to contain "Starting" + Given I have dredd-hooks-php installed + When I run "dredd-hooks-php" interactively, I wait for output to contain "Starting" And I connect to the server And I send a JSON message to the socket: """ {"event": "beforeEach", "uuid": "1234-abcd", "data": {"key":"value"}} """ And I send a newline character as a message delimiter to the socket - Then I should receive same response + Then I should receive the same response And I should be able to gracefully disconnect Scenario: Message exchange for event beforeEachValidation - Given I run `dredd-hooks-php` interactively - When I wait for output to contain "Starting" + Given I have dredd-hooks-php installed + When I run "dredd-hooks-php" interactively, I wait for output to contain "Starting" And I connect to the server And I send a JSON message to the socket: """ {"event": "beforeEachValidation", "uuid": "2234-abcd", "data": {"key":"value"}} """ And I send a newline character as a message delimiter to the socket - Then I should receive same response + Then I should receive the same response And I should be able to gracefully disconnect Scenario: Message exchange for event afterEach - Given I run `dredd-hooks-php` interactively - When I wait for output to contain "Starting" + Given I have dredd-hooks-php installed + When I run "dredd-hooks-php" interactively, I wait for output to contain "Starting" And I connect to the server And I send a JSON message to the socket: """ {"event": "afterEach", "uuid": "3234-abcd", "data": {"key":"value"}} """ And I send a newline character as a message delimiter to the socket - Then I should receive same response + Then I should receive the same response And I should be able to gracefully disconnect Scenario: Message exchange for event beforeAll - Given I run `dredd-hooks-php` interactively - When I wait for output to contain "Starting" + Given I have dredd-hooks-php installed + When I run "dredd-hooks-php" interactively, I wait for output to contain "Starting" And I connect to the server And I send a JSON message to the socket: """ {"event": "beforeAll", "uuid": "4234-abcd", "data": {"key":"value"}} """ And I send a newline character as a message delimiter to the socket - Then I should receive same response + Then I should receive the same response And I should be able to gracefully disconnect Scenario: Message exchange for event afterAll - Given I run `dredd-hooks-php` interactively - When I wait for output to contain "Starting" + Given I have dredd-hooks-php installed + When I run "dredd-hooks-php" interactively, I wait for output to contain "Starting" And I connect to the server And I send a JSON message to the socket: """ {"event": "afterAll", "uuid": "5234-abcd", "data": {"key":"value"}} """ And I send a newline character as a message delimiter to the socket - Then I should receive same response + Then I should receive the same response And I should be able to gracefully disconnect diff --git a/features/wildcard_hooks.feature b/features/wildcard_hooks.feature index e0ed80d..2a71093 100644 --- a/features/wildcard_hooks.feature +++ b/features/wildcard_hooks.feature @@ -1,25 +1,9 @@ Feature: Wildcards in Hooks Background: - Given I have "dredd-hooks-php" command installed - And I have "dredd" command installed - And a file named "wildcards.rb" with: - """ - require 'sinatra' - - post '/categories' do - [201, ''] - end - - get '/categories/:id' do - [200, ''] - end - - delete '/categories/:id' do - [204, ''] - end - """ - + Given I have dredd-hooks-php installed + Given I have Dredd installed + And a file "server.js" with a server responding on "http://localhost:4567" to wildcard hooks And a file named "wildcards.apib" with: """ FORMAT: 1A @@ -44,7 +28,7 @@ Feature: Wildcards in Hooks @announce Scenario: - Given a file named "hooks/wildcards.php" with: + Given a file named "wildcards.php" with: """