specify_cli 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +17 -0
  5. data/Gemfile.lock +117 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.rdoc +43 -0
  8. data/Rakefile +15 -0
  9. data/bin/specify_cli +248 -0
  10. data/lib/specify.rb +45 -0
  11. data/lib/specify/branch_parser.rb +85 -0
  12. data/lib/specify/cli.rb +11 -0
  13. data/lib/specify/cli/database_setup.rb +46 -0
  14. data/lib/specify/cli/stubs.rb +63 -0
  15. data/lib/specify/cli/viewset.rb +21 -0
  16. data/lib/specify/configuration.rb +12 -0
  17. data/lib/specify/configuration/config.rb +120 -0
  18. data/lib/specify/configuration/db_config.rb +162 -0
  19. data/lib/specify/configuration/host_config.rb +37 -0
  20. data/lib/specify/database.rb +140 -0
  21. data/lib/specify/models.rb +43 -0
  22. data/lib/specify/models/accession.rb +33 -0
  23. data/lib/specify/models/agent.rb +138 -0
  24. data/lib/specify/models/app_resource_data.rb +32 -0
  25. data/lib/specify/models/app_resource_dir.rb +43 -0
  26. data/lib/specify/models/auto_numbering_scheme.rb +94 -0
  27. data/lib/specify/models/collecting_event.rb +38 -0
  28. data/lib/specify/models/collection.rb +67 -0
  29. data/lib/specify/models/collection_object.rb +127 -0
  30. data/lib/specify/models/createable.rb +21 -0
  31. data/lib/specify/models/determination.rb +63 -0
  32. data/lib/specify/models/discipline.rb +61 -0
  33. data/lib/specify/models/division.rb +26 -0
  34. data/lib/specify/models/geography.rb +5 -0
  35. data/lib/specify/models/geography/administrative_division.rb +32 -0
  36. data/lib/specify/models/geography/geographic_name.rb +66 -0
  37. data/lib/specify/models/geography/geography.rb +23 -0
  38. data/lib/specify/models/institution.rb +13 -0
  39. data/lib/specify/models/locality.rb +50 -0
  40. data/lib/specify/models/preparation.rb +53 -0
  41. data/lib/specify/models/preparation_type.rb +30 -0
  42. data/lib/specify/models/record_set.rb +55 -0
  43. data/lib/specify/models/record_set_item.rb +29 -0
  44. data/lib/specify/models/taxonomy.rb +6 -0
  45. data/lib/specify/models/taxonomy/common_name.rb +14 -0
  46. data/lib/specify/models/taxonomy/rank.rb +31 -0
  47. data/lib/specify/models/taxonomy/taxon.rb +54 -0
  48. data/lib/specify/models/taxonomy/taxonomy.rb +21 -0
  49. data/lib/specify/models/tree_queryable.rb +55 -0
  50. data/lib/specify/models/updateable.rb +20 -0
  51. data/lib/specify/models/user.rb +104 -0
  52. data/lib/specify/models/view_set_object.rb +32 -0
  53. data/lib/specify/number_format.rb +60 -0
  54. data/lib/specify/services.rb +18 -0
  55. data/lib/specify/services/service.rb +51 -0
  56. data/lib/specify/services/stub_generator.rb +291 -0
  57. data/lib/specify/services/view_loader.rb +177 -0
  58. data/lib/specify/session.rb +77 -0
  59. data/lib/specify/user_type.rb +61 -0
  60. data/lib/specify/version.rb +19 -0
  61. data/man/specify_cli-database.1 +60 -0
  62. data/man/specify_cli-database.1.html +137 -0
  63. data/man/specify_cli-database.1.ronn +53 -0
  64. data/man/specify_cli-repository.1 +55 -0
  65. data/man/specify_cli-repository.1.html +128 -0
  66. data/man/specify_cli-repository.1.ronn +42 -0
  67. data/man/specify_cli-stubs.1 +177 -0
  68. data/man/specify_cli-stubs.1.html +239 -0
  69. data/man/specify_cli-stubs.1.ronn +147 -0
  70. data/man/specify_cli-viewset.1 +92 -0
  71. data/man/specify_cli-viewset.1.html +154 -0
  72. data/man/specify_cli-viewset.1.ronn +72 -0
  73. data/man/specify_cli.1 +213 -0
  74. data/man/specify_cli.1.html +252 -0
  75. data/man/specify_cli.1.ronn +157 -0
  76. data/spec/branch_parser_spec.rb +94 -0
  77. data/spec/cli/stubs_spec.rb +44 -0
  78. data/spec/configuration/config_spec.rb +269 -0
  79. data/spec/configuration/db_config_spec.rb +299 -0
  80. data/spec/configuration/host_config_spec.rb +64 -0
  81. data/spec/database_spec.rb +83 -0
  82. data/spec/examples.txt +217 -0
  83. data/spec/helpers.rb +15 -0
  84. data/spec/models/app_resource_data_spec.rb +38 -0
  85. data/spec/models/app_resource_dir_spec.rb +8 -0
  86. data/spec/models/auto_numbering_scheme_spec.rb +78 -0
  87. data/spec/models/collection_object_spec.rb +92 -0
  88. data/spec/models/collection_spec.rb +32 -0
  89. data/spec/models/discipline_spec.rb +31 -0
  90. data/spec/models/record_set_spec.rb +18 -0
  91. data/spec/models/user_spec.rb +182 -0
  92. data/spec/models/view_set_object_spec.rb +70 -0
  93. data/spec/number_format_spec.rb +43 -0
  94. data/spec/services/stub_generator_spec.rb +635 -0
  95. data/spec/services/view_loader_spec.rb +436 -0
  96. data/spec/session_spec.rb +105 -0
  97. data/spec/spec_helper.rb +116 -0
  98. data/spec/support/db.yml +12 -0
  99. data/spec/support/stub.yaml +17 -0
  100. data/spec/support/stub_locality.yaml +19 -0
  101. data/spec/support/viewsets/paleo.views.xml +30 -0
  102. data/spec/support/viewsets/paleo.xml +30 -0
  103. data/spec/user_type_spec.rb +79 -0
  104. data/specify_cli.gemspec +27 -0
  105. data/specify_cli.rdoc +1 -0
  106. metadata +246 -0
@@ -0,0 +1,157 @@
1
+ specify_cli(1) -- A command line interface for Specify
2
+ ===============================================================================
3
+
4
+ ## SYNOPSIS
5
+
6
+ `specify_cli` [**-D**|**--database** <name>]
7
+ [**-H**|**--host** <name>]
8
+ [**-P**|**--password** <password>]
9
+ [**-U**|**--specify_user** <name>]
10
+ [**-c**|**--db_config** <file>]
11
+ [**-p**|**--port** <number>]
12
+ [**-u**|**--user** <name>]
13
+ <command> [<options>] [<args>]
14
+
15
+ ## DESCRIPTION
16
+ **specify_cli** is a tool that allows certain tasks in a Specify database to be
17
+ carried out from the command line.
18
+ Specify is a management system for biological collections developed by the
19
+ Specify Collections Consortium (http://www.sustain.specifysoftware.org).
20
+
21
+ **specify_cli** is an independent development that is not supported by the
22
+ Specify Collections Consortium. It operates directly on the MySQL/MariaDB
23
+ backend used by Specify. **Use at your own risk**.
24
+
25
+ Tasks currently supported:
26
+
27
+ * upload of user interface form definitions (*.views.xml* files) to the database
28
+ * generation of collection object stub records
29
+
30
+ ## FILES
31
+
32
+ `~/.specify_dbs.rc.yaml` is a YAML file that stores information about
33
+ Specify databases and directories containing *.views.xml* files.
34
+ The file has the general structure:
35
+
36
+ ---
37
+ :dir_names:
38
+ <view_file_directory>: <hostname>
39
+ ...
40
+ :hosts:
41
+ <hostname>:
42
+ :port: <port_number>
43
+ :databases:
44
+ <database_name>:
45
+ :db_user:
46
+ :name: <mysql_username>
47
+ :password: <password>
48
+ :sp_user: <specify_username>
49
+ ...
50
+ ...
51
+
52
+ The section `dir_names` contains directory-host-mappings. A directory-host-
53
+ mapping is a key-value-pair, where the key is the path of a directory on your
54
+ hard drive, the value is a host name. This is used to automatically resolve the
55
+ correct host for a *.views.xml* file based on the directory it is in. Add
56
+ mappings here by editing the file or using the **specify_cli-repository(1)**
57
+ command.
58
+
59
+ The section `hosts` contains settings for Specify databases by grouped by
60
+ `<hostname>`. For each host, the `port` and any `databases` can be configured in
61
+ this file by editing it or by using the **specify_cli-database(1)** command. The `databases` section contains connection settings for the individual databases by
62
+ `<database_name>`. The `db_user` is the MySQL/MariaDB user used to connect to
63
+ the database and will typically be the Specify *master user*. Leave `password`
64
+ blank to be prompted for the password when you run the command (this will not
65
+ work when triggered as a bash script from another application, e.g. a text
66
+ editor). Apart from the `db_user` `specify_cli` also needs a Specify user (a
67
+ user that Specify uses internally), which is provided under `sp_user`. Every
68
+ command (except the configuration commands **specify_cli-database(1)** and **specify_cli-repository(1)**) will start a session in Specify where the Specify
69
+ user will be logged in to the collection. Any records created or modified during
70
+ this session will be marked as created by or modified by the `sp_user`.
71
+
72
+ ## COMMANDS
73
+
74
+ * `specify_cli-database`(1)
75
+ Add a database configuration.
76
+ * `specify_cli-stubs`(1)
77
+ Create stub records
78
+ * `specify_cli-repository`(1)
79
+ Map a git repository to a host for automatic target resolution.
80
+ * `specify_cli-viewset`(1)
81
+ Upload a view to the database.
82
+
83
+ ## OPTIONS
84
+
85
+ * `-D`, `--database` <name>:
86
+ The name of the Specify database to connect to.
87
+ * `-H`, `--host` <name>:
88
+ The name or IP address of the host. Default: `localhost`
89
+ * `-P`, `--password` <password>:
90
+ The password for the MySQL/MariaDB user. Leave blank to be prompted.
91
+ * `-U`, `--specify_user` <name>:
92
+ The name of the Specify user.
93
+ * `-c`, `--db_config` <file>:
94
+ A database configuration file (if other than the default
95
+ `~/.specify_dbs.rc.yaml`)
96
+ * `-p`, `--port` <number>:
97
+ The port for the MySQL/MariaDB host.
98
+ * `-u`, `--user` <name>:
99
+ The MySQL/MariaDB user name.
100
+
101
+ ## EXAMPLES
102
+
103
+ Configure a database `Specify` on the host `specify.example.org`:
104
+
105
+ $ specify_cli --host=specify.example.org database specify
106
+
107
+ Map the current directory to the host `specify.example.org`:
108
+
109
+ $ specify_cli --host=specify.example.org repository -c
110
+
111
+ Map `~/specify_forms/invertpaleo` to `localhost`:
112
+
113
+ $ specify_cli repository ~/specify_forms/invertpaleo
114
+
115
+ Create 10 blank stub records in the collection `Triloites` in the database
116
+ `Specify` on localhost:
117
+
118
+ $ specify_cli -D Specify stubs Trilobites 10
119
+
120
+ Create 10 stub records for herbarium sheets in the collection `Orchids` in the
121
+ database `Specify` on localhost, determined to taxon `Orchidaceae`:
122
+
123
+ $ specify_cli -D Specify stubs --taxon='Family: Orchidaceae'\n
124
+ --preptype=Sheet --perpcount=1 Orchids 10
125
+
126
+ Upload `invertpaleo.views.xml` to discipline level for collection `Trilobites`
127
+ in the database `Specify` on localhost:
128
+
129
+ $ specify_cli -D Specify viewset -d Trilobites invertpaleo.views.xml
130
+
131
+ Upload `invertpaleo.views.xml` auto-resolving target from current Git branch:
132
+
133
+ $ specify_cli viewset -b invertpaleo.views.xml
134
+
135
+ ## LICENSE
136
+
137
+ MIT License
138
+
139
+ Copyright (c) 2018 Martin Stein
140
+
141
+ Permission is hereby granted, free of charge, to any person obtaining a copy
142
+ of this software and associated documentation files (the "Software"), to deal
143
+ in the Software without restriction, including without limitation the rights
144
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
145
+ copies of the Software, and to permit persons to whom the Software is
146
+ furnished to do so, subject to the following conditions:
147
+
148
+ The above copyright notice and this permission notice shall be included in all
149
+ copies or substantial portions of the Software.
150
+
151
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
152
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
153
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
154
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
155
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
156
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
157
+ SOFTWARE.
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+ TEST_BRANCH = 'SPSPEC/TestCollection/user/specuser'
3
+
4
+ # Tests for the
5
+ module Specify
6
+ RSpec.describe BranchParser do
7
+
8
+
9
+ context 'when creating instances from branch names' do
10
+ let :config do
11
+ Pathname.new(Dir.pwd).join('spec', 'support', 'db.yml')
12
+ end
13
+
14
+ let(:path) { 'sp_resource' }
15
+
16
+ let(:collection_level) { 'SPSPEC/TestCollection/collection' }
17
+ let(:discipline_level) { 'SPSPEC/TestCollection/discipline' }
18
+ let(:user_type_level) { 'SPSPEC/TestCollection/Manager' }
19
+ let(:user_level) { 'SPSPEC/TestCollection/user/specuser' }
20
+
21
+ let :config do
22
+ Pathname.new(Dir.pwd).join('spec', 'support', 'db.yml')
23
+ end
24
+
25
+ context 'when collection' do
26
+ subject { described_class.new path, collection_level, config }
27
+
28
+ it do
29
+ is_expected.to have_attributes host: 'localhost',
30
+ database: 'SPSPEC',
31
+ collection: 'Test Collection',
32
+ level: :collection
33
+ end
34
+ end
35
+
36
+ context 'when discipline' do
37
+ subject { described_class.new path, discipline_level, config }
38
+
39
+ it do
40
+ is_expected.to have_attributes host: 'localhost',
41
+ database: 'SPSPEC',
42
+ collection: 'Test Collection',
43
+ level: :discipline
44
+ end
45
+ end
46
+
47
+ context 'when user type' do
48
+ subject { described_class.new path, user_type_level, config }
49
+
50
+ it do
51
+ is_expected.to have_attributes host: 'localhost',
52
+ database: 'SPSPEC',
53
+ collection: 'Test Collection',
54
+ level: { user_type: :manager }
55
+ end
56
+ end
57
+
58
+ context 'when user' do
59
+ subject { described_class.new path, user_level, config }
60
+
61
+ it do
62
+ is_expected.to have_attributes host: 'localhost',
63
+ database: 'SPSPEC',
64
+ collection: 'Test Collection',
65
+ level: { user: 'specuser' }
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'when fetching the git branch', skip: true do
71
+ before :all do
72
+ @origin = `#{GIT_CURRENT_BRANCH}`.chomp
73
+ break if TEST_BRANCH == @origin
74
+ set_branch
75
+ end
76
+
77
+ it 'parses a name from the current branch' do
78
+ expect(described_class.current_branch)
79
+ .to have_attributes database: 'SPSPEC',
80
+ collection: 'Test Collection',
81
+ level: { user: 'specuser' }
82
+ end
83
+
84
+ it 'exits with an error message if the branch name is not parsable' do
85
+ expect { described_class.new('master') }
86
+ .to raise_error ArgumentError, BRANCH_ERROR + 'master'
87
+ end
88
+
89
+ after :all do
90
+ system("git checkout #{@origin}")
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module CLI
5
+ RSpec.describe 'stubs' do
6
+ let :opts do
7
+ geo = 'Continent: North America; Country: United States;'\
8
+ ' locality: Springfield'
9
+ {
10
+ accession: "2018-AA-001",
11
+ cataloger: "specuser",
12
+ dataset: "Test dataset",
13
+ geography: geo,
14
+ locality: "Not transcribed",
15
+ taxon: "Kingdom: Plantae; Division: Tracheophyta",
16
+ preptype: "Sheet",
17
+ prepcount: "1",
18
+ file: nil
19
+ }
20
+ end
21
+
22
+ let :stub_params do
23
+ {
24
+ "dataset_name" => "Test dataset",
25
+ "cataloger" => "specuser",
26
+ "accession" => "2018-AA-001",
27
+ "collecting_data" => { "Continent" => "North America",
28
+ "Country" => "United States",
29
+ locality: 'Springfield' },
30
+ "default_locality_name" => "Not transcribed",
31
+ "determination" => { "Kingdom" => "Plantae",
32
+ "Division" => "Tracheophyta" },
33
+ "preparation" => { type: "Sheet", count: "1" }
34
+ }
35
+ end
36
+
37
+ describe '.stub_parameters' do
38
+ subject { CLI.stub_parameters opts }
39
+
40
+ it { is_expected.to eq stub_params }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ # Tests for the
6
+ module Specify
7
+ module Configuration
8
+ RSpec.describe Config do
9
+ subject { described_class.new file }
10
+
11
+ let(:config) { described_class.empty empty_file }
12
+
13
+ let :file do
14
+ Pathname.new(Dir.pwd).join('spec', 'support', 'db.yml')
15
+ end
16
+
17
+ let :empty_file do
18
+ Pathname.new(Dir.pwd).join('spec', 'support', 'empty.yml')
19
+ end
20
+
21
+ let :file_dir_names do
22
+ a_hash_including('sp_resource' => 'localhost')
23
+ end
24
+
25
+ let :file_hosts do
26
+ db_user = a_hash_including name: 'specmaster',
27
+ password: 'masterpass'
28
+ spspec = a_hash_including db_user: db_user,
29
+ sp_user: 'specuser'
30
+ databases = a_hash_including 'SPSPEC' => spspec
31
+ localhost = a_hash_including port: 3306,
32
+ databases: databases
33
+ a_hash_including 'localhost' => localhost
34
+ end
35
+
36
+ let :hosts do
37
+ a_hash_including 'localhost' => a_hash_including(port: 3600)
38
+ end
39
+
40
+ it do
41
+ is_expected.to have_attributes dir_names: file_dir_names,
42
+ hosts: file_hosts
43
+ end
44
+
45
+ describe '.empty' do
46
+ subject { described_class.empty empty_file }
47
+
48
+ before { FileUtils.touch default_file }
49
+
50
+ let(:default_file) { File.expand_path('~/.specify_dbs.rc.yaml') }
51
+
52
+ let :params do
53
+ a_hash_including dir_names: an_instance_of(Hash).and(be_empty),
54
+ hosts: an_instance_of(Hash).and(be_empty)
55
+ end
56
+
57
+ it { is_expected.to have_attributes params: params }
58
+
59
+ context 'when passed no file but default file exists' do
60
+ subject(:make_empty) { described_class.empty file }
61
+
62
+ it do
63
+ error = "#{file} exists, won't overwrite"
64
+ expect { make_empty }.to raise_error error
65
+ end
66
+ end
67
+
68
+ context 'when passed the dafault file path and default file exists' do
69
+ subject(:make_empty) { described_class.empty default_file }
70
+
71
+ it do
72
+ error = "#{default_file} exists, won't overwrite"
73
+ expect { make_empty }.to raise_error error
74
+ end
75
+ end
76
+
77
+ context 'when given block mapping dir_names' do
78
+ subject do
79
+ described_class.empty(empty_file) do |config|
80
+ config.dir_names['specify_dir'] = 'localhost'
81
+ end
82
+ end
83
+
84
+ let :dir_names do
85
+ a_hash_including 'specify_dir' => 'localhost'
86
+ end
87
+
88
+ it { is_expected.to have_attributes dir_names: dir_names }
89
+ end
90
+
91
+ context 'when given block adding hosts' do
92
+ subject do
93
+ described_class.empty(empty_file) do |config|
94
+ config.add_host 'localhost', 3600
95
+ end
96
+ end
97
+
98
+ it do
99
+ is_expected.to have_attributes hosts: hosts
100
+ end
101
+ end
102
+
103
+ context 'when given block adding databases' do
104
+ subject do
105
+ described_class.empty(empty_file) do |config|
106
+ config.add_database 'SPSPEC', host: 'localhost' do |db|
107
+ db[:db_user][:name] = 'specmaster'
108
+ db[:db_user][:password] = 'masterpass'
109
+ db[:sp_user] = 'specuser'
110
+ end
111
+ end
112
+ end
113
+
114
+ let :hosts do
115
+ db_user = a_hash_including name: 'specmaster',
116
+ password: 'masterpass'
117
+ spspec = a_hash_including db_user: db_user,
118
+ sp_user: 'specuser'
119
+ databases = a_hash_including 'SPSPEC' => spspec
120
+ localhost = a_hash_including databases: databases
121
+ a_hash_including 'localhost' => localhost
122
+ end
123
+
124
+ it { is_expected.to have_attributes hosts: hosts }
125
+ end
126
+ end
127
+
128
+ describe '#add_host' do
129
+ subject(:add_host) { config.add_host 'localhost', 3600 }
130
+
131
+ context 'when adding a new host' do
132
+ it do
133
+ expect { add_host }
134
+ .to change(config, :hosts).from(be_empty).to hosts
135
+ end
136
+ end
137
+
138
+ context 'when adding an existing host' do
139
+ before { config.add_host 'localhost' }
140
+
141
+ it do
142
+ expect { add_host }
143
+ .to raise_error 'Host \'localhost\' already configured'
144
+ end
145
+ end
146
+ end
147
+
148
+ describe '#add_database' do
149
+ subject :add_database do
150
+ config.add_database 'SPSPEC', host: 'localhost' do |db|
151
+ db[:db_user][:name] = 'specmaster'
152
+ db[:db_user][:password] = 'masterpass'
153
+ db[:sp_user] = 'specuser'
154
+ end
155
+ end
156
+
157
+ let :databases do
158
+ db_user = a_hash_including name: 'specmaster',
159
+ password: 'masterpass'
160
+ spspec = a_hash_including db_user: db_user,
161
+ sp_user: 'specuser'
162
+ a_hash_including 'SPSPEC' => spspec
163
+ end
164
+
165
+ context 'when the database is not configured for the host' do
166
+ let :localhost_db do
167
+ localhost = a_hash_including port: nil,
168
+ databases: databases
169
+ a_hash_including 'localhost' => localhost
170
+ end
171
+
172
+ it do
173
+ expect { add_database }
174
+ .to change(config, :hosts)
175
+ .from(be_empty).to(localhost_db)
176
+ end
177
+ end
178
+
179
+ context 'when the database is already configured for the host' do
180
+ before { config.add_database 'SPSPEC', host: 'localhost' }
181
+
182
+ let(:e) { 'Database \'SPSPEC\' on \'localhost\' already configured' }
183
+
184
+ it do
185
+ expect { add_database }
186
+ .to raise_error e
187
+ end
188
+ end
189
+ end
190
+
191
+ describe '#params' do
192
+ subject { described_class.new(file).params }
193
+
194
+ it do
195
+ is_expected.to be_a(Hash)
196
+ .and include(dir_names: file_dir_names,
197
+ hosts: file_hosts)
198
+ end
199
+ end
200
+
201
+ describe '#save' do
202
+ subject(:save) { config.save }
203
+
204
+ let :dir_names do
205
+ a_hash_including 'specify_dir' => 'localhost'
206
+ end
207
+
208
+ let :hosts do
209
+ db_user = a_hash_including name: 'specmaster',
210
+ password: 'masterpass'
211
+ spspec = a_hash_including db_user: db_user,
212
+ sp_user: 'specuser'
213
+ databases = a_hash_including 'SPSPEC' => spspec
214
+ localhost = a_hash_including databases: databases,
215
+ port: 3600
216
+ a_hash_including 'localhost' => localhost
217
+ end
218
+
219
+ before do
220
+ config.dir_names['specify_dir'] = 'localhost'
221
+ config.add_host 'localhost', 3600
222
+ config.add_database 'SPSPEC', host: 'localhost' do |db|
223
+ db[:db_user][:name] = 'specmaster'
224
+ db[:db_user][:password] = 'masterpass'
225
+ db[:sp_user] = 'specuser'
226
+ end
227
+ end
228
+
229
+ it do
230
+ expect { save }
231
+ .to change { Psych.load_file(empty_file) }
232
+ .to include hosts: hosts, dir_names: dir_names
233
+ end
234
+
235
+ it do
236
+ expect { save }
237
+ .to change(config, :saved?).from(be_falsey).to be_truthy
238
+ end
239
+ end
240
+
241
+ describe '#saved?' do
242
+ subject { config.saved? }
243
+
244
+ context 'when the instance has not been modified' do
245
+ it { is_expected.to be_truthy }
246
+ end
247
+
248
+ context 'when the instance has been modified' do
249
+ before { config.touch }
250
+
251
+ it { is_expected.to be_falsey }
252
+ end
253
+ end
254
+
255
+ describe '#touch' do
256
+ subject(:touch) { config.touch }
257
+
258
+ it do
259
+ expect { touch }
260
+ .to change(config, :saved?).from(be_truthy).to be_falsey
261
+ end
262
+ end
263
+
264
+ after do
265
+ File.delete(empty_file) if File.exist?(empty_file)
266
+ end
267
+ end
268
+ end
269
+ end