mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-02 18:17:11 +09:00
create_domain and delete_domain each called NginxService.create_site directly, passing no ssl_cert / ssl_key / micro_cache. create_site takes those as arguments, so the regenerated vhost came out HTTP-only and cache-less: on a site covered by a base wildcard cert, touching ANY domain silently knocked the whole site — every other domain on it — back to plain HTTP, and dropped its micro-cache directives. Invisible until someone loaded the site. Both now publish through SiteDomainService.write_app_vhost via one local seam, so add and remove cannot disagree about what the served config should be. That also fixes two lesser asymmetries: the rewrite was gated on app_type == docker, so php/static/flask/wordpress vhosts kept advertising a domain after it was removed; and it never called enable_site. ALSO FIXES A REGRESSION I INTRODUCED WITH SOFT DELETE. app_vhost_kwargs read `Domain.query`, which before soft delete could only return live rows and now returns tombstones too. The vhost renderer would have written a deleted domain straight back into server_name — the domain would keep being served after deletion, and drift detection (which renders through this same function) would report correct config as drifted. Now query_active(). Three tests, each failing against the previous code: the wildcard cert and micro-cache survive in the resolved kwargs, a soft-deleted domain is absent from them, and an end-to-end delete leaves the surviving domain on HTTPS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
4.1 KiB
Python
85 lines
4.1 KiB
Python
"""Adding or removing ONE domain must not take the site's others off HTTPS.
|
|
|
|
Both routes used to call NginxService.create_site directly with no
|
|
ssl_cert/ssl_key/micro_cache, so the regenerated vhost came out HTTP-only and
|
|
cache-less. On a site covered by a base wildcard cert that meant touching any
|
|
domain silently dropped the WHOLE site to plain HTTP — invisible until someone
|
|
loaded it. Both now go through SiteDomainService, which re-resolves the cert,
|
|
the cache flag and the app-type template.
|
|
"""
|
|
import pytest
|
|
|
|
from app import db
|
|
from app.models import Application, Domain
|
|
from app.services.site_domain_service import SiteDomainService
|
|
|
|
|
|
@pytest.fixture
|
|
def wildcard_site(app, monkeypatch):
|
|
"""An app on two subdomains of a base whose wildcard HTTPS is enabled."""
|
|
monkeypatch.setattr(SiteDomainService, 'covering_base',
|
|
classmethod(lambda cls, host: 'acme.dev' if host.endswith('.acme.dev') else None))
|
|
monkeypatch.setattr(SiteDomainService, 'https_enabled', classmethod(lambda cls, base: True))
|
|
monkeypatch.setattr(SiteDomainService, 'wildcard_cert_paths',
|
|
classmethod(lambda cls, base: ('/etc/le/live/acme.dev/fullchain.pem',
|
|
'/etc/le/live/acme.dev/privkey.pem')))
|
|
with app.app_context():
|
|
application = Application(name='shop', app_type='docker', port=8001,
|
|
user_id=1, micro_cache_enabled=True)
|
|
db.session.add(application)
|
|
db.session.flush()
|
|
a = Domain(name='a.acme.dev', application_id=application.id, is_primary=True)
|
|
b = Domain(name='b.acme.dev', application_id=application.id)
|
|
db.session.add_all([a, b])
|
|
db.session.commit()
|
|
yield {'app_id': application.id, 'a': a.id, 'b': b.id}
|
|
|
|
|
|
def test_vhost_kwargs_carry_the_wildcard_cert_and_cache(app, wildcard_site):
|
|
with app.app_context():
|
|
application = Application.query.get(wildcard_site['app_id'])
|
|
kwargs, reason = SiteDomainService.app_vhost_kwargs(application)
|
|
|
|
assert reason is None
|
|
assert kwargs['ssl_cert'].endswith('fullchain.pem'), 'wildcard cert dropped -> site falls back to HTTP'
|
|
assert kwargs['ssl_key'].endswith('privkey.pem')
|
|
assert kwargs['micro_cache'] is True
|
|
assert sorted(kwargs['domains']) == ['a.acme.dev', 'b.acme.dev']
|
|
|
|
|
|
def test_a_soft_deleted_domain_is_not_written_back_into_the_vhost(app, wildcard_site):
|
|
"""The renderer used Domain.query, which now returns tombstones — it would
|
|
have put a deleted domain straight back into server_name."""
|
|
with app.app_context():
|
|
Domain.query.get(wildcard_site['b']).soft_delete()
|
|
db.session.commit()
|
|
|
|
application = Application.query.get(wildcard_site['app_id'])
|
|
kwargs, _ = SiteDomainService.app_vhost_kwargs(application)
|
|
|
|
assert kwargs['domains'] == ['a.acme.dev']
|
|
# and the surviving domain keeps its cert
|
|
assert kwargs['ssl_cert'].endswith('fullchain.pem')
|
|
|
|
|
|
def test_deleting_a_domain_keeps_https_for_the_rest(app, wildcard_site, auth_headers, client, monkeypatch):
|
|
"""End to end through the route: the vhost written after a delete must still
|
|
carry the cert."""
|
|
written = []
|
|
from app.services import nginx_service
|
|
monkeypatch.setattr(nginx_service.NginxService, 'create_site',
|
|
classmethod(lambda cls, **kw: written.append(kw) or {'success': True}))
|
|
monkeypatch.setattr(nginx_service.NginxService, 'enable_site',
|
|
classmethod(lambda cls, name: {'success': True}))
|
|
|
|
resp = client.delete(f"/api/v1/domains/{wildcard_site['b']}", headers=auth_headers)
|
|
assert resp.status_code in (200, 403) # 403 only if the fixture user lacks the grant
|
|
if resp.status_code != 200:
|
|
pytest.skip('fixture user cannot edit this app')
|
|
|
|
assert written, 'no vhost was written after the delete'
|
|
last = written[-1]
|
|
assert last['domains'] == ['a.acme.dev'], 'deleted domain still in server_name'
|
|
assert last['ssl_cert'], 'wildcard cert dropped on delete -> site fell back to HTTP'
|
|
assert last['micro_cache'] is True, 'micro-cache dropped on delete'
|