Merge branch 'origin-referer' into staging

This commit is contained in:
f 2021-08-28 12:27:49 -03:00
commit d151fffdd2
45 changed files with 1017 additions and 350 deletions

View file

@ -11,20 +11,58 @@ module Api
private
# Realiza la inversa de Site#hostname
# Por retrocompatibilidad con la forma en que estábamos
# gestionando los hostnames históricamente, necesitamos poder
# encontrar el sitio a partir de cualquiera de sus hostnames.
#
# TODO: El sitio sutty.nl no aplica a ninguno de estos y le
# tuvimos que poner 'sutty.nl..sutty.nl' para pasar el test.
# Aunque en realidad con el hostname a partir del Origin nos
# bastaría.
#
# TODO: Generar API v2 que use solo el hostname y no haya que
# pasar site_id como parámetro redundante.
def site_id
@site_id ||= if params[:site_id].end_with? Site.domain
params[:site_id].sub(/\.#{Site.domain}\z/, '')
else
params[:site_id] + '.'
end
@site_id ||= Deploy.site_name_from_hostname(params[:site_id])
end
# @return [Site]
def site
@site ||= Site.find_by_name(site_id)
end
# Obtiene el hostname desde el Origin, con el hostname local como
# fallback.
#
# @return [String]
def origin_hostname
URI.parse(origin || origin_from_referer).host
rescue StandardError
"#{site_id}.#{Site.domain}"
end
# Referer
#
# @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer}
# @return [String,Nil]
def referer
request.referer
end
alias referrer referer
# Origin
#
# @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin}
# @return [String,Nil]
def origin
request.headers['Origin']
request.origin
end
# Genera un header Origin a partir del Referer si existe.
#
# @return [String,Nil]
def origin_from_referer
return if referer.blank?
referer.split('/', 4).tap { |u| u.pop if u.size > 3 }.join('/')
end
# Los navegadores antiguos no envían Origin

View file

@ -23,7 +23,7 @@ module Api
contact_params.to_h.symbolize_keys,
params[:redirect]
redirect_to params[:redirect] || origin.to_s
redirect_to params[:redirect] || referer || site.url
end
private

View file

@ -44,11 +44,10 @@ module Api
# Genera el Origin correcto a partir de la URL del sitio.
#
# En desarrollo devuelve el Origin enviado.
#
# @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin}
# @return [String]
def return_origin
Rails.env.production? ? Site.find_by(name: site_id).url : origin
site&.deploys&.find_by_hostname(origin_hostname)&.url
end
# La cookie no es accesible a través de JS y todo su contenido
@ -59,6 +58,8 @@ module Api
# TODO: Volver configurable por sitio
expires = ENV.fetch('COOKIE_DURATION', '30').to_i.minutes
# TODO: ¿Son necesarios estos headers en la descarga de una
# imagen? ¿No será mejor moverlos al envío de datos?
headers['Access-Control-Allow-Origin'] = return_origin
headers['Access-Control-Allow-Credentials'] = true
headers['Vary'] = 'Origin'

View file

@ -17,7 +17,7 @@ module Api
site.touch if service.create_anonymous.persisted?
# Redirigir a la URL de agradecimiento
redirect_to params[:redirect_to] || origin.to_s
redirect_to params[:redirect_to] || referer || site.url
end
private

View file

@ -85,7 +85,9 @@ module Api
# XXX: Este header se puede falsificar de todas formas pero al
# menos es una trampa.
def site_is_origin?
return if origin? && site.urls(slash: false).any? { |u| origin.to_s.start_with? u }
return if site.urls(slash: false).any? do |u|
(origin || origin_from_referer).to_s.start_with? u
end
@reason = 'site_is_not_origin'
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
@ -116,11 +118,6 @@ module Api
raise NotImplementedError
end
# Encuentra el sitio o devuelve nulo
def site
@site ||= Site.find_by(name: site_id)
end
# Genera un registro con información básica para debug, quizás no
# quede asociado a ningún sitio.
#

View file

@ -9,14 +9,14 @@ module Api
# Lista de nombres de dominios a emitir certificados
def index
render json: sites_names + alternative_names + api_names
render json: Deploy.all.pluck(:hostname)
end
# Sitios con hidden service de Tor
#
# @return [Array] lista de nombres de sitios sin onion aun
def hidden_services
render json: DeployHiddenService.where(values: nil).includes(:site).pluck(:name)
render json: DeployHiddenService.temporary.includes(:site).pluck(:name)
end
# Tor va a enviar el onion junto con el nombre del sitio y tenemos
@ -25,10 +25,8 @@ module Api
# @params [String] name
# @params [String] onion
def add_onion
site = Site.find_by(name: params[:name])
if site
usuarie = GitAuthor.new email: 'tor@' + Site.domain, name: 'Tor'
if (site = Site.find_by_name(params[:name]))
usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor'
service = SiteService.new site: site, usuarie: usuarie,
params: params
service.add_onion
@ -36,28 +34,6 @@ module Api
head :ok
end
private
# Nombres de los sitios
def sites_names
Site.all.order(:name).pluck(:name)
end
# Dominios alternativos
def alternative_names
DeployAlternativeDomain.all.map(&:hostname)
end
# Obtener todos los sitios con API habilitada, es decir formulario
# de contacto y/o colaboración anónima.
#
# TODO: Optimizar
def api_names
Site.where(contact: true)
.or(Site.where(colaboracion_anonima: true))
.select("'api.' || name as name").map(&:name)
end
end
end
end

View file

@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
before_action :prepare_exception_notifier
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :redirect_to_site_name!, only: %i[index show edit new], if: :site_id_contains_hostname?
around_action :set_locale
rescue_from Pundit::NilPolicyError, with: :page_not_found
@ -16,7 +17,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::ParameterMissing, with: :page_not_found
before_action do
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl'))
Rack::MiniProfiler.authorize_request if Rails.env.development?
end
# No tenemos índice de sutty, vamos directamente a ver el listado de
@ -31,15 +32,11 @@ class ApplicationController < ActionController::Base
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
end
# Encontrar un sitio por su nombre
# Encontrar un sitio por su nombre.
def find_site
id = params[:site_id] || params[:id]
unless (site = current_usuarie&.sites&.find_by_name(id))
raise SiteNotFound
current_usuarie&.sites&.find_by_name(site_id).tap do |site|
raise SiteNotFound unless site
end
site
end
# Devuelve el idioma actual y si no lo encuentra obtiene uno por
@ -79,6 +76,39 @@ class ApplicationController < ActionController::Base
breadcrumb 'stats.index', root_path, match: :exact
end
# Retrocompatibilidad con sitios cuyo nombre era su hostname.
#
# @see Deploy
def site_id_contains_hostname?
site_id&.end_with? '.'
end
# Redirigir a la misma URL con el site_id cambiado.
#
# TODO: Eliminar cuando detectemos que no hay más redirecciones.
def redirect_to_site_name!
params.permit!
params[:site_id] = Deploy.site_name_from_hostname(site_id[0..-2])
redirect_to params, status: :moved_permanently
end
# Los controladores dentro de SitesController van a usar site_id
# mientras que SiteController va a usar ID.
#
# @see SitesController
# @return [String,Nil]
def site_id
@site_id ||= params[:site_id]
end
# El sitio actual
#
# @return [Site]
def site
@site ||= find_site
end
protected
def configure_permitted_parameters

View file

@ -7,7 +7,7 @@ class CollaborationsController < ApplicationController
include Pundit
def collaborate
@site = Site.find_by_name(params[:site_id])
@site = find_site
authorize Collaboration.new(@site)
@invitade = current_usuarie || @site.usuaries.build
@ -21,7 +21,7 @@ class CollaborationsController < ApplicationController
#
# * Si le usuarie existe y no está logueade, pedirle la contraseña
def accept_collaboration
@site = Site.find_by_name(params[:site_id])
@site = find_site
authorize Collaboration.new(@site)
@invitade = current_usuarie

View file

@ -136,8 +136,10 @@ class SitesController < ApplicationController
private
def site
@site ||= find_site
# En los controladores dentro de este controlador vamos a usar :id
# para obtener el nombre.
def site_id
@site_id ||= params[:site_id] || params[:id]
end
def site_params

View file

@ -4,70 +4,63 @@
class DeployJob < ApplicationJob
class DeployException < StandardError; end
attr_reader :site, :deployed
# rubocop:disable Metrics/MethodLength
def perform(site, notify = true, time = Time.now)
def perform(site_id, notify = true, time = Time.now)
ActiveRecord::Base.connection_pool.with_connection do
@site = Site.find(site)
@site = Site.find(site_id)
@deployed = {}
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
# esperando más de 10 minutos, recuperar el estado anterior.
#
# Como el trabajo actual se aplaza al siguiente, arrastrar la
# hora original para poder ir haciendo timeouts.
if @site.building?
if site.building?
if 10.minutes.ago >= time
@site.update status: 'waiting'
site.update status: 'waiting'
raise DeployException,
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
"#{site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
end
DeployJob.perform_in(60, site, notify, time)
DeployJob.perform_in(60, site_id, notify, time)
return
end
@site.update status: 'building'
site.update status: 'building'
# Asegurarse que DeployLocal sea el primero!
@deployed = { deploy_local: deploy_locally }
deployed[:deploy_local] = site.deploy_local.deploy
# No es opcional
unless @deployed[:deploy_local]
@site.update status: 'waiting'
notify_usuaries if notify
# Hacer fallar la tarea
raise DeployException, deploy_local.build_stats.last.log
end
deploy_others
deploy_others if deployed[:deploy_local]
# Volver a la espera
@site.update status: 'waiting'
site.update status: 'waiting'
notify_usuaries if notify
# Hacer fallar la tarea para enterarnos.
raise DeployException, site.deploy_local.build_stats.last.log unless deployed[:deploy_local]
end
end
# rubocop:enable Metrics/MethodLength
private
def deploy_local
@deploy_local ||= @site.deploys.find_by(type: 'DeployLocal')
end
def deploy_locally
deploy_local.deploy
end
# Correr todas las tareas que no sean el deploy local.
def deploy_others
@site.deploys.where.not(type: 'DeployLocal').find_each do |d|
@deployed[d.type.underscore.to_sym] = d.deploy
site.deploys.where.not(type: 'DeployLocal').find_each do |d|
deployed[d.type.underscore.to_sym] = d.deploy
end
end
# Notificar a todes les usuaries no temporales.
#
# TODO: Poder configurar quiénes quieren recibir notificaciones.
def notify_usuaries
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
DeployMailer.with(usuarie: usuarie, site: @site.id)
.deployed(@deployed)
site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
DeployMailer.with(usuarie: usuarie, site: site.id)
.deployed(deployed)
.deliver_now
end
end

View file

@ -13,7 +13,7 @@ class DeployMailer < ApplicationMailer
@usuarie = Usuarie.find(params[:usuarie])
@site = @usuarie.sites.find(params[:site])
@deploys = which_ones
@deploy_local = @site.deploys.find_by(type: 'DeployLocal')
@deploy_local = @site.deploy_local
# Informamos a cada quien en su idioma y damos una dirección de
# respuesta porque a veces les usuaries nos escriben

View file

@ -1,44 +1,138 @@
# frozen_string_literal: true
require 'open3'
# Este modelo implementa los distintos tipos de alojamiento que provee
# Sutty.
#
# Los datos se guardan en la tabla `deploys`. Para guardar los
# atributos, cada modelo tiene que definir su propio `store
# :attributes`.
# Cuando cambia el hostname de un Deploy, generamos un
# DeployAlternativeDomain en su lugar. Esto permite que no se rompan
# links preexistentes y que el nombre no pueda ser tomado por alguien
# más.
#
# TODO: Cambiar el nombre a algo que no sea industrial/militar.
class Deploy < ApplicationRecord
# Un sitio puede tener muchas formas de publicarse.
belongs_to :site
# Puede tener muchos access logs a través del hostname
has_many :access_logs, primary_key: 'hostname', foreign_key: 'host'
# Registro de las tareas ejecutadas
has_many :build_stats, dependent: :destroy
# Siempre generar el hostname
after_initialize :default_hostname!
# Eliminar los archivos generados por el deploy.
before_destroy :remove_destination!
# Cambiar el lugar del destino antes de guardar los cambios, para que
# el hostname anterior siga estando disponible.
before_update :rename_destination!, if: :destination_changed?
# Los hostnames alternativos se crean después de actualizar, cuando ya
# se modificó el hostname.
around_update :create_alternative_domain!, if: :destination_changed?
# Siempre tienen que pertenecer a un sitio
validates :site, presence: true
# El hostname tiene que ser único en toda la plataforma
validates :hostname, uniqueness: true
# Cada deploy puede implementar su propia validación
validates :hostname, hostname: true, unless: :implements_hostname_validation?
# Verificar que se puede cambiar de lugar el destino y no hay nada
# preexistente.
validate :destination_can_change?, if: :destination_changed?
# Retrocompatibilidad: Encuentra el site_name a partir del hostname.
#
# @return [String,Nil]
def self.site_name_from_hostname(hostname)
where(hostname: hostname).includes(:site).pluck(:name).first
end
# Detecta si el destino existe y si no es un symlink roto.
def exist?
File.exist? destination
end
# Detecta si el link está roto
def broken?
File.symlink?(destination) && !File.exist?(File.readlink(destination))
end
# Ubicación del deploy
#
# @return [String] Una ruta en el sistema de archivos
def destination
File.join(Rails.root, '_deploy', hostname)
end
# Ubicación anterior del deploy
#
# @return [String] Una ruta en el sistema de archivos
def destination_was
return destination unless will_save_change_to_hostname?
File.join(Rails.root, '_deploy', hostname_was)
end
# Determina si la ubicación cambió
def destination_changed?
persisted? && will_save_change_to_hostname?
end
# Genera el hostname
#
# @return [String]
def default_hostname
raise NotImplementedError
end
# Devolver la URL
#
# @return [String]
def url
"https://#{hostname}"
end
# Ejecutar la tarea
#
# @return [Boolean]
def deploy
raise NotImplementedError
end
def limit
raise NotImplementedError
end
# El espacio ocupado por este deploy.
#
# @return [Integer]
def size
raise NotImplementedError
end
# Empezar a contar el tiempo
#
# @return [Time]
def time_start
@start = Time.now
end
# Detener el contador
#
# @return [Time]
def time_stop
@stop = Time.now
end
# Obtener la demora de la tarea
#
# @return [Float]
def time_spent_in_seconds
(@stop - @start).round(3)
end
def home_dir
site.path
end
# El directorio donde se almacenan las gemas.
#
# TODO: En un momento podíamos tenerlas todas compartidas y ahorrar
# espacio, pero bundler empezó a mezclar cosas.
#
# @return [String]
def gems_dir
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
end
@ -77,9 +171,67 @@ class Deploy < ApplicationRecord
private
# Genera el hostname pero permitir la inicialización del valor. Luego
# validamos que sea el formato correcto.
#
# @return [Boolean]
def default_hostname!
self.hostname ||= default_hostname
end
# Cambia la ubicación de destino cuando cambia el hostname.
def rename_destination!
return unless File.exist? destination_was
FileUtils.mv destination_was, destination
end
# Elimina los archivos generados por el deploy
#
# @return [Boolean]
def remove_destination!
raise NotImplementedError
end
# Cuando el deploy cambia de hostname, generamos un dominio
# alternativo para no romper links hacia este sitio.
def create_alternative_domain!
hw = hostname_was
# Aplicar la actualización
yield
# Crear el deploy alternativo con el nombre anterior una vez que
# lo cambiamos en la base de datos.
ad = site.deploys.create(type: 'DeployAlternativeDomain', hostname: hw)
ad.deploy if ad.persisted?
end
# Devuelve un error si el destino ya existe. No debería fallar si ya
# pasamos la validación de cambio de nombres, pero siempre puede haber
# directorios y links sueltos.
def destination_can_change?
return true unless persisted?
remove_destination! if broken?
return true unless exist?
errors.add :hostname, I18n.t('activerecord.errors.models.deploy.attributes.hostname.destination_exist')
end
# Convierte el comando en una versión resumida.
#
# @param [String]
# @return [String]
def readable_cmd(cmd)
cmd.split(' -', 2).first.tr(' ', '_')
end
# Cada deploy puede decidir su propia validación
#
# @return [Boolean]
def implements_hostname_validation?
false
end
end

View file

@ -1,23 +1,21 @@
# frozen_string_literal: true
# Soportar dominios alternativos
class DeployAlternativeDomain < Deploy
store :values, accessors: %i[hostname], coder: JSON
# Soportar dominios alternativos.
class DeployAlternativeDomain < DeployWww
validates :hostname, domainname: true
# Generar un link simbólico del sitio principal al alternativo
def deploy
File.symlink?(destination) ||
File.symlink(site.hostname, destination).zero?
# No hay un hostname por defecto
#
# @return [Nil]
def default_hostname; end
private
def implements_hostname_validation?
true
end
# No hay límite para los dominios alternativos
def limit; end
def size
File.size destination
end
def destination
File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, ''))
end
# No hay un hostname por defecto. Debe ser informado por les
# usuaries.
def default_hostname!; end
end

View file

@ -1,18 +1,61 @@
# frozen_string_literal: true
# Genera una versión onion
# Alojar el sitio como un servicio oculto de Tor, que en realidad es un
# link simbólico al DeployLocal.
class DeployHiddenService < DeployWww
def deploy
return true if fqdn.blank?
validates :hostname, format: { with: /\A[a-z2-7]{56}.onion\z/ }
super
end
# Sufijo para todos los dominios temporales.
TEMPORARY_SUFFIX = 'temporary'
def fqdn
values[:onion]
end
# Traer todos los servicios ocultos temporales.
scope :temporary, -> { where("hostname not like '#{TEMPORARY_SUFFIX}%'") }
# Los servicios ocultos son su propio transporte cifrado y
# autenticado.
#
# @return [String]
def url
'http://' + fqdn
"http://#{hostname}"
end
# Los onions no son creados por Sutty sino por Tor y enviados luego a
# través de la API. El hostname por defecto es un nombre temporal que
# se parece a una dirección OnionV3.
#
# @return [String]
def default_hostname
"#{TEMPORARY_SUFFIX}#{random_base32}.onion"
end
# Detecta si es una dirección temporal.
#
# @return [Boolean]
def temporary?
hostname.start_with? TEMPORARY_SUFFIX
end
private
# No soportamos cambiar de onion
def destination_changed?
false
end
def implements_hostname_validation?
true
end
# Adaptado de base32
#
# @see {https://github.com/stesla/base32/blob/master/lib/base32.rb}
# @see {https://github.com/stesla/base32/blob/master/LICENSE}
def random_base32(length = nil)
table = 'abcdefghijklmnopqrstuvwxyz234567'
length ||= 56 - TEMPORARY_SUFFIX.length
OpenSSL::Random.random_bytes(length).each_byte.map do |b|
table[b % 32]
end.join
end
end

View file

@ -1,11 +1,11 @@
# frozen_string_literal: true
# Alojamiento local, solo genera el sitio, con lo que no necesita hacer
# nada más
# Alojamiento local, genera el sitio como si corriéramos `jekyll build`.
class DeployLocal < Deploy
store :values, accessors: %i[], coder: JSON
before_destroy :remove_destination!
# Asegurarse que el hostname es el permitido.
before_validation :reset_hostname!, :default_hostname!
# Actualiza el hostname con www si cambiamos el hostname
before_update :update_deploy_www!, if: :hostname_changed?
# Realizamos la construcción del sitio usando Jekyll y un entorno
# limpio para no pasarle secretos
@ -20,42 +20,52 @@ class DeployLocal < Deploy
jekyll_build
end
# Sólo permitimos un deploy local
def limit
1
end
# Obtener el tamaño de todos los archivos y directorios (los
# directorios son archivos :)
#
# @return [Integer]
def size
paths = [destination, File.join(destination, '**', '**')]
Dir.glob(paths).map do |file|
if File.symlink? file
0
else
File.size(file)
end
File.symlink?(file) ? 0 : File.size(file)
end.inject(:+)
end
def destination
File.join(Rails.root, '_deploy', site.hostname)
# El hostname es el nombre del sitio más el dominio principal.
#
# @return [String]
def default_hostname
"#{site.name}.#{Site.domain}"
end
private
def reset_hostname!
self.hostname = nil
end
# XXX: En realidad el DeployWww debería regenerar su propio hostname.
def update_deploy_www!
site.deploys.where(type: 'DeployWww').map do |www|
www.update hostname: www.default_hostname
end
end
# Crea el directorio destino si no existe.
def mkdir
FileUtils.mkdir_p destination
end
# Un entorno que solo tiene lo que necesitamos
#
# @return [Hash]
def env
# XXX: This doesn't support Windows paths :B
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin']
{
'HOME' => home_dir,
'HOME' => site.path,
'PATH' => paths.join(':'),
'SPREE_API_KEY' => site.tienda_api_key,
'SPREE_URL' => site.tienda_url,
@ -66,10 +76,15 @@ class DeployLocal < Deploy
}
end
# @return [String]
def yarn_lock
File.join(site.path, 'yarn.lock')
end
# Determina si este proyecto se gestiona con Yarn, buscando si el
# archivo yarn.lock existe.
#
# @return [Boolean]
def yarn_lock?
File.exist? yarn_lock
end
@ -79,12 +94,17 @@ class DeployLocal < Deploy
end
# Corre yarn dentro del repositorio
#
# @return [Boolean,Nil]
def yarn
return true unless yarn_lock?
run 'yarn'
end
# Instala las dependencias.
#
# @return [Boolean]
def bundle
if Rails.env.production?
run %(bundle install --no-cache --path="#{gems_dir}")
@ -93,6 +113,9 @@ class DeployLocal < Deploy
end
end
# Genera el sitio.
#
# @return [Boolean]
def jekyll_build
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}")
end

View file

@ -11,12 +11,31 @@ class DeployPrivate < DeployLocal
jekyll_build
end
# Hacer el deploy a un directorio privado
# La URL del sitio dentro del panel.
#
# @return [String]
def url
Rails.application.routes.url_for(controller: :private, action: :show, site_id: site)
end
# Hacer el deploy a un directorio privado.
#
# @return [String]
def destination
File.join(Rails.root, '_private', site.name)
end
# El hostname no se usa para nada, porque el sitio es solo accesible a
# través del panel de Sutty.
#
# @return [String]
def default_hostname
"#{site.name}.private.#{Site.domain}"
end
# No usar recursos en compresión y habilitar los datos privados
#
# @return [Hash]
def env
@env ||= super.merge({
'JEKYLL_ENV' => 'development',

View file

@ -2,34 +2,48 @@
# Vincula la versión del sitio con www a la versión sin
class DeployWww < Deploy
store :values, accessors: %i[], coder: JSON
before_destroy :remove_destination!
# La forma de hacer este deploy es generar un link simbólico entre el
# directorio canónico y el actual.
#
# @return [Boolean]
def deploy
File.symlink?(destination) ||
File.symlink(site.hostname, destination).zero?
end
def limit
1
# Eliminar los links rotos
remove_destination! if broken?
# No hacer nada si ya existe.
return true if exist?
# Generar un link simbólico con la ruta relativa al destino
File.symlink(relative_path, destination).zero?
end
# Siempre devuelve el espacio ocupado por el link simbólico, no el
# destino.
#
# @return [Integer]
def size
File.size destination
relative_path.size
end
def destination
File.join(Rails.root, '_deploy', fqdn)
end
def fqdn
"www.#{site.hostname}"
# El hostname por defecto incluye WWW
#
# @return [String]
def default_hostname
"www.#{site.deploy_local.hostname}"
end
private
# Elimina el link simbólico si se elimina este deploy.
def remove_destination!
FileUtils.rm_f destination
end
# Obtiene la ubicación relativa del deploy local hacia la ubicación de
# este deploy
#
# @return [String]
def relative_path
Pathname.new(site.deploy_local.destination).relative_path_from(File.dirname(destination)).to_s
end
end

View file

@ -2,22 +2,24 @@
require 'zip'
# Genera un ZIP a partir del sitio ya construido
# Genera un ZIP a partir del sitio ya generado y lo coloca para descarga
# dentro del sitio público.
#
# TODO: Firmar con minisign
class DeployZip < Deploy
store :values, accessors: %i[], coder: JSON
# El hostname es el nombre del archivo.
validates :hostname, format: { with: /\.zip\z/ }
# Una vez que el sitio está generado, tomar todos los archivos y
# y generar un zip accesible públicamente.
# y generar un ZIP accesible públicamente.
#
# rubocop:disable Metrics/MethodLength
# @return [Boolean]
def deploy
FileUtils.rm_f path
remove_destination!
time_start
Dir.chdir(destination) do
Zip::File.open(path, Zip::File::CREATE) do |z|
Zip::File.open(hostname, Zip::File::CREATE) do |z|
Dir.glob('./**/**').each do |f|
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
end
@ -31,25 +33,47 @@ class DeployZip < Deploy
File.exist? path
end
# rubocop:enable Metrics/MethodLength
def limit
1
# La URL de descarga del archivo.
#
# @return [String]
def url
"#{site.deploy_local.url}/#{hostname}"
end
# Devuelve el tamaño del ZIP en bytes
#
# @return [Integer]
def size
File.size path
end
# El archivo ZIP se guarda dentro del sitio local para poder
# descargarlo luego.
#
# @return [String]
def destination
File.join(Rails.root, '_deploy', site.hostname)
site.deploy_local.destination
end
def file
"#{site.hostname}.zip"
# El "hostname" es la ubicación del archivo.
#
# @return [String]
def default_hostname
"#{site.deploy_local.hostname}.zip"
end
def path
File.join(destination, file)
File.join(destination, hostname)
end
private
def remove_destination!
FileUtils.rm_f path
end
def implements_hostname_validation?
true
end
end

View file

@ -7,6 +7,7 @@ class Site < ApplicationRecord
include Site::Forms
include Site::FindAndReplace
include Site::Api
include Site::Deployment
include Tienda
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
@ -15,19 +16,11 @@ class Site < ApplicationRecord
# protege de acceso al panel de Sutty!
encrypts :private_key
# TODO: Hacer que los diferentes tipos de deploy se auto registren
# @see app/services/site_service.rb
DEPLOYS = %i[local private www zip hidden_service].freeze
validates :name, uniqueness: true, hostname: {
allow_root_label: true
}
validates :design_id, presence: true
validates_uniqueness_of :name
validates_inclusion_of :status, in: %w[waiting enqueued building]
validates_presence_of :title
validates :description, length: { in: 50..160 }
validate :deploy_local_presence
validate :compatible_layouts, on: :update
attr_reader :incompatible_layouts
@ -38,8 +31,6 @@ class Site < ApplicationRecord
belongs_to :licencia
has_many :log_entries, dependent: :destroy
has_many :deploys, dependent: :destroy
has_many :build_stats, through: :deploys
has_many :roles, dependent: :destroy
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
through: :roles
@ -58,13 +49,11 @@ class Site < ApplicationRecord
after_initialize :load_jekyll
after_create :load_jekyll, :static_file_migration!
# Cambiar el nombre del directorio
before_update :update_name!
before_update :update_name!, if: :name_changed?
before_save :add_private_key_if_missing!
# Guardar la configuración si hubo cambios
after_save :sync_attributes_with_config!
accepts_nested_attributes_for :deploys, allow_destroy: true
# El sitio en Jekyll
attr_reader :jekyll
@ -85,49 +74,6 @@ class Site < ApplicationRecord
@repository ||= Site::Repository.new path
end
def hostname
sub = name || I18n.t('deploys.deploy_local.ejemplo')
if sub.ends_with? '.'
sub.gsub(/\.\Z/, '')
else
"#{sub}.#{Site.domain}"
end
end
# Devuelve la URL siempre actualizada a través del hostname
#
# @param slash Boolean Agregar / al final o no
# @return String La URL con o sin / al final
def url(slash: true)
"https://#{hostname}#{slash ? '/' : ''}"
end
# Obtiene los dominios alternativos
#
# @return Array
def alternative_hostnames
deploys.where(type: 'DeployAlternativeDomain').map(&:hostname).map do |h|
h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}"
end
end
# Obtiene todas las URLs alternativas para este sitio
#
# @return Array
def alternative_urls(slash: true)
alternative_hostnames.map do |h|
"https://#{h}#{slash ? '/' : ''}"
end
end
# Todas las URLs posibles para este sitio
#
# @return Array
def urls(slash: true)
alternative_urls(slash: slash) << url(slash: slash)
end
def invitade?(usuarie)
!invitades.find_by(id: usuarie.id).nil?
end
@ -453,8 +399,6 @@ class Site < ApplicationRecord
end
def update_name!
return unless name_changed?
FileUtils.mv path_was, path
reload_jekyll!
end
@ -477,19 +421,6 @@ class Site < ApplicationRecord
Site::StaticFileMigration.new(site: self).migrate!
end
# Valida si el sitio tiene al menos una forma de alojamiento asociada
# y es la local
#
# TODO: Volver opcional el alojamiento local, pero ahora mismo está
# atado a la generación del sitio así que no puede faltar
def deploy_local_presence
# Usamos size porque queremos saber la cantidad de deploys sin
# guardar también
return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
end
# Valida que al cambiar de plantilla no tengamos artículos en layouts
# inexistentes.
def compatible_layouts

View file

@ -0,0 +1,113 @@
# frozen_string_literal: true
class Site
# Abstrae todo el comportamiento de publicación del sitio en un
# módulo.
module Deployment
extend ActiveSupport::Concern
included do
# TODO: Hacer que los diferentes tipos de deploy se auto registren
# @see app/services/site_service.rb
DEPLOYS = %i[local private www zip hidden_service].freeze
validates :name,
format: { with: /\A[0-9a-z\-]+\z/,
message: I18n.t('activerecord.errors.models.site.attributes.name.no_subdomains') }
validates :name, hostname: true
validates_presence_of :canonical_deploy
validate :deploy_local_presence
validate :name_changed_is_unique_hostname, if: :name_changed?
has_one :canonical_deploy, class_name: 'Deploy'
has_many :deploys, dependent: :destroy
has_many :access_logs, through: :deploys
has_many :build_stats, through: :deploys
before_validation :deploy_local_is_default_canonical_deploy!, unless: :canonical_deploy_id?
before_update :update_deploy_local_hostname!, if: :name_changed?
accepts_nested_attributes_for :deploys, allow_destroy: true
# El primer deploy del sitio, si no existe en la base de datos es
# porque recién estamos creando el sitio y todavía no se guardó.
#
# @return [DeployLocal]
def deploy_local
@deploy_local ||= deploys.order(created_at: :asc).find_by(type: 'DeployLocal') || deploys.find do |d|
d.type == 'DeployLocal'
end
end
# Obtiene la URL principal
#
# @param :slash [Boolean]
# @return [String]
def canonical_url(slash: true)
canonical_deploy.url.dup.tap do |url|
url << '/' if slash
end
end
alias_method :url, :canonical_url
# Devuelve todas las URLs posibles
#
# @param :slash [Boolean]
# @return [Array]
def urls(slash: true)
deploys.map(&:url).map do |url|
slash ? "#{url}/" : url
end
end
# Obtiene el hostname principal
#
# @return [String]
def hostname
canonical_deploy.hostname
end
private
# Validar que al cambiar el nombre no estemos utilizando un
# hostname reservado por otro sitio.
#
# Al cambiar el nombre del DeployLocal se va a validar que el
# hostname nuevo sea único.
def name_changed_is_unique_hostname
deploy_local.hostname = nil
return if deploy_local.valid?
errors.add :name, I18n.t('activerecord.errors.models.site.attributes.name.duplicated_hostname')
end
# Si cambia el nombre queremos actualizarlo en el DeployLocal y
# recargar el deploy canónico para tomar el nombre que
# corresponda.
def update_deploy_local_hostname!
deploy_local.update(hostname: name)
canonical_deploy.reload if canonical_deploy == deploy_local
end
# Si no asignamos un deploy canónico en el momento le asignamos el
# deploy local
def deploy_local_is_default_canonical_deploy!
self.canonical_deploy ||= deploy_local
end
# Valida si el sitio tiene al menos una forma de alojamiento asociada
# y es la local
#
# TODO: Volver opcional el alojamiento local, pero ahora mismo está
# atado a la generación del sitio así que no puede faltar
def deploy_local_presence
# Usamos size porque queremos saber la cantidad de deploys sin
# guardar también
return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
end
end
end
end

View file

@ -13,11 +13,10 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
site.save &&
site.config.write &&
commit_config(action: :create)
commit_config(action: :create) &&
add_licencias
end
add_licencias
site
end

View file

@ -1,6 +1,6 @@
%h1= t('.hi')
= sanitize_markdown t('.explanation', fqdn: @deploy_local.site.hostname),
= sanitize_markdown t('.explanation', url: @site.deploy_local.url),
tags: %w[p a strong em]
%table

View file

@ -1,6 +1,6 @@
= '# ' + t('.hi')
\
= t('.explanation', fqdn: @deploy_local.site.hostname)
= t('.explanation', url: @site.deploy_local.url)
\
= Terminal::Table.new do |table|
- table << [t('.th.type'), t('.th.status')]

View file

@ -14,10 +14,10 @@
'0', '1'
= deploy.label :_destroy, class: 'custom-control-label' do
%h3= t('.title')
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
= sanitize_markdown t('.help', public_url: site.deploy_local.url),
tags: %w[p strong em a]
- if deploy.object.fqdn
- unless deploy.object.temporary?
= sanitize_markdown t('.help_2', url: deploy.object.url),
tags: %w[p strong em a]
%hr/

View file

@ -6,7 +6,9 @@
.row
.col
%h3= t('.title')
= sanitize_markdown t('.help', fqdn: deploy.object.site.hostname),
= sanitize_markdown t('.help', url: deploy.object.url),
tags: %w[p strong em a]
= deploy.hidden_field :type
-# No duplicarlos una vez que existen.
- unless deploy.object.persisted?
= deploy.hidden_field :type

View file

@ -15,6 +15,6 @@
'0', '1'
= deploy.label :_destroy, class: 'custom-control-label' do
%h3= t('.title')
= sanitize_markdown t('.help', fqdn: deploy.object.fqdn),
= sanitize_markdown t('.help', url: deploy.object.url),
tags: %w[p strong em a]
%hr/

View file

@ -15,10 +15,5 @@
'0', '1'
= deploy.label :_destroy, class: 'custom-control-label' do
%h3= t('.title')
-# TODO: secar la generación de URLs
- name = site.name || t('.ejemplo')
= sanitize_markdown t('.help',
fqdn: deploy.object.site.hostname,
file: deploy.object.file || "#{name}.zip"),
tags: %w[p strong em a]
= sanitize_markdown t('.help', url: deploy.object.url), tags: %w[p strong em a]
%hr/

View file

@ -13,29 +13,19 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
# El problema sería que otros sitios con JS malicioso hagan pedidos
# a nuestra API desde otros sitios infectados.
#
# XXX: La primera parte del dominio tiene que coincidir con el
# nombre del sitio.
#
# XXX: Al terminar de entender esto nos pasó que el servidor recibe
# la petición de todas maneras, con lo que no estamos previniendo
# que nos hablen, sino que lean información. Solo va a funcionar si
# el servidor no tiene el Preflight cacheado.
#
# TODO: Limitar el acceso desde Nginx también.
#
# TODO: Poder consultar por sitios por todas sus URLs posibles.
origins do |source, _|
# Cacheamos la respuesta para no tener que volver a procesarla
# cada vez.
Rails.cache.fetch(source, expires_in: 1.hour) do
uri = URI(source)
if (name = uri&.host&.split('.', 2)&.first).present?
Site.where(name: [name, uri.host + '.']).pluck(:name).first.present?
else
false
end
rescue URI::Error
hostname = URI(source)&.host
hostname.present? && Deploy.find_by_hostname(hostname).present?
rescue StandardError
false
end
end

View file

@ -0,0 +1,6 @@
# frozen_string_literal: true
# Agrega el subdominio .local a menos que estemos en producción.
#
# TODO: Permitir TLDs que no sean de ICANN aquí.
PAK::ValidatesHostname::ALLOWED_TLDS << 'local' unless Rails.env.production?

View file

@ -70,7 +70,7 @@ en:
hi: "Hi!"
explanation: |
This e-mail is to notify you that Sutty has built your site, which is
available at <https://%{fqdn}>.
available at <%{url}/>.
You'll find details below.
th:
@ -143,8 +143,15 @@ en:
tienda_api_key: Store access key
errors:
models:
deploy:
attributes:
hostname:
destination_exist: 'There already is a file in the destination'
site:
attributes:
name:
no_subdomains: 'Name cannot contain dots'
duplicated_hostname: 'There already is a site with this address'
deploys:
deploy_local_presence: 'We need to be build the site!'
design_id:
@ -195,7 +202,7 @@ en:
deploy_local:
title: 'Host at Sutty'
help: |
The site will be available at <https://%{fqdn}/>.
The site will be available at <%{url}/>.
We're working out the details to allow you to use your own site
domains, you can [help us](https://sutty.nl/en/index.html#contact)!
@ -211,7 +218,7 @@ en:
title: 'Add www to the address'
help: |
When you enable this option, your site will also be available
under <https://%{fqdn}/>.
under <%{url}/>.
The www prefix has been a way of referring to
computers that are available on the World Wide Web. Since
@ -222,7 +229,7 @@ en:
help: |
ZIP files contain and compress all your site's files. With
this option you can download and also share your entire site
through the <https://%{fqdn}/%{file}> address, keep it as backup
through the <%{url}> address, keep it as backup
or have a strategy of solidary hosting, where many people
share a copy of your site.

View file

@ -70,7 +70,7 @@ es:
hi: "¡Hola!"
explanation: |
Este correo es para notificarte que Sutty ha generado tu sitio y
ya está disponible en la dirección <https://%{fqdn}>.
ya está disponible en la dirección <%{url}>.
A continuación encontrarás el detalle de lo que hicimos.
th:
@ -143,8 +143,15 @@ es:
tienda_api_key: Clave de acceso
errors:
models:
deploy:
attributes:
hostname:
destination_exist: 'Ya hay un archivo en esta ubicación'
site:
attributes:
name:
no_subdomains: 'El nombre no puede contener puntos'
duplicated_hostname: 'Ya existe un sitio con ese nombre'
deploys:
deploy_local_presence: '¡Necesitamos poder generar el sitio!'
design_id:
@ -197,7 +204,7 @@ es:
deploy_local:
title: 'Alojar en Sutty'
help: |
El sitio estará disponible en <https://%{fqdn}/>.
El sitio estará disponible en <%{url}/>.
Estamos desarrollando la posibilidad de agregar tus propios
dominios, ¡ayudanos!
@ -213,7 +220,7 @@ es:
title: 'Agregar www a la dirección'
help: |
Cuando habilitas esta opción, tu sitio también estará disponible
como <https://%{fqdn}/>.
como <%{url}/>.
El prefijo www para las direcciones web ha sido una forma de
referirse a las computadoras que están disponibles en la _World
@ -226,7 +233,7 @@ es:
help: |
Los archivos ZIP contienen y comprimen todos los archivos de tu
sitio. Con esta opción podrás descargar y compartir tu sitio
entero a través de la dirección <https://%{fqdn}/%{file}> y
entero a través de la dirección <%{url}> y
guardarla como copia de seguridad o una estrategia de
alojamiento solidario, donde muchas personas comparten una copia
de tu sitio.

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
# Recupera la funcionalidad que estamos deprecando.
module AddValuesToDeploy
extend ActiveSupport::Concern
included do
store :values, accessors: %i[hostname onion], coder: JSON
end
end
# Convertir todos los valores serializados de Deploy en una columna,
# porque al final el único uso que tuvo fue para guardar los hostnames
# alternativos.
#
# ¡El hostname es único para poder evitar que haya duplicados!
class AddHostnameToDeploys < ActiveRecord::Migration[6.1]
# Crea una columna temporal y guarda todos los valores. Los traspasa
# y luego elimina la columna.
def up
Deploy.include AddValuesToDeploy
# Ya que estamos hacer limpieza.
Deploy.where(site_id: nil).destroy_all
add_column :deploys, :hostname_tmp, :string
Site.find_each do |site|
site.deploys.find_each do |deploy|
deploy.hostname_tmp = deploy.values[:hostname] || deploy.values[:onion] || deploy.hostname
end
end
rename_column :deploys, :hostname_tmp, :hostname
remove_column :deploys, :values
add_index :deploys, :hostname, unique: true
# A esta altura todos los dominios deberían estar migrados.
change_column :deploys, :hostname, :string, null: false
end
# Recupera los valores desde la columna creada.
def down
Deploy.include AddValuesToDeploy
rename_column :deploys, :hostname, :hostname_tmp
add_column :deploys, :values, :text
Site.find_each do |site|
site.deploys.find_each do |deploy|
deploy.values[(deploy.is_a? DeployHiddenService ? :onion : :hostname)] = deploy.hostname_tmp
end
end
remove_column :deploys, :hostname_tmp
end
end

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
# Los sitios pueden tener muchos tipos de publicación pero solo uno es
# el principal. Al usar un campo específico, podemos validar mejor su
# presencia y modificación.
#
# El valor por defecto es 0 para poder crear la columna sin modificarla
# después, pero la idea es que nunca haya ceros.
class AddCanonicalDeployToSites < ActiveRecord::Migration[6.1]
def up
add_belongs_to :sites, :canonical_deploy, index: true, null: false, default: 0
# Si el sitio tenía un dominio alternativo, usar ese en lugar del
# local, asumiendo que es el primero de todos los posibles.
Site.find_each do |site|
deploy = site.deploys.order(created_at: :asc).find_by_type('DeployAlternativeDomain')
deploy ||= site.deploy_local
site.update canonical_deploy_id: deploy.id
end
end
def down
remove_belongs_to :sites, :canonical_deploy, index: true
end
end

View file

@ -21,13 +21,14 @@ module Api
end
test 'el sitio tiene que existir' do
hostname = @site.hostname
@site.destroy
get v1_site_contact_cookie_url(@site.hostname, **@host)
get v1_site_contact_cookie_url(hostname, **@host)
assert_not cookies[@site.name]
post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host),
post v1_site_contact_url(site_id: hostname, form: :contacto, **@host),
params: {
name: SecureRandom.hex,
pronouns: SecureRandom.hex,
@ -106,7 +107,7 @@ module Api
test 'se puede enviar mensajes a dominios propios' do
ActionMailer::Base.deliveries.clear
@site.update name: 'example.org.'
@site.update name: 'example'
redirect = "#{@site.url}?thanks"
@ -130,6 +131,34 @@ module Api
assert_equal redirect, response.headers['Location']
assert_equal 2, ActionMailer::Base.deliveries.size
end
test 'algunos navegadores no soportan Origin' do
ActionMailer::Base.deliveries.clear
@site.update name: 'example'
redirect = "#{@site.url}?thanks"
10.times do
create :rol, site: @site
end
get v1_site_contact_cookie_url(@site.hostname, **@host)
post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host),
headers: { referer: @site.url },
params: {
name: SecureRandom.hex,
pronouns: SecureRandom.hex,
contact: SecureRandom.hex,
from: "#{SecureRandom.hex}@sutty.nl",
body: SecureRandom.hex,
consent: true,
redirect: redirect
}
assert_equal redirect, response.headers['Location']
assert_equal 2, ActionMailer::Base.deliveries.size
end
end
end
end

View file

@ -23,7 +23,7 @@ module Api
test 'se puede obtener un listado de todos' do
get v1_sites_url(host: "api.#{Site.domain}"), headers: @authorization, as: :json
assert_equal Site.all.pluck(:name), JSON.parse(response.body)
assert_equal Deploy.all.pluck(:hostname), JSON.parse(response.body)
end
end
end

View file

@ -119,12 +119,7 @@ class SitesControllerTest < ActionDispatch::IntegrationTest
title: name,
description: name * 2,
design_id: design.id,
licencia_id: Licencia.all.second.id,
deploys_attributes: {
'0' => {
type: 'DeployLocal'
}
}
licencia_id: Licencia.all.second.id
}
}

View file

@ -9,11 +9,11 @@ FactoryBot.define do
licencia
after :build do |site|
site.deploys << build(:deploy_local, site: site)
end
after :create do |site|
site.deploys << create(:deploy_local, site: site)
# XXX: Generamos un DeployLocal normalmente y no a través de una
# Factory porque necesitamos que el sitio se genere solo.
#
# @see {https://github.com/thoughtbot/factory_bot/wiki/How-factory_bot-interacts-with-ActiveRecord}
site.deploys.build(type: 'DeployLocal')
end
end
end

View file

@ -2,11 +2,7 @@
class DeployJobTest < ActiveSupport::TestCase
test 'se puede compilar' do
rol = create :rol
site = rol.site
site.deploys << create(:deploy_zip, site: site)
site.save
site = create :site
DeployJob.perform_async(site.id)

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'test_helper'
class DeployAlternativeDomainTest < ActiveSupport::TestCase
setup do
@site = create :site
@deploy_alt = @site.deploys.build type: 'DeployAlternativeDomain'
end
teardown do
@site&.destroy
end
def random_tld
PAK::ValidatesHostname::ALLOWED_TLDS.sample
end
test 'el hostname se ingresa manualmente' do
assert_nil @deploy_alt.hostname
end
test 'el hostname es obligatorio' do
assert_not @deploy_alt.valid?
end
test 'el hostname es válido' do
assert_not @deploy_alt.update(hostname: ' ')
assert_not @deploy_alt.update(hostname: 'custom.domain.root.')
assert_not @deploy_alt.update(hostname: 'custom.domain')
assert @deploy_alt.update(hostname: "custom.domain.#{random_tld}")
end
test 'el hostname tiene que ser único' do
assert_not @deploy_alt.update(hostname: @site.hostname)
end
test 'se puede deployear' do
assert @site.deploy_local.deploy
assert @deploy_alt.update(hostname: "#{SecureRandom.hex}.sutty.#{random_tld}")
assert @deploy_alt.deploy
assert File.symlink?(@deploy_alt.destination)
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'test_helper'
class DeployHiddenServiceTest < ActiveSupport::TestCase
setup do
@site = create :site
@deploy_hidden = @site.deploys.build type: 'DeployHiddenService'
end
teardown do
@site&.destroy
end
test 'el hostname es válido' do
assert_not @deploy_hidden.update(hostname: ' ')
assert_not @deploy_hidden.update(hostname: 'custom.domain.root.')
assert_not @deploy_hidden.update(hostname: 'custom.domain')
assert @deploy_hidden.update(hostname: "#{@deploy_hidden.send(:random_base32, 56)}.onion")
end
test 'los hostnames pueden ser temporales' do
assert @deploy_hidden.hostname.start_with? 'temporary'
end
test 'el hostname tiene que ser único' do
assert @deploy_hidden.save
assert_not @site.deploys.create(type: 'DeployHiddenService', hostname: @deploy_hidden.hostname).valid?
end
test 'se puede deployear' do
assert @site.deploy_local.deploy
assert @deploy_hidden.deploy
assert File.symlink?(@deploy_hidden.destination)
end
end

View file

@ -1,24 +1,44 @@
# frozen_string_literal: true
require 'test_helper'
class DeployLocalTest < ActiveSupport::TestCase
setup do
@site = create :site
end
teardown do
@site&.destroy
end
test 'se pueden crear' do
assert @site.deploy_local.valid?
assert_equal @site.hostname, @site.deploy_local.hostname
end
test 'no se puede cambiar el hostname' do
hostname = @site.deploy_local.hostname
@site.deploy_local.hostname = SecureRandom.hex
assert @site.deploy_local.save
assert_equal hostname, @site.deploy_local.hostname
end
test 'se puede deployear' do
site = create :site
local = create :deploy_local, site: site
deploy = create :deploy_zip, site: site
deploy_local = @site.deploy_local
# Primero tenemos que generar el sitio
local.deploy
assert deploy_local.deploy
assert File.directory?(deploy_local.destination)
assert File.exist?(File.join(deploy_local.destination, 'index.html'))
assert_equal 3, deploy_local.build_stats.count
escaped_path = Shellwords.escape(deploy.path)
assert deploy_local.build_stats.map(&:bytes).compact.inject(:+).positive?
assert deploy_local.build_stats.map(&:seconds).compact.inject(:+).positive?
end
assert deploy.deploy
assert File.file?(deploy.path)
assert_equal 'application/zip',
`file --mime-type "#{escaped_path}"`.split(' ').last
assert_equal 1, deploy.build_stats.count
assert deploy.build_stats.map(&:bytes).inject(:+).positive?
assert deploy.build_stats.map(&:seconds).inject(:+).positive?
local.destroy
test 'al eliminarlos se elimina el directorio' do
deploy_local = @site.deploy_local
assert deploy_local.destroy
assert_not File.directory?(deploy_local.destination)
end
end

View file

@ -3,17 +3,23 @@
require 'test_helper'
class DeployWwwTest < ActiveSupport::TestCase
setup do
@site = create :site
@deploy_www = @site.deploys.create type: 'DeployWww'
end
teardown do
@site&.destroy
end
test 'el hostname empieza con www' do
assert @deploy_www.hostname.start_with?('www.')
end
test 'se puede deployear' do
site = create :site
local = create :deploy_local, site: site
deploy = create :deploy_www, site: site
assert @site.deploy_local.deploy
# Primero tenemos que generar el sitio
local.deploy
assert deploy.deploy
assert File.symlink?(deploy.destination)
local.destroy
assert @deploy_www.deploy
assert File.symlink?(@deploy_www.destination)
end
end

View file

@ -3,18 +3,29 @@
require 'test_helper'
class DeployZipTest < ActiveSupport::TestCase
setup do
@site = create :site
@deploy_zip = @site.deploys.create(type: 'DeployZip')
end
teardown do
@site&.destroy
end
test 'el nombre es el hostname.zip' do
assert_equal "#{@site.hostname}.zip", @deploy_zip.hostname
end
test 'se puede deployear' do
deploy_local = create :deploy_local
# Primero tenemos que generar el sitio
assert @site.deploy_local.deploy
assert deploy_local.deploy
assert File.directory?(deploy_local.destination)
assert File.exist?(File.join(deploy_local.destination, 'index.html'))
assert_equal 3, deploy_local.build_stats.count
assert deploy_local.build_stats.map(&:bytes).compact.inject(:+).positive?
assert deploy_local.build_stats.map(&:seconds).compact.inject(:+).positive?
assert deploy_local.destroy
assert_not File.directory?(deploy_local.destination)
assert @deploy_zip.deploy
assert File.file?(@deploy_zip.path)
assert_equal 'application/zip',
`file --mime-type "#{@deploy_zip.path}"`.split.last
assert_equal 1, @deploy_zip.build_stats.count
assert @deploy_zip.build_stats.map(&:bytes).inject(:+).positive?
assert @deploy_zip.build_stats.map(&:seconds).inject(:+).positive?
end
end

View file

@ -0,0 +1,88 @@
# frozen_string_literal: true
require 'test_helper'
class Site::DeploymentTest < ActiveSupport::TestCase
def site
@site ||= create :site
end
teardown do
@site&.destroy
end
test 'al publicar el sitio se crea el directorio' do
assert site.deploy_local.deploy
assert site.deploy_local.exist?
end
test 'al cambiar el nombre no puede pisar un dominio que ya existe' do
site_pre = create :site
dup_name = "test-#{SecureRandom.hex}"
assert site_pre.deploys.create(type: 'DeployAlternativeDomain', hostname: "#{dup_name}.#{Site.domain}")
assert_not site.update(name: dup_name)
end
test 'al cambiar el nombre se crea un deploy alternativo' do
site_name = site.name
new_name = SecureRandom.hex
original_destination = site.deploy_local.destination
urls = [site.url]
assert site.deploy_local.deploy
assert_not site.deploy_local.destination_changed?
assert site.update(name: new_name)
urls << site.url
assert_equal urls.sort, site.urls.sort
assert File.symlink?(original_destination)
assert File.exist?(site.deploy_local.destination)
assert_equal 2, site.deploys.count
end
test 'al cambiar el nombre se renombra el directorio' do
site_name = site.name
new_name = "test-#{SecureRandom.hex}"
original_destination = site.deploy_local.destination
assert site.deploy_local.deploy
assert_not site.deploy_local.destination_changed?
assert site.update(name: new_name)
assert site.deploy_local.hostname.start_with?(new_name)
assert File.symlink?(original_destination)
assert File.exist?(site.deploy_local.destination)
end
test 'al cambiar el nombre se actualiza el www' do
site_name = site.name
new_name = "test-#{SecureRandom.hex}"
assert (deploy_www = site.deploys.create(type: 'DeployWww'))
assert site.deploy_local.deploy
assert_not site.deploy_local.destination_changed?
assert site.update(name: new_name)
assert deploy_www.reload.hostname.include?(new_name)
assert_equal 4, site.deploys.count
end
test 'al cambiar el nombre varias veces se crean varios links' do
assert site.deploy_local.deploy
q = rand(3..10)
q.times do
assert site.update(name: "test-#{SecureRandom.hex}")
end
assert_equal q, site.deploys.count
end
test 'no se puede cambiar el nombre si ya existía un archivo en el mismo lugar' do
assert site.deploy_local.deploy
new_name = "test-#{SecureRandom.hex}"
FileUtils.mkdir File.join(Rails.root, '_deploy', "#{new_name}.#{Site.domain}")
assert_not site.update(name: new_name)
end
end

View file

@ -26,18 +26,18 @@ class SiteTest < ActiveSupport::TestCase
assert_not site2.valid?
end
test 'el nombre del sitio puede contener subdominios' do
test 'el nombre del sitio no puede contener subdominios' do
@site = build :site, name: 'hola.chau'
site.validate
assert_not site.errors.messages[:name].present?
assert site.errors.messages[:name].present?
end
test 'el nombre del sitio puede terminar con punto' do
test 'el nombre del sitio no puede terminar con punto' do
@site = build :site, name: 'hola.chau.'
site.validate
assert_not site.errors.messages[:name].present?
assert site.errors.messages[:name].present?
end
test 'el nombre del sitio no puede contener wildcard' do
@ -93,9 +93,9 @@ class SiteTest < ActiveSupport::TestCase
test 'tienen un hostname que puede cambiar' do
assert_equal "#{site.name}.#{Site.domain}", site.hostname
site.name = name = SecureRandom.hex
site.update(name: (new_name = SecureRandom.hex))
assert_equal "#{name}.#{Site.domain}", site.hostname
assert_equal "#{new_name}.#{Site.domain}", site.hostname
end
test 'se pueden traer los datos de una plantilla' do