darkofabijan-astrails-safe 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.markdown +237 -0
- data/Rakefile +61 -0
- data/bin/astrails-safe +53 -0
- data/examples/example_helper.rb +19 -0
- data/lib/astrails/safe.rb +61 -0
- data/lib/astrails/safe/archive.rb +24 -0
- data/lib/astrails/safe/backup.rb +20 -0
- data/lib/astrails/safe/cloudfiles.rb +70 -0
- data/lib/astrails/safe/config/builder.rb +60 -0
- data/lib/astrails/safe/config/node.rb +76 -0
- data/lib/astrails/safe/gpg.rb +46 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +70 -0
- data/lib/astrails/safe/mysqldump.rb +32 -0
- data/lib/astrails/safe/pgdump.rb +36 -0
- data/lib/astrails/safe/pipe.rb +17 -0
- data/lib/astrails/safe/s3.rb +86 -0
- data/lib/astrails/safe/sftp.rb +88 -0
- data/lib/astrails/safe/sink.rb +35 -0
- data/lib/astrails/safe/source.rb +47 -0
- data/lib/astrails/safe/stream.rb +20 -0
- data/lib/astrails/safe/svndump.rb +13 -0
- data/lib/astrails/safe/tmp_file.rb +48 -0
- data/lib/extensions/mktmpdir.rb +45 -0
- data/spec/integration/archive_integration_spec.rb +88 -0
- data/spec/integration/cleanup_spec.rb +61 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/unit/archive_spec.rb +67 -0
- data/spec/unit/cloudfiles_spec.rb +170 -0
- data/spec/unit/config_spec.rb +213 -0
- data/spec/unit/gpg_spec.rb +148 -0
- data/spec/unit/gzip_spec.rb +64 -0
- data/spec/unit/local_spec.rb +110 -0
- data/spec/unit/mysqldump_spec.rb +83 -0
- data/spec/unit/pgdump_spec.rb +45 -0
- data/spec/unit/s3_spec.rb +160 -0
- data/spec/unit/svndump_spec.rb +39 -0
- data/templates/script.rb +165 -0
- metadata +179 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
include FileUtils
|
5
|
+
|
6
|
+
describe "tar backup" do
|
7
|
+
before(:all) do
|
8
|
+
# need both local and instance vars
|
9
|
+
# instance variables are used in tests
|
10
|
+
# local variables are used in the backup definition (instance vars can't be seen)
|
11
|
+
@root = root = "tmp/cleanup_example"
|
12
|
+
|
13
|
+
# clean state
|
14
|
+
rm_rf @root
|
15
|
+
mkdir_p @root
|
16
|
+
|
17
|
+
# create source tree
|
18
|
+
@src = src = "#{@root}/src"
|
19
|
+
mkdir_p src
|
20
|
+
|
21
|
+
File.open(qwe = "#{@src}/qwe", "w") {|f| f.write("qwe") }
|
22
|
+
|
23
|
+
@dst = dst = "#{@root}/backup"
|
24
|
+
mkdir_p "#{@dst}/archive"
|
25
|
+
|
26
|
+
@now = Time.now
|
27
|
+
@timestamp = @now.strftime("%y%m%d-%H%M")
|
28
|
+
|
29
|
+
stub(Time).now {@now} # Freeze
|
30
|
+
|
31
|
+
cp qwe, "#{dst}/archive/archive-foo.000001.tar.gz"
|
32
|
+
cp qwe, "#{dst}/archive/archive-foo.000002.tar.gz"
|
33
|
+
cp qwe, "#{dst}/archive/archive-foobar.000001.tar.gz"
|
34
|
+
cp qwe, "#{dst}/archive/archive-foobar.000002.tar.gz"
|
35
|
+
|
36
|
+
Astrails::Safe.safe do
|
37
|
+
local :path => "#{dst}/:kind"
|
38
|
+
tar do
|
39
|
+
keep :local => 1 # only leave the latest
|
40
|
+
archive :foo do
|
41
|
+
files src
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@backup = "#{dst}/archive/archive-foo.#{@timestamp}.tar.gz"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should create backup file" do
|
50
|
+
File.exists?(@backup).should be_true
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should remove old backups" do
|
54
|
+
Dir["#{@dst}/archive/archive-foo.*"].should == [@backup]
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should NOT remove backups with base having same prefix" do
|
58
|
+
Dir["#{@dst}/archive/archive-foobar.*"].should == ["#{@dst}/archive/archive-foobar.000001.tar.gz", "#{@dst}/archive/archive-foobar.000002.tar.gz"]
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'spec'
|
4
|
+
require 'spec/autorun'
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'ruby-debug'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
require 'astrails/safe'
|
11
|
+
|
12
|
+
Spec::Runner.configure do |config|
|
13
|
+
config.mock_with :rr
|
14
|
+
end
|
15
|
+
|
16
|
+
SERVICES_CONFIG = File.open( 'spec/integration_config.yml' ) { |yf| YAML::load( yf ) }
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Archive do
|
4
|
+
|
5
|
+
def def_config
|
6
|
+
{
|
7
|
+
:options => "OPTS",
|
8
|
+
:files => "apples",
|
9
|
+
:exclude => "oranges"
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def archive(id = :foo, config = def_config)
|
14
|
+
Astrails::Safe::Archive.new(id, Astrails::Safe::Config::Node.new(nil, config))
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:each) { Astrails::Safe::TmpFile.cleanup }
|
18
|
+
|
19
|
+
describe :backup do
|
20
|
+
before(:each) do
|
21
|
+
@archive = archive
|
22
|
+
stub(@archive).timestamp {"NOW"}
|
23
|
+
end
|
24
|
+
|
25
|
+
{
|
26
|
+
:id => "foo",
|
27
|
+
:kind => "archive",
|
28
|
+
:extension => ".tar",
|
29
|
+
:filename => "archive-foo.NOW",
|
30
|
+
:command => "tar -cf - OPTS --exclude=oranges apples",
|
31
|
+
}.each do |k, v|
|
32
|
+
it "should set #{k} to #{v}" do
|
33
|
+
@archive.backup.send(k).should == v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe :tar_exclude_files do
|
39
|
+
it "should return '' when no excludes" do
|
40
|
+
archive(:foo, {}).send(:tar_exclude_files).should == ''
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should accept single exclude as string" do
|
44
|
+
archive(:foo, {:exclude => "bar"}).send(:tar_exclude_files).should == '--exclude=bar'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should accept multiple exclude as array" do
|
48
|
+
archive(:foo, {:exclude => ["foo", "bar"]}).send(:tar_exclude_files).should == '--exclude=foo --exclude=bar'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe :tar_files do
|
53
|
+
it "should raise RuntimeError when no files" do
|
54
|
+
lambda {
|
55
|
+
archive(:foo, {}).send(:tar_files)
|
56
|
+
}.should raise_error(RuntimeError, "missing files for tar")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should accept single file as string" do
|
60
|
+
archive(:foo, {:files => "foo"}).send(:tar_files).should == "foo"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should accept multiple files as array" do
|
64
|
+
archive(:foo, {:files => ["foo", "bar"]}).send(:tar_files).should == "foo bar"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Cloudfiles do
|
4
|
+
|
5
|
+
def def_config
|
6
|
+
{
|
7
|
+
:cloudfiles => {
|
8
|
+
:container => "_container",
|
9
|
+
:user => "_user",
|
10
|
+
:api_key => "_api_key",
|
11
|
+
},
|
12
|
+
:keep => {
|
13
|
+
:cloudfiles => 2
|
14
|
+
}
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def def_backup(extra = {})
|
19
|
+
{
|
20
|
+
:kind => "_kind",
|
21
|
+
:filename => "/backup/somewhere/_kind-_id.NOW.bar",
|
22
|
+
:extension => ".bar",
|
23
|
+
:id => "_id",
|
24
|
+
:timestamp => "NOW"
|
25
|
+
}.merge(extra)
|
26
|
+
end
|
27
|
+
|
28
|
+
def cloudfiles(config = def_config, backup = def_backup)
|
29
|
+
Astrails::Safe::Cloudfiles.new(
|
30
|
+
Astrails::Safe::Config::Node.new(nil, config),
|
31
|
+
Astrails::Safe::Backup.new(backup)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
describe :cleanup do
|
36
|
+
|
37
|
+
before(:each) do
|
38
|
+
@cloudfiles = cloudfiles
|
39
|
+
|
40
|
+
@files = [4,1,3,2].to_a.map { |i| "aaaaa#{i}" }
|
41
|
+
|
42
|
+
@container = "container"
|
43
|
+
|
44
|
+
stub(@container).objects(:prefix => "_kind/_id/_kind-_id.") { @files }
|
45
|
+
stub(@container).delete_object(anything)
|
46
|
+
|
47
|
+
stub(CloudFiles::Connection).
|
48
|
+
new('_user', '_api_key', true, false).stub!.
|
49
|
+
container('_container') {@container}
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should check [:keep, :cloudfiles]" do
|
53
|
+
@cloudfiles.config[:keep].data["cloudfiles"] = nil
|
54
|
+
dont_allow(@cloudfiles.backup).filename
|
55
|
+
@cloudfiles.send :cleanup
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should delete extra files" do
|
59
|
+
mock(@container).delete_object('aaaaa1')
|
60
|
+
mock(@container).delete_object('aaaaa2')
|
61
|
+
@cloudfiles.send :cleanup
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
describe :active do
|
67
|
+
before(:each) do
|
68
|
+
@cloudfiles = cloudfiles
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should be true when all params are set" do
|
72
|
+
@cloudfiles.should be_active
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should be false if container is missing" do
|
76
|
+
@cloudfiles.config[:cloudfiles].data["container"] = nil
|
77
|
+
@cloudfiles.should_not be_active
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should be false if user is missing" do
|
81
|
+
@cloudfiles.config[:cloudfiles].data["user"] = nil
|
82
|
+
@cloudfiles.should_not be_active
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should be false if api_key is missing" do
|
86
|
+
@cloudfiles.config[:cloudfiles].data["api_key"] = nil
|
87
|
+
@cloudfiles.should_not be_active
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe :path do
|
92
|
+
before(:each) do
|
93
|
+
@cloudfiles = cloudfiles
|
94
|
+
end
|
95
|
+
it "should use cloudfiles/path 1st" do
|
96
|
+
@cloudfiles.config[:cloudfiles].data["path"] = "cloudfiles_path"
|
97
|
+
@cloudfiles.config[:local] = {:path => "local_path"}
|
98
|
+
@cloudfiles.send(:path).should == "cloudfiles_path"
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should use local/path 2nd" do
|
102
|
+
@cloudfiles.config[:local] = {:path => "local_path"}
|
103
|
+
@cloudfiles.send(:path).should == "local_path"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should use constant 3rd" do
|
107
|
+
@cloudfiles.send(:path).should == "_kind/_id"
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
describe :save do
|
113
|
+
def add_stubs(*stubs)
|
114
|
+
stubs.each do |s|
|
115
|
+
case s
|
116
|
+
when :connection
|
117
|
+
stub(CloudFiles::Authentication).new
|
118
|
+
stub(CloudFiles::Connection).
|
119
|
+
new('_user', '_api_key', true, false).stub!.
|
120
|
+
create_container('_container') {@container}
|
121
|
+
when :stat
|
122
|
+
stub(File).stat("foo").stub!.size {123}
|
123
|
+
when :create_container
|
124
|
+
@container = "container"
|
125
|
+
stub(@container).create_object("_kind/_id/backup/somewhere/_kind-_id.NOW.bar.bar", true) {@object}
|
126
|
+
stub(CloudFiles::Connection).create_container {@container}
|
127
|
+
when :file_open
|
128
|
+
stub(File).open("foo")
|
129
|
+
when :cloudfiles_store
|
130
|
+
@object = "object"
|
131
|
+
mock(@object).write(nil) {true}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
before(:each) do
|
137
|
+
@cloudfiles = cloudfiles(def_config, def_backup(:path => "foo"))
|
138
|
+
@full_path = "_kind/_id/backup/somewhere/_kind-_id.NOW.bar.bar"
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should fail if no backup.file is set" do
|
142
|
+
@cloudfiles.backup.path = nil
|
143
|
+
proc {@cloudfiles.send(:save)}.should raise_error(RuntimeError)
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should establish Cloud Files connection" do
|
147
|
+
add_stubs(:connection, :stat, :create_container, :file_open, :cloudfiles_store)
|
148
|
+
@cloudfiles.send(:save)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should open local file" do
|
152
|
+
add_stubs(:connection, :stat, :create_container, :cloudfiles_store)
|
153
|
+
mock(File).open("foo")
|
154
|
+
@cloudfiles.send(:save)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should upload file" do
|
158
|
+
add_stubs(:connection, :stat, :create_container, :file_open, :cloudfiles_store)
|
159
|
+
@cloudfiles.send(:save)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should fail on files bigger then 5G" do
|
163
|
+
add_stubs(:connection)
|
164
|
+
mock(File).stat("foo").stub!.size {5*1024*1024*1024+1}
|
165
|
+
mock(STDERR).puts(anything)
|
166
|
+
dont_allow(Benchmark).realtime
|
167
|
+
@cloudfiles.send(:save)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Astrails::Safe::Config do
|
4
|
+
it "should parse example config" do
|
5
|
+
config = Astrails::Safe::Config::Node.new do
|
6
|
+
local do
|
7
|
+
path "path"
|
8
|
+
end
|
9
|
+
|
10
|
+
s3 do
|
11
|
+
key "s3 key"
|
12
|
+
secret "secret"
|
13
|
+
bucket "bucket"
|
14
|
+
path "path1"
|
15
|
+
end
|
16
|
+
|
17
|
+
sftp do
|
18
|
+
user "sftp user"
|
19
|
+
password "sftp password"
|
20
|
+
host "sftp host"
|
21
|
+
end
|
22
|
+
|
23
|
+
gpg do
|
24
|
+
key "gpg-key"
|
25
|
+
password "astrails"
|
26
|
+
end
|
27
|
+
|
28
|
+
keep do
|
29
|
+
local 4
|
30
|
+
s3 20
|
31
|
+
end
|
32
|
+
|
33
|
+
mysqldump do
|
34
|
+
options "-ceKq --single-transaction --create-options"
|
35
|
+
|
36
|
+
user "astrails"
|
37
|
+
password ""
|
38
|
+
host "localhost"
|
39
|
+
port 3306
|
40
|
+
socket "/var/run/mysqld/mysqld.sock"
|
41
|
+
|
42
|
+
database :blog
|
43
|
+
|
44
|
+
database :production do
|
45
|
+
keep :local => 3
|
46
|
+
|
47
|
+
gpg do
|
48
|
+
password "custom-production-pass"
|
49
|
+
end
|
50
|
+
|
51
|
+
skip_tables [:logger_exceptions, :request_logs]
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
pgdump do
|
57
|
+
options "-i -x -O"
|
58
|
+
|
59
|
+
user "astrails"
|
60
|
+
password ""
|
61
|
+
host "localhost"
|
62
|
+
port 5432
|
63
|
+
|
64
|
+
database :blog
|
65
|
+
|
66
|
+
database :production do
|
67
|
+
keep :local => 3
|
68
|
+
|
69
|
+
skip_tables [:logger_exceptions, :request_logs]
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
svndump do
|
75
|
+
repo :my_repo do
|
76
|
+
repo_path "/home/svn/my_repo"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
tar do
|
81
|
+
archive "git-repositories" do
|
82
|
+
files "/home/git/repositories"
|
83
|
+
end
|
84
|
+
|
85
|
+
archive "etc-files" do
|
86
|
+
files "/etc"
|
87
|
+
exclude "/etc/puppet/other"
|
88
|
+
end
|
89
|
+
|
90
|
+
archive "dot-configs" do
|
91
|
+
files "/home/*/.[^.]*"
|
92
|
+
end
|
93
|
+
|
94
|
+
archive "blog" do
|
95
|
+
files "/var/www/blog.astrails.com/"
|
96
|
+
exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
|
97
|
+
end
|
98
|
+
|
99
|
+
archive :misc do
|
100
|
+
files [ "/backup/*.rb" ]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
expected = {
|
107
|
+
"local" => {"path" => "path"},
|
108
|
+
|
109
|
+
"s3" => {
|
110
|
+
"key" => "s3 key",
|
111
|
+
"secret" => "secret",
|
112
|
+
"bucket" => "bucket",
|
113
|
+
"path" => "path1",
|
114
|
+
},
|
115
|
+
|
116
|
+
"sftp" => {
|
117
|
+
"user" => "sftp user",
|
118
|
+
"password" => "sftp password",
|
119
|
+
"host" => "sftp host",
|
120
|
+
},
|
121
|
+
|
122
|
+
"gpg" => {"password" => "astrails", "key" => "gpg-key"},
|
123
|
+
|
124
|
+
"keep" => {"s3" => 20, "local" => 4},
|
125
|
+
|
126
|
+
"mysqldump" => {
|
127
|
+
"options" => "-ceKq --single-transaction --create-options",
|
128
|
+
"user" => "astrails",
|
129
|
+
"password" => "",
|
130
|
+
"host" => "localhost",
|
131
|
+
"port" => 3306,
|
132
|
+
"socket" => "/var/run/mysqld/mysqld.sock",
|
133
|
+
|
134
|
+
"databases" => {
|
135
|
+
"blog" => {},
|
136
|
+
"production" => {
|
137
|
+
"keep" => {"local" => 3},
|
138
|
+
"gpg" => {"password" => "custom-production-pass"},
|
139
|
+
"skip_tables" => ["logger_exceptions", "request_logs"],
|
140
|
+
},
|
141
|
+
},
|
142
|
+
},
|
143
|
+
|
144
|
+
"pgdump" => {
|
145
|
+
"options" => "-i -x -O",
|
146
|
+
"user" => "astrails",
|
147
|
+
"password" => "",
|
148
|
+
"host" => "localhost",
|
149
|
+
"port" => 5432,
|
150
|
+
|
151
|
+
"databases" => {
|
152
|
+
"blog" => {},
|
153
|
+
"production" => {
|
154
|
+
"keep" => {"local" => 3},
|
155
|
+
"skip_tables" => ["logger_exceptions", "request_logs"],
|
156
|
+
},
|
157
|
+
},
|
158
|
+
},
|
159
|
+
|
160
|
+
"svndump" => {
|
161
|
+
"repos" => {
|
162
|
+
"my_repo"=> {
|
163
|
+
"repo_path" => "/home/svn/my_repo"
|
164
|
+
}
|
165
|
+
}
|
166
|
+
},
|
167
|
+
|
168
|
+
"tar" => {
|
169
|
+
"archives" => {
|
170
|
+
"git-repositories" => {"files" => "/home/git/repositories"},
|
171
|
+
"etc-files" => {"files" => "/etc", "exclude" => "/etc/puppet/other"},
|
172
|
+
"dot-configs" => {"files" => "/home/*/.[^.]*"},
|
173
|
+
"blog" => {
|
174
|
+
"files" => "/var/www/blog.astrails.com/",
|
175
|
+
"exclude" => ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"],
|
176
|
+
},
|
177
|
+
"misc" => { "files" => ["/backup/*.rb"] },
|
178
|
+
},
|
179
|
+
},
|
180
|
+
}
|
181
|
+
|
182
|
+
config.to_hash.should == expected
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should make an array from multivalues" do
|
186
|
+
config = Astrails::Safe::Config::Node.new do
|
187
|
+
skip_tables "a"
|
188
|
+
skip_tables "b"
|
189
|
+
files "/foo"
|
190
|
+
files "/bar"
|
191
|
+
exclude "/foo/bar"
|
192
|
+
exclude "/foo/bar/baz"
|
193
|
+
end
|
194
|
+
|
195
|
+
expected = {
|
196
|
+
"skip_tables" => ["a", "b"],
|
197
|
+
"files" => ["/foo", "/bar"],
|
198
|
+
"exclude" => ["/foo/bar", "/foo/bar/baz"],
|
199
|
+
}
|
200
|
+
|
201
|
+
config.to_hash.should == expected
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should raise error on key duplication" do
|
205
|
+
proc do
|
206
|
+
Astrails::Safe::Config::Node.new do
|
207
|
+
path "foo"
|
208
|
+
path "bar"
|
209
|
+
end
|
210
|
+
end.should raise_error(ArgumentError, "duplicate value for 'path'")
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|