mina-puma-nginx 1.0.4 → 1.1.0
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.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +27 -0
- data/README.md +40 -0
- data/Rakefile +5 -0
- data/lib/mina/nginx/version.rb +1 -1
- data/lib/mina/nginx.rb +29 -0
- data/lib/mina/templates/nginx.conf.template +10 -0
- data/mina-puma-nginx.gemspec +1 -0
- data/spec/certbot_task_spec.rb +163 -0
- data/spec/edge_cases_spec.rb +149 -0
- data/spec/integration_spec.rb +110 -0
- data/spec/nginx_template_spec.rb +143 -0
- data/spec/spec_helper.rb +25 -0
- metadata +30 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 470153d24108c7994b0de8a55d9ebae2dc436674d33753c23950f7c4e2e3423a
|
4
|
+
data.tar.gz: 9f50fdc9247e428e9bce6835a9c2efc2b52186e69503f687eb55939cd32cc913
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70f310482d6d7f0b9610ba90b24166cb44200ec72fac5a855d7ba5261d218366ea9743775d9722238d4cb33daeeafb11e1d8ee3f09d8e4d3e1a5da7c5e6b4bc1
|
7
|
+
data.tar.gz: 42e0d6ccf0d1e4883ebad2190f3418138f1d8a553c677f51d32c89ecc8538b6b5e4aa72c9b4fc490a50d868e6ca0334b347f4019b97df11bc7c5d41f1a382fb7
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.5
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [1.1.0] - 2025-01-19
|
4
|
+
|
5
|
+
### Added
|
6
|
+
- Environment-specific robots.txt rewrite rule in nginx template
|
7
|
+
- Automatically serves `robots-<environment>.txt` files when present
|
8
|
+
- Falls back to regular `robots.txt` if environment-specific file doesn't exist
|
9
|
+
- New `nginx:certbot` task for automated SSL certificate management
|
10
|
+
- Uses certbot's nginx plugin for seamless integration
|
11
|
+
- Automatically detects domains from `nginx_server_name`
|
12
|
+
- Email is optional (assumes certbot may already be configured)
|
13
|
+
- Supports dry-run/staging mode for testing
|
14
|
+
- Comprehensive test suite with RSpec
|
15
|
+
- 42 tests covering all functionality
|
16
|
+
- Template syntax validation
|
17
|
+
- Edge case handling
|
18
|
+
- Integration tests
|
19
|
+
|
20
|
+
### Changed
|
21
|
+
- Updated README with documentation for new features
|
22
|
+
- Added RSpec as development dependency
|
23
|
+
|
24
|
+
## [1.0.4] - Previous releases
|
25
|
+
- Initial gem functionality
|
26
|
+
- Basic nginx configuration management
|
27
|
+
- Puma integration
|
data/README.md
CHANGED
@@ -14,6 +14,8 @@ This gem provides several mina tasks:
|
|
14
14
|
mina nginx:start # Start Nginx
|
15
15
|
mina nginx:status # Status Nginx
|
16
16
|
mina nginx:stop # Stop Nginx
|
17
|
+
|
18
|
+
mina nginx:certbot # Obtain/renew SSL certificate using Certbot
|
17
19
|
|
18
20
|
## Installation
|
19
21
|
|
@@ -38,6 +40,12 @@ Consider variables used by the nginx config, particularly:
|
|
38
40
|
* `deploy_to` - deployment path
|
39
41
|
* `current_path` - current revision path
|
40
42
|
|
43
|
+
For SSL certificate management with Certbot:
|
44
|
+
|
45
|
+
* `certbot_email` - email for Let's Encrypt notifications (optional if certbot is already configured)
|
46
|
+
* `certbot_domains` - domains to obtain certificate for; defaults to `nginx_server_name` (comma-separated)
|
47
|
+
* `certbot_extra_flags` - additional certbot flags (e.g., `--dry-run --staging` for testing)
|
48
|
+
|
41
49
|
Edit installed template as required.
|
42
50
|
|
43
51
|
## Recommended Usage
|
@@ -49,6 +57,38 @@ Edit installed template as required.
|
|
49
57
|
n.b. if the config template has not been installed locally, `mina-nginx` will
|
50
58
|
fall back to the default template gracefully.
|
51
59
|
|
60
|
+
### SSL Certificate Setup with Certbot
|
61
|
+
|
62
|
+
To obtain an SSL certificate using Let's Encrypt:
|
63
|
+
|
64
|
+
1. Ensure certbot is installed with the nginx plugin:
|
65
|
+
```bash
|
66
|
+
sudo apt-get install certbot python3-certbot-nginx # Debian/Ubuntu
|
67
|
+
```
|
68
|
+
|
69
|
+
2. Configure your `deploy.rb`:
|
70
|
+
```ruby
|
71
|
+
set :nginx_server_name, 'example.com www.example.com'
|
72
|
+
set :certbot_email, 'admin@example.com' # Optional if certbot is already configured
|
73
|
+
```
|
74
|
+
|
75
|
+
3. Run the certbot task:
|
76
|
+
```bash
|
77
|
+
$ bundle exec mina nginx:certbot
|
78
|
+
```
|
79
|
+
|
80
|
+
Certbot will automatically:
|
81
|
+
- Obtain the SSL certificate
|
82
|
+
- Update your nginx configuration
|
83
|
+
- Reload nginx
|
84
|
+
|
85
|
+
For testing, use the staging environment:
|
86
|
+
```ruby
|
87
|
+
set :certbot_extra_flags, '--dry-run --staging'
|
88
|
+
```
|
89
|
+
|
90
|
+
Note: The nginx plugin for certbot automatically handles all nginx configuration updates and service reloads.
|
91
|
+
|
52
92
|
## Contributing
|
53
93
|
|
54
94
|
1. Fork it ( http://github.com/hbin/mina-nginx/fork )
|
data/Rakefile
CHANGED
data/lib/mina/nginx/version.rb
CHANGED
data/lib/mina/nginx.rb
CHANGED
@@ -22,6 +22,11 @@ namespace :nginx do
|
|
22
22
|
set :nginx_ssl_certificate_key, nil
|
23
23
|
set :nginx_ssl_dhparam, nil
|
24
24
|
|
25
|
+
# Certbot settings
|
26
|
+
set :certbot_email, nil
|
27
|
+
set :certbot_domains, -> { fetch(:nginx_server_name, '').split(' ').join(',') }
|
28
|
+
set :certbot_extra_flags, ""
|
29
|
+
|
25
30
|
|
26
31
|
|
27
32
|
desc 'Install Nginx config to repo'
|
@@ -65,6 +70,30 @@ namespace :nginx do
|
|
65
70
|
end
|
66
71
|
end
|
67
72
|
|
73
|
+
desc 'Obtain/renew SSL certificate using Certbot'
|
74
|
+
task :certbot do
|
75
|
+
email = fetch(:certbot_email)
|
76
|
+
domains = fetch(:certbot_domains)
|
77
|
+
extra_flags = fetch(:certbot_extra_flags)
|
78
|
+
|
79
|
+
unless domains && !domains.empty?
|
80
|
+
error! %(No domains found. Please set :nginx_server_name or :certbot_domains)
|
81
|
+
end
|
82
|
+
|
83
|
+
comment %(Running Certbot for domains: #{domains})
|
84
|
+
|
85
|
+
certbot_cmd = "sudo certbot"
|
86
|
+
certbot_cmd += " --nginx" # Use nginx plugin for automatic configuration
|
87
|
+
certbot_cmd += " --non-interactive --agree-tos"
|
88
|
+
certbot_cmd += " --email #{email}" if email && !email.empty?
|
89
|
+
certbot_cmd += " --domains #{domains}"
|
90
|
+
certbot_cmd += " #{extra_flags}" if extra_flags && !extra_flags.empty?
|
91
|
+
|
92
|
+
command certbot_cmd
|
93
|
+
|
94
|
+
comment %(Certbot has automatically updated nginx configuration and reloaded the service)
|
95
|
+
end
|
96
|
+
|
68
97
|
private
|
69
98
|
|
70
99
|
def nginx_template
|
@@ -88,6 +88,16 @@ server {
|
|
88
88
|
rewrite ^(.*)$ /503.html break;
|
89
89
|
}
|
90
90
|
|
91
|
+
<% environment = fetch(:stage, fetch(:rails_env, 'production')) -%>
|
92
|
+
<% if environment && environment != '' -%>
|
93
|
+
location = /robots.txt {
|
94
|
+
if (-f $document_root/robots-<%= environment %>.txt) {
|
95
|
+
rewrite ^(.*)$ /robots-<%= environment %>.txt break;
|
96
|
+
}
|
97
|
+
try_files $uri =404;
|
98
|
+
}
|
99
|
+
<% end -%>
|
100
|
+
|
91
101
|
if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){
|
92
102
|
return 405;
|
93
103
|
}
|
data/mina-puma-nginx.gemspec
CHANGED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'nginx:certbot task' do
|
4
|
+
# We'll test the certbot command generation logic separately
|
5
|
+
# since Mina tasks are difficult to test in isolation
|
6
|
+
|
7
|
+
describe 'certbot command generation' do
|
8
|
+
# Simulate the fetch method behavior
|
9
|
+
def fetch(key, default = nil)
|
10
|
+
value = case key
|
11
|
+
when :certbot_email then @certbot_email
|
12
|
+
when :certbot_domains then @certbot_domains
|
13
|
+
when :certbot_extra_flags then @certbot_extra_flags
|
14
|
+
when :nginx_server_name then @nginx_server_name
|
15
|
+
else
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
if value.nil? && default.respond_to?(:call)
|
20
|
+
default.call
|
21
|
+
elsif value.nil?
|
22
|
+
default
|
23
|
+
else
|
24
|
+
value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
before do
|
29
|
+
@certbot_email = nil
|
30
|
+
@certbot_domains = nil
|
31
|
+
@certbot_extra_flags = ''
|
32
|
+
@nginx_server_name = 'example.com www.example.com'
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'generates basic certbot command using nginx plugin' do
|
36
|
+
domains = fetch(:certbot_domains) || fetch(:nginx_server_name, '').split(' ').join(',')
|
37
|
+
|
38
|
+
cmd = "sudo certbot"
|
39
|
+
cmd += " --nginx"
|
40
|
+
cmd += " --non-interactive --agree-tos"
|
41
|
+
cmd += " --domains #{domains}"
|
42
|
+
|
43
|
+
expect(cmd).to eq(
|
44
|
+
"sudo certbot --nginx --non-interactive --agree-tos" \
|
45
|
+
" --domains example.com,www.example.com"
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'includes email when provided' do
|
50
|
+
@certbot_email = 'admin@example.com'
|
51
|
+
domains = fetch(:certbot_domains) || fetch(:nginx_server_name, '').split(' ').join(',')
|
52
|
+
|
53
|
+
cmd = "sudo certbot"
|
54
|
+
cmd += " --nginx"
|
55
|
+
cmd += " --non-interactive --agree-tos"
|
56
|
+
cmd += " --email #{@certbot_email}"
|
57
|
+
cmd += " --domains #{domains}"
|
58
|
+
|
59
|
+
expect(cmd).to eq(
|
60
|
+
"sudo certbot --nginx --non-interactive --agree-tos" \
|
61
|
+
" --email admin@example.com" \
|
62
|
+
" --domains example.com,www.example.com"
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'works without email (assumes certbot is already configured)' do
|
67
|
+
domains = fetch(:certbot_domains) || fetch(:nginx_server_name, '').split(' ').join(',')
|
68
|
+
email = fetch(:certbot_email)
|
69
|
+
|
70
|
+
cmd = "sudo certbot"
|
71
|
+
cmd += " --nginx"
|
72
|
+
cmd += " --non-interactive --agree-tos"
|
73
|
+
cmd += " --email #{email}" if email && !email.empty?
|
74
|
+
cmd += " --domains #{domains}"
|
75
|
+
|
76
|
+
expect(cmd).not_to include('--email')
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'includes extra flags when specified' do
|
80
|
+
@certbot_extra_flags = '--dry-run --staging'
|
81
|
+
domains = fetch(:certbot_domains) || fetch(:nginx_server_name, '').split(' ').join(',')
|
82
|
+
extra_flags = fetch(:certbot_extra_flags)
|
83
|
+
|
84
|
+
cmd_part = " #{extra_flags}"
|
85
|
+
expect(cmd_part).to eq(" --dry-run --staging")
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'uses custom domains when specified' do
|
89
|
+
@certbot_domains = 'custom.example.com,api.example.com'
|
90
|
+
domains = fetch(:certbot_domains) || fetch(:nginx_server_name, '').split(' ').join(',')
|
91
|
+
|
92
|
+
expect(domains).to eq('custom.example.com,api.example.com')
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'falls back to nginx_server_name for domains' do
|
96
|
+
@nginx_server_name = 'site1.com site2.com site3.com'
|
97
|
+
domains = fetch(:certbot_domains) || fetch(:nginx_server_name, '').split(' ').join(',')
|
98
|
+
|
99
|
+
expect(domains).to eq('site1.com,site2.com,site3.com')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'validation logic' do
|
104
|
+
it 'accepts missing email (certbot already configured)' do
|
105
|
+
email = nil
|
106
|
+
expect(email).to be_nil
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'requires domains to be non-empty' do
|
110
|
+
domains = ''
|
111
|
+
expect(domains).to be_empty
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'accepts valid email when provided' do
|
115
|
+
email = 'admin@example.com'
|
116
|
+
expect(email).not_to be_nil
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'accepts valid domains' do
|
120
|
+
domains = 'example.com,www.example.com'
|
121
|
+
expect(domains).not_to be_empty
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe 'integration with nginx settings' do
|
126
|
+
# Test that certbot settings integrate properly with nginx settings
|
127
|
+
def fetch(key, default = nil)
|
128
|
+
settings = {
|
129
|
+
nginx_server_name: 'myapp.com www.myapp.com cdn.myapp.com',
|
130
|
+
nginx_use_ssl: true
|
131
|
+
}
|
132
|
+
|
133
|
+
value = settings[key]
|
134
|
+
|
135
|
+
if value.nil? && default.respond_to?(:call)
|
136
|
+
# Handle lambda defaults
|
137
|
+
case key
|
138
|
+
when :certbot_domains
|
139
|
+
fetch(:nginx_server_name, '').split(' ').join(',')
|
140
|
+
else
|
141
|
+
default.call
|
142
|
+
end
|
143
|
+
elsif value.nil?
|
144
|
+
default
|
145
|
+
else
|
146
|
+
value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'derives domains from nginx_server_name' do
|
151
|
+
domains = fetch(:certbot_domains, -> { fetch(:nginx_server_name, '').split(' ').join(',') })
|
152
|
+
expect(domains).to eq('myapp.com,www.myapp.com,cdn.myapp.com')
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'uses nginx plugin for automatic configuration' do
|
156
|
+
# The nginx plugin automatically handles:
|
157
|
+
# - Finding the correct nginx config
|
158
|
+
# - Updating SSL certificate paths
|
159
|
+
# - Reloading nginx
|
160
|
+
expect(true).to be true
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
RSpec.describe 'Nginx template edge cases' do
|
5
|
+
let(:template_path) { File.expand_path('../../lib/mina/templates/nginx.conf.template', __FILE__) }
|
6
|
+
let(:template_content) { File.read(template_path) }
|
7
|
+
let(:erb_template) { ERB.new(template_content, trim_mode: '-') }
|
8
|
+
|
9
|
+
# Mock Mina's fetch method with minimal settings
|
10
|
+
def fetch(key, default = nil)
|
11
|
+
value = case key
|
12
|
+
when :nginx_config_unit then nginx_config_unit
|
13
|
+
when :nginx_socket_path then '/tmp/puma.sock'
|
14
|
+
when :nginx_socket_flags then 'fail_timeout=0'
|
15
|
+
when :nginx_use_ssl then false
|
16
|
+
when :nginx_use_http2 then false
|
17
|
+
when :nginx_server_name then 'localhost'
|
18
|
+
when :current_path then '/app/current'
|
19
|
+
when :nginx_downstream_uses_ssl then false
|
20
|
+
when :nginx_sts then false
|
21
|
+
when :nginx_ssl_stapling then false
|
22
|
+
when :nginx_ssl_certificate then nil
|
23
|
+
when :nginx_ssl_certificate_key then nil
|
24
|
+
when :nginx_ssl_dhparam then nil
|
25
|
+
when :stage then stage_value
|
26
|
+
when :rails_env then rails_env_value
|
27
|
+
when :application_name then 'app'
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
value.nil? ? default : value
|
33
|
+
end
|
34
|
+
|
35
|
+
let(:nginx_config_unit) { 'app_test' }
|
36
|
+
let(:stage_value) { nil }
|
37
|
+
let(:rails_env_value) { nil }
|
38
|
+
let(:rendered_template) { erb_template.result(binding) }
|
39
|
+
|
40
|
+
describe 'robots.txt with special characters in environment names' do
|
41
|
+
context 'with hyphenated environment name' do
|
42
|
+
let(:stage_value) { 'pre-production' }
|
43
|
+
|
44
|
+
it 'handles hyphenated environment names correctly' do
|
45
|
+
expect(rendered_template).to include('if (-f $document_root/robots-pre-production.txt)')
|
46
|
+
expect(rendered_template).to include('rewrite ^(.*)$ /robots-pre-production.txt break;')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with underscored environment name' do
|
51
|
+
let(:stage_value) { 'staging_v2' }
|
52
|
+
|
53
|
+
it 'handles underscored environment names correctly' do
|
54
|
+
expect(rendered_template).to include('if (-f $document_root/robots-staging_v2.txt)')
|
55
|
+
expect(rendered_template).to include('rewrite ^(.*)$ /robots-staging_v2.txt break;')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with numeric environment name' do
|
60
|
+
let(:stage_value) { 'staging2' }
|
61
|
+
|
62
|
+
it 'handles numeric environment names correctly' do
|
63
|
+
expect(rendered_template).to include('if (-f $document_root/robots-staging2.txt)')
|
64
|
+
expect(rendered_template).to include('rewrite ^(.*)$ /robots-staging2.txt break;')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'environment precedence' do
|
70
|
+
context 'when both stage and rails_env are set' do
|
71
|
+
let(:stage_value) { 'staging' }
|
72
|
+
let(:rails_env_value) { 'production' }
|
73
|
+
|
74
|
+
it 'prefers stage over rails_env' do
|
75
|
+
expect(rendered_template).to include('if (-f $document_root/robots-staging.txt)')
|
76
|
+
expect(rendered_template).not_to include('robots-production.txt')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'when stage is explicitly nil but rails_env is set' do
|
81
|
+
let(:stage_value) { nil }
|
82
|
+
let(:rails_env_value) { 'development' }
|
83
|
+
|
84
|
+
it 'falls back to rails_env' do
|
85
|
+
expect(rendered_template).to include('if (-f $document_root/robots-development.txt)')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'when stage is false' do
|
90
|
+
let(:stage_value) { false }
|
91
|
+
let(:rails_env_value) { 'test' }
|
92
|
+
|
93
|
+
it 'treats false as a value and does not fall back' do
|
94
|
+
expect(rendered_template).not_to include('location = /robots.txt')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe 'nginx syntax safety' do
|
100
|
+
context 'with various nginx_config_unit values' do
|
101
|
+
['app-name', 'app_name', 'app123', 'my-app_v2'].each do |unit_name|
|
102
|
+
context "with nginx_config_unit = '#{unit_name}'" do
|
103
|
+
let(:nginx_config_unit) { unit_name }
|
104
|
+
let(:stage_value) { 'production' }
|
105
|
+
|
106
|
+
it 'generates valid nginx configuration' do
|
107
|
+
expect(rendered_template).to include("upstream puma_#{unit_name}")
|
108
|
+
expect(rendered_template).to include("location @puma_#{unit_name}")
|
109
|
+
expect(rendered_template).to include("proxy_pass http://puma_#{unit_name};")
|
110
|
+
expect(rendered_template).to include("/var/log/nginx/#{unit_name}.access.log")
|
111
|
+
expect(rendered_template).to include("/var/log/nginx/#{unit_name}.error.log")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe 'complete nginx configuration structure' do
|
119
|
+
let(:stage_value) { 'production' }
|
120
|
+
|
121
|
+
it 'maintains proper nginx configuration order' do
|
122
|
+
# Check that major sections appear in the correct order
|
123
|
+
upstream_pos = rendered_template.index('upstream puma_')
|
124
|
+
server_pos = rendered_template.index('server {')
|
125
|
+
robots_pos = rendered_template.index('location = /robots.txt')
|
126
|
+
|
127
|
+
expect(upstream_pos).to be < server_pos
|
128
|
+
expect(server_pos).to be < robots_pos
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'includes all essential nginx directives' do
|
132
|
+
essential_directives = [
|
133
|
+
'client_max_body_size',
|
134
|
+
'keepalive_timeout',
|
135
|
+
'error_page',
|
136
|
+
'try_files',
|
137
|
+
'proxy_http_version',
|
138
|
+
'proxy_set_header X-Forwarded-For',
|
139
|
+
'proxy_set_header Host',
|
140
|
+
'gzip_static',
|
141
|
+
'expires max'
|
142
|
+
]
|
143
|
+
|
144
|
+
essential_directives.each do |directive|
|
145
|
+
expect(rendered_template).to include(directive)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
RSpec.describe 'Nginx template integration' do
|
5
|
+
let(:template_path) { File.expand_path('../../lib/mina/templates/nginx.conf.template', __FILE__) }
|
6
|
+
let(:template_content) { File.read(template_path) }
|
7
|
+
let(:erb_template) { ERB.new(template_content, trim_mode: '-') }
|
8
|
+
|
9
|
+
# Simulate a real Mina deployment context
|
10
|
+
class MinaContext
|
11
|
+
attr_reader :settings
|
12
|
+
|
13
|
+
def initialize(settings = {})
|
14
|
+
@settings = {
|
15
|
+
nginx_config_unit: 'myapp_production',
|
16
|
+
nginx_socket_path: '/var/www/myapp/shared/tmp/sockets/puma.sock',
|
17
|
+
nginx_socket_flags: 'fail_timeout=0',
|
18
|
+
nginx_use_ssl: true,
|
19
|
+
nginx_use_http2: true,
|
20
|
+
nginx_server_name: 'www.example.com example.com',
|
21
|
+
current_path: '/var/www/myapp/current',
|
22
|
+
nginx_downstream_uses_ssl: false,
|
23
|
+
nginx_sts: true,
|
24
|
+
nginx_ssl_stapling: true,
|
25
|
+
nginx_ssl_certificate: '/etc/letsencrypt/live/example.com/fullchain.pem',
|
26
|
+
nginx_ssl_certificate_key: '/etc/letsencrypt/live/example.com/privkey.pem',
|
27
|
+
nginx_ssl_dhparam: '/etc/ssl/certs/dhparam.pem',
|
28
|
+
application_name: 'myapp',
|
29
|
+
stage: 'production'
|
30
|
+
}.merge(settings)
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch(key, default = nil)
|
34
|
+
@settings.fetch(key, default)
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_template(template_content)
|
38
|
+
ERB.new(template_content, trim_mode: '-').result(binding)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'production deployment with SSL' do
|
43
|
+
let(:context) { MinaContext.new }
|
44
|
+
let(:rendered) { context.render_template(template_content) }
|
45
|
+
|
46
|
+
it 'generates valid nginx configuration' do
|
47
|
+
expect(rendered).to include('upstream puma_myapp_production')
|
48
|
+
expect(rendered).to include('listen 443 ssl http2;')
|
49
|
+
expect(rendered).to include('ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;')
|
50
|
+
expect(rendered).to include('location = /robots.txt')
|
51
|
+
expect(rendered).to include('if (-f $document_root/robots-production.txt)')
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'includes HTTP to HTTPS redirect' do
|
55
|
+
expect(rendered).to include('listen 80;')
|
56
|
+
expect(rendered).to include('return 301 https://$host$1$request_uri;')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'staging deployment without SSL' do
|
61
|
+
let(:context) { MinaContext.new(nginx_use_ssl: false, stage: 'staging', nginx_config_unit: 'myapp_staging') }
|
62
|
+
let(:rendered) { context.render_template(template_content) }
|
63
|
+
|
64
|
+
it 'generates valid nginx configuration for staging' do
|
65
|
+
expect(rendered).to include('upstream puma_myapp_staging')
|
66
|
+
expect(rendered).to include('listen 80;')
|
67
|
+
expect(rendered).not_to include('listen 443')
|
68
|
+
expect(rendered).to include('location = /robots.txt')
|
69
|
+
expect(rendered).to include('if (-f $document_root/robots-staging.txt)')
|
70
|
+
expect(rendered).to include('rewrite ^(.*)$ /robots-staging.txt break;')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'custom environment deployment' do
|
75
|
+
let(:context) { MinaContext.new(stage: 'qa', nginx_config_unit: 'myapp_qa', nginx_use_ssl: false) }
|
76
|
+
let(:rendered) { context.render_template(template_content) }
|
77
|
+
|
78
|
+
it 'uses custom environment for robots.txt' do
|
79
|
+
expect(rendered).to include('if (-f $document_root/robots-qa.txt)')
|
80
|
+
expect(rendered).to include('rewrite ^(.*)$ /robots-qa.txt break;')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'nginx configuration validation' do
|
85
|
+
let(:context) { MinaContext.new }
|
86
|
+
let(:rendered) { context.render_template(template_content) }
|
87
|
+
|
88
|
+
it 'has balanced braces' do
|
89
|
+
open_braces = rendered.count('{')
|
90
|
+
close_braces = rendered.count('}')
|
91
|
+
expect(open_braces).to eq(close_braces)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'has proper location blocks' do
|
95
|
+
location_blocks = rendered.scan(/location\s+[@=~^]*\s*[^\s]+\s*\{/)
|
96
|
+
expect(location_blocks).to include('location @puma_myapp_production {')
|
97
|
+
expect(location_blocks).to include('location ^~ /assets/ {')
|
98
|
+
expect(location_blocks).to include('location @503 {')
|
99
|
+
expect(location_blocks).to include('location = /robots.txt {')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'has all required directives end with semicolons' do
|
103
|
+
# Check common directives that must end with semicolons
|
104
|
+
expect(rendered).to match(/server_name\s+[^;]+;/)
|
105
|
+
expect(rendered).to match(/root\s+[^;]+;/)
|
106
|
+
expect(rendered).to match(/client_max_body_size\s+[^;]+;/)
|
107
|
+
expect(rendered).to match(/keepalive_timeout\s+[^;]+;/)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
RSpec.describe 'nginx.conf.template' do
|
5
|
+
let(:template_path) { File.expand_path('../../lib/mina/templates/nginx.conf.template', __FILE__) }
|
6
|
+
let(:template_content) { File.read(template_path) }
|
7
|
+
let(:erb_template) { ERB.new(template_content, trim_mode: '-') }
|
8
|
+
|
9
|
+
# Mock Mina's fetch method
|
10
|
+
def fetch(key, default = nil)
|
11
|
+
value = case key
|
12
|
+
when :nginx_config_unit then 'myapp_test'
|
13
|
+
when :nginx_socket_path then '/var/www/myapp/shared/tmp/sockets/puma.sock'
|
14
|
+
when :nginx_socket_flags then 'fail_timeout=0'
|
15
|
+
when :nginx_use_ssl then nginx_use_ssl
|
16
|
+
when :nginx_use_http2 then false
|
17
|
+
when :nginx_server_name then 'example.com'
|
18
|
+
when :current_path then '/var/www/myapp/current'
|
19
|
+
when :nginx_downstream_uses_ssl then false
|
20
|
+
when :nginx_sts then false
|
21
|
+
when :nginx_ssl_stapling then false
|
22
|
+
when :nginx_ssl_certificate then nil
|
23
|
+
when :nginx_ssl_certificate_key then nil
|
24
|
+
when :nginx_ssl_dhparam then nil
|
25
|
+
when :stage then stage_value
|
26
|
+
when :rails_env then rails_env_value
|
27
|
+
when :application_name then 'myapp'
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
value.nil? ? default : value
|
33
|
+
end
|
34
|
+
|
35
|
+
let(:nginx_use_ssl) { false }
|
36
|
+
let(:stage_value) { nil }
|
37
|
+
let(:rails_env_value) { nil }
|
38
|
+
let(:rendered_template) { erb_template.result(binding) }
|
39
|
+
|
40
|
+
describe 'robots.txt rewrite rule' do
|
41
|
+
context 'when stage is set to production' do
|
42
|
+
let(:stage_value) { 'production' }
|
43
|
+
|
44
|
+
it 'includes robots.txt location block with production environment' do
|
45
|
+
expect(rendered_template).to include('location = /robots.txt')
|
46
|
+
expect(rendered_template).to include('if (-f $document_root/robots-production.txt)')
|
47
|
+
expect(rendered_template).to include('rewrite ^(.*)$ /robots-production.txt break;')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when stage is set to staging' do
|
52
|
+
let(:stage_value) { 'staging' }
|
53
|
+
|
54
|
+
it 'includes robots.txt location block with staging environment' do
|
55
|
+
expect(rendered_template).to include('location = /robots.txt')
|
56
|
+
expect(rendered_template).to include('if (-f $document_root/robots-staging.txt)')
|
57
|
+
expect(rendered_template).to include('rewrite ^(.*)$ /robots-staging.txt break;')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'when rails_env is set but stage is not' do
|
62
|
+
let(:stage_value) { nil }
|
63
|
+
let(:rails_env_value) { 'development' }
|
64
|
+
|
65
|
+
it 'falls back to rails_env for environment' do
|
66
|
+
expect(rendered_template).to include('location = /robots.txt')
|
67
|
+
expect(rendered_template).to include('if (-f $document_root/robots-development.txt)')
|
68
|
+
expect(rendered_template).to include('rewrite ^(.*)$ /robots-development.txt break;')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'when neither stage nor rails_env is set' do
|
73
|
+
let(:stage_value) { nil }
|
74
|
+
let(:rails_env_value) { nil }
|
75
|
+
|
76
|
+
it 'uses production as default environment' do
|
77
|
+
expect(rendered_template).to include('location = /robots.txt')
|
78
|
+
expect(rendered_template).to include('if (-f $document_root/robots-production.txt)')
|
79
|
+
expect(rendered_template).to include('rewrite ^(.*)$ /robots-production.txt break;')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'when environment is empty string' do
|
84
|
+
let(:stage_value) { '' }
|
85
|
+
|
86
|
+
it 'does not include robots.txt location block' do
|
87
|
+
expect(rendered_template).not_to include('location = /robots.txt')
|
88
|
+
expect(rendered_template).not_to include('robots-')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'nginx configuration structure' do
|
94
|
+
it 'includes upstream configuration' do
|
95
|
+
expect(rendered_template).to include('upstream puma_myapp_test')
|
96
|
+
expect(rendered_template).to include('server unix:/var/www/myapp/shared/tmp/sockets/puma.sock fail_timeout=0;')
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'includes server block' do
|
100
|
+
expect(rendered_template).to include('server {')
|
101
|
+
expect(rendered_template).to include('server_name example.com;')
|
102
|
+
expect(rendered_template).to include('root /var/www/myapp/current/public;')
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'includes maintenance mode handling' do
|
106
|
+
expect(rendered_template).to include('location @503')
|
107
|
+
expect(rendered_template).to include('if (-f $document_root/system/maintenance.html)')
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'with SSL enabled' do
|
111
|
+
let(:nginx_use_ssl) { true }
|
112
|
+
|
113
|
+
it 'includes SSL configuration' do
|
114
|
+
expect(rendered_template).to include('listen 443 ssl;')
|
115
|
+
expect(rendered_template).to include('listen 80;')
|
116
|
+
expect(rendered_template).to include('return 301 https://$host$1$request_uri;')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'without SSL' do
|
121
|
+
let(:nginx_use_ssl) { false }
|
122
|
+
|
123
|
+
it 'only listens on port 80' do
|
124
|
+
expect(rendered_template).to include('listen 80;')
|
125
|
+
expect(rendered_template).not_to include('listen 443')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe 'template syntax' do
|
131
|
+
it 'produces valid nginx configuration' do
|
132
|
+
# Check for balanced braces
|
133
|
+
expect(rendered_template.count('{')).to eq(rendered_template.count('}'))
|
134
|
+
|
135
|
+
# Check for proper semicolon endings on statements
|
136
|
+
expect(rendered_template).to match(/server_name\s+\S+;/)
|
137
|
+
expect(rendered_template).to match(/listen\s+\d+;/)
|
138
|
+
|
139
|
+
# Check location blocks are properly formatted
|
140
|
+
expect(rendered_template).to match(/location\s+[@=~^]?\s*\/[^\s]*\s*\{/)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'mina'
|
3
|
+
require 'mina/nginx'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.expect_with :rspec do |expectations|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
8
|
+
end
|
9
|
+
|
10
|
+
config.mock_with :rspec do |mocks|
|
11
|
+
mocks.verify_partial_doubles = true
|
12
|
+
end
|
13
|
+
|
14
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
15
|
+
config.filter_run_when_matching :focus
|
16
|
+
config.disable_monkey_patching!
|
17
|
+
config.warnings = true
|
18
|
+
|
19
|
+
if config.files_to_run.one?
|
20
|
+
config.default_formatter = 'doc'
|
21
|
+
end
|
22
|
+
|
23
|
+
config.order = :random
|
24
|
+
Kernel.srand config.seed
|
25
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mina-puma-nginx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Elchinov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-07-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mina
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
55
69
|
description: Configuration and managements Mina tasks for Nginx.
|
56
70
|
email:
|
57
71
|
- elik@elik.ru
|
@@ -60,6 +74,9 @@ extensions: []
|
|
60
74
|
extra_rdoc_files: []
|
61
75
|
files:
|
62
76
|
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- ".ruby-version"
|
79
|
+
- CHANGELOG.md
|
63
80
|
- Gemfile
|
64
81
|
- LICENSE.txt
|
65
82
|
- README.md
|
@@ -68,6 +85,11 @@ files:
|
|
68
85
|
- lib/mina/nginx/version.rb
|
69
86
|
- lib/mina/templates/nginx.conf.template
|
70
87
|
- mina-puma-nginx.gemspec
|
88
|
+
- spec/certbot_task_spec.rb
|
89
|
+
- spec/edge_cases_spec.rb
|
90
|
+
- spec/integration_spec.rb
|
91
|
+
- spec/nginx_template_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
71
93
|
homepage: https://github.com/railsblueprint/mina-puma-nginx.git
|
72
94
|
licenses:
|
73
95
|
- MIT
|
@@ -91,4 +113,9 @@ rubygems_version: 3.0.3
|
|
91
113
|
signing_key:
|
92
114
|
specification_version: 4
|
93
115
|
summary: Mina tasks for handle with Nginx.
|
94
|
-
test_files:
|
116
|
+
test_files:
|
117
|
+
- spec/certbot_task_spec.rb
|
118
|
+
- spec/edge_cases_spec.rb
|
119
|
+
- spec/integration_spec.rb
|
120
|
+
- spec/nginx_template_spec.rb
|
121
|
+
- spec/spec_helper.rb
|