agile-proxy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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