anadea-mailsocio-qa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 358f0bf77ff8f08d406dd4b7d46e7141c09b94c2
4
+ data.tar.gz: f482a30a4827d7f173eb02dc6b131184feb77738
5
+ SHA512:
6
+ metadata.gz: c63adb9ba1a8bca9fe7bd5740c0ac145311c882fdeb03ebcbb190aa21bbfd14a1c70395158fa696cbdded59588c30288ac38198b50a9c8e954bee6a07e97d855
7
+ data.tar.gz: 044fd1dfeecddc8959b12192ca3cbd4900608e6d6b19bc4b54b05e4e2dcd6a17411155809064e0b6973c07e9def4d900e88a7e8aa55a09c66470fae934d6e77b
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle
2
+ /log
3
+ /tmp
4
+ *.swp
5
+ .rbenv-version
6
+ .vagrant/*
7
+ provision/cache/*
8
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ anadea-mailsocio-qa (0.0.3)
5
+ activesupport (>= 3.0, < 5.0)
6
+ httpi (>= 2.0)
7
+ json (>= 1.0)
8
+ mail (>= 2.0)
9
+ minitest (>= 4.0)
10
+ nokogiri (>= 1.0)
11
+ settingslogic (>= 2.0)
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activesupport (4.2.1)
17
+ i18n (~> 0.7)
18
+ json (~> 1.7, >= 1.7.7)
19
+ minitest (~> 5.1)
20
+ thread_safe (~> 0.3, >= 0.3.4)
21
+ tzinfo (~> 1.1)
22
+ httpi (2.4.0)
23
+ rack
24
+ i18n (0.7.0)
25
+ json (1.8.2)
26
+ mail (2.6.3)
27
+ mime-types (>= 1.16, < 3)
28
+ mime-types (2.5)
29
+ mini_portile (0.6.2)
30
+ minitest (5.6.1)
31
+ nokogiri (1.6.6.2)
32
+ mini_portile (~> 0.6.0)
33
+ rack (1.6.1)
34
+ settingslogic (2.0.9)
35
+ thread_safe (0.3.5)
36
+ tzinfo (1.2.2)
37
+ thread_safe (~> 0.1)
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ anadea-mailsocio-qa!
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/mailsocio-qa ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ Bundler.setup(:default)
6
+
7
+ require 'mailsocio_qa'
8
+ HTTPI.log = false
9
+
10
+ require 'minitest/autorun'
@@ -0,0 +1,269 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'mailsocio_qa/settings'
4
+ require 'mailsocio_qa/test_client'
5
+ require 'mailsocio_qa/test_html_page'
6
+
7
+ module MailsocioQA
8
+ class MailsocioTest < ::Minitest::Test
9
+ def setup
10
+ @base_domain = Regexp.escape(Settings.mailsocio.base_domain)
11
+ @track_domain = "track.#{Settings.mailsocio.base_domain}"
12
+ @client = TestClient.new
13
+
14
+ addresses = Settings.trap.addresses
15
+ @john = addresses.john
16
+ @jane = addresses.jane
17
+ @invalid = addresses.invalid
18
+ end
19
+
20
+ def test_create_account
21
+ @client.create_account
22
+ refute_nil @client.account_id
23
+ end
24
+
25
+ def test_send_mail
26
+ @client.create_account
27
+ @client.clean_messages
28
+
29
+ @client.deliver(
30
+ 'X-To' => "{\"#{@john}\":{\"name\":\"John Doe\"}}",
31
+ 'Subject' => 'Hello',
32
+ 'Body' => 'Hello, __NAME__'
33
+ )
34
+
35
+ message = @client.read_message
36
+ assert_equal 'Hello', message.subject
37
+ assert_equal 'Hello, John Doe', message.body.raw_source.strip
38
+ end
39
+
40
+ def test_nonascii_mail_css_and_tracking
41
+ @client.create_account
42
+ @client.clean_messages
43
+
44
+ @client.deliver(
45
+ 'X-To' => "[\"#{@john}\"]",
46
+ 'X-Inline-Css' => 1,
47
+ 'X-Track' => 1,
48
+ 'Content-Type' => 'text/html; charset=ISO-8859-1',
49
+ 'Content-Transfer-Encoding' => 'quoted-printable',
50
+ 'Subject' => 'Hello',
51
+ 'Body' => <<NONASCIIHTML
52
+ <html>
53
+ <head>
54
+ <style type=3D"text/css">
55
+ body, .wrapper {
56
+ height: 20px;
57
+ }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <p>English Caf=E9</p>
62
+ </body>
63
+ </html>
64
+ NONASCIIHTML
65
+ )
66
+
67
+ message = @client.read_message
68
+ assert_equal 'Hello', message.subject
69
+ assert_match /\s*<body style=3D"height:20px">\r\n/, message.body.raw_source.strip, 'No inlined css styles'
70
+ assert_match /\s*<p>English Caf=E9<\/p>\r\n/, message.body.raw_source.strip, 'Not original quoted-printable'
71
+ assert_match /<img src=3D".*\/assets\/dot\.png\?token=3D/, message.body.raw_source.strip, 'No tracking image injected'
72
+ end
73
+
74
+ def test_send_newsletter
75
+ @client.create_account
76
+ @client.clean_messages('john')
77
+ @client.clean_messages('jane')
78
+
79
+ @client.deliver(
80
+ 'X-Newsletter-Id' => '15',
81
+ 'X-To' => "[\"#{@john}\", \"#{@jane}\"]"
82
+ )
83
+
84
+ @client.read_message
85
+ stat = @client.newsletter_statistics('15')
86
+ assert_equal 2, stat['queued']
87
+ end
88
+
89
+ def test_newsletter_tracking
90
+ @client.create_account
91
+ @client.clean_messages('john')
92
+ @client.clean_messages('jane')
93
+
94
+ @client.deliver(
95
+ 'X-Newsletter-Id' => '15',
96
+ 'X-To' => "[\"#{@john}\",\"#{@jane}\"]",
97
+ 'X-Track' => 'yes',
98
+ 'Content-Type' => 'text/html',
99
+ 'Body' => '<a href="http://google.com/search">Search</a>'
100
+ )
101
+
102
+ message = @client.read_message('john')
103
+ page = TestHtmlPage.new(message.body.raw_source)
104
+
105
+ stat = @client.newsletter_statistics('15')
106
+ assert_equal 2, stat['queued']
107
+ assert_equal 2, stat['sent']
108
+ assert_equal 0, stat['viewed']
109
+ assert_equal 0, stat['clicked']
110
+
111
+ page.load
112
+ stat = @client.newsletter_statistics('15')
113
+ assert_equal 2, stat['queued']
114
+ assert_equal 2, stat['sent']
115
+ assert_equal 1, stat['viewed']
116
+ assert_equal 0, stat['clicked']
117
+
118
+ assert_equal 'http://google.com/search', page.click
119
+ stat = @client.newsletter_statistics('15')
120
+ assert_equal 2, stat['queued']
121
+ assert_equal 2, stat['sent']
122
+ assert_equal 1, stat['viewed']
123
+ assert_equal 1, stat['clicked']
124
+
125
+ message = @client.read_message('jane')
126
+ page = TestHtmlPage.new(message.body.raw_source)
127
+
128
+ page.load
129
+ stat = @client.newsletter_statistics('15')
130
+ assert_equal 2, stat['queued']
131
+ assert_equal 2, stat['sent']
132
+ assert_equal 2, stat['viewed']
133
+ assert_equal 1, stat['clicked']
134
+
135
+ assert_equal 'http://google.com/search', page.click
136
+ stat = @client.newsletter_statistics('15')
137
+ assert_equal 2, stat['queued']
138
+ assert_equal 2, stat['sent']
139
+ assert_equal 2, stat['viewed']
140
+ assert_equal 2, stat['clicked']
141
+ end
142
+
143
+ def test_hard_bounce
144
+ @client.create_account
145
+ @client.deliver(
146
+ 'X-Newsletter-Id' => '15',
147
+ 'X-To' => "[\"#{@invalid}\"]"
148
+ )
149
+ sleep 90
150
+
151
+ stat = @client.newsletter_statistics('15')
152
+ assert_equal 1, stat['queued']
153
+ assert_equal 1, stat['hard_bounced']
154
+ end
155
+
156
+ def test_newsletter_tracking_subsequent_failures
157
+ skip("still not in production")
158
+
159
+ @client.create_account
160
+
161
+ @client.deliver(
162
+ 'X-Newsletter-Id' => '15',
163
+ 'X-To' => "[\"#{@invalid}\"]",
164
+ )
165
+
166
+ sleep 90
167
+
168
+ stat = @client.newsletter_statistics('15')
169
+ assert_equal 0, stat['new']
170
+ assert_equal 1, stat['queued']
171
+ assert_equal 1, stat['hard_bounced']
172
+ assert_equal 0, stat['skipped']
173
+ assert_equal 1, stat['sent']
174
+ assert_equal 0, stat['delivered']
175
+
176
+ @client.deliver(
177
+ 'X-Newsletter-Id' => '16',
178
+ 'X-To' => "[\"#{@invalid}\"]",
179
+ )
180
+
181
+ sleep 30
182
+
183
+ stat = @client.newsletter_statistics('16')
184
+ assert_equal 0, stat['new']
185
+ assert_equal 1, stat['queued']
186
+ assert_equal 1, stat['hard_bounced']
187
+ assert_equal 1, stat['skipped']
188
+ assert_equal 1, stat['sent']
189
+ assert_equal 0, stat['delivered']
190
+ end
191
+
192
+ def test_authentication_results
193
+ @client.create_account
194
+ @client.clean_messages
195
+ @client.deliver('X-To' => "[\"#{@john}\"]")
196
+
197
+ message = @client.read_message
198
+ auth_results = message['Authentication-Results'].value
199
+ assert_match /spf=pass/, auth_results
200
+ assert_match /dkim=pass/, auth_results
201
+
202
+ assert_match "@#{@track_domain}", message.return_path
203
+ assert_match "d=#{@track_domain}", message['DKIM-Signature'].try(:value)
204
+ end
205
+
206
+ def test_custom_tracking_domain
207
+ domain = Settings.trap.custom_tracking_domain
208
+ attrs = @client.create_account(custom_tracking_domain: domain)
209
+ @client.custom_tracking_domain = domain
210
+ assert_wont_raise('Could not read account via custom tracking domain URL') do
211
+ @client.read_account
212
+ end
213
+ assert attrs['custom_tracking_enabled'], 'Custom tracking is disabled'
214
+ assert attrs['custom_signing_enabled'], 'Custom signing is disabled'
215
+
216
+ @client.clean_messages
217
+ @client.deliver('X-To' => "[\"#{@john}\"]")
218
+
219
+ message = @client.read_message
220
+ auth_results = message['Authentication-Results'].value
221
+ assert_match /spf=pass/, auth_results
222
+ assert_match /dkim=pass/, auth_results
223
+
224
+ assert_match "@#{domain}", message.return_path
225
+ assert_match "d=#{domain}", message['DKIM-Signature'].try(:value)
226
+ end
227
+
228
+ def test_custom_tracking_hard_bounce
229
+ domain = Settings.trap.custom_tracking_domain
230
+ @client.create_account(custom_tracking_domain: domain)
231
+
232
+ @client.clean_messages
233
+ @client.deliver(
234
+ 'X-Newsletter-Id' => '15',
235
+ 'X-To' => "[\"#{@invalid}\"]"
236
+ )
237
+ sleep 90
238
+
239
+ stat = @client.newsletter_statistics('15')
240
+ assert_equal 1, stat['queued']
241
+ assert_equal 1, stat['hard_bounced']
242
+ end
243
+
244
+ def test_transactional_routing
245
+ @client.create_account
246
+ @client.clean_messages
247
+ @client.deliver('X-To' => "[\"#{@john}\"]")
248
+
249
+ received = @client.read_message.received
250
+ assert_match /from (smtp0\.#{@base_domain}) \(\1\. /, received[-3].value
251
+ assert_match 'from localhost.localdomain (localhost ', received[-2].value
252
+ assert_match 'from localhost.localdomain (localhost ', received[-1].value
253
+ end
254
+
255
+ def test_bulk_routing
256
+ @client.create_account
257
+ @client.clean_messages
258
+ @client.deliver(
259
+ 'Precedence' => 'bulk',
260
+ 'X-To' => "[\"#{@john}\"]"
261
+ )
262
+
263
+ received = @client.read_message.received
264
+ assert_match /^from (smtp[1-9]\d*\.#{@base_domain}) \(\1\. /, received[-3].value
265
+ assert_match 'from localhost.localdomain (localhost ', received[-2].value
266
+ assert_match 'from localhost.localdomain (localhost ', received[-1].value
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,5 @@
1
+ require 'settingslogic'
2
+ module MailsocioQA
3
+ class Settings < Settingslogic
4
+ end
5
+ end
@@ -0,0 +1,91 @@
1
+ require 'mailsocio_qa/settings'
2
+
3
+ module MailsocioQA
4
+ class TestClient
5
+ attr_accessor :account_id, :custom_tracking_domain
6
+
7
+ def create_account(attrs = {})
8
+ attrs.reverse_merge!(api_key: Settings.mailsocio.api_key)
9
+ response = HTTPI.post(accounts_url, { account: attrs}.to_query)
10
+ raise "Could not create account:\n#{response.body}" if response.error?
11
+
12
+ JSON.parse(response.body).tap do |json|
13
+ self.account_id = json['id']
14
+ end
15
+ end
16
+
17
+ def read_account
18
+ response = HTTPI.get(accounts_url(self.account_id))
19
+ raise "Could not read account:\n#{response.body}" if response.error?
20
+
21
+ JSON.parse(response.body)
22
+ end
23
+
24
+ def newsletter_statistics(newsletter_id)
25
+ response = HTTPI.get(newsletter_statistics_url(newsletter_id))
26
+ raise "Could not get newsletter statistics:\n#{response.body}" if response.error?
27
+
28
+ JSON.parse(response.body)
29
+ end
30
+
31
+ def deliver(fields = {}, &block)
32
+ config = Settings.mailsocio.mail
33
+ fields.reverse_merge!(
34
+ 'X-Account-Id' => account_id,
35
+ 'X-Api-Key' => Settings.mailsocio.api_key
36
+ )
37
+ fields.reverse_merge!(config.message)
38
+
39
+ Mail::Message.new(fields, &block).tap do |mail|
40
+ mail.subject ||= 'Good news, everyone!'
41
+ delivery = config.delivery
42
+ mail.delivery_method(delivery.method.to_sym, delivery.settings.symbolize_keys)
43
+ mail.deliver
44
+ end
45
+ end
46
+
47
+ def read_message(user = 'john')
48
+ 10.times do
49
+ message = retriever(user).first(:delete_after_find => true)
50
+ return message if message.present?
51
+
52
+ sleep 10
53
+ end
54
+
55
+ raise 'Could not read message'
56
+ end
57
+
58
+ def clean_messages(user = 'john')
59
+ retriever(user).delete_all
60
+ end
61
+
62
+ def retriever(user)
63
+ address = Settings.trap.addresses[user]
64
+ server = Settings.trap.server
65
+ settings = server.settings.symbolize_keys
66
+ settings.merge!(user_name: address)
67
+
68
+ Mail::Configuration.instance.retriever_method(server.method.to_sym, settings)
69
+ end
70
+
71
+ def accounts_url(account_id = nil)
72
+ if account_id.blank?
73
+ app_url('/api/accounts')
74
+ else
75
+ app_url("/api/accounts/#{account_id}")
76
+ end
77
+ end
78
+
79
+ def newsletter_statistics_url(newsletter_id)
80
+ app_url("/api/accounts/#{account_id}/newsletters/#{newsletter_id}/statistics")
81
+ end
82
+
83
+ def app_url(path)
84
+ if custom_tracking_domain.blank?
85
+ "http://app.#{Settings.mailsocio.base_domain}#{path}"
86
+ else
87
+ "http://#{self.custom_tracking_domain}#{path}"
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,21 @@
1
+ module MailsocioQA
2
+ class TestHtmlPage
3
+ def initialize(html)
4
+ @doc = Nokogiri::HTML(html)
5
+ end
6
+
7
+ def load
8
+ @doc.css('img').each do |img|
9
+ open(img['src'])
10
+ end
11
+ end
12
+
13
+ def click
14
+ @doc.css('a').each do |a|
15
+ response = HTTPI.get(a['href'])
16
+ raise "Expected redirect but got #{response.code}" unless response.code == 302
17
+ return response.headers['location']
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/all'
2
+ require 'json'
3
+ require 'nokogiri'
4
+ require 'mail'
5
+ require 'httpi'
6
+
7
+
8
+ require 'mailsocio_qa/settings'
9
+ require 'mailsocio_qa/test_client'
10
+ require 'mailsocio_qa/test_html_page'
11
+
12
+ require 'minitest'
13
+ require 'mailsocio_qa/mailsocio_test'
@@ -0,0 +1,28 @@
1
+ require 'mailsocio_qa/settings'
2
+
3
+ module Minitest
4
+ def self.plugin_mailsocio_qa_options(opts, options)
5
+ opts.on "-c", "--config PATH", String, "Mailsocio QA config file." do |source|
6
+ MailsocioQA::Settings.source source
7
+ end
8
+ end
9
+
10
+ def self.plugin_mailsocio_qa_init(options)
11
+ MailsocioQA::Settings.source File.expand_path('../../../settings.yml', __FILE__)
12
+ MailsocioQA::Settings.load!
13
+ end
14
+
15
+ class Test
16
+ def self.runnable_methods
17
+ methods_matching(/^test_/)
18
+ end
19
+ end
20
+
21
+ module Assertions
22
+ def assert_wont_raise error_message
23
+ yield
24
+ rescue
25
+ assert false, error_message
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'anadea-mailsocio-qa'
3
+ s.version = '0.1.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = "Mailsocio Quality Assurance"
6
+ s.description = s.summary
7
+ s.authors = 'Anadea'
8
+
9
+ s.files = `git ls-files`.split("\n")
10
+ s.executables = 'mailsocio-qa'
11
+ s.require_paths = ['lib']
12
+
13
+ s.add_dependency 'minitest', ['>= 4.0']
14
+ s.add_dependency 'httpi', ['>= 2.0']
15
+ s.add_dependency 'mail', ['>= 2.0']
16
+ s.add_dependency 'json', ['>= 1.0']
17
+ s.add_dependency 'activesupport', ['>= 3.0', '< 5.0']
18
+ s.add_dependency 'settingslogic', ['>= 2.0']
19
+ s.add_dependency 'nokogiri', ['>= 1.0']
20
+ end
data/settings.yml ADDED
@@ -0,0 +1,27 @@
1
+ mailsocio:
2
+ base_domain: sendbrain.dev
3
+ api_key: "91d10da569d04883f0345e21c7b16e6caa7cea3eb84f3538a799cf96f3acb9c4"
4
+ mail:
5
+ delivery:
6
+ method: smtp
7
+ settings:
8
+ address: app.sendbrain.dev
9
+ port: 25
10
+ message:
11
+ from: tester@mailsocio.com
12
+ to: submit@app.sendbrain.dev
13
+ trap:
14
+ server:
15
+ method: pop3
16
+ settings:
17
+ password: epyfnm123
18
+ address: pop.gmail.com
19
+ port: 995
20
+ enable_ssl: true
21
+
22
+ addresses:
23
+ john: john.doe@raven.org.ua
24
+ jane: jane.doe@raven.org.ua
25
+ invalid: invalid@raven.org.ua
26
+
27
+ custom_tracking_domain: emailz.raven.org.ua
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anadea-mailsocio-qa
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Anadea
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: httpi
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mail
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ - - "<"
77
+ - !ruby/object:Gem::Version
78
+ version: '5.0'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '3.0'
86
+ - - "<"
87
+ - !ruby/object:Gem::Version
88
+ version: '5.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: settingslogic
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '2.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: nokogiri
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '1.0'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '1.0'
117
+ description: Mailsocio Quality Assurance
118
+ email:
119
+ executables:
120
+ - mailsocio-qa
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - Gemfile
126
+ - Gemfile.lock
127
+ - Rakefile
128
+ - bin/mailsocio-qa
129
+ - lib/mailsocio_qa.rb
130
+ - lib/mailsocio_qa/mailsocio_test.rb
131
+ - lib/mailsocio_qa/settings.rb
132
+ - lib/mailsocio_qa/test_client.rb
133
+ - lib/mailsocio_qa/test_html_page.rb
134
+ - lib/minitest/mailsocio_qa_plugin.rb
135
+ - mailsocio-qa.gemspec
136
+ - settings.yml
137
+ homepage:
138
+ licenses: []
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.4.5
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Mailsocio Quality Assurance
160
+ test_files: []
161
+ has_rdoc: