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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36045b933a13bcb9028176e8de60101ef660c6d99dde9aca803b73c4ad4a2656
4
- data.tar.gz: 21bcbb9dddbc7f0f94d2c192d0dfb9121926022f4a64b00cd5dd95ef59d5702b
3
+ metadata.gz: 470153d24108c7994b0de8a55d9ebae2dc436674d33753c23950f7c4e2e3423a
4
+ data.tar.gz: 9f50fdc9247e428e9bce6835a9c2efc2b52186e69503f687eb55939cd32cc913
5
5
  SHA512:
6
- metadata.gz: 20f8ae2ffcdd7257049be896b47ce5adea4a23dc9e63c9082c05fe64376d7b58c2fd58ac8e1d45b15e929d58e6756865dda3d077f5c3f1a79ecce49ee1d9f9c7
7
- data.tar.gz: 994d42aa6ef29e010a5377229e54a7ac1132c6ada7fa380a50326a89cf6a6757438216a8dc93e66212af93615dbd8df7a746abf240e78f916307983240d0feae
6
+ metadata.gz: 70f310482d6d7f0b9610ba90b24166cb44200ec72fac5a855d7ba5261d218366ea9743775d9722238d4cb33daeeafb11e1d8ee3f09d8e4d3e1a5da7c5e6b4bc1
7
+ data.tar.gz: 42e0d6ccf0d1e4883ebad2190f3418138f1d8a553c677f51d32c89ecc8538b6b5e4aa72c9b4fc490a50d868e6ca0334b347f4019b97df11bc7c5d41f1a382fb7
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --color
3
+ --format documentation
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
@@ -1 +1,6 @@
1
1
  require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -1,5 +1,5 @@
1
1
  module Mina
2
2
  module Nginx
3
- VERSION = '1.0.4'.freeze
3
+ VERSION = '1.1.0'.freeze
4
4
  end
5
5
  end
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
  }
@@ -22,4 +22,5 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_development_dependency 'bundler', '~> 1.5'
24
24
  spec.add_development_dependency 'rake', '~> 0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
25
26
  end
@@ -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
@@ -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
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: 2023-12-29 00:00:00.000000000 Z
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