Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Set up rake tasks to make running features easier

Three things here:

1. I'm going to translate features over, and mark them as failing when
they are failing. I'll also tag them with gh-N to mark GitHub issue #N
that they go along with.
2. `bin/rake cruise` has been set up to run for CI. Basically, it runs all
the things that aren't failing, and makes sure they pass. Then, it runs
all thet hings that are supposed to be failing, and makes sure they
fail. If either of those are not true, it fails the build, and if
they both pass, they pass. This way we know when something starts
passing again, and to remove the failing tag.
3. `bin/rake focus` is added when developing new scenarios. You just tag
the scenario you're working on with `@focus`, and then `bin/rake focus`
will run just that one scenario.
  • Loading branch information...
commit 999670a5b67ecdf80105e912d89171b4648cd7b6 1 parent 0249dca
@steveklabnik steveklabnik authored
View
2  .travis.yml
@@ -1,4 +1,4 @@
language: ruby
rvm:
- 2.0.0
-script: ./bin/cucumber --format=progress
+script: bin/rake cruise
View
1  Gemfile
@@ -3,3 +3,4 @@ source "https://rubygems.org"
gem "cucumber"
gem "httparty"
gem "json-schema"
+gem "rake"
View
2  Gemfile.lock
@@ -19,6 +19,7 @@ GEM
multi_json (1.8.2)
multi_test (0.0.2)
multi_xml (0.5.5)
+ rake (10.1.0)
PLATFORMS
ruby
@@ -27,3 +28,4 @@ DEPENDENCIES
cucumber
httparty
json-schema
+ rake
View
31 README.md
@@ -44,3 +44,34 @@ Running Tests
$ bundle
$ bin/cucumber
```
+
+This requires Ruby. We run the changes with Ruby 2.0, but other versions will
+probably work as well. [Let us
+know](https://github.com/balanced/balanced-api/issues/new) if you have any
+problems running these specs, and we'd be happy to help.
+
+If you're working on a new scenario, the 'focus' task is useful. Tag a scenario
+with `@focus`, and then run `bin/rake focus`. It will only run that single scenario.
+
+### Discussions around changes
+
+Speaking of issues, that's the second purpose of this repository. If you would
+like to see a new feature implemented in the Balanced API, please [open an
+issue](https://github.com/balanced/balanced-api/issues/new) and we'll discuss
+it.
+
+For example, one of our biggest requests is to support non-USD currencies.
+[Here](https://github.com/balanced/balanced-api/issues/23) is the issue with
+the discussion, and when we support this feature, we close the issue via a pull
+request that implements the specification, and then everyone on the issue gets
+notified.
+
+Sometimes, Issues are great for collecting feedback, as well. For example,
+[all implementation of the current framework was done via
+PR](https://github.com/balanced/balanced-api/pull/431), and some issues need
+more explanation around use cases by those who want the feature, like [Bitcoin
+support](https://github.com/balanced/balanced-api/issues/204).
+
+We try to do as much 'internal' discussion in these issues as well, it's not
+just for public feedback. If you want to know what we're thinking, just search
+for a relevant issue!
View
52 Rakefile
@@ -0,0 +1,52 @@
+require 'cucumber/rake/task'
+
+class BuildFailure < Exception;
+ def initialize(message = nil)
+ message ||= "Build failed"
+ super(message)
+ end
+end;
+
+Cucumber::Rake::Task.new do |t|
+ t.cucumber_opts = "--format progress --tags ~@failing"
+end
+
+Cucumber::Rake::Task.new(:focus) do |t|
+ t.cucumber_opts = "--format progress --tags @focus"
+end
+
+namespace :features do
+ desc "Run all features"
+ Cucumber::Rake::Task.new(:all) do |t|
+ t.cucumber_opts = "--format progress"
+ end
+
+ desc "Run in-progress features"
+ Cucumber::Rake::Task.new(:in_progress) do |t|
+ t.cucumber_opts = "--require formatters/ --format Cucumber::Formatter::InProgress --tags @failing"
+ end
+end
+
+desc "Run complete feature build"
+task :cruise do
+ finished_successful = run_and_check_for_exception("cucumber")
+ in_progress_successful = run_and_check_for_exception("features:in_progress")
+
+ unless finished_successful && in_progress_successful
+ puts
+ puts("Finished features had failing steps") unless finished_successful
+ puts("In-progress Scenario/s passed when they should fail or be pending") unless in_progress_successful
+ puts
+ raise BuildFailure
+ end
+end
+
+def run_and_check_for_exception(task_name)
+ puts "*** Running #{task_name} features ***"
+ begin
+ Rake::Task["#{task_name}"].invoke
+ rescue Exception => e
+ return false
+ end
+ true
+end
View
16 bin/rake
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rake' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rake', 'rake')
View
7 features/cards.feature
@@ -2,13 +2,13 @@ Feature: Tokenize a credit card
"Tokenizing" a credit card is the process of sending Balanced a credit card
number for storage. The API will then give the customer back a token that the
customer will store. The idea is that if a customer stored the credit card data
- directly, the customer would legally be required to take extra precautions for
- safely storing the information, whereas with the token, Balanced worries about
+ directly, the customer would legally be required to take extra precautions for safely storing the information, whereas with the token, Balanced worries about
that, and the customer just charges against the token rather than the card.
For more on tokenization as a concept, see ['tokenization' on
Wikipedia.](http://en.wikipedia.org/wiki/Tokenization_%28data_security%29)
+ @failing
Scenario: Tokenize a card without a secret key
Cards are able to be tokenized without sending along a secret key. When
this happens, the customer gets less information than if the key was sent.
@@ -35,3 +35,6 @@ Feature: Tokenize a credit card
}
"""
+ When I GET "cards.href" from the previous response
+ Then I should get a 200 OK status code
+ And the response is valid according to the "cards" schema
View
24 features/step_definitions/card_steps.rb
@@ -1,4 +1,3 @@
-require 'httparty'
require 'json-schema'
When(/^I POST to (\/.*) without my secret key with the JSON API body:$/) do |url, body|
@@ -20,3 +19,26 @@
Then(/^the response has this schema:$/) do |schema|
assert JSON::Validator.validate(JSON.parse(schema), @response_body)
end
+
+When(/^I GET "(.*?)" from the previous response$/) do |keys|
+ # hax to access a Ruby hash like dot notation
+ # Array shennanigans is because we only support the first element
+ url = keys.split('.').inject(@response_body) {|o, k| Array(o[k])[0] }
+ options = {
+ headers: {
+ "Accept" => "application/vnd.api+json;revision=1.1",
+ },
+ basic_auth: {
+ username: $api_secret,
+ password: "",
+ }
+ }
+
+ response = HTTParty.get("https://api.balancedpayments.com#{url}", options)
+ @response_code = response.code
+ @response_body = JSON.parse(response.body)
+end
+
+Then(/^the response is valid according to the "(.*?)" schema$/) do |filename|
+ assert JSON::Validator.validate(File.join("fixtures", "#{filename}.json"), @response_body), "The response failed the '#{filename}' schema. Here's the body: #{@response_body}"
+end
View
27 features/support/initial_setup.rb
@@ -0,0 +1,27 @@
+require 'httparty'
+require 'json'
+
+# First, we need to create an API key. This is as easy as making a POST request.
+
+options = {
+ headers: {
+ "Accept" => "application/vnd.api+json;revision=1.1",
+ },
+}
+response = HTTParty.post("https://api.balancedpayments.com/api_keys", options)
+$api_secret = JSON.parse(response.body)["api_keys"][0]["secret"]
+
+# Now that we have our key, we need to make a marketplace. Lots of our scenarios
+# will fail unless we've made at least one.
+
+options = {
+ headers: {
+ "Accept" => "application/vnd.api+json;revision=1.1",
+ },
+ basic_auth: {
+ username: $api_secret,
+ password: ""
+ },
+}
+
+HTTParty.post("https://api.balancedpayments.com/marketplaces", options)
View
10 features/support/secret.rb
@@ -1,10 +0,0 @@
-require 'httparty'
-require 'json'
-
-options = {
- headers: {
- "Accept" => "application/vnd.api+json;revision=1.1",
- },
-}
-response = HTTParty.post("https://api.balancedpayments.com/api_keys", options)
-$api_secret = JSON.parse(response.body)["api_keys"][0]["secret"]
View
54 fixtures/_models/address.json
@@ -0,0 +1,54 @@
+{
+ "$schema": "http://json-schema.org/draft-03/schema#",
+ "type": "object",
+ "properties": {
+ "line1": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "required": true
+ },
+ "line2": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "required": true
+ },
+ "city": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "required": true
+ },
+ "state": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "required": true
+ },
+ "postal_code": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "required": true
+ },
+ "country_code": {
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string",
+ "pattern": "[A-Z]{2}"
+ }
+ ],
+ "required": true
+ }
+ },
+ "additionalProperties": false
+}
View
210 fixtures/_models/card.json
@@ -0,0 +1,210 @@
+{
+ "$schema": "http://json-schema.org/draft-03/schema#",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "required": true,
+ "pattern": "CC[a-zA-Z0-9]{16,32}"
+ },
+ "href": {
+ "type": "string",
+ "format": "uri",
+ "required": true
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "required": true
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "required": true
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "required": true
+ },
+ "number": {
+ "type": "string",
+ "pattern": "x{11,12}[0-9]{4}",
+ "required": true
+ },
+ "expiration_month": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 12,
+ "required": true
+ },
+ "expiration_year": {
+ "type": "integer",
+ "required": true
+ },
+ "cvv": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "x{3,4}"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "required": true
+ },
+ "cvv_match": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "yes",
+ "no",
+ "unsupported"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "required": true
+ },
+ "cvv_result": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "Suspicious transaction",
+ "Failed data validation check",
+ "Match",
+ "No Match",
+ "Not Processed",
+ "Should have been present",
+ "Issuer unable to process request",
+ "Card does not support verification"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "required": true
+ },
+ "address": {
+ "$ref": "address.json",
+ "required": true
+ },
+ "avs_street_match": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "yes",
+ "no",
+ "unsupported"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "required": true
+ },
+ "avs_postal_match": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "yes",
+ "no",
+ "unsupported"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "required": true
+ },
+ "avs_result": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "Street address matches, but 5-digit and 9-digit postal code do not match.",
+ "Street address matches, but postal code not verified.",
+ "Street address and postal code do not match.",
+ "Street address and postal code match.",
+ "AVS data is invalid or AVS is not allowed for this card type.",
+ "Card member's name does not match, but billing postal code matches.",
+ "Non-U.S. issuing bank does not support AVS.",
+ "Card member's name does not match. Street address and postal code match.",
+ "Address not verified.",
+ "Card member's name, billing address, and postal code match. Shipping information verified and chargeback protection guaranteed through the Fraud Protection Program.",
+ "Card member's name matches but billing address and billing postal code do not match.",
+ "Card member's name and billing postal code match, but billing address does not match.",
+ "Street address and postal code match.",
+ "Street address and postal code do not match.",
+ "Card member's name and billing address match, but billing postal code does not match.",
+ "Postal code matches, but street address not verified.",
+ "Card member's name, billing address, and postal code match. Shipping information verified but chargeback protection not guaranteed.",
+ "System unavailable.",
+ "U.S.-issuing bank does not support AVS.",
+ "Card member's name does not match, but street address matches.",
+ "Address information unavailable.",
+ "Card member's name, billing address, and billing postal code match.",
+ "Street address does not match, but 9-digit postal code matches.",
+ "Street address and 9-digit postal code match.",
+ "Street address and 5-digit postal code match.",
+ "Street address does not match, but 5-digit postal code matches."
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "required": true
+ },
+ "fingerprint": {
+ "type": "string",
+ "required": true
+ },
+ "brand": {
+ "type": "string",
+ "enum": [
+ "Visa",
+ "MasterCard",
+ "Discover",
+ "American Express"
+ ],
+ "required": false
+ },
+ "is_verified": {
+ "type": "boolean",
+ "required": true
+ },
+ "meta": {
+ "type": "object",
+ "required": true
+ },
+ "links": {
+ "type": "object",
+ "properties": {
+ "customer": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "pattern": "(CU|AC)[a-zA-Z0-9]{16,32}",
+ "required": true
+ }
+ },
+ "required": true,
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
View
44 fixtures/cards.json
@@ -0,0 +1,44 @@
+{
+ "type": "object",
+ "properties": {
+ "links": {
+ "type": "object",
+ "properties": {
+ "cards.card_holds": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "/cards/{cards.id}/card_holds"
+ },
+ "cards.debits": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "/cards/{cards.id}/debits"
+ },
+ "cards.customer": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "/customers/{cards.customer}"
+ }
+ },
+ "required": [
+ "cards.customer",
+ "cards.debits",
+ "cards.card_holds"
+ ]
+ },
+ "meta": {
+ "type": "object"
+ },
+ "cards": {
+ "items": {
+ "$ref": "_models/card.json"
+ },
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ },
+ "required": [
+ "cards"
+ ]
+}
View
60 formatters/in_progress.rb
@@ -0,0 +1,60 @@
+require 'cucumber/formatter/progress'
+
+module Cucumber
+ module Formatter
+ class InProgress < Progress
+ FAILURE_CODE = 1
+ SUCCESS_CODE = 0
+
+ FORMATS[:invalid_pass] = Proc.new{ |string| ::Term::ANSIColor.blue(string) }
+
+ def initialize(step_mother, io, options)
+ super(step_mother, io, options)
+ @scenario_passed = true
+ @passing_scenarios = []
+ @feature_element_count = 0
+ end
+
+ def visit_feature_element(feature_element)
+ super
+
+ @passing_scenarios << feature_element if @scenario_passed
+ @scenario_passed = true
+ @feature_element_count += 1
+
+ @io.flush
+ end
+
+ def visit_exception(exception, status)
+ @scenario_passed = false
+ super
+ end
+
+ private
+
+ def print_summary(features)
+ unless @passing_scenarios.empty?
+ @io.puts format_string("(::) Scenarios passing which should be failing or pending (::)", :invalid_pass)
+ @io.puts
+ @passing_scenarios.each do |element|
+ @io.puts(format_string(element.backtrace_line, :invalid_pass))
+ end
+ @io.puts
+ end
+
+ unless @passing_scenarios.empty?
+ override_exit_code(FAILURE_CODE)
+ else
+ override_exit_code(SUCCESS_CODE)
+ end
+ end
+
+ def override_exit_code(status_code)
+ at_exit do
+ Kernel.exit(status_code)
+ end
+ end
+
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.