agile-proxy-jruby 0.1.25-jruby

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/.bowerrc +3 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +36 -0
  6. data/.travis.yml +10 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +20 -0
  9. data/LICENSE +22 -0
  10. data/README.md +131 -0
  11. data/Rakefile +15 -0
  12. data/agile-proxy.gemspec +60 -0
  13. data/assets/index.html +39 -0
  14. data/assets/ui/app/AgileProxyApi.js +31 -0
  15. data/assets/ui/app/app.js +1 -0
  16. data/assets/ui/app/controller/Stubs.js +64 -0
  17. data/assets/ui/app/controller/main.js +12 -0
  18. data/assets/ui/app/directive/AppEnhancedFormElement.js +21 -0
  19. data/assets/ui/app/directive/AppFor.js +16 -0
  20. data/assets/ui/app/directive/AppResponseEditor.js +54 -0
  21. data/assets/ui/app/model/RequestSpec.js +6 -0
  22. data/assets/ui/app/routes.js +11 -0
  23. data/assets/ui/app/service/Dialog.js +49 -0
  24. data/assets/ui/app/service/DomId.js +10 -0
  25. data/assets/ui/app/service/Error.js +7 -0
  26. data/assets/ui/app/service/Stub.js +36 -0
  27. data/assets/ui/app/view/404.html +2 -0
  28. data/assets/ui/app/view/dialog/error.html +10 -0
  29. data/assets/ui/app/view/dialog/yesNo.html +8 -0
  30. data/assets/ui/app/view/responses/editForm.html +78 -0
  31. data/assets/ui/app/view/status.html +1 -0
  32. data/assets/ui/app/view/stubs.html +19 -0
  33. data/assets/ui/app/view/stubs/edit.html +58 -0
  34. data/assets/ui/css/main.css +3 -0
  35. data/bin/agile_proxy +4 -0
  36. data/bower.json +27 -0
  37. data/config.yml +6 -0
  38. data/db.yml +10 -0
  39. data/db/migrations/20140818110800_create_users.rb +9 -0
  40. data/db/migrations/20140818134700_create_applications.rb +10 -0
  41. data/db/migrations/20140818135200_create_request_specs.rb +13 -0
  42. data/db/migrations/20140821115300_create_responses.rb +14 -0
  43. data/db/migrations/20140823082900_add_method_to_request_specs.rb +7 -0
  44. data/db/migrations/20140823083900_rename_request_spec_columns.rb +8 -0
  45. data/db/migrations/20141031072100_add_url_type_to_request_specs.rb +8 -0
  46. data/db/migrations/20141105125600_add_conditions_to_request_specs.rb +7 -0
  47. data/db/migrations/20141106083100_add_username_and_password_to_applications.rb +8 -0
  48. data/db/migrations/20141119143800_add_record_to_applications.rb +7 -0
  49. data/db/migrations/20141119174300_create_recordings.rb +18 -0
  50. data/db/migrations/20150221152500_add_record_requests_to_request_specs.rb +7 -0
  51. data/db/schema.rb +78 -0
  52. data/db/seed.rb +26 -0
  53. data/echo_server.rb +19 -0
  54. data/examples/README.md +1 -0
  55. data/examples/facebook_api.html +59 -0
  56. data/examples/tumblr_api.html +22 -0
  57. data/lib/agile_proxy.rb +8 -0
  58. data/lib/agile_proxy/api/applications.rb +77 -0
  59. data/lib/agile_proxy/api/recordings.rb +52 -0
  60. data/lib/agile_proxy/api/request_spec_recordings.rb +52 -0
  61. data/lib/agile_proxy/api/request_specs.rb +86 -0
  62. data/lib/agile_proxy/api/root.rb +45 -0
  63. data/lib/agile_proxy/cli.rb +116 -0
  64. data/lib/agile_proxy/config.rb +66 -0
  65. data/lib/agile_proxy/handlers/handler.rb +43 -0
  66. data/lib/agile_proxy/handlers/proxy_handler.rb +111 -0
  67. data/lib/agile_proxy/handlers/request_handler.rb +75 -0
  68. data/lib/agile_proxy/handlers/stub_handler.rb +146 -0
  69. data/lib/agile_proxy/mitm.crt +22 -0
  70. data/lib/agile_proxy/mitm.key +27 -0
  71. data/lib/agile_proxy/model/application.rb +20 -0
  72. data/lib/agile_proxy/model/recording.rb +17 -0
  73. data/lib/agile_proxy/model/request_spec.rb +48 -0
  74. data/lib/agile_proxy/model/response.rb +51 -0
  75. data/lib/agile_proxy/model/user.rb +17 -0
  76. data/lib/agile_proxy/proxy_connection.rb +112 -0
  77. data/lib/agile_proxy/rack/get_only_cache.rb +30 -0
  78. data/lib/agile_proxy/route.rb +106 -0
  79. data/lib/agile_proxy/router.rb +99 -0
  80. data/lib/agile_proxy/server.rb +119 -0
  81. data/lib/agile_proxy/servers/api.rb +40 -0
  82. data/lib/agile_proxy/servers/request_spec.rb +40 -0
  83. data/lib/agile_proxy/servers/request_spec_direct.rb +35 -0
  84. data/lib/agile_proxy/version.rb +6 -0
  85. data/load_proxy.js +39 -0
  86. data/log/.gitkeep +0 -0
  87. data/spec/common_helper.rb +32 -0
  88. data/spec/fixtures/example_static_file.html +1 -0
  89. data/spec/fixtures/test-server.crt +15 -0
  90. data/spec/fixtures/test-server.key +15 -0
  91. data/spec/integration/helpers/request_spec_helper.rb +84 -0
  92. data/spec/integration/specs/lib/server_spec.rb +474 -0
  93. data/spec/integration_spec_helper.rb +16 -0
  94. data/spec/spec_helper.rb +39 -0
  95. data/spec/support/test_server.rb +105 -0
  96. data/spec/unit/agile_proxy/api/applications_spec.rb +102 -0
  97. data/spec/unit/agile_proxy/api/common_helper.rb +31 -0
  98. data/spec/unit/agile_proxy/api/recordings_spec.rb +115 -0
  99. data/spec/unit/agile_proxy/api/request_spec_recordings_spec.rb +119 -0
  100. data/spec/unit/agile_proxy/api/request_specs_spec.rb +159 -0
  101. data/spec/unit/agile_proxy/handlers/handler_spec.rb +8 -0
  102. data/spec/unit/agile_proxy/handlers/proxy_handler_spec.rb +138 -0
  103. data/spec/unit/agile_proxy/handlers/request_handler_spec.rb +76 -0
  104. data/spec/unit/agile_proxy/handlers/stub_handler_spec.rb +177 -0
  105. data/spec/unit/agile_proxy/model/recording_spec.rb +0 -0
  106. data/spec/unit/agile_proxy/model/request_spec_spec.rb +45 -0
  107. data/spec/unit/agile_proxy/model/response_spec.rb +38 -0
  108. data/spec/unit/agile_proxy/server_spec.rb +91 -0
  109. data/spec/unit/agile_proxy/servers/api_spec.rb +35 -0
  110. data/spec/unit/agile_proxy/servers/request_spec_direct_spec.rb +51 -0
  111. data/spec/unit/agile_proxy/servers/request_spec_spec.rb +35 -0
  112. metadata +736 -0
@@ -0,0 +1,40 @@
1
+ require 'rack'
2
+ require 'grape'
3
+ require 'goliath/api'
4
+ require 'goliath/runner'
5
+ require 'agile_proxy/api/root'
6
+
7
+ module AgileProxy
8
+ module Servers
9
+ #
10
+ # The API Server
11
+ #
12
+ # This server is a RACK server responsible for providing access to the system
13
+ # using REST requests.
14
+ # This allows remote programming of the proxy using either a client adapter or the built in user interface
15
+ module Api
16
+ ROOT = File.expand_path '../../../', File.dirname(__FILE__)
17
+ class << self
18
+ #
19
+ # Starts the webserver on the given host and port
20
+ # @param webserver_host [String] The host for the server to run on
21
+ # @param webserver_port [Integer] The port for the server to run on
22
+ def start(webserver_host, webserver_port)
23
+ #
24
+ # The API runner
25
+ runner = ::Goliath::Runner.new([], nil)
26
+ runner.address = webserver_host
27
+ runner.port = webserver_port
28
+ runner.app = ::Goliath::Rack::Builder.app do
29
+ use ::Rack::Static, root: File.join(ROOT, 'assets'), urls: ['/ui'], index: 'index.html'
30
+ map '/api' do
31
+ run ::AgileProxy::Api::Root.new
32
+ end
33
+ end
34
+ runner.run
35
+
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ require 'eventmachine'
2
+ require 'goliath/api'
3
+ require 'goliath/proxy'
4
+ module AgileProxy
5
+ module Servers
6
+ #
7
+ # The 'Request Spec' server
8
+ # This server is responsible for handling or passing through a request, depending
9
+ # on if it has a matching 'Request Specification'
10
+ class RequestSpec
11
+
12
+ # Starts the server
13
+ def self.start(options = {})
14
+ new(options).start
15
+ end
16
+ def initialize(options = {})
17
+ @request_handler = AgileProxy::RequestHandler.new enable_cache: options[:enable_cache]
18
+ end
19
+ # Starts the server
20
+ def start
21
+ #
22
+ # The API runner
23
+ runner = ::Goliath::Proxy::Runner.new([], nil)
24
+ runner.address = '127.0.0.1'
25
+ runner.port = AgileProxy.config.proxy_port
26
+ app = @request_handler
27
+ runner.app = app
28
+ runner.run
29
+ self
30
+
31
+ end
32
+ # The port the server is running on
33
+ # @return [Integer] The port the server is running on
34
+ def port
35
+ return AgileProxy.config.proxy_port
36
+ end
37
+ private
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ require 'rack'
2
+ require 'goliath/api'
3
+ require 'goliath/runner'
4
+ module AgileProxy
5
+ module Servers
6
+ #
7
+ # The API Server
8
+ #
9
+ # This server is a RACK server responsible for providing access to the system
10
+ # using REST requests.
11
+ # This allows remote programming of the proxy using either a client adapter or the built in user interface
12
+ module RequestSpecDirect
13
+ ROOT = Dir.pwd
14
+ class << self
15
+ #
16
+ # Starts the server on the given host and port
17
+ # @param server_host [String] The host for the server to run on
18
+ # @param server_port [Integer] The port for the server to run on
19
+ def start(server_host, server_port, static_dirs = [])
20
+
21
+ runner = ::Goliath::Runner.new([], nil)
22
+ runner.address = server_host
23
+ runner.port = server_port
24
+ notFoundApp = -> {[404, {}, 'Not Found']}
25
+ runner.app = ::Goliath::Rack::Builder.app do
26
+ map '/' do
27
+ run ::Rack::Cascade.new([::Rack::Static.new(notFoundApp, root: ROOT, urls: ['']), ::AgileProxy::StubHandler.new])
28
+ end
29
+ end
30
+ runner.run
31
+ end
32
+ end
33
+ end
34
+ end
35
+ 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.25'
6
+ end
@@ -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")});
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 @@
1
+ Hello World
@@ -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,84 @@
1
+ require 'rest_client'
2
+ module AgileProxy
3
+ module Test
4
+ module Integration
5
+ # A helper for 'request spec' integration tests
6
+ module RequestSpecHelper
7
+ class Spec
8
+ attr_accessor :url, :name, :body
9
+ def self.for(url, name, body)
10
+ obj = new
11
+ obj.url=url + '/'
12
+ obj.name = name
13
+ obj.body = body
14
+ obj.body = obj.body.with_indifferent_access if obj.body.is_a? Hash
15
+ obj
16
+ end
17
+ end
18
+
19
+ def load_small_set_of_request_specs(options = {})
20
+ let(:recordings_resource) { RestClient::Resource.new "http://localhost:#{api_port}/api/v1/users/1/applications/#{@recording_application_id}/recordings", headers: { content_type: 'application/json' } }
21
+ before :context do
22
+ @stubs_with_recordings = []
23
+ def configure_applications
24
+ application_resource.delete # Delete all applications
25
+ @non_recording_application_id = JSON.parse(application_resource.post user_id: 1, name: 'Non recording app', username: 'anonymous', password: 'password')['id']
26
+ @recording_application_id = JSON.parse(application_resource.post user_id: 1, name: 'Recording app', username: 'recording', password: 'password', record_requests: true)['id']
27
+ @direct_application_id = JSON.parse(application_resource.post user_id: 1, name: 'Direct app', username: nil, password: nil, record_requests: false)['id']
28
+ end
29
+
30
+ def application_resource
31
+ @__application_resource ||= RestClient::Resource.new "http://localhost:#{api_port}/api/v1/users/1/applications", headers: { content_type: 'application/json' }
32
+ end
33
+
34
+ def create_request_spec(attrs)
35
+ non_recording_resource.post ActiveSupport::JSON.encode attrs
36
+ recording_response = JSON.parse recording_resource.post(ActiveSupport::JSON.encode attrs).body
37
+ direct_resource.post ActiveSupport::JSON.encode attrs
38
+ recording_response
39
+ end
40
+
41
+ def non_recording_resource
42
+ @__non_recording_resource ||= RestClient::Resource.new "http://localhost:#{api_port}/api/v1/users/1/applications/#{@non_recording_application_id}/request_specs", headers: { content_type: 'application/json' }
43
+ end
44
+
45
+ def recording_resource
46
+ @__recording_resource ||= RestClient::Resource.new "http://localhost:#{api_port}/api/v1/users/1/applications/#{@recording_application_id}/request_specs", headers: { content_type: 'application/json' }
47
+ end
48
+
49
+ def direct_resource
50
+ @__direct_resource ||= RestClient::Resource.new "http://localhost:#{api_port}/api/v1/users/1/applications/#{@direct_application_id}/request_specs", headers: { content_type: 'application/json' }
51
+ end
52
+
53
+
54
+ # Delete all first
55
+ configure_applications
56
+ # Now, add some stubs via the REST interface
57
+ [@http_url, @https_url, @http_url_no_proxy, @https_url_no_proxy].each do |url|
58
+ @stubs_with_recordings.push Spec.for url, 'index_old', create_request_spec(url: "#{url}/index.html", response: { content_type: 'text/html', content: '<html><body>This Is An Older Mock</body></html>' }) #This is intentional - the system should always use the latest
59
+ @stubs_with_recordings.push Spec.for url, 'index', create_request_spec(url: "#{url}/index.html", response: { content_type: 'text/html', content: '<html><body>Mocked Content</body></html>' })
60
+ @stubs_with_recordings.push Spec.for url, 'index_recording', create_request_spec(url: "#{url}/indexRecording.html", response: { content_type: 'text/html', content: '<html><body>Mocked Content</body></html>' })
61
+ @stubs_with_recordings.push Spec.for url, 'api_forums', create_request_spec(url: "#{url}/api/forums", response: { content_type: 'application/json', content: JSON.pretty_generate(forums: [], total: 0) })
62
+ @stubs_with_recordings.push Spec.for url, 'api_forums_post', create_request_spec(url: "#{url}/api/forums", http_method: 'POST', response: { content_type: 'application/json', content: '{"created": true}' })
63
+ @stubs_with_recordings.push Spec.for url, 'api_forum_post', 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 })
64
+ @stubs_with_recordings.push Spec.for url, 'api_forum_post_put', create_request_spec(url: "#{url}/api/forums/:forum_id/:post_id", http_method: 'PUT', response: { content_type: 'application/json', content: '{"updated": true}' })
65
+ @stubs_with_recordings.push Spec.for url, 'api_forum_post_delete', create_request_spec(url: "#{url}/api/forums/:forum_id/:post_id", http_method: 'DELETE', response: { content_type: 'application/json', content: '{"deleted": true}' })
66
+ @stubs_with_recordings.push Spec.for url, 'api_forum_post_special', 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 })
67
+ @stubs_with_recordings.push Spec.for url, 'api_forum_post_ever_so_special', 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 })
68
+ @stubs_with_recordings.push Spec.for url, 'api_forum_update', create_request_spec(url: "#{url}/api/forums/:forum_id", http_method: 'POST',response: { content_type: 'text/html', content: '<html><body><h1></h1><h2>WRONG RESULT</h2><h3>{{forum_id}}</h3><p>This is an incorrect result probably because the conditions are being ignored ?</p></body></html>'})
69
+ @stubs_with_recordings.push Spec.for url, 'api_forum_update_special', 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 })
70
+ @stubs_with_recordings.push Spec.for url, 'api_forums_post', create_request_spec(url: "#{url}/api/forums/:forum_id/posts", response: { content_type: 'application/json', content: JSON.pretty_generate(posts: [
71
+ { forum_id: '{{forum_id}}', subject: 'My first post' },
72
+ { forum_id: '{{forum_id}}', subject: 'My second post' },
73
+ { forum_id: '{{forum_id}}', subject: 'My third post' }
74
+ ], total: 3), is_template: true })
75
+ end
76
+ end
77
+ before :each do
78
+ recordings_resource.delete if options.key?(:recording) && options[:recording]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,474 @@
1
+ require 'integration_spec_helper'
2
+
3
+ # require 'spec_helper'
4
+ require 'agile_proxy'
5
+ require 'resolv'
6
+ require 'rest_client'
7
+
8
+ shared_examples_for 'a proxy server' do |options = {}|
9
+ if options.key?(:recording) && options[:recording]
10
+ after :each do
11
+ expect(JSON.parse(recordings_resource.get)['total']).to eql(1)
12
+ end
13
+ end
14
+ it 'should proxy GET requests' do
15
+ expect(http.get('/echo').body).to eql 'GET /echo'
16
+ end
17
+
18
+ it 'should proxy POST requests' do
19
+ expect(http.post('/echo', foo: 'bar').body).to eql "POST /echo\nfoo=bar"
20
+ end
21
+
22
+ it 'should proxy PUT requests' do
23
+ expect(http.post('/echo', foo: 'bar').body).to eql "POST /echo\nfoo=bar"
24
+ end
25
+
26
+ it 'should proxy HEAD requests' do
27
+ expect(http.head('/echo').headers['HTTP-X-EchoServer']).to eql 'HEAD /echo'
28
+ end
29
+
30
+ it 'should proxy DELETE requests' do
31
+ expect(http.delete('/echo').body).to eql 'DELETE /echo'
32
+ end
33
+ end
34
+
35
+ shared_examples_for 'a request stub' do |options = {}|
36
+ recording = false
37
+ if options.key?(:recording) && options[:recording]
38
+ recording = true
39
+ after :each do
40
+ data = recordings_resource.get
41
+ count = JSON.parse(data)['total']
42
+ expect(count).to eql(1)
43
+ end
44
+ end
45
+ def find_stub(client, name)
46
+ @stubs_with_recordings.select { |stub| stub.url == client.url_prefix.to_s && stub.name == name}.first
47
+ end
48
+ def rest_client_for_stub(stub)
49
+ RestClient::Resource.new("http://localhost:#{api_port}/api/v1/users/1/applications/#{@recording_application_id}/request_specs/#{stub.body[:id]}/recordings", content_type: :json )
50
+ end
51
+ def recordings_for(name)
52
+ stub = find_stub(http, name)
53
+ JSON.parse(rest_client_for_stub(stub).get).with_indifferent_access
54
+ end
55
+ def recordings_matcher_for(name, path)
56
+ stub = find_stub(http, name)
57
+ {recordings: a_collection_containing_exactly(a_hash_including request_body: '', request_url: "#{http.url_prefix}#{path.gsub(/^\//, '')}", request_method: 'GET', request_spec_id: stub.body[:id])}
58
+ end
59
+
60
+ it 'should stub GET requests' do
61
+ response = http.get('/index.html')
62
+ expect(response.body).to eql '<html><body>Mocked Content</body></html>'
63
+ expect(response.headers).to include('content-length' => "40")
64
+ expect(recordings_for 'index').to include(recordings_matcher_for('index', '/index.html')) if recording
65
+ end
66
+
67
+ it 'should stub GET response statuses' do
68
+ response = http.get('/index.html')
69
+ expect(response.status).to eql 200
70
+ end
71
+
72
+ #TODO When time allows, work out how to easily test this as the stubs are not recorded for anything but recorded tests
73
+ #the lookup table would need to contain all stubs and we would need a way of indexing them to allow the test to find
74
+ # the correct one.
75
+ xit 'Should record a stub if specified in the stub irrespective of whether app is recording or not' do
76
+ expect(http.get('/indexRecording.html').status).to eql 200
77
+ expect(recordings_for('index_recording')).to include(recordings_matcher_for('index_recording', '/indexRecording.html'))
78
+ end
79
+
80
+ it 'Should stub a different get request with json response' do
81
+ resp = http.get('/api/forums')
82
+ expect(ActiveSupport::JSON.decode(resp.body).symbolize_keys).to eql forums: [], total: 0
83
+ expect(resp.status).to eql 200
84
+ expect(resp.headers).to include('content-type' => 'application/json', 'content-length' => resp.body.length.to_s)
85
+ expect(recordings_for 'api_forums').to include(recordings_matcher_for('api_forums', '/api/forums')) if recording
86
+ end
87
+
88
+ it 'Should get the mocked content with parameter substitution for the /api/forums/:forum_id/posts url' do
89
+ resp = http.get '/api/forums/my_forum/posts'
90
+ 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
91
+ expect(resp.status).to eql 200
92
+ expect(resp.headers).to include('content-type' => 'application/json', 'content-length' => resp.body.length.to_s)
93
+ end
94
+
95
+ it 'Should get the mocked content for api/forums/:forum_id/:post_id with parameter substitution including query parameters' do
96
+ resp = http.get '/api/forums/my_forum/my_post?sort=id'
97
+ expect(resp.body).to eql '<html><body><h1>Sorted By: id</h1><h2>my_forum</h2><h3>my_post</h3></body></html>'
98
+ expect(resp.status).to eql 200
99
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
100
+ end
101
+ it 'Should get the mocked content for api/forums/:forum_id/:post_id.html with parameter substitution including query parameters' do
102
+ resp = http.get '/api/forums/my_forum/my_post.html?sort=id'
103
+ expect(resp.body).to eql '<html><body><h1>Sorted By: id</h1><h2>my_forum</h2><h3>my_post</h3></body></html>'
104
+ expect(resp.status).to eql 200
105
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
106
+ end
107
+ it 'Should respond with an error for api/forums/:forum_id/:post_id.html with parameter substitution with a missing query parameter' do
108
+ resp = http.get '/api/forums/my_forum/my_post.html'
109
+ expect(resp.body).to eql '<html><body><h1>Sorted By: </h1><h2>my_forum</h2><h3>my_post</h3></body></html>'
110
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
111
+ expect(resp.status).to eql 200
112
+ end
113
+ it 'Should match the route by posted json data and the posted data can be output via the template' do
114
+ resp = http.post '/api/forums/my_forum', '{"posted_var": "special_value"}', 'Content-Type' => 'application/json'
115
+ 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>'
116
+ expect(resp.status).to eql 200
117
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
118
+ end
119
+ it 'Should match the route by posted plain text data and the posted data can be output via the template' do
120
+ resp = http.post '/api/forums/my_forum', "posted_var=special_value\n", 'Content-Type' => 'text/plain'
121
+ 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>'
122
+ expect(resp.status).to eql 200
123
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
124
+ end
125
+ it 'Should match the route by posted plain text data and the posted data can be output via the template' do
126
+ resp = http.post '/api/forums/my_forum', "dummy=\nposted_var=special_value\n", 'Content-Type' => 'text/plain'
127
+ 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>'
128
+ expect(resp.status).to eql 200
129
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
130
+ end
131
+ # it 'Should match the route by posted xml data and the posted data can be output via the template' do
132
+ # resp = http.post "/api/forums/my_forum", '<posted_var>special_value</posted_var>', {'Content-Type' => 'application/xml'}
133
+ # 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>'
134
+ # expect(resp.status).to eql 200
135
+ # end
136
+ it 'Should match the route by posted url encoded data and the posted data can be output via the template' do
137
+ resp = http.post '/api/forums/my_forum', 'posted_var=special_value', 'Content-Type' => 'application/x-www-form-urlencoded'
138
+ 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>'
139
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
140
+ expect(resp.status).to eql 200
141
+ end
142
+ it 'Should match the route by posted multipart encoded data and the posted data can be output via the template' do
143
+ resp = http.post '/api/forums/my_forum', 'posted_var=special_value', 'Content-Type' => 'multipart/form-data'
144
+ 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>'
145
+ expect(resp.headers).to include('content-type' => 'text/html', 'content-length' => resp.body.length.to_s)
146
+ expect(resp.status).to eql 200
147
+ end
148
+
149
+ it 'should stub POST requests' do
150
+ resp = http.post('/api/forums', foo: :bar)
151
+ expect(resp.body).to eql '{"created": true}'
152
+ expect(resp.headers).to include('content-type' => 'application/json', 'content-length' => resp.body.length.to_s)
153
+
154
+ end
155
+
156
+ it 'should stub PUT requests' do
157
+ resp = http.put('/api/forums/forum_1/my_post', foo: :bar)
158
+ expect(resp.body).to eql '{"updated": true}'
159
+ expect(resp.headers).to include('content-type' => 'application/json', 'content-length' => resp.body.length.to_s)
160
+ end
161
+
162
+ it 'should stub DELETE requests' do
163
+ resp = http.delete('/api/forums/forum_1/my_post')
164
+ expect(resp.body).to eql '{"deleted": true}'
165
+ expect(resp.headers).to include('content-type' => 'application/json', 'content-length' => resp.body.length.to_s)
166
+ end
167
+ end
168
+
169
+ shared_examples_for 'a cache' do
170
+
171
+ context 'whitelisted GET requests' do
172
+ it 'should not be cached' do
173
+ assert_noncached_url
174
+ end
175
+
176
+ context 'with ports' do
177
+ before do
178
+ rack_app_url = URI(http.url_prefix)
179
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
180
+ end
181
+
182
+ it 'should not be cached ' do
183
+ assert_noncached_url
184
+ end
185
+ end
186
+ end
187
+
188
+ context 'non-whitelisted GET requests' do
189
+ before do
190
+ AgileProxy.config.whitelist = []
191
+ end
192
+
193
+ it 'should be cached' do
194
+ assert_cached_url
195
+ end
196
+
197
+ context 'with ports' do
198
+ before do
199
+ rack_app_url = URI(http.url_prefix)
200
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port + 1}"]
201
+ end
202
+
203
+ it 'should be cached' do
204
+ assert_cached_url
205
+ end
206
+ end
207
+ end
208
+
209
+ context 'ignore_params GET requests' do
210
+ before do
211
+ AgileProxy.config.ignore_params = ['/analytics']
212
+ end
213
+
214
+ it 'should be cached' do
215
+ r = http.get('/analytics?some_param=5')
216
+ expect(r.body).to eql 'GET /analytics'
217
+ expect do
218
+ expect do
219
+ r = http.get('/analytics?some_param=20')
220
+ end.to change { r.headers['HTTP-X-EchoCount'].to_i }.by(1)
221
+ end.to_not change { r.body }
222
+ end
223
+ end
224
+
225
+ context 'path_blacklist GET requests' do
226
+ before do
227
+ AgileProxy.config.path_blacklist = ['/api']
228
+ end
229
+
230
+ it 'should be cached' do
231
+ assert_cached_url('/api')
232
+ end
233
+ end
234
+
235
+ context 'cache persistence' do
236
+ let(:cached_key) { proxy.cache.key('get', "#{url}/foo", '') }
237
+ let(:cached_file) do
238
+ f = cached_key + '.yml'
239
+ File.join(AgileProxy.config.cache_path, f)
240
+ end
241
+
242
+ before { AgileProxy.config.whitelist = [] }
243
+
244
+ after do
245
+ File.delete(cached_file) if File.exist?(cached_file)
246
+ end
247
+
248
+ context 'enabled' do
249
+ before { AgileProxy.config.persist_cache = true }
250
+
251
+ it 'should persist' do
252
+ http.get('/foo')
253
+ expect(File.exist?(cached_file)).to be_true
254
+ end
255
+
256
+ it 'should be read initially from persistent cache' do
257
+ File.open(cached_file, 'w') do |f|
258
+ cached = {
259
+ headers: {},
260
+ content: 'GET /foo cached'
261
+ }
262
+ f.write(cached.to_yaml(Encoding: :Utf8))
263
+ end
264
+
265
+ r = http.get('/foo')
266
+ expect(r.body).to eql 'GET /foo cached'
267
+ end
268
+
269
+ context 'cache_request_headers requests' do
270
+ it 'should not be cached by default' do
271
+ http.get('/foo')
272
+ saved_cache = AgileProxy.proxy.cache.fetch_from_persistence(cached_key)
273
+ expect(saved_cache.keys).not_to include :request_headers
274
+ end
275
+
276
+ context 'when enabled' do
277
+ before do
278
+ AgileProxy.config.cache_request_headers = true
279
+ end
280
+
281
+ it 'should be cached' do
282
+ http.get('/foo')
283
+ saved_cache = AgileProxy.proxy.cache.fetch_from_persistence(cached_key)
284
+ expect(saved_cache.keys).to include :request_headers
285
+ end
286
+ end
287
+ end
288
+
289
+ context 'ignore_cache_port requests' do
290
+ it 'should be cached without port' do
291
+ r = http.get('/foo')
292
+ url = URI(r.env[:url])
293
+ saved_cache = AgileProxy.proxy.cache.fetch_from_persistence(cached_key)
294
+
295
+ expect(saved_cache[:url]).to_not eql(url.to_s)
296
+ expect(saved_cache[:url]).to eql(url.to_s.gsub(":#{url.port}", ''))
297
+ end
298
+ end
299
+
300
+ context 'non_whitelisted_requests_disabled requests' do
301
+ before { AgileProxy.config.non_whitelisted_requests_disabled = true }
302
+
303
+ it 'should raise error when disabled' do
304
+ # TODO: Suppress stderr output: https://gist.github.com/adamstegman/926858
305
+ expect { http.get('/foo') }.to raise_error(Faraday::Error::ConnectionFailed, 'end of file reached')
306
+ end
307
+ end
308
+
309
+ context 'non_successful_cache_disabled requests' do
310
+ before do
311
+ rack_app_url = URI(http_error.url_prefix)
312
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
313
+ AgileProxy.config.non_successful_cache_disabled = true
314
+ end
315
+
316
+ it 'should not cache non-successful response when enabled' do
317
+ http_error.get('/foo')
318
+ expect(File.exist?(cached_file)).to be_false
319
+ end
320
+
321
+ it 'should cache successful response when enabled' do
322
+ assert_cached_url
323
+ end
324
+ end
325
+
326
+ context 'non_successful_error_level requests' do
327
+ before do
328
+ rack_app_url = URI(http_error.url_prefix)
329
+ AgileProxy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
330
+ AgileProxy.config.non_successful_error_level = :error
331
+ end
332
+
333
+ it 'should raise error for non-successful responses when :error' do
334
+ # When this config setting is set, the EventMachine running the test servers is killed upon error raising
335
+ # The `raise` is required to bubble up the error to the test running it
336
+ # The Faraday error is raised upon `close_connection` so this can be non-pending if we can do one of the following:
337
+ # 1) Remove the `raise error_message` conditionally for this test
338
+ # 2) Restart the test servers if they aren't running
339
+ # 3) Change the test servers to start/stop for each test instead of before all
340
+ # 4) Remove the test server completely and rely on the server instantiated by the app
341
+ pending 'Unable to test this without affecting the running test servers'
342
+ expect { http_error.get('/foo') }.to raise_error(Faraday::Error::ConnectionFailed)
343
+ end
344
+ end
345
+ end
346
+ context 'disabled' do
347
+ before { AgileProxy.config.persist_cache = false }
348
+
349
+ it 'shouldnt persist' do
350
+ http.get('/foo')
351
+ expect(File.exist?(cached_file)).to be_false
352
+ end
353
+ end
354
+ end
355
+
356
+ def assert_noncached_url(url = '/foo')
357
+ r = http.get(url)
358
+ expect(r.body).to eql "GET #{url}"
359
+ expect do
360
+ expect do
361
+ r = http.get(url)
362
+ end.to change { r.headers['HTTP-X-EchoCount'].to_i }.by(1)
363
+ end.to_not change { r.body }
364
+ end
365
+
366
+ def assert_cached_url(url = '/foo')
367
+ r = http.get(url)
368
+ expect(r.body).to eql "GET #{url}"
369
+ expect do
370
+ expect do
371
+ r = http.get(url)
372
+ end.to_not change { r.headers['HTTP-X-EchoCount'] }
373
+ end.to_not change { r.body }
374
+ end
375
+ end
376
+
377
+ shared_examples_for 'a static server' do
378
+ it 'Should serve a file from the current directory' do
379
+ expect(http.get('/spec/fixtures/example_static_file.html').body).to eql 'Hello World'
380
+ end
381
+ end
382
+ describe AgileProxy::Server, :type => :integration do
383
+ extend AgileProxy::Test::Integration::RequestSpecHelper
384
+ describe 'Without recording' do
385
+ load_small_set_of_request_specs
386
+ before do
387
+ # Adding non-valid Faraday options throw an error: https://github.com/arsduo/koala/pull/311
388
+ # Valid options: :request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class
389
+ faraday_options = {
390
+ request: {timeout: 10.0}
391
+ }
392
+ faraday_options_with_proxy = faraday_options.merge({
393
+ proxy: { uri: "http://anonymous:password@localhost:#{proxy_port}" }
394
+ })
395
+ @http = Faraday.new @http_url, faraday_options_with_proxy
396
+ @https = Faraday.new @https_url, faraday_options_with_proxy.merge(ssl: { verify: false })
397
+ @http_no_proxy = Faraday.new @http_url_no_proxy, faraday_options
398
+ @https_no_proxy = Faraday.new @https_url_no_proxy, faraday_options.merge(ssl: { verify: false })
399
+ @http_error = Faraday.new @error_url, faraday_options_with_proxy
400
+ @http_error_no_proxy = Faraday.new @error_url, faraday_options_with_proxy
401
+ end
402
+ context 'proxying' do
403
+ context 'HTTP' do
404
+ let!(:http) { @http }
405
+ it_should_behave_like 'a proxy server'
406
+ end
407
+ context 'HTTPS' do
408
+ let!(:http) { @https }
409
+ it_should_behave_like 'a proxy server'
410
+ end
411
+ end
412
+ context 'stubbing' do
413
+ context 'In Proxy Mode' do
414
+ context 'HTTP' do
415
+ let!(:url) { @http_url }
416
+ let!(:http) { @http }
417
+ it_should_behave_like 'a request stub'
418
+ end
419
+
420
+ context 'HTTPS' do
421
+ let!(:url) { @https_url }
422
+ let!(:http) { @https }
423
+ it_should_behave_like 'a request stub'
424
+ end
425
+ end
426
+ #Server mode only supports http - no real point for https at the moment
427
+ context 'In Server Mode' do
428
+ context 'HTTP' do
429
+ let!(:url) { @http_url_no_proxy }
430
+ let!(:http) { @http_no_proxy }
431
+ it_should_behave_like 'a request stub'
432
+ it_should_behave_like 'a static server'
433
+ end
434
+ end
435
+ end
436
+ end
437
+ describe 'With recording' do
438
+ load_small_set_of_request_specs recording: true
439
+ before do
440
+ # Adding non-valid Faraday options throw an error: https://github.com/arsduo/koala/pull/311
441
+ # Valid options: :request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class
442
+ faraday_options = {
443
+ proxy: { uri: 'http://recording:password@localhost:3101' },
444
+ request: { timeout: 10.0 }
445
+ }
446
+
447
+ @http = Faraday.new @http_url, faraday_options
448
+ @https = Faraday.new @https_url, faraday_options.merge(ssl: { verify: false })
449
+ @http_error = Faraday.new @error_url, faraday_options
450
+ end
451
+ context 'proxying' do
452
+ context 'HTTP' do
453
+ let!(:http) { @http }
454
+ it_should_behave_like 'a proxy server', recording: true
455
+ end
456
+ context 'HTTPS' do
457
+ let!(:http) { @https }
458
+ it_should_behave_like 'a proxy server', recording: true
459
+ end
460
+ end
461
+ context 'stubbing' do
462
+ context 'HTTP' do
463
+ let!(:url) { @http_url }
464
+ let!(:http) { @http }
465
+ it_should_behave_like 'a request stub', recording: true
466
+ end
467
+ context 'HTTPS' do
468
+ let!(:url) { @https_url }
469
+ let!(:http) { @https }
470
+ it_should_behave_like 'a request stub', recording: true
471
+ end
472
+ end
473
+ end
474
+ end