agile-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/.bowerrc +3 -0
  3. data/.gitignore +8 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +36 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +267 -0
  9. data/Guardfile +20 -0
  10. data/LICENSE +22 -0
  11. data/README.md +93 -0
  12. data/Rakefile +13 -0
  13. data/agile-proxy.gemspec +50 -0
  14. data/assets/index.html +39 -0
  15. data/assets/ui/app/HttpFlexibleProxyApi.js +31 -0
  16. data/assets/ui/app/app.js +1 -0
  17. data/assets/ui/app/controller/Stubs.js +64 -0
  18. data/assets/ui/app/controller/main.js +12 -0
  19. data/assets/ui/app/directive/AppEnhancedFormElement.js +21 -0
  20. data/assets/ui/app/directive/AppFor.js +16 -0
  21. data/assets/ui/app/directive/AppResponseEditor.js +54 -0
  22. data/assets/ui/app/model/RequestSpec.js +6 -0
  23. data/assets/ui/app/routes.js +10 -0
  24. data/assets/ui/app/service/Dialog.js +49 -0
  25. data/assets/ui/app/service/DomId.js +10 -0
  26. data/assets/ui/app/service/Error.js +7 -0
  27. data/assets/ui/app/service/Stub.js +36 -0
  28. data/assets/ui/app/view/404.html +2 -0
  29. data/assets/ui/app/view/dialog/error.html +10 -0
  30. data/assets/ui/app/view/dialog/yesNo.html +8 -0
  31. data/assets/ui/app/view/responses/editForm.html +78 -0
  32. data/assets/ui/app/view/status.html +1 -0
  33. data/assets/ui/app/view/stubs.html +19 -0
  34. data/assets/ui/app/view/stubs/edit.html +58 -0
  35. data/assets/ui/css/main.css +3 -0
  36. data/bin/agile_proxy +113 -0
  37. data/bower.json +27 -0
  38. data/config.yml +6 -0
  39. data/db.yml +10 -0
  40. data/db/migrations/20140818110800_create_users.rb +9 -0
  41. data/db/migrations/20140818134700_create_applications.rb +10 -0
  42. data/db/migrations/20140818135200_create_request_specs.rb +13 -0
  43. data/db/migrations/20140821115300_create_responses.rb +14 -0
  44. data/db/migrations/20140823082900_add_method_to_request_specs.rb +7 -0
  45. data/db/migrations/20140823083900_rename_request_spec_columns.rb +8 -0
  46. data/db/migrations/20141031072100_add_url_type_to_request_specs.rb +8 -0
  47. data/db/migrations/20141105125600_add_conditions_to_request_specs.rb +7 -0
  48. data/db/migrations/20141106083100_add_username_and_password_to_applications.rb +8 -0
  49. data/db/migrations/20141119143800_add_record_to_applications.rb +7 -0
  50. data/db/migrations/20141119174300_create_recordings.rb +18 -0
  51. data/db/schema.rb +78 -0
  52. data/examples/README.md +1 -0
  53. data/examples/facebook_api.html +59 -0
  54. data/examples/tumblr_api.html +22 -0
  55. data/lib/agile_proxy.rb +8 -0
  56. data/lib/agile_proxy/api/applications.rb +77 -0
  57. data/lib/agile_proxy/api/recordings.rb +52 -0
  58. data/lib/agile_proxy/api/request_specs.rb +85 -0
  59. data/lib/agile_proxy/api/root.rb +41 -0
  60. data/lib/agile_proxy/config.rb +63 -0
  61. data/lib/agile_proxy/handlers/handler.rb +43 -0
  62. data/lib/agile_proxy/handlers/proxy_handler.rb +110 -0
  63. data/lib/agile_proxy/handlers/request_handler.rb +57 -0
  64. data/lib/agile_proxy/handlers/stub_handler.rb +113 -0
  65. data/lib/agile_proxy/mitm.crt +22 -0
  66. data/lib/agile_proxy/mitm.key +27 -0
  67. data/lib/agile_proxy/model/application.rb +20 -0
  68. data/lib/agile_proxy/model/recording.rb +16 -0
  69. data/lib/agile_proxy/model/request_spec.rb +47 -0
  70. data/lib/agile_proxy/model/response.rb +56 -0
  71. data/lib/agile_proxy/model/user.rb +17 -0
  72. data/lib/agile_proxy/proxy_connection.rb +113 -0
  73. data/lib/agile_proxy/route.rb +106 -0
  74. data/lib/agile_proxy/router.rb +99 -0
  75. data/lib/agile_proxy/server.rb +85 -0
  76. data/lib/agile_proxy/servers/api.rb +41 -0
  77. data/lib/agile_proxy/servers/request_spec.rb +30 -0
  78. data/lib/agile_proxy/version.rb +6 -0
  79. data/load_proxy.js +39 -0
  80. data/log/.gitkeep +0 -0
  81. data/spec/common_helper.rb +32 -0
  82. data/spec/fixtures/test-server.crt +15 -0
  83. data/spec/fixtures/test-server.key +15 -0
  84. data/spec/integration/helpers/request_spec_helper.rb +60 -0
  85. data/spec/integration/specs/lib/server_spec.rb +407 -0
  86. data/spec/integration_spec_helper.rb +18 -0
  87. data/spec/spec_helper.rb +39 -0
  88. data/spec/support/test_server.rb +75 -0
  89. data/spec/unit/agile_proxy/api/applications_spec.rb +102 -0
  90. data/spec/unit/agile_proxy/api/common_helper.rb +31 -0
  91. data/spec/unit/agile_proxy/api/recordings_spec.rb +115 -0
  92. data/spec/unit/agile_proxy/api/request_specs_spec.rb +159 -0
  93. data/spec/unit/agile_proxy/handlers/handler_spec.rb +8 -0
  94. data/spec/unit/agile_proxy/handlers/proxy_handler_spec.rb +138 -0
  95. data/spec/unit/agile_proxy/handlers/request_handler_spec.rb +55 -0
  96. data/spec/unit/agile_proxy/handlers/stub_handler_spec.rb +154 -0
  97. data/spec/unit/agile_proxy/model/recording_spec.rb +0 -0
  98. data/spec/unit/agile_proxy/model/request_spec_spec.rb +45 -0
  99. data/spec/unit/agile_proxy/model/response_spec.rb +38 -0
  100. data/spec/unit/agile_proxy/server_spec.rb +88 -0
  101. data/spec/unit/agile_proxy/servers/api_spec.rb +31 -0
  102. data/spec/unit/agile_proxy/servers/request_spec_spec.rb +32 -0
  103. metadata +618 -0
@@ -0,0 +1,85 @@
1
+ require 'active_record'
2
+ require 'yaml'
3
+ require 'cgi'
4
+ require 'uri'
5
+ require 'eventmachine'
6
+ require 'thin'
7
+ require 'grape'
8
+ require 'agile_proxy/api/root'
9
+ require 'agile_proxy/servers/api'
10
+ require 'agile_proxy/servers/request_spec'
11
+
12
+ module AgileProxy
13
+ #
14
+ # This class is responsible for controlling the underlying proxy and web servers
15
+ #
16
+ class Server
17
+ ROOT = File.expand_path '../../', File.dirname(__FILE__)
18
+ extend Forwardable
19
+ attr_reader :request_handler
20
+
21
+ def_delegators :request_handler, :reset_cache, :restore_cache, :handle_request
22
+
23
+ def initialize
24
+ environment = AgileProxy.config.environment
25
+ dbconfig = YAML.load(File.read(AgileProxy.config.database_config_file)).with_indifferent_access
26
+ ActiveRecord::Base.configurations = dbconfig
27
+ ActiveRecord::Base.establish_connection dbconfig[environment.to_s]
28
+ end
29
+
30
+ # Starts the proxy and web servers
31
+ def start
32
+ main_loop
33
+ end
34
+
35
+ # The url that the proxy server is running on
36
+ # @return [String] The URL
37
+ def url
38
+ "http://#{host}:#{port}"
39
+ end
40
+
41
+ # The url that the web server can be accessed from
42
+ # @return [String] The URL
43
+ def webserver_url
44
+ "http://#{webserver_host}:#{webserver_port}"
45
+ end
46
+
47
+ # The host that the proxy server is running on
48
+ # @return [String] The host
49
+ def host
50
+ 'localhost'
51
+ end
52
+
53
+ # The port that the proxy server is running on
54
+ # @return [String] The port
55
+ def port
56
+ @request_spec_server.port
57
+ end
58
+
59
+ # The host that the webserver is running on
60
+ # @return [String] The host
61
+ def webserver_host
62
+ AgileProxy.config.webserver_host
63
+ end
64
+
65
+ # The port that the webserver is running on
66
+ # @return [String] The port
67
+ def webserver_port
68
+ AgileProxy.config.webserver_port
69
+ end
70
+
71
+ protected
72
+
73
+ def main_loop
74
+ EM.run do
75
+ EM.error_handler do |e|
76
+ puts e.class.name, e
77
+ puts e.backtrace.join("\n")
78
+ end
79
+ AgileProxy::Servers::Api.start(webserver_host, webserver_port)
80
+ @request_spec_server = AgileProxy::Servers::RequestSpec.start
81
+ AgileProxy.log(:info, "agile-proxy: Proxy listening on #{url} and webserver listening on #{webserver_url}")
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ require 'rack'
2
+ require 'thin'
3
+ require 'grape'
4
+ require 'agile_proxy/api/root'
5
+
6
+ module AgileProxy
7
+ module Servers
8
+ #
9
+ # The API Server
10
+ #
11
+ # This server is a RACK server responsible for providing access to the system
12
+ # using REST requests.
13
+ # This allows remote programming of the proxy using either a client adapter or the built in user interface
14
+ module Api
15
+ ROOT = File.expand_path '../../../', File.dirname(__FILE__)
16
+ class << self
17
+ #
18
+ # Starts the webserver on the given host and port
19
+ # @param webserver_host [String] The host for the server to run on
20
+ # @param webserver_port [Integer] The port for the server to run on
21
+ def start(webserver_host, webserver_port)
22
+ # The sinatra web server
23
+ dispatch = Rack::Builder.app do
24
+ use Rack::Static, root: File.join(ROOT, 'assets'), urls: ['/ui'], index: 'index.html'
25
+ map '/api' do
26
+ run ::AgileProxy::Api::Root.new
27
+ end
28
+ end
29
+ # Start the web server.
30
+ ::Rack::Server.start(
31
+ app: dispatch,
32
+ server: 'thin',
33
+ Host: webserver_host,
34
+ Port: webserver_port,
35
+ signals: false
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ require 'eventmachine'
2
+ module AgileProxy
3
+ module Servers
4
+ #
5
+ # The 'Request Spec' server
6
+ # This server is responsible for handling or passing through a request, depending
7
+ # on if it has a matching 'Request Specification'
8
+ class RequestSpec
9
+ # Starts the server
10
+ def self.start
11
+ new.start
12
+ end
13
+ def initialize
14
+ @request_handler = AgileProxy::RequestHandler.new
15
+ end
16
+ # Starts the server
17
+ def start
18
+ @signature = EM.start_server('127.0.0.1', AgileProxy.config.proxy_port, ProxyConnection) do |p|
19
+ p.handler = @request_handler
20
+ end
21
+ self
22
+ end
23
+ # The port the server is running on
24
+ # @return [Integer] The port the server is running on
25
+ def port
26
+ Socket.unpack_sockaddr_in(EM.get_sockname(@signature)).first
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ # Agile Proxy
2
+ #
3
+ # The Agile Proxy module is a common namespace for all classes / sub modules.
4
+ module AgileProxy
5
+ VERSION = '0.1.0'
6
+ end
data/load_proxy.js ADDED
@@ -0,0 +1,39 @@
1
+ var proxy, contracts;
2
+ proxy = require('flexible_proxy_client');
3
+ debugger;
4
+ contracts = [];
5
+ for (i=0; i< 100; i=i+1) {
6
+ contracts.push({
7
+ "created_at": "2014-10-21T09:30:45.217Z",
8
+ "customer_company_id": "1",
9
+ "customer_contact_id": "1",
10
+ "daily_rate_currency": "GBP",
11
+ "daily_rate_pennies": 10000,
12
+ "duration": 2678400,
13
+ "end_date": "2013-01-31T00:00:00.000Z",
14
+ "fq_name": "My First Contract",
15
+ "id": "544627c51fedb0383c0000" + i,
16
+ "name": "My Contract No " + (i + 1),
17
+ "owner_company_id": "1",
18
+ "owner_id": "544627c51fedb0383c000092",
19
+ "start_date": "2013-01-01T00:00:00.000Z",
20
+ "status": "accepted",
21
+ "supplier_company_id": "1",
22
+ "updated_at": "2014-10-21T09:30:45.217Z",
23
+ "value_currency": "GBP",
24
+ "value_pennies": 100000,
25
+ "notes": [
26
+
27
+ ]
28
+ });
29
+ }
30
+ proxy.define([
31
+ proxy.stub('http://localhost:9000/api/v1/contracts.json').andReturn({json: {
32
+ "contracts": contracts,
33
+ "total": contracts.length,
34
+ "success": true
35
+ }}),
36
+ proxy.stub('http://localhost:35729/livereload.js').andReturn({
37
+ json: {}
38
+ })
39
+ ], function () {console.log("YEP")});
data/log/.gitkeep ADDED
File without changes
@@ -0,0 +1,32 @@
1
+ module AgileProxy
2
+ module Test
3
+ # Common helpers for the test suite
4
+ module Common
5
+ def to_rack_env(opts = {})
6
+ fake_input_buffer = StringIO.new(opts[:body] || '')
7
+ fake_error_buffer = StringIO.new
8
+ url_parsed = URI.parse(opts[:url])
9
+ env = {
10
+ 'rack.input' => Rack::Lint::InputWrapper.new(fake_input_buffer),
11
+ 'rack.errors' => Rack::Lint::ErrorWrapper.new(fake_error_buffer),
12
+ 'REQUEST_METHOD' => (opts[:method] || 'GET').upcase,
13
+ 'REQUEST_PATH' => url_parsed.path,
14
+ 'PATH_INFO' => url_parsed.path,
15
+ 'QUERY_STRING' => url_parsed.query || '',
16
+ 'REQUEST_URI' => url_parsed.path + (url_parsed.query.nil? ? '' : url_parsed.query),
17
+ 'rack.url_scheme' => url_parsed.scheme,
18
+ 'CONTENT_LENGTH' => (opts[:body] || '').length,
19
+ 'SERVER_NAME' => url_parsed.host,
20
+ 'SERVER_PORT' => url_parsed.port
21
+ }
22
+ (opts[:headers] || {}).each do |name, value|
23
+ converted_name = 'HTTP_' + (name.gsub(/-/, '_').upcase)
24
+ env[converted_name] = value
25
+ end
26
+ env['CONTENT_TYPE'] = env.delete('HTTP_CONTENT_TYPE') if env.key?('HTTP_CONTENT_TYPE')
27
+ env['CONTENT_LENGTH'] = env.delete('HTTP_CONTENT_LENGTH') if env.key?('HTTP_CONTENT_LENGTH')
28
+ env
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICazCCAdQCCQD9MxXmqmRKtTANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV
3
+ UzELMAkGA1UECBMCQ0ExITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
4
+ ZDEWMBQGA1UEAxMNUHVmZmluZyBCaWxseTEjMCEGCSqGSIb3DQEJARYUb2xseS5z
5
+ bWl0aEBnbWFpbC5jb20wHhcNMTIxMDA0MDgwNzMxWhcNMTIxMTAzMDgwNzMxWjB6
6
+ MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExITAfBgNVBAoTGEludGVybmV0IFdp
7
+ ZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAxMNUHVmZmluZyBCaWxseTEjMCEGCSqGSIb3
8
+ DQEJARYUb2xseS5zbWl0aEBnbWFpbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0A
9
+ MIGJAoGBAKsUa8bzUeTfC05JFwjBXnD+nBlil5aVTDOB719WJkiq1eDY5WWBaMhk
10
+ OqlZkNhl1rg7LmG/0NhBBUdICYTblNRXzhEfaejqOIGQ3FZMR3W/b87cBp4S+8b/
11
+ q9xJPSUgFvQUzX4v+2+//wdiO8qPioinDdRG9RhxwNvS34bqJqpfAgMBAAEwDQYJ
12
+ KoZIhvcNAQEFBQADgYEADnHq4MP+LgTdp4+ycyearUZh4uhAPzxYXTGDirkkFFQ4
13
+ YA2QT0zXgPq1JmrZwV5Wo1wKoIq8LtfeYoDou0VDKtZ6Up13c98R+1yuQE2kgNoG
14
+ 9VGn/2la4mvLpj+9Zt8wfNxibFi+ajZ7/zsebI7UM+pW0a3SDowDeU0nVPAmDRM=
15
+ -----END CERTIFICATE-----
@@ -0,0 +1,15 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIICXgIBAAKBgQCrFGvG81Hk3wtOSRcIwV5w/pwZYpeWlUwzge9fViZIqtXg2OVl
3
+ gWjIZDqpWZDYZda4Oy5hv9DYQQVHSAmE25TUV84RH2no6jiBkNxWTEd1v2/O3Aae
4
+ EvvG/6vcST0lIBb0FM1+L/tvv/8HYjvKj4qIpw3URvUYccDb0t+G6iaqXwIDAQAB
5
+ AoGBAJccJ4KIYzqUZGkWmBjsq92Elx64/gpM/wyz5VpBPvmKo/XBvwWkg4gVN9dj
6
+ vFPXyAvcgkBm7DJHZEEs+PN3/IEMyLHWVA+C+C3AHisR/E2yl/AyD9oX8F0KDu+Q
7
+ 8bnsL1rFL5zP1saw+QiyofQ13HKtCOj8zjhAUCyZl1NBdiqxAkEA2PNxBHFBSnPU
8
+ L6+PiCvDOJcbqrHQa5kYcS312SNqX/VQ9BtIJfIQ7QA/ueKIUd40OfNBc/jiOC1x
9
+ mlSwlsLnFwJBAMnfWagBvlinIwXw4yhAgsqt012gOzPIHCM7FWipJPbCm9LtJykE
10
+ dGuA06TRE2ZKQq+Oh2yomNni4/LfPVQ6Y/kCQHkej/4W7IiQWem1bcBsDjVNx1ho
11
+ pR8s/YRSUGrFZuHjpyphAMqOdfyaovk4CzsJfsbLk8MXM9SBKmcq2NuSPEkCQQCM
12
+ MvTmTIewtCsLtjdcvijXsA9KR7y2ArUf9qmwrUABrDhiLdfzkad0/dx+68FIWiyk
13
+ Fh2RZin5sKzVARtrwr2pAkEAgIDCCl3YC7Jk5GOm1xVjb999hZSyasOasiVsAafa
14
+ 4UzoA4HlDjTWEROObX8ijbsWRU16/yotbWXDa9XXxnDV3A==
15
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,60 @@
1
+ module AgileProxy
2
+ module Test
3
+ module Integration
4
+ # A helper for 'request spec' integration tests
5
+ module RequestSpecHelper
6
+ def load_small_set_of_request_specs(options = {})
7
+ let(:recordings_resource) { RestClient::Resource.new "http://localhost:3020/api/v1/users/1/applications/#{@recording_application_id}/recordings", headers: { content_type: 'application/json' } }
8
+ before :context do
9
+
10
+ def configure_applications
11
+ application_resource.delete # Delete all applications
12
+ @non_recording_application_id = JSON.parse(application_resource.post user_id: 1, name: 'Non recording app', username: 'anonymous', password: 'password')['id']
13
+ @recording_application_id = JSON.parse(application_resource.post user_id: 1, name: 'Recording app', username: 'recording', password: 'password', record_requests: true)['id']
14
+ end
15
+
16
+ def application_resource
17
+ @__application_resource ||= RestClient::Resource.new 'http://localhost:3020/api/v1/users/1/applications', headers: { content_type: 'application/json' }
18
+ end
19
+
20
+ def create_request_spec(attrs)
21
+ non_recording_resource.post ActiveSupport::JSON.encode attrs
22
+ recording_resource.post ActiveSupport::JSON.encode attrs
23
+ end
24
+
25
+ def non_recording_resource
26
+ @__non_recording_resource ||= RestClient::Resource.new "http://localhost:3020/api/v1/users/1/applications/#{@non_recording_application_id}/request_specs", headers: { content_type: 'application/json' }
27
+ end
28
+
29
+ def recording_resource
30
+ @__recording_resource ||= RestClient::Resource.new "http://localhost:3020/api/v1/users/1/applications/#{@recording_application_id}/request_specs", headers: { content_type: 'application/json' }
31
+ end
32
+
33
+ # Delete all first
34
+ configure_applications
35
+ # Now, add some stubs via the REST interface
36
+ [@http_url, @https_url].each do |url|
37
+ create_request_spec url: "#{url}/index.html", response: { content_type: 'text/html', content: '<html><body>Mocked Content</body></html>' }
38
+ create_request_spec url: "#{url}/api/forums", response: { content_type: 'application/json', content: JSON.pretty_generate(forums: [], total: 0) }
39
+ create_request_spec url: "#{url}/api/forums", http_method: 'POST', response: { content_type: 'application/json', content: '{"created": true}' }
40
+ create_request_spec url: "#{url}/api/forums/:forum_id/posts", response: { content_type: 'application/json', content: JSON.pretty_generate(posts: [
41
+ { forum_id: '{{forum_id}}', subject: 'My first post' },
42
+ { forum_id: '{{forum_id}}', subject: 'My second post' },
43
+ { forum_id: '{{forum_id}}', subject: 'My third post' }
44
+ ], total: 3), is_template: true }
45
+ create_request_spec url: "#{url}/api/forums/:forum_id/:post_id", response: { content_type: 'text/html', content: '<html><body><h1>Sorted By: {{sort}}</h1><h2>{{forum_id}}</h2><h3>{{post_id}}</h3></body></html>', is_template: true }
46
+ create_request_spec url: "#{url}/api/forums/:forum_id/:post_id", http_method: 'PUT', response: { content_type: 'application/json', content: '{"updated": true}' }
47
+ create_request_spec url: "#{url}/api/forums/:forum_id/:post_id", http_method: 'DELETE', response: { content_type: 'application/json', content: '{"deleted": true}' }
48
+ create_request_spec url: "#{url}/api/forums/:forum_id/:post_id", conditions: '{"post_id": "special"}', response: { content_type: 'text/html', content: '<html><body><h1>Sorted By: {{sort}}</h1><h2>{{forum_id}}</h2><h3>{{post_id}}</h3><p>This is a special response</p></body></html>', is_template: true }
49
+ create_request_spec url: "#{url}/api/forums/:forum_id/:post_id", conditions: '{"post_id": "special", "sort": "eversospecial"}', response: { content_type: 'text/html', content: '<html><body><h1>Sorted By: {{sort}}</h1><h2>{{forum_id}}</h2><h3>{{post_id}}</h3><p>This is an ever so special response</p></body></html>', is_template: true }
50
+ create_request_spec url: "#{url}/api/forums/:forum_id", http_method: 'POST', conditions: '{"posted_var":"special_value"}', response: { content_type: 'text/html', content: '<html><body><h1></h1><h2>{{posted_var}}</h2><h3>{{forum_id}}</h3><p>This should get data from the POSTed data</p></body></html>', is_template: true }
51
+ end
52
+ end
53
+ before :each do
54
+ recordings_resource.delete if options.key?(:recording) && options[:recording]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,407 @@
1
+ require 'integration_spec_helper'
2
+
3
+ # require 'spec_helper'
4
+ require 'agile_proxy'
5
+ require 'resolv'
6
+
7
+ shared_examples_for 'a proxy server' do |options = {}|
8
+ if options.key?(:recording) && options[:recording]
9
+ after :each do
10
+ expect(JSON.parse(recordings_resource.get)['total']).to eql(1)
11
+ end
12
+ end
13
+ it 'should proxy GET requests' do
14
+ expect(http.get('/echo').body).to eql 'GET /echo'
15
+ end
16
+
17
+ it 'should proxy POST requests' do
18
+ expect(http.post('/echo', foo: 'bar').body).to eql "POST /echo\nfoo=bar"
19
+ end
20
+
21
+ it 'should proxy PUT requests' do
22
+ expect(http.post('/echo', foo: 'bar').body).to eql "POST /echo\nfoo=bar"
23
+ end
24
+
25
+ it 'should proxy HEAD requests' do
26
+ expect(http.head('/echo').headers['HTTP-X-EchoServer']).to eql 'HEAD /echo'
27
+ end
28
+
29
+ it 'should proxy DELETE requests' do
30
+ expect(http.delete('/echo').body).to eql 'DELETE /echo'
31
+ end
32
+ end
33
+
34
+ shared_examples_for 'a request stub' do |options = {}|
35
+ if options.key?(:recording) && options[:recording]
36
+ after :each do
37
+ data = recordings_resource.get
38
+ count = JSON.parse(data)['total']
39
+ expect(count).to eql(1)
40
+ end
41
+ end
42
+ it 'should stub GET requests' do
43
+ expect(http.get('/index.html').body).to eql '<html><body>Mocked Content</body></html>'
44
+ end
45
+
46
+ it 'should stub GET response statuses' do
47
+ expect(http.get('/index.html').status).to eql 200
48
+ end
49
+
50
+ it 'Should stub a different get request with json response' do
51
+ resp = http.get('/api/forums')
52
+ expect(ActiveSupport::JSON.decode(resp.body).symbolize_keys).to eql forums: [], total: 0
53
+ expect(resp.status).to eql 200
54
+ expect(resp.headers['Content-Type']).to eql 'application/json'
55
+ end
56
+
57
+ it 'Should get the mocked content with parameter substitution for the /api/forums/:forum_id/posts url' do
58
+ resp = http.get '/api/forums/my_forum/posts'
59
+ expect(ActiveSupport::JSON.decode(resp.body)).to eql 'posts' => [{ 'forum_id' => 'my_forum', 'subject' => 'My first post' }, { 'forum_id' => 'my_forum', 'subject' => 'My second post' }, { 'forum_id' => 'my_forum', 'subject' => 'My third post' }], 'total' => 3
60
+ expect(resp.status).to eql 200
61
+ expect(resp.headers['Content-Type']).to eql 'application/json'
62
+ end
63
+
64
+ it 'Should get the mocked content for api/forums/:forum_id/:post_id with parameter substitution including query parameters' do
65
+ resp = http.get '/api/forums/my_forum/my_post?sort=id'
66
+ expect(resp.body).to eql '<html><body><h1>Sorted By: id</h1><h2>my_forum</h2><h3>my_post</h3></body></html>'
67
+ expect(resp.status).to eql 200
68
+ expect(resp.headers['Content-Type']).to eql 'text/html'
69
+ end
70
+ it 'Should get the mocked content for api/forums/:forum_id/:post_id.html with parameter substitution including query parameters' do
71
+ resp = http.get '/api/forums/my_forum/my_post.html?sort=id'
72
+ expect(resp.body).to eql '<html><body><h1>Sorted By: id</h1><h2>my_forum</h2><h3>my_post</h3></body></html>'
73
+ expect(resp.status).to eql 200
74
+ expect(resp.headers['Content-Type']).to eql 'text/html'
75
+ end
76
+ it 'Should respond with an error for api/forums/:forum_id/:post_id.html with parameter substitution with a missing query parameter' do
77
+ resp = http.get '/api/forums/my_forum/my_post.html'
78
+ expect(resp.body).to eql "Missing var or method 'sort' in data."
79
+ expect(resp.status).to eql 500
80
+ end
81
+ it 'Should match the route by posted json data and the posted data can be output via the template' do
82
+ resp = http.post '/api/forums/my_forum', '{"posted_var": "special_value"}', 'Content-Type' => 'application/json'
83
+ expect(resp.body).to eql '<html><body><h1></h1><h2>special_value</h2><h3>my_forum</h3><p>This should get data from the POSTed data</p></body></html>'
84
+ expect(resp.status).to eql 200
85
+ expect(resp.headers['Content-Type']).to eql 'text/html'
86
+ end
87
+ # it 'Should match the route by posted xml data and the posted data can be output via the template' do
88
+ # resp = http.post "/api/forums/my_forum", '<posted_var>special_value</posted_var>', {'Content-Type' => 'application/xml'}
89
+ # expect(resp.body).to eql '<html><body><h1></h1><h2>special_value</h2><h3>my_forum</h3><p>This should get data from the POSTed data</p></body></html>'
90
+ # expect(resp.status).to eql 200
91
+ # end
92
+ it 'Should match the route by posted url encoded data and the posted data can be output via the template' do
93
+ resp = http.post '/api/forums/my_forum', 'posted_var=special_value', 'Content-Type' => 'application/x-www-form-urlencoded'
94
+ expect(resp.body).to eql '<html><body><h1></h1><h2>special_value</h2><h3>my_forum</h3><p>This should get data from the POSTed data</p></body></html>'
95
+ expect(resp.status).to eql 200
96
+ end
97
+ it 'Should match the route by posted multipart encoded data and the posted data can be output via the template' do
98
+ resp = http.post '/api/forums/my_forum', 'posted_var=special_value', 'Content-Type' => 'multipart/form-data'
99
+ expect(resp.body).to eql '<html><body><h1></h1><h2>special_value</h2><h3>my_forum</h3><p>This should get data from the POSTed data</p></body></html>'
100
+ expect(resp.status).to eql 200
101
+ end
102
+
103
+ it 'should stub POST requests' do
104
+ resp = http.post('/api/forums', foo: :bar)
105
+ expect(resp.body).to eql '{"created": true}'
106
+ expect(resp.headers['Content-Type']).to eql 'application/json'
107
+
108
+ end
109
+
110
+ it 'should stub PUT requests' do
111
+ resp = http.put('/api/forums/forum_1/my_post', foo: :bar)
112
+ expect(resp.body).to eql '{"updated": true}'
113
+ expect(resp.headers['Content-Type']).to eql 'application/json'
114
+ end
115
+
116
+ it 'should stub DELETE requests' do
117
+ resp = http.delete('/api/forums/forum_1/my_post')
118
+ expect(resp.body).to eql '{"deleted": true}'
119
+ expect(resp.headers['Content-Type']).to eql 'application/json'
120
+ end
121
+ end
122
+
123
+ shared_examples_for 'a cache' do
124
+
125
+ context 'whitelisted GET requests' do
126
+ it 'should not be cached' do
127
+ assert_noncached_url
128
+ end
129
+
130
+ context 'with ports' do
131
+ before do
132
+ rack_app_url = URI(http.url_prefix)
133
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
134
+ end
135
+
136
+ it 'should not be cached ' do
137
+ assert_noncached_url
138
+ end
139
+ end
140
+ end
141
+
142
+ context 'non-whitelisted GET requests' do
143
+ before do
144
+ AgileProxy.config.whitelist = []
145
+ end
146
+
147
+ it 'should be cached' do
148
+ assert_cached_url
149
+ end
150
+
151
+ context 'with ports' do
152
+ before do
153
+ rack_app_url = URI(http.url_prefix)
154
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port + 1}"]
155
+ end
156
+
157
+ it 'should be cached' do
158
+ assert_cached_url
159
+ end
160
+ end
161
+ end
162
+
163
+ context 'ignore_params GET requests' do
164
+ before do
165
+ AgileProxy.config.ignore_params = ['/analytics']
166
+ end
167
+
168
+ it 'should be cached' do
169
+ r = http.get('/analytics?some_param=5')
170
+ expect(r.body).to eql 'GET /analytics'
171
+ expect do
172
+ expect do
173
+ r = http.get('/analytics?some_param=20')
174
+ end.to change { r.headers['HTTP-X-EchoCount'].to_i }.by(1)
175
+ end.to_not change { r.body }
176
+ end
177
+ end
178
+
179
+ context 'path_blacklist GET requests' do
180
+ before do
181
+ AgileProxy.config.path_blacklist = ['/api']
182
+ end
183
+
184
+ it 'should be cached' do
185
+ assert_cached_url('/api')
186
+ end
187
+ end
188
+
189
+ context 'cache persistence' do
190
+ let(:cached_key) { proxy.cache.key('get', "#{url}/foo", '') }
191
+ let(:cached_file) do
192
+ f = cached_key + '.yml'
193
+ File.join(AgileProxy.config.cache_path, f)
194
+ end
195
+
196
+ before { AgileProxy.config.whitelist = [] }
197
+
198
+ after do
199
+ File.delete(cached_file) if File.exist?(cached_file)
200
+ end
201
+
202
+ context 'enabled' do
203
+ before { AgileProxy.config.persist_cache = true }
204
+
205
+ it 'should persist' do
206
+ http.get('/foo')
207
+ expect(File.exist?(cached_file)).to be_true
208
+ end
209
+
210
+ it 'should be read initially from persistent cache' do
211
+ File.open(cached_file, 'w') do |f|
212
+ cached = {
213
+ headers: {},
214
+ content: 'GET /foo cached'
215
+ }
216
+ f.write(cached.to_yaml(Encoding: :Utf8))
217
+ end
218
+
219
+ r = http.get('/foo')
220
+ expect(r.body).to eql 'GET /foo cached'
221
+ end
222
+
223
+ context 'cache_request_headers requests' do
224
+ it 'should not be cached by default' do
225
+ http.get('/foo')
226
+ saved_cache = AgileProxy.proxy.cache.fetch_from_persistence(cached_key)
227
+ expect(saved_cache.keys).not_to include :request_headers
228
+ end
229
+
230
+ context 'when enabled' do
231
+ before do
232
+ AgileProxy.config.cache_request_headers = true
233
+ end
234
+
235
+ it 'should be cached' do
236
+ http.get('/foo')
237
+ saved_cache = AgileProxy.proxy.cache.fetch_from_persistence(cached_key)
238
+ expect(saved_cache.keys).to include :request_headers
239
+ end
240
+ end
241
+ end
242
+
243
+ context 'ignore_cache_port requests' do
244
+ it 'should be cached without port' do
245
+ r = http.get('/foo')
246
+ url = URI(r.env[:url])
247
+ saved_cache = AgileProxy.proxy.cache.fetch_from_persistence(cached_key)
248
+
249
+ expect(saved_cache[:url]).to_not eql(url.to_s)
250
+ expect(saved_cache[:url]).to eql(url.to_s.gsub(":#{url.port}", ''))
251
+ end
252
+ end
253
+
254
+ context 'non_whitelisted_requests_disabled requests' do
255
+ before { AgileProxy.config.non_whitelisted_requests_disabled = true }
256
+
257
+ it 'should raise error when disabled' do
258
+ # TODO: Suppress stderr output: https://gist.github.com/adamstegman/926858
259
+ expect { http.get('/foo') }.to raise_error(Faraday::Error::ConnectionFailed, 'end of file reached')
260
+ end
261
+ end
262
+
263
+ context 'non_successful_cache_disabled requests' do
264
+ before do
265
+ rack_app_url = URI(http_error.url_prefix)
266
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
267
+ AgileProxy.config.non_successful_cache_disabled = true
268
+ end
269
+
270
+ it 'should not cache non-successful response when enabled' do
271
+ http_error.get('/foo')
272
+ expect(File.exist?(cached_file)).to be_false
273
+ end
274
+
275
+ it 'should cache successful response when enabled' do
276
+ assert_cached_url
277
+ end
278
+ end
279
+
280
+ context 'non_successful_error_level requests' do
281
+ before do
282
+ rack_app_url = URI(http_error.url_prefix)
283
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
284
+ AgileProxy.config.non_successful_error_level = :error
285
+ end
286
+
287
+ it 'should raise error for non-successful responses when :error' do
288
+ # When this config setting is set, the EventMachine running the test servers is killed upon error raising
289
+ # The `raise` is required to bubble up the error to the test running it
290
+ # The Faraday error is raised upon `close_connection` so this can be non-pending if we can do one of the following:
291
+ # 1) Remove the `raise error_message` conditionally for this test
292
+ # 2) Restart the test servers if they aren't running
293
+ # 3) Change the test servers to start/stop for each test instead of before all
294
+ # 4) Remove the test server completely and rely on the server instantiated by the app
295
+ pending 'Unable to test this without affecting the running test servers'
296
+ expect { http_error.get('/foo') }.to raise_error(Faraday::Error::ConnectionFailed)
297
+ end
298
+ end
299
+ end
300
+ context 'disabled' do
301
+ before { AgileProxy.config.persist_cache = false }
302
+
303
+ it 'shouldnt persist' do
304
+ http.get('/foo')
305
+ expect(File.exist?(cached_file)).to be_false
306
+ end
307
+ end
308
+ end
309
+
310
+ def assert_noncached_url(url = '/foo')
311
+ r = http.get(url)
312
+ expect(r.body).to eql "GET #{url}"
313
+ expect do
314
+ expect do
315
+ r = http.get(url)
316
+ end.to change { r.headers['HTTP-X-EchoCount'].to_i }.by(1)
317
+ end.to_not change { r.body }
318
+ end
319
+
320
+ def assert_cached_url(url = '/foo')
321
+ r = http.get(url)
322
+ expect(r.body).to eql "GET #{url}"
323
+ expect do
324
+ expect do
325
+ r = http.get(url)
326
+ end.to_not change { r.headers['HTTP-X-EchoCount'] }
327
+ end.to_not change { r.body }
328
+ end
329
+ end
330
+ describe AgileProxy::Server do
331
+ extend AgileProxy::Test::Integration::RequestSpecHelper
332
+ describe 'Without recording' do
333
+ load_small_set_of_request_specs
334
+ before do
335
+ # Adding non-valid Faraday options throw an error: https://github.com/arsduo/koala/pull/311
336
+ # Valid options: :request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class
337
+ faraday_options = {
338
+ proxy: { uri: 'http://anonymous:password@localhost:3100' },
339
+ request: { timeout: 10.0 }
340
+ }
341
+ @http = Faraday.new @http_url, faraday_options
342
+ @https = Faraday.new @https_url, faraday_options.merge(ssl: { verify: false })
343
+ @http_error = Faraday.new @error_url, faraday_options
344
+ end
345
+ context 'proxying' do
346
+ context 'HTTP' do
347
+ let!(:http) { @http }
348
+ it_should_behave_like 'a proxy server'
349
+ end
350
+ context 'HTTPS' do
351
+ let!(:http) { @https }
352
+ it_should_behave_like 'a proxy server'
353
+ end
354
+ end
355
+ context 'stubbing' do
356
+ context 'HTTP' do
357
+ let!(:url) { @http_url }
358
+ let!(:http) { @http }
359
+ it_should_behave_like 'a request stub'
360
+ end
361
+
362
+ context 'HTTPS' do
363
+ let!(:url) { @https_url }
364
+ let!(:http) { @https }
365
+ it_should_behave_like 'a request stub'
366
+ end
367
+
368
+ end
369
+ end
370
+ describe 'With recording' do
371
+ load_small_set_of_request_specs recording: true
372
+ before do
373
+ # Adding non-valid Faraday options throw an error: https://github.com/arsduo/koala/pull/311
374
+ # Valid options: :request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class
375
+ faraday_options = {
376
+ proxy: { uri: 'http://recording:password@localhost:3100' },
377
+ request: { timeout: 10.0 }
378
+ }
379
+
380
+ @http = Faraday.new @http_url, faraday_options
381
+ @https = Faraday.new @https_url, faraday_options.merge(ssl: { verify: false })
382
+ @http_error = Faraday.new @error_url, faraday_options
383
+ end
384
+ context 'proxying' do
385
+ context 'HTTP' do
386
+ let!(:http) { @http }
387
+ it_should_behave_like 'a proxy server', recording: true
388
+ end
389
+ context 'HTTPS' do
390
+ let!(:http) { @https }
391
+ it_should_behave_like 'a proxy server', recording: true
392
+ end
393
+ end
394
+ context 'stubbing' do
395
+ context 'HTTP' do
396
+ let!(:url) { @http_url }
397
+ let!(:http) { @http }
398
+ it_should_behave_like 'a request stub', recording: true
399
+ end
400
+ context 'HTTPS' do
401
+ let!(:url) { @https_url }
402
+ let!(:http) { @https }
403
+ it_should_behave_like 'a request stub', recording: true
404
+ end
405
+ end
406
+ end
407
+ end