Compare commits

...

12 Commits

Author SHA1 Message Date
20cca87cdd Run rubocop checks as part of CI
Some checks failed
Check usage of free licenses / check-licenses (pull_request) Failing after 40s
Run unit tests and Rubocop / unit_tests (pull_request) Failing after 1m28s
Add copyright notice / copyright_notice (pull_request) Failing after 1m39s
2024-12-28 18:35:04 +01:00
cb10d50d9e Rename .github -> .gitea 2024-12-28 18:32:46 +01:00
b16ef1e237 Run Rubocop autocorrect on the rest of the project 2024-12-28 18:30:54 +01:00
0c7c69ee5e Run Rubocop autocorrect on app/controllers 2024-12-28 18:28:38 +01:00
4fc95185fb Run Rubocop autocorrect on app/helpers 2024-12-28 18:21:16 +01:00
02fcd03b0e Run Rubocop autocorrect on app/services 2024-12-28 18:20:09 +01:00
fbc6926402 Run Rubocop autocorrect on app/queries 2024-12-28 18:16:36 +01:00
19dcb551fa Run Rubocop autocorrect on app/models 2024-12-28 18:13:57 +01:00
2fc8a6340b Run Rubocop autocorrect on spec/ 2024-12-28 18:07:40 +01:00
c15d0806a8 Add exception to some Rubocop offenses 2024-12-28 17:49:00 +01:00
27c7feebee Run Rubocop autocorrect on spec/queries 2024-12-28 17:37:49 +01:00
b85e2ef932 Run Rubocop autocorrect on spec/extensions 2024-12-28 17:37:49 +01:00
76 changed files with 464 additions and 297 deletions

View File

@ -1,4 +1,4 @@
name: Run unit tests name: Run unit tests and Rubocop
on: on:
push: push:
branches: branches:
@ -24,6 +24,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: ruby/setup-ruby@v1.202.0 - uses: ruby/setup-ruby@v1.202.0
- run: bundle install - run: bundle install
- run: bundle exec rubocop --force-exclusion --parallel
- name: Wait until Postgres is ready to accept connections - name: Wait until Postgres is ready to accept connections
run: | run: |
apt-get update && apt-get install -f -y postgresql-client apt-get update && apt-get install -f -y postgresql-client

View File

@ -9,9 +9,21 @@ AllCops:
- 'db/**/*' - 'db/**/*'
- 'config/**/*' - 'config/**/*'
- 'script/**/*' - 'script/**/*'
- 'bin/{rails,rake}' - 'bin/*'
- '*.yml' - '*.yml'
Layout/LineLength: Layout/LineLength:
Max: 120 Max: 120
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 10 Enabled: false
Metrics/ModuleLength:
Enabled: false
RSpec/MultipleMemoizedHelpers:
Enabled: false
Style/Documentation:
Enabled: false
Metrics/MethodLength:
Max: 20
Rails/SkipsModelValidations:
Enabled: false
Metrics/AbcSize:
Enabled: false

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
# Add your own tasks in files placed in lib/tasks ending in .rake, # Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application" require_relative 'config/application'
Rails.application.load_tasks Rails.application.load_tasks

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module ApplicationCable module ApplicationCable

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module ApplicationCable module ApplicationCable

View File

@ -6,22 +6,27 @@ class AffinitiesController < ApplicationController
before_action :set_group before_action :set_group
def index def index
overridden_affinities = @group.affinities overridden = @group.affinities.each_with_object({}) do |affinity, acc|
.each_with_object({}) { |affinity, acc| acc[affinity.another_group(@group).id] = affinity.discomfort } acc[affinity.another_group(@group).id] = affinity.discomfort
Group.where.not(id: @group.id).pluck(:id).index_with { |group_id| GroupAffinity::MAX_DISCOMFORT - (overridden_affinities[group_id] || GroupAffinity::NEUTRAL) } end
.then { |affinities| render json: affinities } Group.where.not(id: @group.id)
.pluck(:id)
.index_with { |group_id| GroupAffinity::MAX_DISCOMFORT - (overridden[group_id] || GroupAffinity::NEUTRAL) }
.then { |affinities| render json: affinities }
end end
def bulk_update def bulk_update
params.expect(affinities: [[:group_id, :affinity]]).map(&:to_h).map do |affinity| affinities = params.expect(affinities: [%i[group_id affinity]]).map(&:to_h).map do |affinity|
{ {
group_a_id: @group.id, group_a_id: @group.id,
group_b_id: affinity[:group_id], group_b_id: affinity[:group_id],
discomfort: GroupAffinity::MAX_DISCOMFORT - affinity[:affinity] discomfort: GroupAffinity::MAX_DISCOMFORT - affinity[:affinity]
} }
end.then { |affinities| GroupAffinity.upsert_all(affinities) } end
render json: {}, status: :ok
GroupAffinity.upsert_all(affinities)
render json: {}, status: :ok
rescue ActiveRecord::InvalidForeignKey rescue ActiveRecord::InvalidForeignKey
render json: { error: 'At least one of the group IDs provided does not exist.' }, status: :bad_request render json: { error: 'At least one of the group IDs provided does not exist.' }, status: :bad_request
rescue ActiveRecord::StatementInvalid rescue ActiveRecord::StatementInvalid

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
@ -40,9 +42,9 @@ class ApplicationController < ActionController::Base
end end
def captcha_params def captcha_params
params.expect(captcha: [:id, :answer]) params.expect(captcha: %i[id answer])
end end
def default_url_options(options = {}) def default_url_options(options = {})
options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug }) options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug })
end end
@ -53,7 +55,7 @@ class ApplicationController < ActionController::Base
def development_swagger? def development_swagger?
Rails.env.test? || Rails.env.test? ||
Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html') (Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html'))
end end
def set_csrf_cookie def set_csrf_cookie

View File

@ -1,10 +1,12 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class CaptchaController < ApplicationController class CaptchaController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_before_action :set_tenant skip_before_action :set_tenant
def create def create
id = LibreCaptcha.new.get_id id = LibreCaptcha.new.id
render json: { render json: {
id:, id:,
media_url: media_captcha_index_url(id:) media_url: media_captcha_index_url(id:)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class ExpensesController < ApplicationController class ExpensesController < ApplicationController
@ -6,7 +8,7 @@ class ExpensesController < ApplicationController
end end
def index def index
render json: Expense.all.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) render json: Expense.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type])
end end
def create def create

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class GroupsController < ApplicationController class GroupsController < ApplicationController
@ -39,6 +41,6 @@ class GroupsController < ApplicationController
end end
def group_params def group_params
params.expect(group: [:name, :icon, :color]) params.expect(group: %i[name icon color])
end end
end end

View File

@ -1,10 +1,12 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'csv' require 'csv'
class GuestsController < ApplicationController class GuestsController < ApplicationController
def index def index
render json: Guest.all.includes(:group) render json: Guest.includes(:group)
.left_joins(:group) .left_joins(:group)
.order('groups.name' => :asc, name: :asc) .order('groups.name' => :asc, name: :asc)
.as_json(only: %i[id name status], include: { group: { only: %i[id name] } }) .as_json(only: %i[id name status], include: { group: { only: %i[id name] } })

View File

@ -1,29 +1,43 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class SummaryController < ApplicationController class SummaryController < ApplicationController
def index def index
expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call
guest_summary = Guest.group(:status).count
render json: { render json: {
expenses: { expenses:,
projected: { guests:
total: expense_summary['total_projected'], }
guests: expense_summary['projected_guests'] end
},
confirmed: { private
total: expense_summary['total_confirmed'],
guests: expense_summary['confirmed_guests'] def guests
}, guest_summary = Guest.group(:status).count
status: {
paid: 0 {
} total: guest_summary.except('considered').values.sum,
confirmed: guest_summary['confirmed'].to_i,
declined: guest_summary['declined'].to_i,
tentative: guest_summary['tentative'].to_i,
invited: guest_summary['invited'].to_i
}
end
def expenses
expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call
{
projected: {
total: expense_summary['total_projected'],
guests: expense_summary['projected_guests']
}, },
guests: { confirmed: {
total: guest_summary.except('considered').values.sum, total: expense_summary['total_confirmed'],
confirmed: guest_summary['confirmed'].to_i, guests: expense_summary['confirmed_guests']
declined: guest_summary['declined'].to_i, },
tentative: guest_summary['tentative'].to_i, status: {
invited: guest_summary['invited'].to_i paid: 0
} }
} }
end end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class TablesArrangementsController < ApplicationController class TablesArrangementsController < ApplicationController
def index def index
render json: TablesArrangement.all.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort]) render json: TablesArrangement.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort])
end end
def show def show

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class TokensController < ApplicationController class TokensController < ApplicationController

View File

@ -1,23 +1,27 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class Users::ConfirmationsController < Devise::ConfirmationsController module Users
clear_respond_to class ConfirmationsController < Devise::ConfirmationsController
respond_to :json clear_respond_to
respond_to :json
def show def show
super do |resource| super do |resource|
if resource.errors.empty? if resource.errors.empty?
respond_to do |format| respond_to do |format|
format.json { render json: resource, status: :ok } format.json { render json: resource, status: :ok }
format.any { redirect_to root_path } format.any { redirect_to root_path }
end
else
render json: {
message: 'Record invalid',
errors: resource.errors.full_messages
}, status: :unprocessable_entity
end end
else return
render json: {
message: 'Record invalid',
errors: resource.errors.full_messages
}, status: :unprocessable_entity
end end
return
end end
end end
end end

View File

@ -1,28 +1,32 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class Users::RegistrationsController < Devise::RegistrationsController module Users
clear_respond_to class RegistrationsController < Devise::RegistrationsController
respond_to :json clear_respond_to
respond_to :json
before_action :validate_captcha!, only: :create before_action :validate_captcha!, only: :create
def create def create
wedding = Wedding.create(slug: params[:slug]) wedding = Wedding.create(slug: params[:slug])
unless wedding.persisted? unless wedding.persisted?
render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity
return return
end end
ActsAsTenant.with_tenant(wedding) do ActsAsTenant.with_tenant(wedding) do
super do |user| super do |user|
wedding.destroy unless user.persisted? wedding.destroy unless user.persisted?
end
end end
end end
end
private private
def set_tenant def set_tenant
set_current_tenant(nil) set_current_tenant(nil)
end
end end
end end

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class Users::SessionsController < Devise::SessionsController module Users
clear_respond_to class SessionsController < Devise::SessionsController
respond_to :json clear_respond_to
end respond_to :json
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module TreeNodeExtension module TreeNodeExtension

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module ApplicationHelper module ApplicationHelper

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module ExpensesHelper module ExpensesHelper

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module GroupsHelper module GroupsHelper

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module GuestsHelper module GuestsHelper

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module TablesArrangementsHelper module TablesArrangementsHelper

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class ApplicationJob < ActiveJob::Base class ApplicationJob < ActiveJob::Base

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class TableSimulatorJob < ApplicationJob class TableSimulatorJob < ApplicationJob

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: "from@example.com" default from: 'from@example.com'
layout "mailer" layout 'mailer'
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class ApplicationRecord < ActiveRecord::Base class ApplicationRecord < ActiveRecord::Base

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information
@ -31,7 +33,7 @@ class Group < ApplicationRecord
validates :name, uniqueness: true validates :name, uniqueness: true
validates :name, :order, presence: true validates :name, :order, presence: true
has_many :children, class_name: 'Group', foreign_key: 'parent_id' has_many :children, class_name: 'Group', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent
belongs_to :parent, class_name: 'Group', optional: true belongs_to :parent, class_name: 'Group', optional: true
before_create :set_color before_create :set_color
@ -41,10 +43,7 @@ class Group < ApplicationRecord
has_many :guests, dependent: :nullify has_many :guests, dependent: :nullify
def colorize_children(generation = 1) def colorize_children(generation = 1)
derived_colors = generation == 1 ? color.paint.palette.analogous(size: children.count) : color.paint.palette.decreasing_saturation children.zip(palette(generation)) do |child, raw_color|
children.zip(derived_colors) do |child, raw_color|
final_color = raw_color.paint final_color = raw_color.paint
final_color.brighten(60) if final_color.dark? final_color.brighten(60) if final_color.dark?
@ -53,13 +52,21 @@ class Group < ApplicationRecord
child.colorize_children(generation + 1) child.colorize_children(generation + 1)
end end
end end
def affinities def affinities
GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id)) GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id))
end end
private private
def palette(generation)
if generation == 1
color.paint.palette.analogous(size: children.count)
else
color.paint.palette.decreasing_saturation
end
end
def set_color def set_color
return if color.present? return if color.present?

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information
@ -30,7 +32,8 @@ class GroupAffinity < ApplicationRecord
belongs_to :group_a, class_name: 'Group' belongs_to :group_a, class_name: 'Group'
belongs_to :group_b, class_name: 'Group' belongs_to :group_b, class_name: 'Group'
validates :discomfort, numericality: { greater_than_or_equal_to: MIN_DISCOMFORT, less_than_or_equal_to: MAX_DISCOMFORT } validates :discomfort,
numericality: { greater_than_or_equal_to: MIN_DISCOMFORT, less_than_or_equal_to: MAX_DISCOMFORT }
def another_group(group) def another_group(group)
return nil if group != group_a && group != group_b return nil if group != group_a && group != group_b

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information
@ -39,8 +41,8 @@ class Guest < ApplicationRecord
scope :potential, -> { where.not(status: %i[declined considered]) } scope :potential, -> { where.not(status: %i[declined considered]) }
after_save :recalculate_simulations, if: :saved_change_to_status?
after_destroy :recalculate_simulations after_destroy :recalculate_simulations
after_save :recalculate_simulations, if: :saved_change_to_status?
has_many :seats, dependent: :delete_all has_many :seats, dependent: :delete_all

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information
@ -21,7 +23,7 @@
# #
class TablesArrangement < ApplicationRecord class TablesArrangement < ApplicationRecord
acts_as_tenant :wedding acts_as_tenant :wedding
has_many :seats has_many :seats, dependent: :delete_all
has_many :guests, through: :seats has_many :guests, through: :seats
before_create :assign_name before_create :assign_name

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == Schema Information # == Schema Information

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module Expenses module Expenses
@ -16,7 +18,7 @@ module Expenses
private private
def query def query
<<~SQL <<~SQL.squish
WITH guest_count AS (#{guest_count_per_status}), WITH guest_count AS (#{guest_count_per_status}),
expense_summary AS (#{expense_summary}) expense_summary AS (#{expense_summary})
SELECT guest_count.confirmed as confirmed_guests, SELECT guest_count.confirmed as confirmed_guests,
@ -28,7 +30,7 @@ module Expenses
end end
def expense_summary def expense_summary
<<~SQL <<~SQL.squish
SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed,
coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable
FROM expenses FROM expenses
@ -37,7 +39,7 @@ module Expenses
end end
def guest_count_per_status def guest_count_per_status
<<~SQL <<~SQL.squish
SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed, SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed,
COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected
FROM guests FROM guests

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module Groups module Groups
@ -9,13 +11,21 @@ module Groups
:icon, :icon,
:parent_id, :parent_id,
:color, :color,
*count_expressions
)
end
private
def count_expressions
[
Arel.sql('count(*) filter (where status IS NOT NULL) as total'), Arel.sql('count(*) filter (where status IS NOT NULL) as total'),
Arel.sql('count(*) filter (where status = 0) as considered'), Arel.sql('count(*) filter (where status = 0) as considered'),
Arel.sql('count(*) filter (where status = 10) as invited'), Arel.sql('count(*) filter (where status = 10) as invited'),
Arel.sql('count(*) filter (where status = 20) as confirmed'), Arel.sql('count(*) filter (where status = 20) as confirmed'),
Arel.sql('count(*) filter (where status = 30) as declined'), Arel.sql('count(*) filter (where status = 30) as declined'),
Arel.sql('count(*) filter (where status = 40) as tentative'), Arel.sql('count(*) filter (where status = 40) as tentative')
) ]
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class SerializableGroup < JSONAPI::Serializable::Resource class SerializableGroup < JSONAPI::Serializable::Resource

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class SerializableGuest < JSONAPI::Serializable::Resource class SerializableGuest < JSONAPI::Serializable::Resource

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class AffinityGroupsHierarchy < Array class AffinityGroupsHierarchy < Array

View File

@ -1,20 +1,20 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class LibreCaptcha class LibreCaptcha
def get_id def id
HTTParty.post("http://libre-captcha:8888/v2/captcha", HTTParty.post('http://libre-captcha:8888/v2/captcha',
body: { body: {
input_type: "text", input_type: 'text',
level: :hard, level: :hard,
media: 'image/png', media: 'image/png',
size: '350x100' size: '350x100'
}.to_json }.to_json).then { |raw| JSON.parse(raw)['id'] }
).then { |raw| JSON.parse(raw)['id'] }
end end
def valid?(id:, answer:) def valid?(id:, answer:)
HTTParty.post("http://libre-captcha:8888/v2/answer", HTTParty.post('http://libre-captcha:8888/v2/answer',
body: { id:, answer: }.to_json body: { id:, answer: }.to_json).then { |raw| JSON.parse(raw)['result'] == 'True' }
).then { |raw| JSON.parse(raw)['result'] == 'True' }
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module Tables module Tables
@ -7,6 +9,7 @@ module Tables
distance = AffinityGroupsHierarchy.instance.distance(id_a, id_b) distance = AffinityGroupsHierarchy.instance.distance(id_a, id_b)
return 1 if distance.nil? return 1 if distance.nil?
Rational(distance, distance + 1) Rational(distance, distance + 1)
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require_relative '../../extensions/tree_node_extension' require_relative '../../extensions/tree_node_extension'

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module Tables module Tables

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module Tables module Tables

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module Tables module Tables

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module VNS module VNS

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
# This file is used by Rack-based servers to start the application. # This file is used by Rack-based servers to start the application.
require_relative "config/environment" require_relative 'config/environment'
run Rails.application run Rails.application
Rails.application.load_server Rails.application.load_server

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# This rake task was added by annotate_rb gem. # This rake task was added by annotate_rb gem.
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this # Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil? if Rails.env.development? && ENV['ANNOTATERB_SKIP_ON_DB_TASKS'].nil?
require "annotate_rb" require 'annotate_rb'
AnnotateRb::Core.load_rake_tasks AnnotateRb::Core.load_rake_tasks
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'
@ -5,66 +7,66 @@ require 'rails_helper'
module Tree module Tree
RSpec.describe TreeNode do RSpec.describe TreeNode do
describe '#distance_to_common_ancestor' do describe '#distance_to_common_ancestor' do
def assert_distance(node_1, node_2, distance) def assert_distance(node1, node2, distance)
aggregate_failures do aggregate_failures do
expect(node_1.distance_to_common_ancestor(node_2)).to eq(distance) expect(node1.distance_to_common_ancestor(node2)).to eq(distance)
expect(node_2.distance_to_common_ancestor(node_1)).to eq(distance) expect(node2.distance_to_common_ancestor(node1)).to eq(distance)
end end
end end
context 'when the two nodes are the same' do context 'when the two nodes are the same' do
it 'returns 0 when comparing the root itself' do it 'returns 0 when comparing the root itself' do
root = Tree::TreeNode.new('root') root = described_class.new('root')
assert_distance(root, root, 0) assert_distance(root, root, 0)
end end
it 'returns 0 when comparing a child to itself' do it 'returns 0 when comparing a child to itself' do
root = Tree::TreeNode.new('root') root = described_class.new('root')
child = root << Tree::TreeNode.new('child') child = root << described_class.new('child')
assert_distance(child, child, 0) assert_distance(child, child, 0)
end end
end end
context 'when the two nodes are siblings' do context 'when the two nodes are siblings' do
it 'returns 1 when comparing siblings' do it 'returns 1 when comparing siblings' do
root = Tree::TreeNode.new('root') root = described_class.new('root')
child1 = root << Tree::TreeNode.new('child1') child1 = root << described_class.new('child1')
child2 = root << Tree::TreeNode.new('child2') child2 = root << described_class.new('child2')
assert_distance(child1, child2, 1) assert_distance(child1, child2, 1)
end end
end end
context 'when one node is parent of the other' do context 'when one node is parent of the other' do
it 'returns 1 when comparing parent to child' do it 'returns 1 when comparing parent to child' do
root = Tree::TreeNode.new('root') root = described_class.new('root')
child = root << Tree::TreeNode.new('child') child = root << described_class.new('child')
assert_distance(root, child, 1) assert_distance(root, child, 1)
end end
end end
context 'when one node is grandparent of the other' do context 'when one node is grandparent of the other' do
it 'returns 2 when comparing grandparent to grandchild' do it 'returns 2 when comparing grandparent to grandchild' do
root = Tree::TreeNode.new('root') root = described_class.new('root')
child = root << Tree::TreeNode.new('child') child = root << described_class.new('child')
grandchild = child << Tree::TreeNode.new('grandchild') grandchild = child << described_class.new('grandchild')
assert_distance(root, grandchild, 2) assert_distance(root, grandchild, 2)
end end
end end
context 'when the two nodes are cousins' do context 'when the two nodes are cousins' do
it 'returns 2 when comparing cousins' do it 'returns 2 when comparing cousins' do
root = Tree::TreeNode.new('root') root = described_class.new('root')
child1 = root << Tree::TreeNode.new('child1') child1 = root << described_class.new('child1')
child2 = root << Tree::TreeNode.new('child2') child2 = root << described_class.new('child2')
grandchild1 = child1 << Tree::TreeNode.new('grandchild1') grandchild1 = child1 << described_class.new('grandchild1')
grandchild2 = child2 << Tree::TreeNode.new('grandchild2') grandchild2 = child2 << described_class.new('grandchild2')
assert_distance(grandchild1, grandchild2, 2) assert_distance(grandchild1, grandchild2, 2)
end end
end end
context 'when the two nodes are not related' do context 'when the two nodes are not related' do
it 'returns nil' do it 'returns nil' do
root = Tree::TreeNode.new('root') root = described_class.new('root')
another_root = Tree::TreeNode.new('another_root') another_root = described_class.new('another_root')
assert_distance(root, another_root, nil) assert_distance(root, another_root, nil)
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'
@ -61,8 +63,8 @@ module Expenses
end end
it 'returns the sum of fixed and variable expenses', :aggregate_failures do it 'returns the sum of fixed and variable expenses', :aggregate_failures do
expect(response['total_confirmed']).to eq(100 + 200 + 50 * 2) expect(response['total_confirmed']).to eq(100 + 200 + (50 * 2))
expect(response['total_projected']).to eq(100 + 200 + 11 * 50) expect(response['total_projected']).to eq(100 + 200 + (11 * 50))
expect(response['confirmed_guests']).to eq(2) expect(response['confirmed_guests']).to eq(2)
expect(response['projected_guests']).to eq(2 + 4 + 5) expect(response['projected_guests']).to eq(2 + 4 + 5)
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'
@ -5,7 +7,7 @@ require 'rails_helper'
module Groups module Groups
RSpec.describe SummaryQuery do RSpec.describe SummaryQuery do
describe '#call' do describe '#call' do
subject { described_class.new.call } subject(:result) { described_class.new.call }
context 'when there are no groups' do context 'when there are no groups' do
it { is_expected.to eq([]) } it { is_expected.to eq([]) }
@ -17,7 +19,7 @@ module Groups
context 'when there are no guests' do context 'when there are no guests' do
it 'returns the summary of groups' do it 'returns the summary of groups' do
is_expected.to contain_exactly( expect(result).to contain_exactly(
{ 'id' => parent.id, { 'id' => parent.id,
'name' => 'Friends', 'name' => 'Friends',
'icon' => 'icon-1', 'icon' => 'icon-1',
@ -58,11 +60,11 @@ module Groups
create_list(:guest, 8, group: child, status: :invited) create_list(:guest, 8, group: child, status: :invited)
create_list(:guest, 9, group: child, status: :confirmed) create_list(:guest, 9, group: child, status: :confirmed)
create_list(:guest, 10, group: child, status: :declined) create_list(:guest, 10, group: child, status: :declined)
create_list(:guest, 11, group: child, status: :tentative) create_list(:guest, 11, group: child, status: :tentative) # rubocop:disable FactoryBot/ExcessiveCreateList
end end
it 'returns the summary of groups' do it 'returns the summary of groups' do
is_expected.to contain_exactly( expect(result).to contain_exactly(
{ {
'id' => parent.id, 'id' => parent.id,
'name' => 'Friends', 'name' => 'Friends',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# This file is copied to spec/ when you run 'rails generate rspec:install' # This file is copied to spec/ when you run 'rails generate rspec:install'
@ -5,7 +7,7 @@ require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test' ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment' require_relative '../config/environment'
# Prevent database truncation if the environment is production # Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production? abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails' require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point! # Add additional requires below this line. Rails is not loaded until this point!

View File

@ -4,7 +4,7 @@
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'affinities', type: :request do RSpec.describe 'affinities' do
path '/{slug}/groups/{group_id}/affinities' do path '/{slug}/groups/{group_id}/affinities' do
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter name: 'group_id', in: :path, type: :string, format: :uuid, description: 'group_id' parameter name: 'group_id', in: :path, type: :string, format: :uuid, description: 'group_id'
@ -46,7 +46,7 @@ RSpec.describe 'affinities', type: :request do
} }
} }
response_empty_200 response_empty200
end end
end end
end end

View File

@ -1,22 +1,23 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'captcha', type: :request do RSpec.describe 'captcha' do
path '/captcha' do path '/captcha' do
post('create a CAPTCHA challenge') do post('create a CAPTCHA challenge') do
tags 'CAPTCHA' tags 'CAPTCHA'
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
response(201, 'created') do response(201, 'created') do
schema type: :object, schema type: :object,
required: %i[id], required: %i[id],
properties: { properties: {
id: { type: :string, format: :uuid }, id: { type: :string, format: :uuid },
media_url: { type: :string, format: :uri }, media_url: { type: :string, format: :uri }
} }
xit xit
end end
end end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'expenses', type: :request do RSpec.describe 'expenses' do
path '/{slug}/expenses' do path '/{slug}/expenses' do
get('list expenses') do get('list expenses') do
tags 'Expenses' tags 'Expenses'
@ -42,8 +44,8 @@ RSpec.describe 'expenses', type: :request do
} }
} }
response_empty_201 response_empty201
response_422 response422
regular_api_responses regular_api_responses
end end
end end
@ -60,9 +62,9 @@ RSpec.describe 'expenses', type: :request do
properties: Swagger::Schema::EXPENSE properties: Swagger::Schema::EXPENSE
} }
response_empty_200 response_empty200
response_422 response422
response_404 response404
regular_api_responses regular_api_responses
end end
@ -71,8 +73,8 @@ RSpec.describe 'expenses', type: :request do
produces 'application/json' produces 'application/json'
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter Swagger::Schema::ID parameter Swagger::Schema::ID
response_empty_200 response_empty200
response_404 response404
regular_api_responses regular_api_responses
end end
end end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'groups', type: :request do RSpec.describe 'groups' do
path '/{slug}/groups' do path '/{slug}/groups' do
get('list groups') do get('list groups') do
tags 'Groups' tags 'Groups'
@ -10,29 +12,29 @@ RSpec.describe 'groups', type: :request do
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
response(200, 'successful') do response(200, 'successful') do
schema type: :array, schema type: :array,
items: { items: {
type: :object, type: :object,
required: %i[id name icon parent_id color attendance], required: %i[id name icon parent_id color attendance],
properties: { properties: {
id: { type: :string, format: :uuid, required: true }, id: { type: :string, format: :uuid, required: true },
name: { type: :string }, name: { type: :string },
icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' }, icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' },
parent_id: { type: :string, format: :uuid }, parent_id: { type: :string, format: :uuid },
color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' }, color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' },
attendance: { attendance: {
type: :object, type: :object,
required: %i[total considered invited confirmed declined tentative], required: %i[total considered invited confirmed declined tentative],
properties: { properties: {
total: { type: :integer, minimum: 0, description: 'Total number of guests in any status' }, total: { type: :integer, minimum: 0, description: 'Total number of guests in any status' },
considered: { type: :integer, minimum: 0 }, considered: { type: :integer, minimum: 0 },
invited: { type: :integer, minimum: 0 }, invited: { type: :integer, minimum: 0 },
confirmed: { type: :integer, minimum: 0 }, confirmed: { type: :integer, minimum: 0 },
declined: { type: :integer, minimum: 0 }, declined: { type: :integer, minimum: 0 },
tentative: { type: :integer, minimum: 0 } tentative: { type: :integer, minimum: 0 }
} }
} }
}
} }
}
xit xit
end end
regular_api_responses regular_api_responses
@ -99,8 +101,8 @@ RSpec.describe 'groups', type: :request do
produces 'application/json' produces 'application/json'
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter name: :id, in: :path, type: :string, format: :uuid parameter name: :id, in: :path, type: :string, format: :uuid
response_empty_200 response_empty200
regular_api_responses regular_api_responses
end end
end end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'guests', type: :request do RSpec.describe 'guests' do
path '/{slug}/guests' do path '/{slug}/guests' do
get('list guests') do get('list guests') do
tags 'Guests' tags 'Guests'
@ -51,8 +53,8 @@ RSpec.describe 'guests', type: :request do
} }
} }
response_empty_201 response_empty201
response_422 response422
regular_api_responses regular_api_responses
end end
end end
@ -80,9 +82,9 @@ RSpec.describe 'guests', type: :request do
} }
} }
response_empty_200 response_empty200
response_422 response422
response_404 response404
regular_api_responses regular_api_responses
end end
@ -92,8 +94,8 @@ RSpec.describe 'guests', type: :request do
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter name: 'id', in: :path, type: :string, format: :uuid parameter name: 'id', in: :path, type: :string, format: :uuid
response_empty_200 response_empty200
response_404 response404
regular_api_responses regular_api_responses
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module Swagger module Swagger
@ -7,13 +9,13 @@ module Swagger
email: { type: :string, format: :email }, email: { type: :string, format: :email },
created_at: SwaggerResponseHelper::TIMESTAMP, created_at: SwaggerResponseHelper::TIMESTAMP,
updated_at: SwaggerResponseHelper::TIMESTAMP updated_at: SwaggerResponseHelper::TIMESTAMP
} }.freeze
ID = { ID = { # rubocop:disable Style/MutableConstant -- rswag modifies in: :path parameters
name: 'id', name: 'id',
in: :path, in: :path,
type: :string, type: :string,
format: :uuid, format: :uuid
} }
GROUP = { GROUP = {
@ -21,18 +23,18 @@ module Swagger
icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' }, icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' },
parent_id: { type: :string, format: :uuid }, parent_id: { type: :string, format: :uuid },
color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' } color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' }
} }.freeze
EXPENSE = { EXPENSE = {
name: { type: :string }, name: { type: :string },
amount: { type: :number, minimum: 0 }, amount: { type: :number, minimum: 0 },
pricing_type: { type: :string, enum: Expense.pricing_types.keys } pricing_type: { type: :string, enum: Expense.pricing_types.keys }
} }.freeze
SLUG = { SLUG = { # rubocop:disable Style/MutableConstant -- rswag modifies in: :path parameters
name: 'slug', name: 'slug',
in: :path, in: :path,
type: :string, type: :string,
pattern: Wedding::SLUG_REGEX, pattern: Wedding::SLUG_REGEX,
example: :default, example: :default,
description: 'Wedding slug' description: 'Wedding slug'
@ -47,6 +49,6 @@ module Swagger
answer: { type: :string } answer: { type: :string }
} }
} }
} }.freeze
end end
end end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'summary', type: :request do RSpec.describe 'summary' do
path '/{slug}/summary' do path '/{slug}/summary' do
get('list summaries') do get('list summaries') do
tags 'Summary' tags 'Summary'

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'tables_arrangements', type: :request do RSpec.describe 'tables_arrangements' do
path '/{slug}/tables_arrangements' do path '/{slug}/tables_arrangements' do
get('list tables arrangements') do get('list tables arrangements') do
tags 'Tables Arrangements' tags 'Tables Arrangements'

View File

@ -1,15 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'tokens', type: :request do
path '/token' do
get('get a cookie with CSRF token') do
tags 'CSRF token'
consumes 'application/json'
produces 'application/json'
response_empty_200
end
end
end

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'users/confirmations', type: :request do RSpec.describe 'users/confirmations' do
path '/{slug}/users/confirmation' do path '/{slug}/users/confirmation' do
get('confirm user email') do get('confirm user email') do
tags 'Users' tags 'Users'
@ -17,7 +18,7 @@ RSpec.describe 'users/confirmations', type: :request do
xit xit
end end
response_422 response422
end end
end end
end end

View File

@ -1,25 +1,26 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'users/registrations', type: :request do RSpec.describe 'users/registrations' do
path '/{slug}/users' do path '/{slug}/users' do
post('create registration') do post('create registration') do
tags 'Users Registrations' tags 'Users Registrations'
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter name: :body, in: :body, schema: { parameter name: :body, in: :body, schema: {
type: :object, type: :object,
required: [:user, :wedding], required: %i[user wedding],
properties: { properties: {
user: { user: {
type: :object, type: :object,
required: %i[email password password_confirmation], required: %i[email password password_confirmation],
properties: { properties: {
email: { type: :string, format: :email}, email: { type: :string, format: :email },
password: SwaggerResponseHelper::PASSWORD, password: SwaggerResponseHelper::PASSWORD,
password_confirmation: SwaggerResponseHelper::PASSWORD password_confirmation: SwaggerResponseHelper::PASSWORD
} }

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'users/sessions', type: :request do RSpec.describe 'users/sessions' do
path '/{slug}/users/sign_in' do path '/{slug}/users/sign_in' do
post('create session') do post('create session') do
tags 'Users Sessions' tags 'Users Sessions'
consumes 'application/json' consumes 'application/json'
@ -32,7 +32,7 @@ RSpec.describe 'users/sessions', type: :request do
xit xit
end end
response_401(message: 'Invalid Email or password.') response401(message: 'Invalid Email or password.')
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'
@ -12,8 +14,7 @@ module Tables
describe '#calculate' do describe '#calculate' do
before do before do
allow(calculator).to receive(:table_size_penalty).and_return(2) allow(calculator).to receive_messages(table_size_penalty: 2, cohesion_discomfort: 3)
allow(calculator).to receive(:cohesion_discomfort).and_return(3)
end end
let(:table) { Table.new(create_list(:guest, 6)) } let(:table) { Table.new(create_list(:guest, 6)) }
@ -29,6 +30,7 @@ module Tables
table.min_per_table = 5 table.min_per_table = 5
table.max_per_table = 7 table.max_per_table = 7
end end
context 'when the number of guests is in the lower bound' do context 'when the number of guests is in the lower bound' do
let(:table) { Table.new(create_list(:guest, 5)) } let(:table) { Table.new(create_list(:guest, 5)) }
@ -88,6 +90,7 @@ module Tables
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, school.id).and_return(4) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, school.id).and_return(4)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(work.id, school.id).and_return(5) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(work.id, school.id).and_return(5)
end end
context 'when the table contains just two guests' do context 'when the table contains just two guests' do
context 'when they belong to the same group' do context 'when they belong to the same group' do
let(:table) { create_list(:guest, 2, group: family) } let(:table) { create_list(:guest, 2, group: family) }
@ -102,6 +105,7 @@ module Tables
create(:guest, group: friends) create(:guest, group: friends)
] ]
end end
it { expect(calculator.send(:cohesion_discomfort)).to eq(1) } it { expect(calculator.send(:cohesion_discomfort)).to eq(1) }
end end
@ -205,7 +209,7 @@ module Tables
end end
it 'returns the sum of the penalties for each pair of guests' do it 'returns the sum of the penalties for each pair of guests' do
expect(calculator.send(:cohesion_discomfort)).to eq(4 * 1 + 4 * Rational(1, 2) + 4 * Rational(2, 3)) expect(calculator.send(:cohesion_discomfort)).to eq((4 * 1) + (4 * Rational(1, 2)) + (4 * Rational(2, 3)))
end end
end end
@ -219,7 +223,7 @@ module Tables
end end
it 'returns the sum of the penalties for each pair of guests' do it 'returns the sum of the penalties for each pair of guests' do
expect(calculator.send(:cohesion_discomfort)).to eq(6 * 1 + 2 * Rational(1, 2) + 3 * Rational(2, 3)) expect(calculator.send(:cohesion_discomfort)).to eq((6 * 1) + (2 * Rational(1, 2)) + (3 * Rational(2, 3)))
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'
@ -5,19 +7,19 @@ require 'rails_helper'
module Tables module Tables
RSpec.describe Distribution do RSpec.describe Distribution do
describe '#random_distribution' do describe '#random_distribution' do
let(:subject) { described_class.new(min_per_table: 5, max_per_table: 10) } subject(:distribution) { described_class.new(min_per_table: 5, max_per_table: 10) }
context 'when there are fewer people than the minimum per table' do context 'when there are fewer people than the minimum per table' do
it 'creates one table' do it 'creates one table' do
subject.random_distribution([1, 2, 3, 4]) distribution.random_distribution([1, 2, 3, 4])
expect(subject.tables.count).to eq(1) expect(distribution.tables.count).to eq(1)
end end
end end
context 'when there are more people than the maximum per table' do context 'when there are more people than the maximum per table' do
it 'creates multiple tables' do it 'creates multiple tables' do
subject.random_distribution([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) distribution.random_distribution([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
expect(subject.tables.count).to be > 1 expect(distribution.tables.count).to be > 1
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'
@ -7,7 +9,7 @@ module Tables
describe '#each' do describe '#each' do
let(:shifts) do let(:shifts) do
acc = [] acc = []
described_class.new(initial_solution).each do |solution| described_class.new(initial_solution).each do |solution| # rubocop:disable Style/MapIntoArray -- #map is not implemented
acc << solution.tables.map(&:dup) acc << solution.tables.map(&:dup)
end end
acc acc

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'
@ -7,7 +9,7 @@ module Tables
describe '#each' do describe '#each' do
let(:swaps) do let(:swaps) do
acc = [] acc = []
described_class.new(initial_solution).each do |solution| described_class.new(initial_solution).each do |solution| # rubocop:disable Style/MapIntoArray -- #map is not implemented
acc << solution.tables.map(&:dup) acc << solution.tables.map(&:dup)
end end
acc acc

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
require 'rails_helper' require 'rails_helper'

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# This file was generated by the `rails generate rspec:install` command. Conventionally, all # This file was generated by the `rails generate rspec:install` command. Conventionally, all
@ -46,51 +48,49 @@ RSpec.configure do |config|
# triggering implicit auto-inclusion in groups with matching metadata. # triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups config.shared_context_metadata_behavior = :apply_to_host_groups
# The settings below are suggested to provide a good initial experience # The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content. # with RSpec, but feel free to customize to your heart's content.
=begin # # This allows you to limit a spec run to individual examples or groups
# This allows you to limit a spec run to individual examples or groups # # you care about by tagging them with `:focus` metadata. When nothing
# you care about by tagging them with `:focus` metadata. When nothing # # is tagged with `:focus`, all examples get run. RSpec also provides
# is tagged with `:focus`, all examples get run. RSpec also provides # # aliases for `it`, `describe`, and `context` that include `:focus`
# aliases for `it`, `describe`, and `context` that include `:focus` # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
# metadata: `fit`, `fdescribe` and `fcontext`, respectively. # config.filter_run_when_matching :focus
config.filter_run_when_matching :focus #
# # Allows RSpec to persist some state between runs in order to support
# Allows RSpec to persist some state between runs in order to support # # the `--only-failures` and `--next-failure` CLI options. We recommend
# the `--only-failures` and `--next-failure` CLI options. We recommend # # you configure your source control system to ignore this file.
# you configure your source control system to ignore this file. # config.example_status_persistence_file_path = "spec/examples.txt"
config.example_status_persistence_file_path = "spec/examples.txt" #
# # Limits the available syntax to the non-monkey patched syntax that is
# Limits the available syntax to the non-monkey patched syntax that is # # recommended. For more details, see:
# recommended. For more details, see: # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
# https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ # config.disable_monkey_patching!
config.disable_monkey_patching! #
# # Many RSpec users commonly either run the entire suite or an individual
# Many RSpec users commonly either run the entire suite or an individual # # file, and it's useful to allow more verbose output when running an
# file, and it's useful to allow more verbose output when running an # # individual spec file.
# individual spec file. # if config.files_to_run.one?
if config.files_to_run.one? # # Use the documentation formatter for detailed output,
# Use the documentation formatter for detailed output, # # unless a formatter has already been configured
# unless a formatter has already been configured # # (e.g. via a command-line flag).
# (e.g. via a command-line flag). # config.default_formatter = "doc"
config.default_formatter = "doc" # end
end #
# # Print the 10 slowest examples and example groups at the
# Print the 10 slowest examples and example groups at the # # end of the spec run, to help surface which specs are running
# end of the spec run, to help surface which specs are running # # particularly slow.
# particularly slow. # config.profile_examples = 10
config.profile_examples = 10 #
# # Run specs in random order to surface order dependencies. If you find an
# Run specs in random order to surface order dependencies. If you find an # # order dependency and want to debug it, you can fix the order by providing
# order dependency and want to debug it, you can fix the order by providing # # the seed, which is printed after each run.
# the seed, which is printed after each run. # # --seed 1234
# --seed 1234 # config.order = :random
config.order = :random #
# # Seed global randomization in this process using the `--seed` CLI option.
# Seed global randomization in this process using the `--seed` CLI option. # # Setting this allows you to use `--seed` to deterministically reproduce
# Setting this allows you to use `--seed` to deterministically reproduce # # test failures related to randomization by passing the same `--seed` value
# test failures related to randomization by passing the same `--seed` value # # as the one that triggered the failure.
# as the one that triggered the failure. # Kernel.srand config.seed
Kernel.srand config.seed
=end
end end

View File

@ -3,10 +3,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require_relative './swagger_response_helper' require_relative 'swagger_response_helper'
require_relative './requests/schemas.rb' require_relative 'requests/schemas'
include SwaggerResponseHelper include SwaggerResponseHelper # rubocop:disable Style/MixinUsage
RSpec.configure do |config| RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated # Specify a root folder where Swagger JSON files are generated

View File

@ -1,18 +1,19 @@
# frozen_string_literal: true
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
module SwaggerResponseHelper module SwaggerResponseHelper
TIMESTAMP_FORMAT = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z' TIMESTAMP_FORMAT = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'
TIMESTAMP_EXAMPLE = Time.zone.now.iso8601(3) TIMESTAMP_EXAMPLE = Time.zone.now.iso8601(3)
TIMESTAMP = {type: :string,pattern: TIMESTAMP_FORMAT,example: TIMESTAMP_EXAMPLE}.freeze TIMESTAMP = { type: :string, pattern: TIMESTAMP_FORMAT, example: TIMESTAMP_EXAMPLE }.freeze
PASSWORD = { type: :string, minLength: User.password_length.begin, maxLength: User.password_length.end } PASSWORD = { type: :string, minLength: User.password_length.begin, maxLength: User.password_length.end }.freeze
def regular_api_responses def regular_api_responses
response_401 response401
end end
def response_422 def response422
response(422, 'Validation errors in input parameters') do response(422, 'Validation errors in input parameters') do
produces 'application/json' produces 'application/json'
error_schema error_schema
@ -20,7 +21,7 @@ module SwaggerResponseHelper
end end
end end
def response_empty_200 def response_empty200
response(200, 'Success') do response(200, 'Success') do
produces 'application/json' produces 'application/json'
schema type: :object schema type: :object
@ -28,7 +29,7 @@ module SwaggerResponseHelper
end end
end end
def response_empty_201 def response_empty201
response(201, 'Created') do response(201, 'Created') do
produces 'application/json' produces 'application/json'
schema type: :object schema type: :object
@ -36,7 +37,7 @@ module SwaggerResponseHelper
end end
end end
def response_404 def response404
response(404, 'Record not found') do response(404, 'Record not found') do
produces 'application/json' produces 'application/json'
error_schema error_schema
@ -44,14 +45,14 @@ module SwaggerResponseHelper
end end
end end
def response_401(message: nil) def response401(message: nil)
response(401, 'Unauthorized') do response(401, 'Unauthorized') do
produces 'application/json' produces 'application/json'
schema type: :object, schema type: :object,
required: %i[error], required: %i[error],
properties: { properties: {
error: { type: :string, example: message || 'You need to sign in or sign up before continuing.' } error: { type: :string, example: message || 'You need to sign in or sign up before continuing.' }
} }
xit xit
end end
end end