newman 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +23 -0
- data/Gemfile +3 -0
- data/README.md +126 -7
- data/examples/config/dreamhost.rb.example +5 -1
- data/examples/config/gmail.rb.example +4 -0
- data/examples/live_test.rb +4 -7
- data/examples/simple_mailing_list.rb +2 -2
- data/lib/newman.rb +97 -1
- data/lib/newman/application.rb +238 -1
- data/lib/newman/controller.rb +93 -8
- data/lib/newman/email_logger.rb +54 -0
- data/lib/newman/filters.rb +123 -0
- data/lib/newman/mailer.rb +98 -32
- data/lib/newman/mailing_list.rb +83 -0
- data/lib/newman/recorder.rb +132 -0
- data/lib/newman/request_logger.rb +41 -0
- data/lib/newman/response_logger.rb +41 -0
- data/lib/newman/server.rb +197 -23
- data/lib/newman/settings.rb +72 -4
- data/lib/newman/store.rb +68 -4
- data/lib/newman/test_mailer.rb +79 -6
- data/lib/newman/version.rb +5 -2
- data/test/helper.rb +18 -0
- data/{examples → test/integration}/acid_tests.rb +30 -20
- data/test/integration/skip_response_test.rb +22 -0
- data/test/integration/subject_filter_test.rb +25 -0
- data/test/settings.rb +4 -0
- data/test/suite.rb +6 -0
- metadata +74 -9
- data/lib/newman/commands.rb +0 -72
- data/lib/newman/store/recorder.rb +0 -63
data/lib/newman/settings.rb
CHANGED
@@ -1,9 +1,60 @@
|
|
1
|
+
# `Newman::Settings` provides the base functionality that is used by Newman's
|
2
|
+
# configuration files. It is currently a thin wrapper on top of Ruby's
|
3
|
+
# `OpenStruct` construct, but will later add some domain specific validations
|
4
|
+
# and transformations for the various configuration options it supports.
|
5
|
+
#
|
6
|
+
# Unless you need to customize the way that Newman's configuration system
|
7
|
+
# works or make changes to your settings objects at runtime, you probably don't
|
8
|
+
# need to worry about how this object is implemented. Both
|
9
|
+
# `Newman::Server.simple` and `Newman::Server.test_mode` create a
|
10
|
+
# `Newman::Settings` object for you automatically, and you will typically be
|
11
|
+
# tweaking a sample settings file rather than crafting one from scratch.
|
12
|
+
#
|
13
|
+
# `Newman::Settings`is part of Newman's **internal API**, but
|
14
|
+
# the setting file format and various settings that Newman depends
|
15
|
+
# on should be considered part of the **external API**.
|
16
|
+
|
1
17
|
module Newman
|
2
18
|
class Settings
|
19
|
+
|
20
|
+
# ---
|
21
|
+
|
22
|
+
# The `Newman::Settings.from_file` method is used to create
|
23
|
+
# a new `Newman::Settings` object and populate it with the data
|
24
|
+
# contained in a settings file, i.e.
|
25
|
+
#
|
26
|
+
# settings = Newman::Settings.from_file('config/environment.rb')
|
27
|
+
#
|
28
|
+
# This method is purely syntactic sugar, and is functionally equivalent to
|
29
|
+
# the following code:
|
30
|
+
#
|
31
|
+
# settings = Newman::Settings.new
|
32
|
+
# settings.load_config('config/environment.rb')
|
33
|
+
#
|
34
|
+
# Because there currently is little advantage to explicitly instantiating a
|
35
|
+
# blank `Newman::Settings` object, this method is the preferred way of doing
|
36
|
+
# things.
|
37
|
+
#
|
3
38
|
def self.from_file(filename)
|
4
39
|
new.tap { |o| o.load_config(filename) }
|
5
40
|
end
|
6
|
-
|
41
|
+
|
42
|
+
# ---
|
43
|
+
|
44
|
+
# A `Newman::Settings` object is a blank slate upon creation. It simply
|
45
|
+
# assigns an empty `OpenStruct` object for each type of settings data it
|
46
|
+
# supports.
|
47
|
+
#
|
48
|
+
# In most situations, you will not instantiate a `Newman::Settings` object
|
49
|
+
# directly but instead will make use of a configuration file and the
|
50
|
+
# `Newman::Settings.from_file` method. Newman provides sample configuration
|
51
|
+
# files in its `examples/` and `test/` directories, and applications should do the
|
52
|
+
# same. This will help users discover what fields can be set.
|
53
|
+
#
|
54
|
+
# We are aware of the fact that the current configuration system is way too
|
55
|
+
# flexible and a breeding ground for subtle bugs. This will be fixed in a
|
56
|
+
# future version of Newman.
|
57
|
+
|
7
58
|
def initialize
|
8
59
|
self.imap = OpenStruct.new
|
9
60
|
self.smtp = OpenStruct.new
|
@@ -11,11 +62,28 @@ module Newman
|
|
11
62
|
self.application = OpenStruct.new
|
12
63
|
end
|
13
64
|
|
14
|
-
|
15
|
-
eval(File.read(filename), binding)
|
16
|
-
end
|
65
|
+
# ---
|
17
66
|
|
67
|
+
# The `imap` and `smtp` fields are used by `Newman::Mailer`,
|
68
|
+
# the `service` field is used throughout Newman (particularly in
|
69
|
+
# `Newman::Server`), and the `application` field is reserved for
|
70
|
+
# application-specific configurations.
|
18
71
|
|
19
72
|
attr_accessor :imap, :smtp, :service, :application
|
73
|
+
|
74
|
+
|
75
|
+
# ---
|
76
|
+
|
77
|
+
# `Newman::Settings#load_config` is used for evaluating
|
78
|
+
# the contents of a file within the context of a `Newman::Settings`
|
79
|
+
# instance.
|
80
|
+
#
|
81
|
+
# In practice, this method is typically called by
|
82
|
+
# `Newman::Settings#from_file`, but can also be used to apply
|
83
|
+
# multiple settings files to a single `Newman::Settings` object
|
84
|
+
|
85
|
+
def load_config(filename)
|
86
|
+
eval(File.read(filename), binding)
|
87
|
+
end
|
20
88
|
end
|
21
89
|
end
|
data/lib/newman/store.rb
CHANGED
@@ -1,7 +1,34 @@
|
|
1
|
-
|
1
|
+
# `Newman::Store` is a minimal persistence layer for storing non-relational
|
2
|
+
# data. It is meant to make the task of building small applications with simple
|
3
|
+
# data storage needs easier.
|
4
|
+
#
|
5
|
+
# For an example of how `Newman::Store` can be used in your applications, you
|
6
|
+
# can take a look at how `Newman::MailingList` is implemented. A similar
|
7
|
+
# approach could be used to develop arbitrary persistent models.
|
8
|
+
#
|
9
|
+
# `Newman::Store` is part of Newman's **external interface**.
|
2
10
|
|
3
11
|
module Newman
|
4
12
|
class Store
|
13
|
+
|
14
|
+
# ---
|
15
|
+
|
16
|
+
# To initialize a `Newman::Store` object, a `filename` string must
|
17
|
+
# be provided, i.e.
|
18
|
+
#
|
19
|
+
# store = Newman::Store.new("simple.store")
|
20
|
+
#
|
21
|
+
# This filename will be used to initialize a `PStore` object after first
|
22
|
+
# running `FileUtils.mkdir_p` to create any directories within the path to
|
23
|
+
# the filename if they do not already exist. Once that `PStore` object is
|
24
|
+
# created, two root keys will be mapped to empty Hash objects if they
|
25
|
+
# are not set already: `:indentifers` and `:columns`.
|
26
|
+
#
|
27
|
+
# While it's okay to treat the `PStore` object as an implementation detail,
|
28
|
+
# we will treat our interactions with it as part of Newman's **external
|
29
|
+
# interface**, so that we are more conservative about making backwards
|
30
|
+
# incompatible changes to the databases created by `Newman::Store`.
|
31
|
+
|
5
32
|
def initialize(filename)
|
6
33
|
FileUtils.mkdir_p(File.dirname(filename))
|
7
34
|
|
@@ -13,22 +40,59 @@ module Newman
|
|
13
40
|
end
|
14
41
|
end
|
15
42
|
|
16
|
-
|
43
|
+
# ---
|
44
|
+
|
45
|
+
# `Newman::Store#[]` is syntactic sugar for initializing a
|
46
|
+
# `Newman::Recorder` object, and is meant to be used for
|
47
|
+
# accessing and manipulating column data by `column_key`, i.e.
|
48
|
+
#
|
49
|
+
# store[:subscriptions].create("gregory.t.brown@gmail.com")
|
50
|
+
#
|
51
|
+
# This method is functionally equivalent to the following code:
|
52
|
+
#
|
53
|
+
# recorder = Newman::Recorder.new(:subscriptions, store)
|
54
|
+
# recorder.create("gregory.t.brown@gmail.com")
|
55
|
+
#
|
56
|
+
# For aesthetic reasons and for forward compatibility, it is
|
57
|
+
# preferable to use `Newman::Store#[]` rather than instantiating
|
58
|
+
# a `Newman::Recorder` object directly.
|
17
59
|
|
18
|
-
def [](
|
19
|
-
Recorder.new(
|
60
|
+
def [](column_key)
|
61
|
+
Recorder.new(column_key, self)
|
20
62
|
end
|
21
63
|
|
64
|
+
# ---
|
65
|
+
|
66
|
+
# `Newman::Store#read` initiates a read only transaction and then yields
|
67
|
+
# the underlying `PStore` object stored in the `data` field.
|
68
|
+
|
22
69
|
def read
|
23
70
|
data.transaction(:read_only) { yield(data) }
|
24
71
|
end
|
25
72
|
|
73
|
+
# ---
|
74
|
+
|
75
|
+
# `Newman::Store#read` initiates a read/write transaction and then yields
|
76
|
+
# the underlying `PStore` object stored in the `data` field.
|
77
|
+
|
26
78
|
def write
|
27
79
|
data.transaction { yield(data) }
|
28
80
|
end
|
29
81
|
|
82
|
+
# ---
|
83
|
+
|
84
|
+
# **NOTE: Methods below this point in the file are implementation
|
85
|
+
# details, and should not be depended upon**
|
86
|
+
|
30
87
|
private
|
31
88
|
|
89
|
+
# ---
|
90
|
+
|
91
|
+
# The `data` accessor is kept private because a `Newman::Store` object is
|
92
|
+
# meant to wrap a single `PStore` object once created, and because we want
|
93
|
+
# to force every interaction with a `Newman::Store` to be transactional in
|
94
|
+
# nature.
|
95
|
+
|
32
96
|
attr_accessor :data
|
33
97
|
end
|
34
98
|
end
|
data/lib/newman/test_mailer.rb
CHANGED
@@ -1,14 +1,74 @@
|
|
1
|
+
# `Newman::TestMailer` is a drop-in replacement for `Newman::Mailer` meant for
|
2
|
+
# use in automated testing. It is a thin wrapper on top of the built in testing
|
3
|
+
# functionality provided by the mail gem.
|
4
|
+
#
|
5
|
+
# `Newman::TestMailer` may be a useful tool for Newman application developers,
|
6
|
+
# at some point but is a bit tricky to work with due to the fact that it
|
7
|
+
# relies on global state. This is a known issue and will hopefully be solved in
|
8
|
+
# a future version of Newman.
|
9
|
+
#
|
10
|
+
# `Newman::TestMailer` is part of Newman's **internal interface**, but may
|
11
|
+
# become part of the **external interface** if we can make it less brittle.
|
12
|
+
# Patches are welcome!
|
13
|
+
|
1
14
|
module Newman
|
2
|
-
TestMailer
|
15
|
+
class TestMailer
|
16
|
+
# ---
|
17
|
+
|
18
|
+
# To initialize a `Newman::TestMailer` object, a settings object must be
|
19
|
+
# provided, i.e.
|
20
|
+
#
|
21
|
+
# settings = Newman::Settings.from_file('config/environment.rb')
|
22
|
+
# mailer = Newman::TestMailer.new(settings)
|
23
|
+
#
|
24
|
+
# However, there are handful of caveats worth knowing about this
|
25
|
+
# constructing an instance of this particular object.
|
26
|
+
#
|
27
|
+
# 1) Most unit tests won't need a `Newman::TestMailer` object present, and most
|
28
|
+
# integration tests can make use of `Newman::Server.test_mode`, preventing
|
29
|
+
# the need to ever explicitly instantiate a `Newman::TestMailer` object.
|
30
|
+
#
|
31
|
+
# 2) Because there isn't an obvious way to work with test objects in the
|
32
|
+
# underlying mail gem without relying on global state,
|
33
|
+
# `Newman::TestMailer` actually implements the singleton pattern and
|
34
|
+
# returns references to a single instance rather than creating new
|
35
|
+
# instances. The constructor interface is simply preserved so that it
|
36
|
+
# can be a drop-in replacement for a `Newman::Mailer` object.
|
37
|
+
#
|
38
|
+
# 3) The settings object is not actually used, and is only part of the
|
39
|
+
# signature for API compatibility reasons.
|
40
|
+
#
|
41
|
+
# With these caveats in mind, be sure to think long and hard about whether
|
42
|
+
# you actually need to explicitly build instances of this object before
|
43
|
+
# doing so :)
|
44
|
+
|
45
|
+
class << self
|
46
|
+
def new(settings)
|
47
|
+
return self.instance if instance
|
48
|
+
|
49
|
+
Mail.defaults do
|
50
|
+
retriever_method :test
|
51
|
+
delivery_method :test
|
52
|
+
end
|
3
53
|
|
4
|
-
|
5
|
-
def configure(settings)
|
6
|
-
Mail.defaults do
|
7
|
-
retriever_method :test
|
8
|
-
delivery_method :test
|
54
|
+
self.instance = allocate
|
9
55
|
end
|
56
|
+
|
57
|
+
attr_accessor :instance
|
10
58
|
end
|
11
59
|
|
60
|
+
# ---
|
61
|
+
|
62
|
+
# `Newman::TestMailer#messages` is used to retrieve all messages currently in the inbox
|
63
|
+
# and then delete them from the underlying `Mail::TestMailer` object so that
|
64
|
+
# the inbox gets cleared. This method returns an array of
|
65
|
+
# `Mail::Message` objects if any messages were found, and returns
|
66
|
+
# an empty array otherwise.
|
67
|
+
#
|
68
|
+
# Keep in mind that because only a single `Newman::TestMailer` ever gets
|
69
|
+
# instantiated no matter how many times you call `Newman::TestMailer.new`,
|
70
|
+
# you only get one test inbox per process.
|
71
|
+
|
12
72
|
def messages
|
13
73
|
msgs = Marshal.load(Marshal.dump(Mail::TestMailer.deliveries))
|
14
74
|
Mail::TestMailer.deliveries.clear
|
@@ -16,10 +76,23 @@ module Newman
|
|
16
76
|
msgs
|
17
77
|
end
|
18
78
|
|
79
|
+
# ---
|
80
|
+
|
81
|
+
# `Newman::TestMailer#new_message` is used to construct a new `Mail::Message` object,
|
82
|
+
# with the delivery settings set to test mode.
|
83
|
+
# This method passes all its arguments on to `Mail.new`, so be sure
|
84
|
+
# to refer to the [mail gem's documentation](http://github.com/mikel/mail)
|
85
|
+
# for details.
|
86
|
+
|
19
87
|
def new_message(*a, &b)
|
20
88
|
Mail.new(*a, &b)
|
21
89
|
end
|
22
90
|
|
91
|
+
# ---
|
92
|
+
|
93
|
+
# `Newman::TestMailer#deliver_message` method is used to construct and immediately deliver a
|
94
|
+
# message with the delivery settings set to test mode.
|
95
|
+
|
23
96
|
def deliver_message(*a, &b)
|
24
97
|
new_message(*a, &b).deliver
|
25
98
|
end
|
data/lib/newman/version.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# Newman::Version is used to determine which version of Newman is currently
|
2
|
+
# running and is part of Newman's **external api**.
|
3
|
+
|
1
4
|
module Newman
|
2
5
|
module Version
|
3
6
|
MAJOR = 0
|
4
|
-
MINOR =
|
5
|
-
TINY =
|
7
|
+
MINOR = 2
|
8
|
+
TINY = 0
|
6
9
|
STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
|
7
10
|
end
|
8
11
|
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
gem "minitest"
|
2
|
+
|
3
|
+
require "minitest/autorun"
|
4
|
+
require "purdytest"
|
5
|
+
require_relative "../lib/newman"
|
6
|
+
|
7
|
+
module Newman
|
8
|
+
TEST_DIR = File.dirname(__FILE__)
|
9
|
+
|
10
|
+
def self.new_test_server(app)
|
11
|
+
server = Newman::Server.test_mode(TEST_DIR + "/settings.rb")
|
12
|
+
server.apps << app
|
13
|
+
server.settings.application.simplelist_db = TEST_DIR + "/test.store"
|
14
|
+
server.settings.service.templates_dir = TEST_DIR + "/../examples/views"
|
15
|
+
|
16
|
+
server
|
17
|
+
end
|
18
|
+
end
|
@@ -1,39 +1,42 @@
|
|
1
|
-
require_relative "
|
2
|
-
require_relative "simple_mailing_list"
|
1
|
+
require_relative "../helper"
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
server = Newman::Server
|
7
|
-
server.test_mode("config/test.rb")
|
8
|
-
|
9
|
-
mailer = server.mailer
|
3
|
+
require_relative "../../examples/ping_pong"
|
4
|
+
require_relative "../../examples/simple_mailing_list"
|
10
5
|
|
11
6
|
describe "Ping Pong" do
|
7
|
+
let(:server) { Newman.new_test_server(Newman::Examples::PingPong) }
|
8
|
+
let(:mailer) { server.mailer }
|
9
|
+
|
12
10
|
it "responds to an email sent to test+ping@test.com" do
|
13
11
|
mailer.deliver_message(:to => "test+ping@test.com")
|
14
|
-
server.tick
|
12
|
+
server.tick
|
15
13
|
mailer.messages.first.subject.must_equal("pong")
|
16
14
|
end
|
17
15
|
|
18
16
|
it "responds to an email sent to test+bad@test.com" do
|
19
17
|
mailer.deliver_message(:to => "test+bad@test.com")
|
20
|
-
server.tick
|
18
|
+
server.tick
|
21
19
|
mailer.messages.first.subject.must_equal("unknown command")
|
22
20
|
end
|
23
21
|
end
|
24
22
|
|
25
23
|
describe "SimpleList" do
|
24
|
+
let(:server) { Newman.new_test_server(Newman::Examples::SimpleList) }
|
25
|
+
let(:mailer) { server.mailer }
|
26
|
+
|
26
27
|
it "emulates a simple mailing list" do
|
27
|
-
mailer.deliver_message(:from
|
28
|
-
:to
|
28
|
+
mailer.deliver_message(:from => "tester@test.com",
|
29
|
+
:to => "test@test.com")
|
30
|
+
|
31
|
+
server.tick
|
29
32
|
|
30
|
-
server.tick(Newman::Examples::SimpleList)
|
31
33
|
mailer.messages.first.subject.must_equal("You are not subscribed")
|
32
34
|
|
33
|
-
mailer.deliver_message(:from
|
34
|
-
:to
|
35
|
+
mailer.deliver_message(:from => "tester@test.com",
|
36
|
+
:to => "test+subscribe@test.com",
|
37
|
+
:subject => "subscribe")
|
35
38
|
|
36
|
-
server.tick
|
39
|
+
server.tick
|
37
40
|
mailer.messages.first.subject.must_equal("SUBSCRIBED!")
|
38
41
|
|
39
42
|
|
@@ -41,13 +44,14 @@ describe "SimpleList" do
|
|
41
44
|
:to => "test@test.com",
|
42
45
|
:subject => "WIN!")
|
43
46
|
|
44
|
-
server.tick
|
47
|
+
server.tick
|
45
48
|
mailer.messages.first.subject.must_equal("WIN!")
|
46
49
|
|
47
|
-
mailer.deliver_message(:from
|
48
|
-
:to
|
50
|
+
mailer.deliver_message(:from => "tester@test.com",
|
51
|
+
:to => "test@test.com",
|
52
|
+
:subject => "unsubscribe")
|
49
53
|
|
50
|
-
server.tick
|
54
|
+
server.tick
|
51
55
|
mailer.messages.first.subject.must_equal("UNSUBSCRIBED!")
|
52
56
|
end
|
53
57
|
|
@@ -57,3 +61,9 @@ describe "SimpleList" do
|
|
57
61
|
end
|
58
62
|
end
|
59
63
|
end
|
64
|
+
|
65
|
+
|
66
|
+
# move this when test suite set up
|
67
|
+
|
68
|
+
|
69
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative "../helper"
|
2
|
+
|
3
|
+
describe "Server handling request without responding" do
|
4
|
+
|
5
|
+
let(:noop) do
|
6
|
+
Newman::Application.new do
|
7
|
+
default do
|
8
|
+
skip_response
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:server) { Newman.new_test_server(noop) }
|
14
|
+
let(:mailer) { server.mailer }
|
15
|
+
|
16
|
+
it "should not deliver message" do
|
17
|
+
mailer.deliver_message(:from => 'tester@test.com',
|
18
|
+
:to => 'test+noop@test.com')
|
19
|
+
server.tick
|
20
|
+
assert_empty mailer.messages
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative "../helper"
|
2
|
+
|
3
|
+
describe "subject filter" do
|
4
|
+
let(:app) do
|
5
|
+
Newman::Application.new do
|
6
|
+
subject :match, "hello" do
|
7
|
+
respond(:subject => "HELLOOOOOOOO!")
|
8
|
+
end
|
9
|
+
|
10
|
+
default do
|
11
|
+
respond(:subject => "No subject, no greeting")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:server) { Newman.new_test_server(app) }
|
17
|
+
let(:mailer) { server.mailer }
|
18
|
+
|
19
|
+
it "should automatically not match emails without subjects" do
|
20
|
+
mailer.deliver_message(:to => "test@test.com")
|
21
|
+
server.tick
|
22
|
+
|
23
|
+
mailer.messages.first.subject.must_equal("No subject, no greeting")
|
24
|
+
end
|
25
|
+
end
|