├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.markdown ├── Rakefile ├── bin └── stubb ├── lib ├── stubb.rb └── stubb │ ├── combined_logger.rb │ ├── counter.rb │ ├── finder.rb │ ├── match_finder.rb │ ├── naive_finder.rb │ ├── request.rb │ ├── response.rb │ ├── sequence_finder.rb │ └── sequence_match_finder.rb ├── stubb.gemspec ├── stubb.png └── test ├── fixtures ├── collection │ ├── GET │ ├── GET.json │ ├── POST │ ├── member.GET │ ├── member.GET.json │ ├── member.PUT.json │ ├── member_template.GET │ └── member_template.POST ├── looping_sequence │ ├── GET.0 │ ├── GET.1 │ ├── GET.2 │ ├── member.GET.0 │ ├── member.GET.1 │ └── member.GET.2 ├── matching │ ├── _wildcard_collection_ │ │ ├── GET │ │ ├── GET.json │ │ ├── static.GET │ │ ├── static.GET.json │ │ ├── template.GET │ │ └── template.POST │ ├── collection │ │ ├── _wildcard_member_.GET │ │ └── _wildcard_member_.GET.json │ └── sequences │ │ └── _wildcard_ │ │ ├── looping │ │ ├── GET.0 │ │ ├── GET.1 │ │ ├── GET.2 │ │ ├── member.GET.0 │ │ ├── member.GET.1 │ │ └── member.GET.2 │ │ └── stalling │ │ ├── GET.1 │ │ ├── GET.2 │ │ ├── GET.3 │ │ ├── member.GET.1 │ │ ├── member.GET.2 │ │ ├── member.GET.3 │ │ ├── template.GET.1 │ │ └── template.POST.1 ├── stalling_sequence │ ├── GET.1 │ ├── GET.2 │ ├── GET.3 │ ├── member.GET.1 │ ├── member.GET.2 │ ├── member.GET.3 │ ├── template.GET.1 │ └── template.POST.1 └── users │ └── :id │ └── photos │ └── :photo_id.GET.json ├── test_counter.rb ├── test_match_finder.rb ├── test_naive_finder.rb ├── test_request.rb ├── test_response.rb ├── test_sequence_finder.rb └── test_sequence_match_finder.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .DS_Store 3 | responses/* 4 | resources/* 5 | info/* 6 | .rvmrc 7 | .rbenv-version 8 | *.gem 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Johannes Emerich 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ![Stubb](https://github.com/knuton/stubb/raw/master/stubb.png) 2 | 3 | Stubb is a testing and development tool for frontend developers and anyone else depending on HTTP-requesting resources for their work. It allows **setting up a REST API stub by putting responses in files** organized in a directory tree. Which file is picked in response to a particular HTTP request is primarily determined by the request's **method**, **path** and **accept header**. Thus adding a response for a certain type of request is as easy as adding a file with a matching name. For example, the file 4 | 5 | whales/narwhal.GET.json 6 | 7 | in your base directory will be picked to deliver the response to the request 8 | 9 | GET /whales/narwhal.json HTTP/1.1 10 | 11 | or alternatively 12 | 13 | GET /whales/narwhal HTTP/1.1 14 | Accept: application/json 15 | 16 | . 17 | 18 | Additionally, **sequences of responses** to repeated identical requests can be defined through infix numerals in file names. 19 | 20 | Getting Started 21 | --------------- 22 | 23 | Simply install the Stubb gem by running 24 | 25 | gem install stubb 26 | 27 | and you are ready to run the stubb CLI: 28 | 29 | $ stubb 30 | Tasks: 31 | stubb help [TASK] # Describe available tasks or one specific task 32 | stubb server # Starts the server 33 | stubb version # Print the version of Stubb 34 | 35 | $ echo Ahoy > hello-world.GET 36 | $ ls 37 | hello-world.GET 38 | $ stubb server & 39 | $ curl http://localhost:4040/hello-world 40 | Ahoy 41 | 42 | By default the server runs on port 4040 and looks for response files in the working directory. Run `stubb help server` for information on configuration options. 43 | 44 | Directory Structure and Response Files 45 | -------------------------------------- 46 | 47 | All requests are served from the *base directory*, that is the directory Stubb was started from. The directory tree in your base directory determines the path hierarchy of your stubbed REST API. Request paths are mapped to relative paths within the base directory to locate a response file. 48 | 49 | ### Response Files 50 | 51 | A *response file* is a file containing an API response. There are two kinds of response files, member response files and collection response files. They differ only in concept and naming. 52 | 53 | #### Member Response Files 54 | 55 | A *member response file* is a file containing an API response for a member resource, named after the scheme 56 | 57 | REQUEST_PATH_WITHOUT_EXTENSION.HTTP_METHOD[.SEQUENCE_NUMBER][.FILE_TYPE] 58 | 59 | , where `SEQUENCE_NUMBER` is optional and only needed when defining response sequences, and `FILE_TYPE` is also optional and only needed when a file type is implied by request path or accept header. 60 | 61 | Examples: 62 | 63 | whales/narwhal.GET 64 | whales/narwhal.GET.json 65 | whales/narwhal.GET.1 66 | whales/narwhal.GET.1.json 67 | 68 | #### Collection Response Files 69 | 70 | A *collection response file* is a file containing an API response for a collection resource, named after the scheme 71 | 72 | REQUEST_PATH_WITHOUT_EXTENSION/HTTP_METHOD[.SEQUENCE_NUMBER][.FILE_TYPE] 73 | 74 | , where `SEQUENCE_NUMBER` is optional and only needed when defining response sequences, and `FILE_TYPE` is also optional and only needed when a file type is implied by request path or accept header. 75 | 76 | Examples: 77 | 78 | whales/GET 79 | whales/GET.json 80 | whales/GET.1 81 | whales/GET.1.json 82 | 83 | ### Response Files as ERB Templates 84 | 85 | Any matching response file will be evaluated as an ERB template, with `GET` or `POST` parameters available in a `params` hash. This can come in handy when stubbing `POST` and `PUT` requests or serving JSONP. 86 | 87 | ### YAML Frontmatter 88 | 89 | Response files may contain YAML frontmatter before the response text, allowing to set custom values for response status and header: 90 | 91 | --- 92 | status: 201 93 | header: 94 | Cache-Control: no-cache 95 | --- 96 | {"name":"Stubb"} 97 | 98 | ### Missing Responses 99 | 100 | If no matching response file is found, Stubb replies with a status of `404`. You can customize error responses for types of requests by creating a matching response file that contains your custom response. 101 | 102 | Path Matching 103 | ------------- 104 | 105 | Paths in the base directory may include wildcards to allow one response file to match for a whole range of request paths instead of just one. Both Directory names and file names may be wildcards. Wildcards are marked by starting and ending in an underscore (`_`). A wildcard segment matches any equally positioned segment of a request path. 106 | 107 | For example 108 | 109 | whales/_default_whale_.GET.json 110 | 111 | matches 112 | 113 | GET /whales/pygmy_sperm_whale.json HTTP/1.1 114 | 115 | as well as 116 | 117 | GET /whales/blackfish.json HTTP/1.1 118 | 119 | . 120 | 121 | If a literal match exists, it will be chosen over a wildcard match. 122 | 123 | Response Sequences 124 | ------------------ 125 | 126 | A *response sequence* is a sequence of response files whose members are being used as responses to a sequence of requests of the same type. This allows for controlled stubbing of changes in the API. 127 | 128 | ### Stalling Sequences (1..._n_) 129 | 130 | A *stalling sequence* keeps responding with the last response file in the response sequence after _n_ requests of the same type. Stalling sequences are specified by adding response files with indices 1 through _n_. 131 | 132 | GET.1.format, GET.2.format, ..., GET._n-1_.format, GET._n_.format 133 | 134 | Example: 135 | 136 | whales/GET.1.json 137 | whales/GET.2.json 138 | whales/GET.3.json 139 | 140 | From the third request on, the response to 141 | 142 | GET /whales.json HTTP/1.1 143 | 144 | will be the one given in `whales/GET.3.json`. 145 | 146 | ### Looping Sequences (0..._n-1_) 147 | 148 | A *looping sequence* starts from the first response file in the response sequence after _n_ requests of the same type. Looping sequences are specified by adding response files with indices 0 through _n_-1. 149 | 150 | GET.0.format, GET.1.format, ..., GET._n-2_.format, GET._n-1_.format 151 | 152 | Example: 153 | 154 | whales/GET.0.json 155 | whales/GET.1.json 156 | whales/GET.2.json 157 | 158 | The response to the fourth request to 159 | 160 | GET /whales.json HTTP/1.1 161 | 162 | will again be the one given in `whales/GET.0.json`, and so forth. 163 | 164 | Dependencies 165 | ------------ 166 | 167 | Stubb depends on 168 | 169 | - Rack for processing and serving requests, and 170 | - Thor for adding a CLI executable. 171 | 172 | Acknowledgements 173 | ---------------- 174 | 175 | The logo for Stubb was kindly provided by Andres Colmenares of [Wawawiwa](https://www.facebook.com/pages/Wawawiwa-design/201009879921770). 176 | 177 | License 178 | ------- 179 | 180 | Copyright (c) 2011 Johannes Emerich 181 | 182 | MIT-style licensing, for details see file `LICENSE`. 183 | 184 |
185 | 186 | [![Build Status](https://travis-ci.org/knuton/stubb.png?branch=master)](https://travis-ci.org/knuton/stubb) 187 | 188 | _'Why,' thinks I, 'what's the row? It's not a real leg, only a false leg.'_ 189 | --Stubb in _Moby Dick_ 190 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rake/testtask' 4 | 5 | Bundler::GemHelper.install_tasks 6 | 7 | Rake::TestTask.new(:test) do |test| 8 | test.pattern = 'test/**/test_*.rb' 9 | test.verbose = true 10 | end 11 | 12 | task :default => [:test] 13 | -------------------------------------------------------------------------------- /bin/stubb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 4 | 5 | require 'thor' 6 | require 'stubb' 7 | 8 | module Stubb 9 | class CLI < Thor 10 | map '-v' => :version 11 | 12 | desc 'server', 'Starts the server' 13 | method_option :port, :type => :numeric, :default => 4040, :aliases => '-p', :desc => 'Specifies the port for the server to use' 14 | method_option :root, :type => :string, :default => '', :aliases => '-r', :desc => 'Specifies the root directory to serve from' 15 | method_option :verbose, :type => :boolean, :aliases => '-v', :desc => 'Print debug messages' 16 | def server 17 | Stubb.run :Port => options[:port], :root => options[:root], :verbose => options[:verbose] 18 | end 19 | 20 | desc 'version', 'Print the version of Stubb' 21 | def version 22 | puts Stubb::VERSION 23 | end 24 | end 25 | end 26 | 27 | Stubb::CLI.start 28 | -------------------------------------------------------------------------------- /lib/stubb.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | 3 | module Stubb 4 | 5 | VERSION = '0.2.0' 6 | 7 | class ResponseNotFound < Exception 8 | end 9 | 10 | @config = { 11 | :matcher_pattern => '_*_' 12 | } 13 | 14 | def self.method_missing(m, *attrs) 15 | # Ease access to @config 16 | if @config[m.to_sym] 17 | @config[m.to_sym] 18 | elsif @config[m.to_s.chomp('=').to_sym] 19 | @config[m.to_s.chomp('=').to_sym] = attrs[0] 20 | else 21 | super 22 | end 23 | end 24 | 25 | def self.app(options = {}) 26 | Rack::Builder.new { 27 | use CombinedLogger 28 | use Counter 29 | 30 | run Rack::Cascade.new [ 31 | SequenceFinder.new(options), 32 | NaiveFinder.new(options), 33 | SequenceMatchFinder.new(options), 34 | MatchFinder.new(options) 35 | ] 36 | }.to_app 37 | end 38 | 39 | def self.run(options = {}) 40 | Rack::Handler.default.run( 41 | app({:root => ''}.update(options)), 42 | {:Port => 4040}.update(options) 43 | ) 44 | end 45 | 46 | end 47 | 48 | require 'stubb/request' 49 | require 'stubb/response' 50 | require 'stubb/counter' 51 | require 'stubb/combined_logger' 52 | require 'stubb/finder' 53 | require 'stubb/naive_finder' 54 | require 'stubb/sequence_finder' 55 | require 'stubb/match_finder' 56 | require 'stubb/sequence_match_finder' 57 | -------------------------------------------------------------------------------- /lib/stubb/combined_logger.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class CombinedLogger < Rack::CommonLogger 4 | FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f "%s" "%s"\n} 5 | 6 | def log(env, status, header, began_at) 7 | now = Time.now 8 | length = extract_content_length(header) 9 | 10 | logger = @logger || env['rack.errors'] 11 | logger.write FORMAT % [ 12 | env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-', 13 | env['REMOTE_USER'] || '-', 14 | now.strftime('%d/%b/%Y %H:%M:%S'), 15 | env['REQUEST_METHOD'], 16 | env['PATH_INFO'], 17 | env['QUERY_STRING'].empty? ? '' : '?'+env['QUERY_STRING'], 18 | env['HTTP_VERSION'], 19 | status.to_s[0..3], 20 | length, 21 | now - began_at, 22 | header['X-Stubb-Response-File'] || 'None', 23 | "YAML Frontmatter: #{header['X-Stubb-Frontmatter'] || 'No'}" 24 | ] 25 | 26 | end 27 | 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/stubb/counter.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class Counter 4 | 5 | def initialize(app) 6 | @app = app 7 | @request_history = {} 8 | trap(:INT) { |signal| reset_or_quit signal } 9 | end 10 | 11 | def call(env) 12 | env['stubb.request_sequence_index'] = count(env['REQUEST_METHOD'], env['PATH_INFO'], env['HTTP_ACCEPT']) 13 | @app.call(env) 14 | end 15 | 16 | private 17 | def count(method, path, accept) 18 | fingerprint = "#{method}-#{path}-#{accept}" 19 | @request_history[fingerprint] = (@request_history[fingerprint] || 0) + 1 20 | end 21 | 22 | def reset_or_quit(signal) 23 | if @request_history.empty? 24 | exit! signal 25 | else 26 | @request_history.clear 27 | puts "\n\nReset request history. Interrupt again to quit.\n\n" 28 | end 29 | end 30 | 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/stubb/finder.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class NotFound < Exception; end 4 | 5 | class Finder 6 | 7 | attr_accessor :request, :root 8 | 9 | def initialize(options = {}) 10 | @root = File.expand_path options[:root] || '' 11 | @verbose = options[:verbose] || false 12 | end 13 | 14 | def call(env) 15 | @request = Request.new env 16 | 17 | respond 18 | 19 | rescue Errno::ENOENT, Errno::ELOOP 20 | [404, {"Content-Type" => "text/plain"}, ["Not found."]] 21 | rescue Exception => e 22 | debug e.message, e.backtrace.join("\n") 23 | [500, {'Content-Type' => 'text/plain'}, ['Internal server error.']] 24 | end 25 | 26 | private 27 | def respond 28 | response_file_path = projected_path 29 | response_body = File.open(response_file_path, 'r') {|f| f.read } 30 | Response.new( 31 | response_body, 32 | request.params, 33 | 200, 34 | {'Content-Type' => content_type, 'X-Stubb-Response-File' => response_file_path} 35 | ).finish 36 | rescue NotFound => e 37 | debug e.message 38 | [404, {}, [e.message]] 39 | end 40 | 41 | def request_options_as_file_ending 42 | "#{request.request_method}#{request.extension}" 43 | end 44 | 45 | def exists?(relative_path) 46 | File.exists? local_path_for(relative_path) 47 | end 48 | 49 | def local_path_for(relative_path) 50 | File.join root, relative_path 51 | end 52 | 53 | def glob(pattern) 54 | Dir.glob(pattern).sort 55 | end 56 | 57 | def content_type 58 | Rack::Mime.mime_type(request.extension) || "text/html" 59 | end 60 | 61 | def debug(*messages) 62 | log(*messages) if @verbose 63 | end 64 | 65 | def log(*messages) 66 | if request.env['rack.errors'] && request.env['rack.errors'].respond_to?('write') 67 | request.env['rack.errors'].write messages.join(" ") << "\n" 68 | else 69 | puts messages.join(" ") 70 | end 71 | end 72 | 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /lib/stubb/match_finder.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class NoMatch < NotFound; end 4 | 5 | class MatchFinder < Finder 6 | private 7 | def projected_path 8 | built_path = [] 9 | last_is_dir = false 10 | request.path_parts.each_with_index do |level, index| 11 | if match = literal_directory(built_path, level) 12 | last_is_dir = true 13 | elsif match = literal_file(built_path, level) 14 | last_is_dir = false 15 | elsif match = matching_directory(built_path) 16 | last_is_dir = true 17 | elsif match = matching_file(built_path) 18 | last_is_dir = false 19 | else 20 | raise NoMatch.new("Not found.") 21 | end 22 | 23 | built_path << match 24 | end 25 | 26 | if last_is_dir 27 | File.join local_path_for(built_path), request_options_as_file_ending 28 | else 29 | local_path_for built_path 30 | end 31 | end 32 | 33 | def literal_directory(current_path, level) 34 | File.directory?(local_path_for(current_path + [level])) ? level: nil 35 | end 36 | 37 | def literal_file(current_path, level) 38 | parts = level.split('.') 39 | level = parts.size > 1 ? parts[0..-2].join('.') : parts.first 40 | filename = "#{level}.#{request_options_as_file_ending}" 41 | File.exists?(local_path_for(current_path + [filename])) ? filename : nil 42 | end 43 | 44 | def matching_directory(current_path) 45 | matches = glob local_path_for(current_path + [Stubb.matcher_pattern]) 46 | for match in matches 47 | continue unless File.directory? match 48 | return File.split(match).last 49 | end 50 | nil 51 | end 52 | 53 | def matching_file(current_path) 54 | matches = glob local_path_for(current_path + ["#{Stubb.matcher_pattern}.#{request_options_as_file_ending}"]) 55 | 56 | matches.empty? ? nil : File.split(matches.first).last 57 | end 58 | 59 | end 60 | 61 | 62 | end 63 | -------------------------------------------------------------------------------- /lib/stubb/naive_finder.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class NaiveFinder < Finder 4 | 5 | private 6 | def projected_path 7 | relative_path = if File.directory? local_path_for(request.resource_path) 8 | File.join request.resource_path, request_options_as_file_ending 9 | else 10 | "#{request.resource_path}.#{request_options_as_file_ending}" 11 | end 12 | 13 | local_path_for relative_path 14 | end 15 | 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/stubb/request.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class Request < Rack::Request 4 | 5 | def path_parts 6 | relative_path.empty? ? [''] : relative_path.split('/') 7 | end 8 | 9 | def path_dir_parts 10 | parts = path_parts 11 | parts.size > 1 ? parts[0..-2] : [] 12 | end 13 | 14 | def file_name 15 | path_parts.last 16 | end 17 | 18 | def resource_name 19 | parts = file_name.split('.') 20 | parts.size > 1 ? parts[0..-2].join('.') : parts.first 21 | end 22 | 23 | def resource_path 24 | File.join((path_dir_parts << resource_name).compact) 25 | end 26 | 27 | def extension 28 | extension_by_path.empty? ? extension_by_header : extension_by_path 29 | end 30 | 31 | def extension_by_path 32 | File.extname(relative_path) 33 | end 34 | 35 | def extension_by_header 36 | Rack::Mime::MIME_TYPES.invert[accept] 37 | end 38 | 39 | # TODO parse, sort 40 | def accept 41 | @env['HTTP_ACCEPT'].to_s.split(',').first 42 | end 43 | 44 | def relative_path 45 | # Strip slashes at string end and start 46 | path_info.gsub /(\A\/|\/\Z)/, '' 47 | end 48 | 49 | def sequence_index 50 | @env['stubb.request_sequence_index'] || 1 51 | end 52 | 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/stubb/response.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'yaml' 3 | 4 | module Stubb 5 | 6 | class Response < Rack::Response 7 | 8 | attr_accessor :body, :params, :status, :header 9 | 10 | def initialize(body=[], params={}, status=200, header={}) 11 | @body = body 12 | @params = params 13 | @status = status 14 | @header = header 15 | 16 | process_yaml 17 | render_template 18 | 19 | super self.body, self.status, self.header 20 | end 21 | 22 | private 23 | def process_yaml 24 | if self.body =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m 25 | self.body = self.body[($1.size + $2.size)..-1] 26 | begin 27 | data = YAML.load($1) 28 | 29 | # Use specified HTTP status 30 | self.status = data['status'] if data['status'] 31 | # Fill header information 32 | data['header'].each { |field, value| self.header[field] = value } if data['header'].kind_of? Hash 33 | self.header['X-Stubb-Frontmatter'] = 'Yes' 34 | rescue 35 | self.header['X-Stubb-Frontmatter'] = 'Error' 36 | end 37 | else 38 | self.header['X-Stubb-Frontmatter'] = 'No' 39 | end 40 | end 41 | 42 | def render_template 43 | erb = ERB.new @body 44 | @body = erb.result binding 45 | end 46 | 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/stubb/sequence_finder.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class NoSuchSequence < NotFound; end 4 | 5 | class SequenceFinder < Finder 6 | private 7 | def projected_path 8 | sequence_members = glob local_path_for(sequenced_path_pattern) 9 | raise NoSuchSequence.new("Nothing found for sequence pattern `#{sequenced_path_pattern}`.") if sequence_members.empty? 10 | 11 | loop? ? pick_loop_member(sequence_members) : pick_stall_member(sequence_members) 12 | end 13 | 14 | def pick_loop_member(sequence_members) 15 | sequence_members[(request.sequence_index - 1) % sequence_members.size] 16 | end 17 | 18 | def pick_stall_member(sequence_members) 19 | request.sequence_index > sequence_members.size ? sequence_members.last : sequence_members[request.sequence_index - 1] 20 | end 21 | 22 | def sequenced_path(index) 23 | if File.directory? local_path_for(request.relative_path) 24 | File.join request.relative_path, request_options_as_file_ending(index) 25 | else 26 | "#{request.relative_path}.#{request_options_as_file_ending(index)}" 27 | end 28 | end 29 | 30 | def sequenced_path_pattern 31 | sequenced_path('[0-9]') 32 | end 33 | 34 | def request_options_as_file_ending(index) 35 | "#{request.request_method}.#{index}#{request.extension}" 36 | end 37 | 38 | def loop? 39 | exists? sequenced_path(0) 40 | end 41 | 42 | end 43 | 44 | 45 | end 46 | -------------------------------------------------------------------------------- /lib/stubb/sequence_match_finder.rb: -------------------------------------------------------------------------------- 1 | module Stubb 2 | 3 | class SequenceMatchFinder < SequenceFinder 4 | private 5 | def sequenced_path(sequence_index) 6 | built_path = [] 7 | last_is_dir = false 8 | request.path_parts.each_with_index do |level, index| 9 | if match = literal_directory(built_path, level) 10 | last_is_dir = true 11 | elsif match = literal_file(built_path, level, sequence_index) 12 | last_is_dir = false 13 | elsif match = matching_directory(built_path) 14 | last_is_dir = true 15 | elsif match = matching_file(built_path, sequence_index) 16 | last_is_dir = false 17 | else 18 | return 'NOT FOUND' 19 | end 20 | 21 | built_path << match 22 | end 23 | 24 | if last_is_dir 25 | File.join built_path, request_options_as_file_ending(sequence_index) 26 | else 27 | File.join built_path 28 | end 29 | end 30 | 31 | def literal_directory(current_path, level) 32 | File.directory?(local_path_for(current_path + [level])) ? level : nil 33 | end 34 | 35 | def literal_file(current_path, level, index) 36 | filename = "#{level}.#{request_options_as_file_ending(index)}" 37 | sequence_members = glob local_path_for(current_path + [filename]) 38 | sequence_members.empty? ? nil : filename 39 | end 40 | 41 | def matching_directory(current_path) 42 | matches = glob local_path_for(current_path + [Stubb.matcher_pattern]) 43 | for match in matches 44 | continue unless File.directory? match 45 | return File.split(match).last 46 | end 47 | nil 48 | end 49 | 50 | def matching_file(current_path, index) 51 | matches = glob local_path_for(current_path + ["#{Stubb.matcher_pattern}.#{request_options_as_file_ending(index)}"]) 52 | 53 | matches.empty? ? nil : File.split(matches.first).last 54 | end 55 | 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /stubb.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'stubb' 3 | s.version = '0.2.0' 4 | s.date = '2014-10-03' 5 | s.summary = 'Specify REST API stubs using your file system' 6 | s.description = 'Specify REST API stubs using your file system' 7 | s.authors = ['Johannes Emerich'] 8 | s.email = 'johannes@emerich.de' 9 | s.homepage = 'http://github.com/knuton/stubb' 10 | s.files = [ 11 | 'lib/stubb.rb', 12 | 'lib/stubb/request.rb', 13 | 'lib/stubb/response.rb', 14 | 'lib/stubb/counter.rb', 15 | 'lib/stubb/combined_logger.rb', 16 | 'lib/stubb/finder.rb', 17 | 'lib/stubb/naive_finder.rb', 18 | 'lib/stubb/sequence_finder.rb', 19 | 'lib/stubb/match_finder.rb', 20 | 'lib/stubb/sequence_match_finder.rb', 21 | 'bin/stubb', 22 | 'LICENSE', 23 | 'README.markdown' 24 | ] 25 | s.executables = ['stubb'] 26 | 27 | s.add_development_dependency 'rake' 28 | 29 | s.add_runtime_dependency 'rack', '>=1.2.0' 30 | s.add_runtime_dependency 'thor' 31 | 32 | end 33 | -------------------------------------------------------------------------------- /stubb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knuton/stubb/88c90d83ba5acf380e443e6f463d5c4b2000f8d6/stubb.png -------------------------------------------------------------------------------- /test/fixtures/collection/GET: -------------------------------------------------------------------------------- 1 | GET collection -------------------------------------------------------------------------------- /test/fixtures/collection/GET.json: -------------------------------------------------------------------------------- 1 | GET collection.json -------------------------------------------------------------------------------- /test/fixtures/collection/POST: -------------------------------------------------------------------------------- 1 | POST collection -------------------------------------------------------------------------------- /test/fixtures/collection/member.GET: -------------------------------------------------------------------------------- 1 | GET member -------------------------------------------------------------------------------- /test/fixtures/collection/member.GET.json: -------------------------------------------------------------------------------- 1 | GET member.json -------------------------------------------------------------------------------- /test/fixtures/collection/member.PUT.json: -------------------------------------------------------------------------------- 1 | PUT member -------------------------------------------------------------------------------- /test/fixtures/collection/member_template.GET: -------------------------------------------------------------------------------- 1 | GET <%= params['name'] %> -------------------------------------------------------------------------------- /test/fixtures/collection/member_template.POST: -------------------------------------------------------------------------------- 1 | POST <%= params['name'] %> -------------------------------------------------------------------------------- /test/fixtures/looping_sequence/GET.0: -------------------------------------------------------------------------------- 1 | GET collection 0 -------------------------------------------------------------------------------- /test/fixtures/looping_sequence/GET.1: -------------------------------------------------------------------------------- 1 | GET collection 1 -------------------------------------------------------------------------------- /test/fixtures/looping_sequence/GET.2: -------------------------------------------------------------------------------- 1 | GET collection 2 -------------------------------------------------------------------------------- /test/fixtures/looping_sequence/member.GET.0: -------------------------------------------------------------------------------- 1 | GET member 0 -------------------------------------------------------------------------------- /test/fixtures/looping_sequence/member.GET.1: -------------------------------------------------------------------------------- 1 | GET member 1 -------------------------------------------------------------------------------- /test/fixtures/looping_sequence/member.GET.2: -------------------------------------------------------------------------------- 1 | GET member 2 -------------------------------------------------------------------------------- /test/fixtures/matching/_wildcard_collection_/GET: -------------------------------------------------------------------------------- 1 | GET matching collection -------------------------------------------------------------------------------- /test/fixtures/matching/_wildcard_collection_/GET.json: -------------------------------------------------------------------------------- 1 | GET matching collection.json -------------------------------------------------------------------------------- /test/fixtures/matching/_wildcard_collection_/static.GET: -------------------------------------------------------------------------------- 1 | GET static -------------------------------------------------------------------------------- /test/fixtures/matching/_wildcard_collection_/static.GET.json: -------------------------------------------------------------------------------- 1 | GET static.json -------------------------------------------------------------------------------- /test/fixtures/matching/_wildcard_collection_/template.GET: -------------------------------------------------------------------------------- 1 | GET matching <%= params['name'] %> -------------------------------------------------------------------------------- /test/fixtures/matching/_wildcard_collection_/template.POST: -------------------------------------------------------------------------------- 1 | POST matching <%= params['name'] %> -------------------------------------------------------------------------------- /test/fixtures/matching/collection/_wildcard_member_.GET: -------------------------------------------------------------------------------- 1 | GET matching member -------------------------------------------------------------------------------- /test/fixtures/matching/collection/_wildcard_member_.GET.json: -------------------------------------------------------------------------------- 1 | GET matching member.json -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/looping/GET.0: -------------------------------------------------------------------------------- 1 | GET matching collection 0 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/looping/GET.1: -------------------------------------------------------------------------------- 1 | GET matching collection 1 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/looping/GET.2: -------------------------------------------------------------------------------- 1 | GET matching collection 2 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/looping/member.GET.0: -------------------------------------------------------------------------------- 1 | GET matching member 0 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/looping/member.GET.1: -------------------------------------------------------------------------------- 1 | GET matching member 1 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/looping/member.GET.2: -------------------------------------------------------------------------------- 1 | GET matching member 2 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/GET.1: -------------------------------------------------------------------------------- 1 | GET matching collection 1 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/GET.2: -------------------------------------------------------------------------------- 1 | GET matching collection 2 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/GET.3: -------------------------------------------------------------------------------- 1 | GET matching collection 3 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/member.GET.1: -------------------------------------------------------------------------------- 1 | GET matching member 1 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/member.GET.2: -------------------------------------------------------------------------------- 1 | GET matching member 2 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/member.GET.3: -------------------------------------------------------------------------------- 1 | GET matching member 3 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/template.GET.1: -------------------------------------------------------------------------------- 1 | GET matching <%= params['name'] %> 1 -------------------------------------------------------------------------------- /test/fixtures/matching/sequences/_wildcard_/stalling/template.POST.1: -------------------------------------------------------------------------------- 1 | POST matching <%= params['name'] %> 1 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/GET.1: -------------------------------------------------------------------------------- 1 | GET collection 1 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/GET.2: -------------------------------------------------------------------------------- 1 | GET collection 2 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/GET.3: -------------------------------------------------------------------------------- 1 | GET collection 3 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/member.GET.1: -------------------------------------------------------------------------------- 1 | GET member 1 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/member.GET.2: -------------------------------------------------------------------------------- 1 | GET member 2 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/member.GET.3: -------------------------------------------------------------------------------- 1 | GET member 3 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/template.GET.1: -------------------------------------------------------------------------------- 1 | GET <%= params['name'] %> 1 -------------------------------------------------------------------------------- /test/fixtures/stalling_sequence/template.POST.1: -------------------------------------------------------------------------------- 1 | POST <%= params['name'] %> 1 -------------------------------------------------------------------------------- /test/fixtures/users/:id/photos/:photo_id.GET.json: -------------------------------------------------------------------------------- 1 | {'id':'nested_member'} 2 | -------------------------------------------------------------------------------- /test/test_counter.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'stubb' 4 | 5 | class TestCounter < Test::Unit::TestCase 6 | 7 | def setup 8 | @env = Rack::MockRequest.env_for '/request/path' 9 | @counter = Stubb::Counter.new lambda { |env| [200, {}, [env['stubb.request_sequence_index']]] } 10 | end 11 | 12 | def test_initial_request 13 | result = @counter.call(@env) 14 | assert_equal 1, @env['stubb.request_sequence_index'] 15 | assert_equal result.last.last, @env['stubb.request_sequence_index'] 16 | end 17 | 18 | def test_repeated_request 19 | @counter.call(@env) 20 | result = @counter.call(@env) 21 | assert_equal 2, result.last.last 22 | end 23 | 24 | def test_side_effect_on_different_path 25 | @counter.call(@env) 26 | result = @counter.call Rack::MockRequest.env_for('/request/other') 27 | assert_equal 1, result.last.last 28 | end 29 | 30 | def test_side_effect_on_different_method 31 | @counter.call(@env) 32 | result = @counter.call Rack::MockRequest.env_for('/request/path', 'REQUEST_METHOD' => 'POST') 33 | assert_equal 1, result.last.last 34 | end 35 | 36 | def test_side_effect_on_different_accept_header 37 | @counter.call(@env) 38 | result = @counter.call Rack::MockRequest.env_for('/request/path', 'HTTP_ACCEPT' => 'application/json') 39 | assert_equal 1, result.last.last 40 | end 41 | 42 | end -------------------------------------------------------------------------------- /test/test_match_finder.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'stubb' 4 | 5 | class TestMatchFinder < Test::Unit::TestCase 6 | 7 | def setup 8 | @finder = Stubb::MatchFinder.new :root => 'test/fixtures' 9 | end 10 | 11 | def test_trailing_matching_collection 12 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic', 'REQUEST_METHOD' => 'GET') 13 | assert_equal 200, response.first 14 | assert_equal ['GET matching collection'], response.last.body 15 | end 16 | 17 | def test_trailing_matching_collection_as_json_explicitly 18 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic.json', 'REQUEST_METHOD' => 'GET') 19 | assert_equal 200, response.first 20 | assert_equal ['GET matching collection.json'], response.last.body 21 | assert_equal 'application/json', response[1]['Content-Type'] 22 | end 23 | 24 | def test_trailing_matching_collection_as_json_implicitly 25 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic', 'REQUEST_METHOD' => 'GET', 'HTTP_ACCEPT' => 'application/json') 26 | assert_equal 200, response.first 27 | assert_equal ['GET matching collection.json'], response.last.body 28 | assert_equal 'application/json', response[1]['Content-Type'] 29 | end 30 | 31 | def test_embedded_matching_collection 32 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic/static', 'REQUEST_METHOD' => 'GET') 33 | assert_equal 200, response.first 34 | assert_equal ['GET static'], response.last.body 35 | end 36 | 37 | def test_embedded_matching_collection_as_json_explicitly 38 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic/static.json', 'REQUEST_METHOD' => 'GET') 39 | assert_equal 200, response.first 40 | assert_equal ['GET static.json'], response.last.body 41 | assert_equal 'application/json', response[1]['Content-Type'] 42 | end 43 | 44 | def test_trailing_matching_collection_as_json_implicitly 45 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic/static', 'REQUEST_METHOD' => 'GET', 'HTTP_ACCEPT' => 'application/json') 46 | assert_equal 200, response.first 47 | assert_equal ['GET static.json'], response.last.body 48 | assert_equal 'application/json', response[1]['Content-Type'] 49 | end 50 | 51 | def test_trailing_matching_member 52 | response = @finder.call Rack::MockRequest.env_for('/matching/collection/dynamic', 'REQUEST_METHOD' => 'GET') 53 | assert_equal 200, response.first 54 | assert_equal ['GET matching member'], response.last.body 55 | end 56 | 57 | def test_trailing_matching_member_as_json_explicitly 58 | response = @finder.call Rack::MockRequest.env_for('/matching/collection/dynamic.json', 'REQUEST_METHOD' => 'GET') 59 | assert_equal 200, response.first 60 | assert_equal ['GET matching member.json'], response.last.body 61 | assert_equal 'application/json', response[1]['Content-Type'] 62 | end 63 | 64 | def test_trailing_matching_member_as_json_implicitly 65 | response = @finder.call Rack::MockRequest.env_for('/matching/collection/dynamic.json', 'REQUEST_METHOD' => 'GET', 'HTTP_ACCEPT' => 'application/json') 66 | assert_equal 200, response.first 67 | assert_equal ['GET matching member.json'], response.last.body 68 | assert_equal 'application/json', response[1]['Content-Type'] 69 | end 70 | 71 | def test_get_matching_member_template 72 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic/template?name=Karl', 'REQUEST_METHOD' => 'GET') 73 | assert_equal 200, response.first 74 | assert_equal ['GET matching Karl'], response.last.body 75 | end 76 | 77 | def test_post_matching_member_template 78 | response = @finder.call Rack::MockRequest.env_for('/matching/dynamic/template', 'REQUEST_METHOD' => 'POST', :input => 'name=Karl') 79 | assert_equal 200, response.first 80 | assert_equal ['POST matching Karl'], response.last.body 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/test_naive_finder.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'stubb' 4 | 5 | class TestNaiveFinder < Test::Unit::TestCase 6 | 7 | def setup 8 | @finder = Stubb::NaiveFinder.new :root => 'test/fixtures' 9 | end 10 | 11 | def test_get_collection 12 | response = @finder.call Rack::MockRequest.env_for('/collection', 'REQUEST_METHOD' => 'GET') 13 | assert_equal 200, response.first 14 | assert_equal ['GET collection'], response.last.body 15 | end 16 | 17 | def test_get_collection_as_root 18 | @finder = Stubb::NaiveFinder.new :root => 'test/fixtures/collection' 19 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET') 20 | assert_equal 200, response.first 21 | assert_equal ['GET collection'], response.last.body 22 | end 23 | 24 | def test_get_collection_as_json_explicitly 25 | response = @finder.call Rack::MockRequest.env_for('/collection.json', 'REQUEST_METHOD' => 'GET') 26 | assert_equal 200, response.first 27 | assert_equal ['GET collection.json'], response.last.body 28 | assert_equal 'application/json', response[1]['Content-Type'] 29 | end 30 | 31 | def test_get_collection_as_json_implicitly 32 | response = @finder.call Rack::MockRequest.env_for('/collection', 'REQUEST_METHOD' => 'GET', 'HTTP_ACCEPT' => 'application/json, text/html') 33 | assert_equal 200, response.first 34 | assert_equal ['GET collection.json'], response.last.body 35 | assert_equal 'application/json', response[1]['Content-Type'] 36 | end 37 | 38 | def test_post_collection 39 | response = @finder.call Rack::MockRequest.env_for('/collection', 'REQUEST_METHOD' => 'POST') 40 | assert_equal 200, response.first 41 | assert_equal ['POST collection'], response.last.body 42 | end 43 | 44 | def test_get_member 45 | response = @finder.call Rack::MockRequest.env_for('/collection/member', 'REQUEST_METHOD' => 'GET') 46 | assert_equal 200, response.first 47 | assert_equal ['GET member'], response.last.body 48 | end 49 | 50 | def test_get_member_as_json_explicitly 51 | response = @finder.call Rack::MockRequest.env_for('/collection/member.json', 'REQUEST_METHOD' => 'GET') 52 | assert_equal 200, response.first 53 | assert_equal ['GET member.json'], response.last.body 54 | assert_equal 'application/json', response[1]['Content-Type'] 55 | end 56 | 57 | def test_get_member_as_json_implicitly 58 | response = @finder.call Rack::MockRequest.env_for('/collection/member', 'REQUEST_METHOD' => 'GET', 'HTTP_ACCEPT' => 'application/json') 59 | assert_equal 200, response.first 60 | assert_equal ['GET member.json'], response.last.body 61 | assert_equal 'application/json', response[1]['Content-Type'] 62 | end 63 | 64 | def test_get_member_template 65 | response = @finder.call Rack::MockRequest.env_for('/collection/member_template?name=Karl', 'REQUEST_METHOD' => 'GET') 66 | assert_equal 200, response.first 67 | assert_equal ['GET Karl'], response.last.body 68 | end 69 | 70 | def test_post_member_template 71 | response = @finder.call Rack::MockRequest.env_for('/collection/member_template', 'REQUEST_METHOD' => 'POST', :input => 'name=Karl') 72 | assert_equal 200, response.first 73 | assert_equal ['POST Karl'], response.last.body 74 | end 75 | 76 | end 77 | 78 | -------------------------------------------------------------------------------- /test/test_request.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'stubb' 4 | 5 | class TestRequest < Test::Unit::TestCase 6 | 7 | def setup 8 | @request = Stubb::Request.new Rack::MockRequest.env_for('/test/the/functionality.html.erb') 9 | end 10 | 11 | def test_path_parts 12 | assert_equal ['test', 'the', 'functionality.html.erb'], @request.path_parts 13 | end 14 | 15 | def test_path_dir_parts 16 | assert_equal ['test', 'the'], @request.path_dir_parts 17 | end 18 | 19 | def test_file_name 20 | assert_equal 'functionality.html.erb', @request.file_name 21 | end 22 | 23 | def test_resource_name 24 | assert_equal 'functionality.html', @request.resource_name 25 | end 26 | 27 | def test_extension 28 | assert_equal '.erb', @request.extension 29 | end 30 | 31 | def test_relative_path 32 | assert_equal 'test/the/functionality.html.erb', @request.relative_path 33 | end 34 | 35 | end 36 | 37 | -------------------------------------------------------------------------------- /test/test_response.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'stubb' 4 | 5 | class TestResponse < Test::Unit::TestCase 6 | 7 | def test_yaml_frontmatter 8 | response = Stubb::Response.new( 9 | "---\nstatus: 201\nheader:\n Foo: Baz\n---\nBody", 10 | {}, 11 | 200, 12 | {'Foo' => 'Bar'} 13 | ).finish 14 | assert_equal ['Body'], response.last.body 15 | assert_equal 'Baz', response[1]['Foo'] 16 | end 17 | 18 | def test_templating 19 | response = Stubb::Response.new( 20 | "<%= params['foo'] %>", 21 | {'foo' => 'Bar'}, 22 | 200, 23 | {} 24 | ).finish 25 | assert_equal ['Bar'], response.last.body 26 | end 27 | 28 | end 29 | 30 | -------------------------------------------------------------------------------- /test/test_sequence_finder.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'stubb' 4 | 5 | class TestSequenceFinder < Test::Unit::TestCase 6 | 7 | def setup 8 | @finder = Stubb::SequenceFinder.new :root => 'test/fixtures' 9 | end 10 | 11 | def test_get_stalling_collection 12 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 13 | assert_equal ['GET collection 1'], response.last.body 14 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 15 | assert_equal ['GET collection 2'], response.last.body 16 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 17 | assert_equal ['GET collection 3'], response.last.body 18 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 19 | assert_equal ['GET collection 3'], response.last.body 20 | end 21 | 22 | def test_get_stalling_collection_as_root 23 | @finder = Stubb::SequenceFinder.new :root => 'test/fixtures/stalling_sequence' 24 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 25 | assert_equal ['GET collection 1'], response.last.body 26 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 27 | assert_equal ['GET collection 2'], response.last.body 28 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 29 | assert_equal ['GET collection 3'], response.last.body 30 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 31 | assert_equal ['GET collection 3'], response.last.body 32 | end 33 | 34 | def test_get_stalling_member 35 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 36 | assert_equal ['GET member 1'], response.last.body 37 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 38 | assert_equal ['GET member 2'], response.last.body 39 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 40 | assert_equal ['GET member 3'], response.last.body 41 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 42 | assert_equal ['GET member 3'], response.last.body 43 | end 44 | 45 | def test_get_looping_collection 46 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 47 | assert_equal ['GET collection 0'], response.last.body 48 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 49 | assert_equal ['GET collection 1'], response.last.body 50 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 51 | assert_equal ['GET collection 2'], response.last.body 52 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 53 | assert_equal ['GET collection 0'], response.last.body 54 | end 55 | 56 | def test_get_looping_collection_as_root 57 | @finder = Stubb::SequenceFinder.new :root => 'test/fixtures/looping_sequence' 58 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 59 | assert_equal ['GET collection 0'], response.last.body 60 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 61 | assert_equal ['GET collection 1'], response.last.body 62 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 63 | assert_equal ['GET collection 2'], response.last.body 64 | response = @finder.call Rack::MockRequest.env_for('/', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 65 | assert_equal ['GET collection 0'], response.last.body 66 | end 67 | 68 | def test_get_looping_member 69 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 70 | assert_equal ['GET member 0'], response.last.body 71 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 72 | assert_equal ['GET member 1'], response.last.body 73 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 74 | assert_equal ['GET member 2'], response.last.body 75 | response = @finder.call Rack::MockRequest.env_for('/looping_sequence/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 76 | assert_equal ['GET member 0'], response.last.body 77 | end 78 | 79 | def test_get_template_sequence 80 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence/template?name=Karl', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 81 | assert_equal 200, response.first 82 | assert_equal ['GET Karl 1'], response.last.body 83 | end 84 | 85 | def test_post_template_sequence 86 | response = @finder.call Rack::MockRequest.env_for('/stalling_sequence/template', 'REQUEST_METHOD' => 'POST', 'stubb.request_sequence_index' => 1, :input => 'name=Karl') 87 | assert_equal 200, response.first 88 | assert_equal ['POST Karl 1'], response.last.body 89 | end 90 | 91 | end 92 | 93 | -------------------------------------------------------------------------------- /test/test_sequence_match_finder.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'stubb' 4 | 5 | class TestSequenceMatchFinder < Test::Unit::TestCase 6 | 7 | def setup 8 | @finder = Stubb::SequenceMatchFinder.new :root => 'test/fixtures' 9 | end 10 | 11 | def test_get_matched_stalling_collection 12 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 13 | assert_equal ['GET matching collection 1'], response.last.body 14 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 15 | assert_equal ['GET matching collection 2'], response.last.body 16 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 17 | assert_equal ['GET matching collection 3'], response.last.body 18 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 19 | assert_equal ['GET matching collection 3'], response.last.body 20 | end 21 | 22 | def test_get_matched_stalling_member 23 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 24 | assert_equal ['GET matching member 1'], response.last.body 25 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 26 | assert_equal ['GET matching member 2'], response.last.body 27 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 28 | assert_equal ['GET matching member 3'], response.last.body 29 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 30 | assert_equal ['GET matching member 3'], response.last.body 31 | end 32 | 33 | def test_get_matched_looping_collection 34 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 35 | assert_equal ['GET matching collection 0'], response.last.body 36 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 37 | assert_equal ['GET matching collection 1'], response.last.body 38 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 39 | assert_equal ['GET matching collection 2'], response.last.body 40 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 41 | assert_equal ['GET matching collection 0'], response.last.body 42 | end 43 | 44 | def test_get_matched_looping_member 45 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 46 | assert_equal ['GET matching member 0'], response.last.body 47 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 2) 48 | assert_equal ['GET matching member 1'], response.last.body 49 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 3) 50 | assert_equal ['GET matching member 2'], response.last.body 51 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/looping/member', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 4) 52 | assert_equal ['GET matching member 0'], response.last.body 53 | end 54 | 55 | def test_get_matched_template_sequence 56 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling/template?name=Karl', 'REQUEST_METHOD' => 'GET', 'stubb.request_sequence_index' => 1) 57 | assert_equal 200, response.first 58 | assert_equal ['GET matching Karl 1'], response.last.body 59 | end 60 | 61 | def test_post_matched_template_sequence 62 | response = @finder.call Rack::MockRequest.env_for('/matching/sequences/dynamic/stalling/template', 'REQUEST_METHOD' => 'POST', 'stubb.request_sequence_index' => 1, :input => 'name=Karl') 63 | assert_equal 200, response.first 64 | assert_equal ['POST matching Karl 1'], response.last.body 65 | end 66 | 67 | end 68 | 69 | --------------------------------------------------------------------------------