1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

Merge branch 'dev' for release 5.6.6

This commit is contained in:
Sylvain 2023-01-23 09:31:12 +01:00
commit 2afdb2824a
25 changed files with 410 additions and 89 deletions

View File

@ -1,5 +1,18 @@
# Changelog Fab-manager
## v5.6.6 2023 January 23
- Add more context data to sentry reports
- Improved SSO testing
- Ability to map the external ID from the SSO
- Ability to soft-destroy a reserved event
- Fix a bug: unable to run task fix_invoice_item when some invoice items are associated with errors
- Fix a bug: invalid event date reported when the timezone in before UTC
- Fix a bug: unable to run accounting export if a line label was not defined
- Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-44571)
- Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799)
- [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate`
## v5.6.5 2023 January 9
- Moved the buttons to create a new machine or availability to the admin section
@ -24,7 +37,7 @@
- Fix a bug: cryptic error message when failed to create a manager
- Fix a bug: unable to restore accounting periods closed by a deleted admin
- Fix a bug: unable to build an accounting archive if the operator was deleted
- Fix a bug: unable to udpate an event category
- Fix a bug: unable to update an event category
## v5.6.1 2023 January 6

View File

@ -160,7 +160,7 @@ GEM
fugit (1.5.3)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.0.0)
globalid (1.0.1)
activesupport (>= 5.0)
hashdiff (1.0.1)
hashery (2.1.2)
@ -222,7 +222,7 @@ GEM
mini_magick (4.10.1)
mini_mime (1.1.2)
mini_portile2 (2.8.0)
minitest (5.16.2)
minitest (5.17.0)
minitest-reporters (1.4.2)
ansi
builder
@ -303,7 +303,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.1)
rack (2.2.4)
rack (2.2.6.2)
rack-oauth2 (1.19.0)
activesupport
attr_required

View File

@ -3,7 +3,6 @@
# API Controller for resources of type AuthProvider
# AuthProvider are used to connect users through single-sign on systems
class API::AuthProvidersController < API::ApiController
before_action :set_provider, only: %i[show update destroy]
def index
@providers = policy_scope(AuthProvider)
@ -64,13 +63,13 @@ class API::AuthProvidersController < API::ApiController
user = User.find_by('lower(email) = ?', params[:email]&.downcase)
if user&.auth_token
if AuthProvider.active.providable_type != DatabaseProvider.name
if AuthProvider.active.providable_type == DatabaseProvider.name
render json: { status: 'error', error: I18n.t('members.current_authentication_method_no_code') }, status: :bad_request
else
NotificationCenter.call type: 'notify_user_auth_migration',
receiver: user,
attached_object: user
render json: { status: 'processing' }, status: :ok
else
render json: { status: 'error', error: I18n.t('members.current_authentication_method_no_code') }, status: :bad_request
end
else
render json: { status: 'error', error: I18n.t('members.requested_account_does_not_exists') }, status: :bad_request
@ -92,18 +91,18 @@ class API::AuthProvidersController < API::ApiController
providable_attributes: %i[id base_url token_endpoint authorization_endpoint
profile_url client_id client_secret scopes],
auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type,
:_destroy, transformation: [:type, :format, :true_value, :false_value,
mapping: %i[from to]]])
:_destroy, { transformation: [:type, :format, :true_value, :false_value,
{ mapping: %i[from to] }] }])
elsif params['auth_provider']['providable_type'] == OpenIdConnectProvider.name
params.require(:auth_provider)
.permit(:id, :name, :providable_type,
providable_attributes: [:id, :issuer, :discovery, :client_auth_method, :prompt, :send_scope_to_token_endpoint,
:client__identifier, :client__secret, :client__authorization_endpoint, :client__token_endpoint,
:client__userinfo_endpoint, :client__jwks_uri, :client__end_session_endpoint, :profile_url,
scope: []],
{ scope: [] }],
auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type,
:_destroy, transformation: [:type, :format, :true_value, :false_value,
mapping: %i[from to]]])
:_destroy, { transformation: [:type, :format, :true_value, :false_value,
{ mapping: %i[from to] }] }])
end
end
end

View File

@ -36,7 +36,8 @@ class API::EventsController < API::ApiController
limit = params[:limit]
@events = Event.includes(:event_image, :event_files, :availability, :category)
.where('events.nb_total_places != -1 OR events.nb_total_places IS NULL')
.order('availabilities.start_at ASC').references(:availabilities)
.where(deleted_at: nil)
.order('availabilities.start_at').references(:availabilities)
.limit(limit)
@events = case Setting.get('upcoming_events_shown')
@ -49,7 +50,9 @@ class API::EventsController < API::ApiController
end
end
def show; end
def show
head :not_found if @event.deleted_at
end
def create
authorize Event

View File

@ -8,6 +8,7 @@ class OpenAPI::V1::EventsController < OpenAPI::V1::BaseController
def index
@events = Event.includes(:event_image, :event_files, :availability, :category)
.where(deleted_at: nil)
@events = if upcoming
@events.references(:availabilities)
.where('availabilities.end_at >= ?', DateTime.current)

View File

@ -1,6 +1,6 @@
import moment, { unitOfTime } from 'moment';
import { IFablab } from '../models/fablab';
import { TDateISO, TDateISODate, THours, TMinutes } from '../typings/date-iso';
import { TDateISO, TDateISODate, TDateISOShortTime } from '../typings/date-iso';
declare let Fablab: IFablab;
@ -17,7 +17,15 @@ export default class FormatLib {
*/
static isDateISO = (value: string): boolean => {
if (typeof value !== 'string') return false;
return !!value?.match(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\d/);
return !!value?.match(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d/);
};
/**
* Check if the provided variable is string representing a short date, according to ISO 8601 (e.g. 2023-01-12)
*/
static isShortDateISO = (value: string): boolean => {
if (typeof value !== 'string') return false;
return !!value.match(/^\d\d\d\d-\d\d-\d\d$/);
};
/**
@ -32,7 +40,36 @@ export default class FormatLib {
* Return the formatted localized date for the given date
*/
static date = (date: Date|TDateISO|TDateISODate): string => {
return Intl.DateTimeFormat().format(moment(date).toDate());
let tempDate: Date;
if (FormatLib.isShortDateISO(date as string) || FormatLib.isDateISO(date as string)) {
tempDate = FormatLib.parseISOdate(date as TDateISO);
} else {
tempDate = moment(date).toDate();
}
return Intl.DateTimeFormat(Fablab.intl_locale).format(tempDate);
};
/**
* Parse the provided datetime or date string (as ISO8601 format) and return the equivalent Date object
*/
private static parseISOdate = (date: TDateISO|TDateISODate, res: Date = new Date()): Date => {
const isoDateMatch = (date as string)?.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/);
res.setFullYear(parseInt(isoDateMatch[1], 10));
res.setMonth(parseInt(isoDateMatch[2], 10) - 1);
res.setDate(parseInt(isoDateMatch[3], 10));
return res;
};
/**
* Parse the provided datetime or time string (as ISO8601 format) and return the equivalent Date object
*/
private static parseISOtime = (date: TDateISO|TDateISOShortTime, res: Date = new Date()): Date => {
const isoTimeMatch = (date as string)?.match(/(^|T)(\d\d):(\d\d)/);
res.setHours(parseInt(isoTimeMatch[2], 10));
res.setMinutes(parseInt(isoTimeMatch[3], 10));
return res;
};
/**
@ -45,13 +82,10 @@ export default class FormatLib {
/**
* Return the formatted localized time for the given date
*/
static time = (date: Date|TDateISO|`${THours}:${TMinutes}`): string => {
static time = (date: Date|TDateISO|TDateISOShortTime): string => {
let tempDate: Date;
if (FormatLib.isShortTimeISO(date as string)) {
const isoTimeMatch = (date as string)?.match(/^(\d\d):(\d\d)$/);
tempDate = new Date();
tempDate.setHours(parseInt(isoTimeMatch[1], 10));
tempDate.setMinutes(parseInt(isoTimeMatch[2], 10));
if (FormatLib.isShortTimeISO(date as string) || FormatLib.isDateISO(date as string)) {
tempDate = FormatLib.parseISOtime(date as TDateISOShortTime);
} else {
tempDate = moment(date).toDate();
}

View File

@ -1,4 +1,4 @@
// from https://gist.github.com/MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054
// inspired from https://gist.github.com/MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054
type TYear = `${number}${number}${number}${number}`;
type TMonth = `${number}${number}`;
@ -13,10 +13,15 @@ type TMilliseconds = `${number}${number}${number}`;
*/
type TDateISODate = `${TYear}-${TMonth}-${TDay}`;
/**
* Represent a string like `14:42`
*/
type TDateISOShortTime = `${THours}:${TMinutes}`;
/**
* Represent a string like `14:42:34.678`
*/
type TDateISOTime = `${THours}:${TMinutes}:${TSeconds}`|`${THours}:${TMinutes}:${TSeconds}.${TMilliseconds}`;
type TDateISOTime = `${TDateISOShortTime}:${TSeconds}`|`${TDateISOShortTime}:${TSeconds}.${TMilliseconds}`;
/**
* Represent a timezone like `+0100`

View File

@ -53,13 +53,12 @@ class Event < ApplicationRecord
.references(:availabilities)
end
def safe_destroy
reservations = Reservation.where(reservable_type: 'Event', reservable_id: id)
if reservations.size.zero?
destroy
else
false
end
def destroyable?
Reservation.where(reservable_type: 'Event', reservable_id: id).count.zero?
end
def soft_destroy!
update(deleted_at: DateTime.current)
end
##

View File

@ -28,7 +28,7 @@ class Profile < ApplicationRecord
blacklist = %w[id user_id created_at updated_at]
# model-relationships must be added manually
additional = [%w[avatar string], %w[address string], %w[organization_name string], %w[organization_address string],
%w[gender boolean], %w[birthday date]]
%w[gender boolean], %w[birthday date], %w[external_id string]]
Profile.columns_hash
.map { |k, v| [k, v.type.to_s] }
.delete_if { |col| blacklist.include?(col[0]) }

View File

@ -6,12 +6,16 @@ class EventPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.nil? || (user && !user.admin? && !user.manager?)
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes, :event_themes)
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes,
:event_themes)
.where('availabilities.start_at >= ?', DateTime.current)
.where(deleted_at: nil)
.order('availabilities.start_at ASC')
.references(:availabilities)
else
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes, :event_themes)
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes,
:event_themes)
.where(deleted_at: nil)
.references(:availabilities)
end
end

View File

@ -62,9 +62,9 @@ class Accounting::AccountingExportService
when 'date'
row << line.date&.strftime(date_format)
when 'account_code'
row << line.account_code
row << line.account_code.to_s
when 'account_label'
row << line.account_label
row << line.account_label.to_s
when 'piece'
row << line.invoice.reference
when 'line_label'

View File

@ -70,8 +70,9 @@ class EventService
end
events.each do |e|
# we use double negation because safe_destroy can return either a boolean (false) or an Availability (in case of delete success)
results.push status: !!e.safe_destroy, event: e # rubocop:disable Style/DoubleNegation
method = e.destroyable? ? :destroy : :soft_destroy!
# we use double negation because destroy can return either a boolean (false) or an Event (in case of delete success)
results.push status: !!e.send(method), event: e # rubocop:disable Style/DoubleNegation
end
results
end

View File

@ -40,6 +40,11 @@ class UserSetterService
@user.statistic_profile.birthday = data
end
def assign_external_id(data)
@user.invoicing_profile ||= InvoicingProfile.new
@user.invoicing_profile.external_id = data
end
def assign_profile_attribute(attribute, data)
@user.profile[attribute[8..].to_sym] = data
end
@ -65,6 +70,8 @@ class UserSetterService
assign_gender(data)
when 'profile.birthday'
assign_birthday(data)
when 'profile.external_id'
assign_external_id(data)
else
assign_profile_attribute(attribute, data)
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'version'
Sentry.init do |config|
config.excluded_exceptions += ['Pundit::NotAuthorizedError']
@ -20,6 +22,11 @@ Sentry.init do |config|
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
config.traces_sample_rate = 0.01
config.traces_sample_rate = 0.1
config.environment = Rails.env
config.release = Version.current
end
Sentry.configure_scope do |scope|
scope.set_tags(instance: ENV.fetch('DEFAULT_HOST'))
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Allow soft destroy of events.
# Events with existing reservation cannot be destroyed because we need them for rebuilding invoices, statistics, etc.
# This attribute allows to make a "soft destroy" of an Event, marking it as destroyed so it doesn't appear anymore in
# the interface (as if it was destroyed) but still lives in the database so we can use it to build data.
class AddDeletedAtToEvent < ActiveRecord::Migration[5.2]
def change
add_column :events, :deleted_at, :datetime
add_index :events, :deleted_at
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_01_06_081943) do
ActiveRecord::Schema.define(version: 2023_01_19_143245) do
# These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch"
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
enable_extension "unaccent"
create_table "abuses", id: :serial, force: :cascade do |t|
t.integer "signaled_id"
t.string "signaled_type"
t.integer "signaled_id"
t.string "first_name"
t.string "last_name"
t.string "email"
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
t.string "locality"
t.string "country"
t.string "postal_code"
t.integer "placeable_id"
t.string "placeable_type"
t.integer "placeable_id"
t.datetime "created_at"
t.datetime "updated_at"
end
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
end
create_table "assets", id: :serial, force: :cascade do |t|
t.integer "viewable_id"
t.string "viewable_type"
t.integer "viewable_id"
t.string "attachment"
t.string "type"
t.datetime "created_at"
@ -176,8 +176,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
end
create_table "credits", id: :serial, force: :cascade do |t|
t.integer "creditable_id"
t.string "creditable_type"
t.integer "creditable_id"
t.integer "plan_id"
t.integer "hours"
t.datetime "created_at"
@ -226,8 +226,10 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
t.integer "recurrence_id"
t.integer "age_range_id"
t.integer "category_id"
t.datetime "deleted_at"
t.index ["availability_id"], name: "index_events_on_availability_id"
t.index ["category_id"], name: "index_events_on_category_id"
t.index ["deleted_at"], name: "index_events_on_deleted_at"
t.index ["recurrence_id"], name: "index_events_on_recurrence_id"
end
@ -417,15 +419,15 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
create_table "notifications", id: :serial, force: :cascade do |t|
t.integer "receiver_id"
t.integer "attached_object_id"
t.string "attached_object_type"
t.integer "attached_object_id"
t.integer "notification_type_id"
t.boolean "is_read", default: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "receiver_type"
t.boolean "is_send", default: false
t.jsonb "meta_data", default: {}
t.jsonb "meta_data", default: "{}"
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
end
@ -665,8 +667,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
create_table "prices", id: :serial, force: :cascade do |t|
t.integer "group_id"
t.integer "plan_id"
t.integer "priceable_id"
t.string "priceable_type"
t.integer "priceable_id"
t.integer "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -867,8 +869,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "reservable_id"
t.string "reservable_type"
t.integer "reservable_id"
t.integer "nb_reserve_places"
t.integer "statistic_profile_id"
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
@ -877,8 +879,8 @@ ActiveRecord::Schema.define(version: 2023_01_06_081943) do
create_table "roles", id: :serial, force: :cascade do |t|
t.string "name"
t.integer "resource_id"
t.string "resource_type"
t.integer "resource_id"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"

View File

@ -177,7 +177,7 @@ namespace :fablab do
desc '[release 4.4.3] fix duration of recurring availabilities'
task availabilities_duration: :environment do
Availability.select(:occurrence_id).where(is_recurrent: true).group(:occurrence_id).each do |a|
Availability.select('occurrence_id').where(is_recurrent: true).group('occurrence_id').each do |a|
occurrences = Availability.where(occurrence_id: a.occurrence_id)
next unless occurrences.map(&:slot_duration).uniq.size > 1
@ -231,7 +231,7 @@ namespace :fablab do
statistic_profile_ids = StatisticProfilePrepaidPack.all.map(&:statistic_profile_id).uniq
# find the reservations that use prepaid pack by machine_ids, members and preriod
reservations = Reservation.where(reservable_type: 'Machine', reservable_id: machine_ids, statistic_profile_id: statistic_profile_ids,
created_at: start_date..end_date).order(statistic_profile_id: 'ASC', created_at: 'ASC')
created_at: start_date..end_date).order(statistic_profile_id: :asc, created_at: :asc)
infos = []
reservations.each do |reservation|
# find pack by pack's created_at before reservation's create_at and pack's expries_at before start_date
@ -243,7 +243,7 @@ namespace :fablab do
.where(statistic_profile_id: reservation.statistic_profile_id)
.where('statistic_profile_prepaid_packs.created_at <= ?', reservation.created_at)
.where('expires_at is NULL or expires_at > ?', start_date)
.order(created_at: 'ASC')
.order(created_at: :asc)
# passe reservation if cannot find any pack
next if packs.empty?
@ -279,5 +279,15 @@ namespace :fablab do
puts i
end
end
desc '[release 5.6.6] fix invoice items in error'
task invoice_items_in_error: :environment do
next if InvoiceItem.where(object_type: 'Error').count.zero?
InvoiceItem.where(object_type: 'Error').update_all(object_id: 0) # rubocop:disable Rails/SkipsModelValidations
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
Rake::Task['fablab:chain:invoices_items'].invoke
end
end
end

View File

@ -36,13 +36,13 @@ namespace :fablab do
puts "Operator: #{invoice.operator_profile&.user&.profile&.full_name} (#{invoice.operator_profile&.user&.email})"
puts "Date: #{invoice.created_at}"
puts '=============================================='
puts "Concerned item: #{ii.id}"
puts "Concerned item: #{ii.id} (#{ii.object_type} #{ii.object_id})"
puts "Item subject: #{ii.description}."
other_items.find_each do |oii|
puts '=============================================='
puts "Other item: #{oii.description} (#{oii.id})"
puts "Other item object: #{oii.object_type} #{oii.object_id}"
puts "Other item slots: #{oii.object.try(:slots)&.map { |s| "#{s.start_at} - #{s.end_at}" }}"
puts "Other item slots: #{oii.object.try(:slots)&.map { |s| "#{s.start_at} - #{s.end_at}" }}" if oii.object_type == 'Reservation'
print "\e[1;34m[ ? ]\e[0m Associate the item with #{oii.object_type} #{oii.object_id} ? (y/N) > "
confirm = $stdin.gets.chomp
if confirm == 'y'

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "5.6.5",
"version": "5.6.6",
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
"keywords": [
"fablab",

View File

@ -0,0 +1,36 @@
import FormatLib from 'lib/format';
import { IFablab } from 'models/fablab';
declare const Fablab: IFablab;
describe('FormatLib', () => {
test('format a date', () => {
Fablab.intl_locale = 'fr-FR';
const str = FormatLib.date(new Date('2023-01-12T12:00:00+0100'));
expect(str).toBe('12/01/2023');
});
test('format an iso8601 short date', () => {
Fablab.intl_locale = 'fr-FR';
const str = FormatLib.date('2023-01-12');
expect(str).toBe('12/01/2023');
});
test('format an iso8601 date', () => {
Fablab.intl_locale = 'fr-CA';
const str = FormatLib.date('2023-01-12T23:59:14-0500');
expect(str).toBe('2023-01-12');
});
test('format a time', () => {
Fablab.intl_locale = 'fr-FR';
const str = FormatLib.time(new Date('2023-01-12T23:59:14+0100'));
expect(str).toBe('23:59');
});
test('format an iso8601 short time', () => {
Fablab.intl_locale = 'fr-FR';
const str = FormatLib.time('23:59');
expect(str).toBe('23:59');
});
test('format an iso8601 time', () => {
Fablab.intl_locale = 'fr-CA';
const str = FormatLib.time('2023-01-12T23:59:14-0500');
expect(str).toBe('23 h 59');
});
});

View File

@ -2,7 +2,7 @@
"references": [
{ "path": "../../" }
],
"include": ["components/**/*", "__fixtures__/**/*", "__lib__/**/*"],
"include": ["components/**/*", "lib/**/*", "__fixtures__/**/*", "__lib__/**/*"],
"compilerOptions": {
"jsx": "react-jsx",
"target": "ES2015",

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
# Provides methods to help authentication providers
module AuthProviderHelper
def github_provider_params(name)
{
name: name,
providable_type: 'OAuth2Provider',
providable_attributes: {
authorization_endpoint: 'authorize',
token_endpoint: 'access_token',
base_url: 'https://github.com/login/oauth/',
profile_url: 'https://github.com/settings/profile',
client_id: ENV.fetch('OAUTH_CLIENT_ID', 'github-oauth-app-id'),
client_secret: ENV.fetch('OAUTH_CLIENT_SECRET', 'github-oauth-app-secret')
},
auth_provider_mappings_attributes: [
{
api_data_type: 'json',
api_endpoint: 'https://api.github.com/user',
api_field: 'id',
local_field: 'uid',
local_model: 'user'
},
{
api_data_type: 'json',
api_endpoint: 'https://api.github.com/user',
api_field: 'html_url',
local_field: 'github',
local_model: 'profile'
}
]
}
end
end

View File

@ -1,45 +1,22 @@
# frozen_string_literal: true
require 'test_helper'
require 'helpers/auth_provider_helper'
class AuthProvidersTest < ActionDispatch::IntegrationTest
include AuthProviderHelper
def setup
@admin = User.find_by(username: 'admin')
login_as(@admin, scope: :user)
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
end
test 'create an auth external provider and activate it' do
name = 'GitHub'
post '/api/auth_providers',
params: {
auth_provider: {
name: name,
providable_type: 'OAuth2Provider',
providable_attributes: {
authorization_endpoint: 'authorize',
token_endpoint: 'access_token',
base_url: 'https://github.com/login/oauth/',
profile_url: 'https://github.com/settings/profile',
client_id: ENV.fetch('OAUTH_CLIENT_ID', 'github-oauth-app-id'),
client_secret: ENV.fetch('OAUTH_CLIENT_SECRET', 'github-oauth-app-secret')
},
auth_provider_mappings_attributes: [
{
api_data_type: 'json',
api_endpoint: 'https://api.github.com/user',
api_field: 'id',
local_field: 'uid',
local_model: 'user'
},
{
api_data_type: 'json',
api_endpoint: 'https://api.github.com/user',
api_field: 'html_url',
local_field: 'github',
local_model: 'profile'
}
]
}
auth_provider: github_provider_params(name)
}.to_json,
headers: default_headers
@ -58,8 +35,7 @@ class AuthProvidersTest < ActionDispatch::IntegrationTest
assert_equal 2, provider[:auth_provider_mappings_attributes].length
# now let's activate this new provider
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
Rake::Task['fablab:auth:switch_provider'].invoke(name)
Rake::Task['fablab:auth:switch_provider'].execute(Rake::TaskArguments.new([:provider], [name]))
db_provider&.reload
assert_equal 'active', db_provider&.status
@ -68,4 +44,111 @@ class AuthProvidersTest < ActionDispatch::IntegrationTest
assert_not_nil u.auth_token
end
end
test 'update an authentication provider' do
provider = AuthProvider.create!(github_provider_params('GitHub'))
patch "/api/auth_providers/#{provider.id}",
params: {
auth_provider: {
providable_type: 'OAuth2Provider',
auth_provider_mappings_attributes: [
{ api_data_type: 'json', api_endpoint: 'https://api.github.com/user',
api_field: 'avatar_url', local_field: 'avatar', local_model: 'profile' }
]
}
}.to_json,
headers: default_headers
# Check response format & status
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
provider.reload
# Check the provider was updated
res = json_response(response.body)
assert_equal provider.id, res[:id]
assert_equal 3, provider.auth_provider_mappings.count
assert_not_nil provider.auth_provider_mappings.find_by(api_field: 'avatar_url')
end
test 'build an oauth2 strategy name' do
get '/api/auth_providers/strategy_name?providable_type=OAuth2Provider&name=Sleede'
assert_response :success
assert_equal 'oauth2-sleede', response.body
end
test 'build an openid strategy name' do
get '/api/auth_providers/strategy_name?providable_type=OpenIdConnectProvider&name=Sleede'
assert_response :success
assert_equal 'openidconnect-sleede', response.body
end
test 'show an authentication provider' do
provider = AuthProvider.first
get "/api/auth_providers/#{provider.id}"
# Check response format & status
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the provider was updated
res = json_response(response.body)
assert_equal provider.id, res[:id]
assert_equal provider.providable_type, res[:providable_type]
end
test 'show fields available for mapping' do
get '/api/auth_providers/mapping_fields'
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the returned fields
res = json_response(response.body)
assert_not_empty res[:user]
assert_not_empty res[:profile]
assert_not res[:user].map(&:first).include?('encrypted_password')
assert(res[:user].map(&:last).all? { |type| %w[string boolean integer datetime].include?(type) })
end
test 'get the current active provider' do
get '/api/auth_providers/active'
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the returned fields
res = json_response(response.body)
assert_equal AuthProvider.active.id, res[:id]
assert_nil res[:previous_provider]
end
test 'send auth migration token' do
# create an enable an oauth2 provider
name = 'TokenTest'
AuthProvider.create!(github_provider_params(name))
Rake::Task['fablab:auth:switch_provider'].execute(Rake::TaskArguments.new([:provider], [name]))
# send the migration token
user = User.find(10)
post '/api/auth_providers/send_code',
params: {
email: user.email
}.to_json,
headers: default_headers
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
# check resulting notification
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_user_auth_migration'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'User',
attached_object_id: user.id
)
assert_not_nil notification, 'user notification was not created'
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'test_helper'
module Events; end
class Events::DeleteTest < ActionDispatch::IntegrationTest
setup do
admin = User.with_role(:admin).first
login_as(admin, scope: :user)
end
test 'delete an event' do
event = Event.first
delete "/api/events/#{event.id}?mode=single", headers: default_headers
# Check response format & status
assert_response :success
assert_equal Mime[:json], response.content_type
res = json_response(response.body)
assert_equal 1, res[:deleted]
# Check the event was correctly deleted
assert_raise ActiveRecord::RecordNotFound do
event.reload
end
end
test 'soft delete an event' do
event = Event.first
# Make a reservation on this event
post '/api/local_payment/confirm_payment',
params: {
customer_id: User.find_by(username: 'pdurand').id,
items: [
{
reservation: {
reservable_id: event.id,
reservable_type: 'Event',
nb_reserve_places: 2,
slots_reservations_attributes: [
{
slot_id: event.availability.slots.first&.id,
offered: false
}
]
}
}
]
}.to_json,
headers: default_headers
# Check response format & status
assert_equal 201, response.status, response.body
assert_not event.destroyable?
delete "/api/events/#{event.id}?mode=single", headers: default_headers
assert_response :success
res = json_response(response.body)
assert_equal 1, res[:deleted]
event.reload
assert_not_nil event.deleted_at
end
end

View File

@ -69,9 +69,13 @@ class MachinesTest < ActionDispatch::IntegrationTest
end
test 'delete a machine' do
delete '/api/machines/3', headers: default_headers
machine = Machine.find(3)
delete "/api/machines/#{machine.id}", headers: default_headers
assert_response :success
assert_empty response.body
assert_raise ActiveRecord::RecordNotFound do
machine.reload
end
end
test 'soft delete a machine' do