develop #3

Merged
kbe merged 227 commits from develop into main 2025-09-16 14:35:23 +00:00
13 changed files with 287 additions and 29 deletions
Showing only changes of commit 4cde466f9a - Show all commits

View File

@@ -75,6 +75,7 @@ group :test do
gem "rails-controller-testing" gem "rails-controller-testing"
# For mocking and stubbing # For mocking and stubbing
gem "mocha" gem "mocha"
gem "timecop"
end end
gem "devise", "~> 4.9" gem "devise", "~> 4.9"

View File

@@ -381,6 +381,7 @@ GEM
thruster (0.1.15-aarch64-linux) thruster (0.1.15-aarch64-linux)
thruster (0.1.15-x86_64-darwin) thruster (0.1.15-x86_64-darwin)
thruster (0.1.15-x86_64-linux) thruster (0.1.15-x86_64-linux)
timecop (0.9.10)
timeout (0.4.3) timeout (0.4.3)
ttfunk (1.8.0) ttfunk (1.8.0)
bigdecimal (~> 3.1) bigdecimal (~> 3.1)
@@ -452,6 +453,7 @@ DEPENDENCIES
stimulus-rails stimulus-rails
stripe (~> 15.5) stripe (~> 15.5)
thruster thruster
timecop
turbo-rails turbo-rails
tzinfo-data tzinfo-data
web-console web-console

View File

@@ -1,5 +1,5 @@
# Contrôleur API pour la gestion des ressources d'événements # API Controller for managing event resources
# Fournit des points de terminaison RESTful pour les opérations CRUD sur le modèle Event # Provides RESTful endpoints for CRUD operations on the Event model
module Api module Api
module V1 module V1
@@ -7,27 +7,27 @@ module Api
# Skip API key authentication for store_cart action (used by frontend forms) # Skip API key authentication for store_cart action (used by frontend forms)
skip_before_action :authenticate_api_key, only: [ :store_cart ] skip_before_action :authenticate_api_key, only: [ :store_cart ]
# Charge l'évén avant certaines actions pour réduire les duplications # Loads the event before certain actions to reduce duplications
before_action :set_event, only: [ :show, :update, :destroy, :store_cart ] before_action :set_event, only: [ :show, :update, :destroy, :store_cart ]
# GET /api/v1/events # GET /api/v1/events
# Récupère tous les événements triés par date de création (du plus récent au plus ancien) # Retrieves all events sorted by creation date (most recent first)
def index def index
@events = Event.all.order(created_at: :desc) @events = Event.all.order(created_at: :desc)
render json: @events, status: :ok render json: @events, status: :ok
end end
# GET /api/v1/events/:id # GET /api/v1/events/:id
# Récupère un seul événement par son ID # Retrieves a single event by its ID
# Retourne 404 si l'événement n'est pas trouvé # Returns 404 if the event is not found
def show def show
render json: @event, status: :ok render json: @event, status: :ok
end end
# POST /api/v1/events # POST /api/v1/events
# Crée un nouvel événement avec les attributs fournis # Creates a new event with the provided attributes
# Retourne 201 Created en cas de succès avec les données de l'événement # Returns 201 Created on success with the event data
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec # Returns 422 Unprocessable Entity with error messages on failure
def create def create
@event = Event.new(event_params) @event = Event.new(event_params)
if @event.save if @event.save
@@ -38,9 +38,9 @@ module Api
end end
# PATCH/PUT /api/v1/events/:id # PATCH/PUT /api/v1/events/:id
# Met à jour un événement existant avec les attributs fournis # Updates an existing event with the provided attributes
# Retourne 200 OK avec les données mises à jour en cas de succès # Returns 200 OK with updated data on success
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec # Returns 422 Unprocessable Entity with error messages on failure
def update def update
if @event.update(event_params) if @event.update(event_params)
render json: @event, status: :ok render json: @event, status: :ok
@@ -50,8 +50,8 @@ module Api
end end
# DELETE /api/v1/events/:id # DELETE /api/v1/events/:id
# Supprime définitivement un événement # Permanently deletes an event
# Retourne 204 No Content en cas de succès # Returns 204 No Content on success
def destroy def destroy
@event.destroy @event.destroy
head :no_content head :no_content
@@ -66,33 +66,37 @@ module Api
render json: { status: "success", message: "Cart stored successfully" } render json: { status: "success", message: "Cart stored successfully" }
rescue => e rescue => e
error_message = e.message.present? ? e.message : "Erreur inconnue" error_message = e.message.present? ? e.message : "Unknown error"
Rails.logger.error "Error storing cart: #{error_message}" Rails.logger.error "Error storing cart: #{error_message}"
render json: { status: "error", message: "Failed to store cart" }, status: 500 render json: { status: "error", message: "Failed to store cart" }, status: 500
end end
private private
# Trouve un événement par son ID ou retourne 404 Introuvable # Finds an event by its ID or returns 404 Not Found
# Utilisé comme before_action pour les actions show, update et destroy # Used as before_action for the show, update, and destroy actions
def set_event def set_event
@event = Event.find(params[:id]) @event = Event.find(params[:id])
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render json: { error: "Événement non trouvé" }, status: :not_found render json: { error: "Event not found" }, status: :not_found
end end
# Paramètres forts pour la création et la mise à jour des événements # Strong parameters for creating and updating events
# Liste blanche des attributs autorisés pour éviter les vulnérabilités de mass assignment # Whitelist of allowed attributes to avoid mass assignment vulnerabilities
def event_params def event_params
params.require(:event).permit( params.require(:event).permit(
:name, :name,
:slug,
:description, :description,
:state, :state,
:venue_name, :venue_name,
:venue_address, :venue_address,
:start_time,
:end_time,
:latitude, :latitude,
:longitude, :longitude,
:featured :featured,
:user_id
) )
end end
end end

View File

@@ -38,8 +38,6 @@ class ApplicationController < ActionController::Base
# Skip for API endpoints # Skip for API endpoints
controller_name.start_with?("api/") || controller_name.start_with?("api/") ||
# Skip for health checks # Skip for health checks
controller_name == "rails/health" || controller_name == "rails/health"
# Skip for home page (when not signed in)
(controller_name == "pages" && action_name == "home")
end end
end end

View File

@@ -107,10 +107,7 @@
</div> </div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Aucun événement disponible</h3> <h3 class="text-2xl font-bold text-gray-900 mb-4">Aucun événement disponible</h3>
<p class="text-gray-600 mb-8 max-w-md mx-auto">Il n'y a aucun événement à venir pour le moment. Revenez bientôt pour découvrir de nouvelles sorties!</p> <p class="text-gray-600 mb-8 max-w-md mx-auto">Il n'y a aucun événement à venir pour le moment. Revenez bientôt pour découvrir de nouvelles sorties!</p>
<%= link_to "Retour à l'accueil", root_path, class: "inline-flex items-center bg-purple-600 text-white px-6 py-3 rounded-full font-semibold hover:bg-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %> <%= link_to "<i data-lucide=\"home\" class=\"w-4 h-4 mr-2\"></i> Retour à l'accueil".html_safe, root_path, class: "inline-flex items-center bg-purple-600 text-white px-6 py-3 rounded-full font-semibold hover:bg-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" %>
<i data-lucide="home" class="w-4 h-4 mr-2"></i>
Retour à l'accueil
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@@ -28,6 +28,7 @@ development:
test: test:
<<: *default <<: *default
database: aperonight_test database: aperonight_test
isolation_level: READ UNCOMMITTED
# adapter: sqlite3 # adapter: sqlite3
# pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> # pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
# database: data/test.sqlite3 # database: data/test.sqlite3

View File

@@ -0,0 +1,58 @@
require "test_helper"
class Api::V1::EventsControllerTest < ActionDispatch::IntegrationTest
setup do
ENV["API_KEY"] = "test_key"
@user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
@event = Event.create!(name: "Test Event", slug: "test-event", description: "A description that is long enough for validation", latitude: 48.8566, longitude: 2.3522, venue_name: "Venue", venue_address: "Address", user: @user, start_time: 1.week.from_now, end_time: 1.week.from_now + 3.hours, state: :published)
end
test "should get index" do
get api_v1_events_url, headers: headers_api_key
assert_response :success
assert_kind_of Array, json_response
end
test "should show event" do
get api_v1_event_url(@event.id), headers: headers_api_key
assert_response :success
assert_equal @event.id, json_response["id"]
end
test "should create event" do
assert_difference("Event.count") do
post api_v1_events_url, params: { event: { name: "New Event", slug: "new-event", description: "New description that is long enough", latitude: 48.8566, longitude: 2.3522, venue_name: "New Venue", venue_address: "New Address", user_id: @user.id, start_time: "2024-01-01 10:00:00", end_time: "2024-01-01 13:00:00", state: "published" } }, as: :json, headers: headers_api_key
end
assert_response :created
end
test "should update event" do
patch api_v1_event_url(@event.id), params: { event: { name: "Updated Event" } }, as: :json, headers: headers_api_key
assert_response :ok
@event.reload
assert_equal "Updated Event", @event.name
end
test "should destroy event" do
assert_difference("Event.count", -1) do
delete api_v1_event_url(@event.id), headers: headers_api_key
end
assert_response :no_content
end
test "should store cart" do
post store_cart_api_v1_event_url(@event), params: { cart: { ticket_type_id: 1, quantity: 2 } }, as: :json
assert_response :success
assert_equal @event.id, session[:event_id]
end
private
def json_response
JSON.parse(response.body)
end
def headers_api_key
{ "X-API-Key" => "test_key" }
end
end

View File

@@ -41,7 +41,7 @@ class ApplicationControllerOnboardingTest < ActionDispatch::IntegrationTest
test "should redirect signed in incomplete users from home to onboarding" do test "should redirect signed in incomplete users from home to onboarding" do
sign_in @user_without_onboarding sign_in @user_without_onboarding
get root_path get root_path
assert_redirected_to dashboard_path # Home redirects to dashboard for signed in users assert_redirected_to onboarding_path
end end
test "should not interfere with devise controllers" do test "should not interfere with devise controllers" do

View File

@@ -0,0 +1,33 @@
require "test_helper"
class StripeConcernTest < ActionDispatch::IntegrationTest
setup do
Rails.application.config.stripe = { secret_key: nil }
end
test "stripe_configured? returns false when no secret key" do
controller = ApplicationController.new
controller.extend StripeConcern
assert_not controller.stripe_configured?
end
test "stripe_configured? returns true when secret key present" do
Rails.application.config.stripe = { secret_key: "sk_test_key" }
controller = ApplicationController.new
controller.extend StripeConcern
assert controller.stripe_configured?
end
test "initialize_stripe returns false when not configured" do
controller = ApplicationController.new
controller.extend StripeConcern
assert_not controller.initialize_stripe
end
test "initialize_stripe returns true when configured" do
Rails.application.config.stripe = { secret_key: "sk_test_key" }
controller = ApplicationController.new
controller.extend StripeConcern
assert controller.initialize_stripe
end
end

View File

@@ -0,0 +1,30 @@
require "test_helper"
class Promoter::EventsControllerTest < ActionDispatch::IntegrationTest
setup do
@promoter = User.create!(email: "promoter@example.com", password: "password123", password_confirmation: "password123", is_professionnal: true, onboarding_completed: true)
@event = Event.create!(name: "Test Event", slug: "test-event", description: "A valid description for the test event that is long enough to meet the minimum character requirement", latitude: 48.8566, longitude: 2.3522, venue_name: "Venue", venue_address: "Address", user: @promoter, start_time: 1.week.from_now, end_time: 1.week.from_now + 3.hours, state: :draft)
end
test "should require authentication for index" do
get promoter_events_path
assert_redirected_to new_user_session_path
end
test "should get index for authenticated promoter" do
sign_in @promoter
get promoter_events_path
assert_response :success
end
test "should show promoter's events only" do
sign_in @promoter
other_event = Event.create!(name: "Other Event", slug: "other", description: "Valid description for the event", latitude: 48.0, longitude: 2.0, venue_name: "V", venue_address: "A", user_id: users(:one).id, start_time: 1.day.from_now, end_time: 2.days.from_now, state: :draft)
get promoter_events_path
assert_response :success
assert_includes assigns(:events), @event
assert_not_includes assigns(:events), other_event
end
# Add tests for new, create, etc. as needed
end

View File

@@ -0,0 +1,22 @@
require "test_helper"
class Promoter::TicketTypesControllerTest < ActionDispatch::IntegrationTest
setup do
@promoter = User.create!(email: "promoter@example.com", password: "password123", password_confirmation: "password123", is_professionnal: true, onboarding_completed: true)
@event = Event.create!(name: "Test Event", slug: "test-event", description: "A valid description for the test event that is long enough to meet the minimum character requirement", latitude: 48.8566, longitude: 2.3522, venue_name: "Venue", venue_address: "Address", user: @promoter, start_time: 1.week.from_now, end_time: 1.week.from_now + 3.hours, state: :draft)
@ticket_type = TicketType.create!(name: "General", description: "General admission", price_cents: 2500, quantity: 100, sale_start_at: Time.current, sale_end_at: @event.start_time, event: @event)
end
test "should require authentication for index" do
get promoter_event_ticket_types_path(@event)
assert_redirected_to new_user_session_path
end
test "should get index for promoter's event" do
sign_in @promoter
get promoter_event_ticket_types_path(@event)
assert_response :success
end
# Add more tests for create, update, destroy
end

View File

@@ -1,4 +1,5 @@
require "test_helper" require "test_helper"
require "timecop"
class EventTest < ActiveSupport::TestCase class EventTest < ActiveSupport::TestCase
# Test that Event model exists # Test that Event model exists
@@ -160,4 +161,114 @@ class EventTest < ActiveSupport::TestCase
test "should respond to search_by_name scope" do test "should respond to search_by_name scope" do
assert_respond_to Event, :search_by_name assert_respond_to Event, :search_by_name
end end
test "upcoming scope should return only published future events" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
future_published = Event.create!(name: "Future", slug: "future", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.day.from_now, state: :published)
past_published = Event.create!(name: "Past", slug: "past", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.day.ago, state: :published)
future_draft = Event.create!(name: "Draft", slug: "draft", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.day.from_now, state: :draft)
upcoming = Event.upcoming
assert_includes upcoming, future_published
assert_not_includes upcoming, past_published
assert_not_includes upcoming, future_draft
end
test "geocoding_successful? should return true for valid coordinates" do
event = Event.new(latitude: 48.8566, longitude: 2.3522, name: "Test", slug: "test", description: "A description that is sufficiently long", venue_name: "v", venue_address: "a")
assert event.geocoding_successful?
end
test "geocoding_successful? should return false for fallback coordinates" do
event = Event.new(latitude: 46.603354, longitude: 1.888334, name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a")
assert_not event.geocoding_successful?
end
test "geocoding_status_message should return message when not successful" do
event = Event.new(latitude: 46.603354, longitude: 1.888334, name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a")
assert_match(/coordonnées/, event.geocoding_status_message)
end
test "geocoding_status_message should return nil when successful" do
event = Event.new(latitude: 48.8566, longitude: 2.3522, name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a")
assert_nil event.geocoding_status_message
end
test "booking_allowed? should be true for published future event" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "A description that is sufficiently long", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.day.from_now, state: :published)
assert event.booking_allowed?
end
test "booking_allowed? should be false for draft event" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.day.from_now, state: :draft)
assert_not event.booking_allowed?
end
test "booking_allowed? should be false for canceled event" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.day.from_now, state: :canceled)
assert_not event.booking_allowed?
end
test "booking_allowed? should be false for sold_out event" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "A description that is sufficiently long", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.day.from_now, state: :sold_out)
assert_not event.booking_allowed?
end
test "booking_allowed? should be false during event without allow_booking_during_event" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.hour.ago, end_time: 2.hours.from_now, state: :published, allow_booking_during_event: false)
assert_not event.booking_allowed?
end
test "booking_allowed? should be true during event with allow_booking_during_event" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.hour.ago, end_time: 2.hours.from_now, state: :published, allow_booking_during_event: true)
assert event.booking_allowed?
end
test "event_started? should be true after start_time" do
Timecop.freeze(1.hour.from_now) do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "A description that is sufficiently long", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.hour.ago)
assert event.event_started?
end
end
test "event_started? should be false before start_time" do
Timecop.freeze(1.hour.ago) do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.hour.from_now)
assert_not event.event_started?
end
end
test "event_ended? should be true after end_time" do
Timecop.freeze(1.hour.from_now) do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "Valid description for the event", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.hour.ago, end_time: 30.minutes.ago)
assert event.event_ended?
end
end
test "event_ended? should be false before end_time" do
Timecop.freeze(1.hour.ago) do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Test", slug: "test", description: "A description that is sufficiently long", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.hour.ago, end_time: 1.hour.from_now)
assert_not event.event_ended?
end
end
test "allow_booking_during_event? should return true when set to true" do
event = Event.new(allow_booking_during_event: true)
assert event.allow_booking_during_event?
end
test "allow_booking_during_event? should return false when nil" do
event = Event.new
assert_not event.allow_booking_during_event?
end
end end

View File

@@ -12,6 +12,7 @@ module ActiveSupport
class TestCase class TestCase
# Run tests in parallel with specified workers # Run tests in parallel with specified workers
parallelize(workers: :number_of_processors) parallelize(workers: :number_of_processors)
use_transactional_fixtures = true
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all fixtures :all