diff --git a/Gemfile b/Gemfile index f3137b7..0cf4a1b 100644 --- a/Gemfile +++ b/Gemfile @@ -51,6 +51,7 @@ group :development, :test do gem "rubocop-rails-omakase", require: false gem "rspec-rails", "~> 8.0.0" + gem "factory_bot_rails" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 3e09b30..901d9aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,6 +113,11 @@ GEM erubi (1.13.1) et-orbi (1.2.11) tzinfo + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -435,6 +440,7 @@ DEPENDENCIES capybara cssbundling-rails (~> 1.4) debug + factory_bot_rails haml importmap-rails jbuilder diff --git a/spec/factories/moods.rb b/spec/factories/moods.rb new file mode 100644 index 0000000..e75d860 --- /dev/null +++ b/spec/factories/moods.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :mood do + mode { "croisiere" } + recorded_at { DateTime.now } + association :user + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 0000000..26f4019 --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :user do + sequence(:email_address) { |n| "user#{n}@example.com" } + password_digest { BCrypt::Password.create('password123') } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 79811b8..b972cd2 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -9,21 +9,23 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ # return unless Rails.env.test? require 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! +require 'factory_bot_rails' -# Requires supporting ruby files with custom matchers and macros, etc, in -# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are -# run as spec files by default. This means that files in spec/support that end -# in _spec.rb will both be required and run as specs, causing the specs to be -# run twice. It is recommended that you do not name files matching this glob to -# end with _spec.rb. You can configure this pattern with the --pattern -# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -# -# The following line is provided for convenience purposes. It has the downside -# of increasing the boot-up time by auto-requiring all files in the support -# directory. Alternatively, in the individual `*_spec.rb` files, manually -# require only the support files necessary. -# -# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + # Requires supporting ruby files with custom matchers and macros, etc, in + # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are + # run as spec files by default. This means that files in spec/support that end + # in _spec.rb will both be required and run as specs, causing the specs to be + # run twice. It is recommended that you do not name files matching this glob to + # end with _spec.rb. You can configure this pattern with the --pattern + # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. + # + # The following line is provided for convenience purposes. It has the downside + # of increasing the boot-up time by auto-requiring all files in the support + # directory. Alternatively, in the individual `*_spec.rb` files, manually + # require only the support files necessary. + # + # Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } # Ensures that the test database schema matches the current schema file. # If there are pending migrations it will invoke `db:test:prepare` to diff --git a/spec/services/mood_calendar_service_spec.rb b/spec/services/mood_calendar_service_spec.rb new file mode 100644 index 0000000..5cc4a55 --- /dev/null +++ b/spec/services/mood_calendar_service_spec.rb @@ -0,0 +1,422 @@ +# spec/services/mood_calendar_service_spec.rb +require 'rails_helper' + +RSpec.describe MoodCalendarService do + describe '.generate_calendar' do + context 'avec un recorded_at pour chaque jour' do + it 'génère un calendrier complet sans jours nil' do + # Données du 1er au 7 février 2025 (une semaine complète) + (1..7).each do |day| + create(:mood, mode: "mood_#{day}", recorded_at: Time.zone.parse("2025-02-0#{day} 10:00:00")) + end + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-02-28") + ) + + expect(result.length).to eq(1) # Un seul mois + expect(result[0][:month]).to eq(Date.parse("2025-02-01")) + + # Première semaine commence le lundi 3 février + first_week = result[0][:weeks][0] + expect(first_week.length).to eq(7) # Du lundi 3 au dimanche 9 + + # Vérifier que tous les jours du 3 au 7 ont un mode + (3..7).each do |day| + mood = first_week.find { |m| m[:recorded_at].day == day } + expect(mood[:mode]).to eq("mood_#{day}") + expect(mood).not_to have_key(:guess) + end + + # Le 8 et 9 février devraient avoir mode: nil avec guess: "mood_7" + mood_8 = first_week.find { |m| m[:recorded_at].day == 8 } + expect(mood_8[:mode]).to be_nil + expect(mood_8[:guess]).to eq("mood_7") + + mood_9 = first_week.find { |m| m[:recorded_at].day == 9 } + expect(mood_9[:mode]).to be_nil + expect(mood_9[:guess]).to eq("mood_7") + end + end + + context 'avec des jours manquants' do + it 'remplit les jours manquants avec mode: nil et guess égal au dernier mode' do + create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00")) + create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00")) + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-02-18 16:45:00")) + + moods = Mood.all + result = MoodCalendarService.generate_calendar( + moods, + end_date: Date.parse("2025-02-28") + ) + + expect(result.length).to eq(1) + expect(result[0][:month]).to eq(Date.parse("2025-02-01")) + + # Trouver tous les jours + all_days = result[0][:weeks].flatten + + # Le 12 février devrait avoir le dernier mood (content à 14:20) + mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") } + expect(mood_12[:mode]).to eq("content") + expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i) + + # Le 13 février devrait être nil avec guess: "content" + mood_13 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-13") } + expect(mood_13[:mode]).to be_nil + expect(mood_13[:guess]).to eq("content") + + # Le 14 février devrait être nil avec guess: "content" + mood_14 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-14") } + expect(mood_14[:mode]).to be_nil + expect(mood_14[:guess]).to eq("content") + + # Le 15 février devrait avoir le mood "heureux" + mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") } + expect(mood_15[:mode]).to eq("heureux") + + # Le 16 février devrait être nil avec guess: "heureux" + mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") } + expect(mood_16[:mode]).to be_nil + expect(mood_16[:guess]).to eq("heureux") + + # Les jours avant le 12 (à partir du premier lundi 3 février) devraient avoir guess: nil + mood_3 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-03") } + expect(mood_3[:mode]).to be_nil + expect(mood_3[:guess]).to be_nil + end + + it 'gère correctement plusieurs mois avec des jours manquants' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + create(:mood, mode: "joyeux", recorded_at: Time.zone.parse("2025-04-20 11:30:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-04-30") + ) + + # Devrait avoir exactement février, mars et avril + expect(result.length).to eq(3) + + # Vérifier février + february = result.find { |m| m[:month] == Date.parse("2025-02-01") } + expect(february).not_to be_nil + + # Vérifier mars (tous les jours devraient avoir mode: nil et guess: "heureux") + march = result.find { |m| m[:month] == Date.parse("2025-03-01") } + expect(march).not_to be_nil + all_march_days = march[:weeks].flatten + + # Tous les jours de mars (y compris ceux qui débordent début avril) devraient avoir mode: nil + expect(all_march_days.all? { |d| d[:mode].nil? }).to be true + + # Tous les jours de mars devraient avoir guess: "heureux" + # (le mois de mars va du 3 mars au 6 avril, avant le mood du 20 avril) + all_march_days.each do |day| + expect(day[:guess]).to eq("heureux"), + "Le jour #{day[:recorded_at].to_date} devrait avoir guess: 'heureux' mais a: #{day[:guess].inspect}" + end + + # Vérifier avril + april = result.find { |m| m[:month] == Date.parse("2025-04-01") } + expect(april).not_to be_nil + all_april_days = april[:weeks].flatten + mood_20_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-20") } + expect(mood_20_april[:mode]).to eq("joyeux") + + # Les jours du 7 au 19 avril devraient avoir guess: "heureux" + mood_10_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-10") } + expect(mood_10_april[:guess]).to eq("heureux") + + # Les jours après le 20 avril devraient avoir guess: "joyeux" + mood_21_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-21") } + expect(mood_21_april[:guess]).to eq("joyeux") + end + end + + context 'sans aucun recorded_at' do + it 'retourne un tableau vide' do + result = MoodCalendarService.generate_calendar(Mood.none) + + expect(result).to eq([]) + end + end + + context 'avec plusieurs moods le même jour' do + it 'garde uniquement le mood le plus récent' do + create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00")) + create(:mood, mode: "neutre", recorded_at: Time.zone.parse("2025-02-12 12:00:00")) + create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00")) + create(:mood, mode: "fatigué", recorded_at: Time.zone.parse("2025-02-12 09:15:00")) + + moods = Mood.all + result = MoodCalendarService.generate_calendar( + moods, + end_date: Date.parse("2025-02-28") + ) + + all_days = result[0][:weeks].flatten + mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") } + + expect(mood_12[:mode]).to eq("content") + expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i) + end + end + + context 'structure du calendrier' do + it 'commence chaque mois par le premier lundi' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-02-28") + ) + + february = result[0] + first_day = february[:weeks][0][0] + + # Le premier jour de février 2025 devrait être le lundi 3 février + expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03")) + expect(first_day[:recorded_at].to_date.monday?).to be true + end + + it 'va du premier lundi du mois au dimanche avant le premier lundi du mois suivant' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-03-31") + ) + + february = result[0] + all_days = february[:weeks].flatten + + # Premier jour : lundi 3 février 2025 + first_day = all_days.first + expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03")) + expect(first_day[:recorded_at].to_date.monday?).to be true + + # Dernier jour : dimanche 2 mars 2025 (veille du premier lundi de mars qui est le 3) + last_day = all_days.last + expect(last_day[:recorded_at].to_date).to eq(Date.parse("2025-03-02")) + expect(last_day[:recorded_at].to_date.sunday?).to be true + + # Le 3 mars (premier lundi de mars) ne devrait PAS être dans février + expect(all_days.any? { |d| d[:recorded_at].to_date == Date.parse("2025-03-03") }).to be false + end + + it 'toutes les semaines ont exactement 7 jours' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-02-28") + ) + + february = result[0] + + # Toutes les semaines devraient avoir exactement 7 jours + february[:weeks].each do |week| + expect(week.length).to eq(7) + end + end + + it 'retourne month comme Date (le premier du mois)' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-02-28") + ) + + february = result[0] + + # month devrait être une Date + expect(february[:month]).to be_a(Date) + expect(february[:month]).to eq(Date.parse("2025-02-01")) + expect(february[:month].day).to eq(1) # Premier du mois + end + end + + context 'avec paramètres par défaut' do + it 'utilise le premier mood comme start_date et aujourd\'hui comme end_date' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar(Mood.all) + + expect(result).not_to be_empty + + # Devrait commencer en février + expect(result.first[:month]).to eq(Date.parse("2025-02-01")) + end + end + + context 'avec start_date spécifié' do + it 'commence à la date spécifiée' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + start_date: Date.parse("2025-03-01"), + end_date: Date.parse("2025-03-31") + ) + + # Devrait commencer en mars même si le mood est en février + expect(result.first[:month]).to eq(Date.parse("2025-03-01")) + + # Ne devrait pas contenir février + expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be false + + # Tous les jours de mars devraient avoir guess: "heureux" (du 15 février) + all_march_days = result.first[:weeks].flatten + puts all_march_days.inspect + expect(all_march_days.all? { |d| d[:guess] == "heureux" }).to be true + end + end + + context 'avec end_date spécifié' do + it 'se termine à la date spécifiée' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-02-28") + ) + + # Devrait s'arrêter en février + expect(result.last[:month]).to eq(Date.parse("2025-02-01")) + + # Ne devrait pas contenir mars ou au-delà + expect(result.any? { |m| m[:month] == Date.parse("2025-03-01") }).to be false + end + end + + context 'avec start_date et end_date spécifiés' do + it 'génère le calendrier pour la plage spécifiée' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-04-20 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + start_date: Date.parse("2025-03-01"), + end_date: Date.parse("2025-03-31") + ) + + # Devrait contenir uniquement mars + expect(result.length).to eq(1) + expect(result.first[:month]).to eq(Date.parse("2025-03-01")) + + # Tous les jours de mars devraient avoir guess: "heureux" (du 15 février) + all_days = result.first[:weeks].flatten + expect(all_days.all? { |d| d[:mode].nil? }).to be true + expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true + end + + it 'gère une plage de plusieurs mois' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + start_date: Date.parse("2025-02-01"), + end_date: Date.parse("2025-04-30") + ) + + # Devrait contenir février, mars et avril + months = result.map { |m| m[:month] } + expect(months).to include( + Date.parse("2025-02-01"), + Date.parse("2025-03-01"), + Date.parse("2025-04-01") + ) + end + + it 'fonctionne avec DateTime en paramètres' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + # Passer des DateTime au lieu de Date + result = MoodCalendarService.generate_calendar( + Mood.all, + start_date: Time.zone.parse("2025-02-01 00:00:00"), + end_date: Time.zone.parse("2025-02-28 23:59:59") + ) + + expect(result).not_to be_empty + expect(result.first[:month]).to eq(Date.parse("2025-02-01")) + end + end + + context 'avec une plage ne contenant aucun mood' do + it 'génère un calendrier avec tous les jours à nil et guess du dernier mood avant la plage' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + start_date: Date.parse("2025-04-01"), + end_date: Date.parse("2025-04-30") + ) + + # Devrait contenir avril + expect(result.first[:month]).to eq(Date.parse("2025-04-01")) + + # Tous les jours devraient avoir mode: nil et guess: "heureux" + all_days = result.first[:weeks].flatten + expect(all_days.all? { |d| d[:mode].nil? }).to be true + expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true + end + end + + context 'avec plusieurs mois' do + it 'chaque mois commence et finit correctement' do + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00")) + create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-03-20 10:00:00")) + + result = MoodCalendarService.generate_calendar( + Mood.all, + end_date: Date.parse("2025-03-31") + ) + + february = result.find { |m| m[:month] == Date.parse("2025-02-01") } + march = result.find { |m| m[:month] == Date.parse("2025-03-01") } + + # Février se termine le dimanche 2 mars + last_day_february = february[:weeks].flatten.last + expect(last_day_february[:recorded_at].to_date).to eq(Date.parse("2025-03-02")) + + # Mars commence le lundi 3 mars + first_day_march = march[:weeks].flatten.first + expect(first_day_march[:recorded_at].to_date).to eq(Date.parse("2025-03-03")) + + # Pas de chevauchement + expect(last_day_february[:recorded_at].to_date + 1.day).to eq(first_day_march[:recorded_at].to_date) + end + end + + context 'avec un scope spécifique' do + it 'fonctionne avec des moods filtrés par user' do + user1 = create(:user) + user2 = create(:user) + + create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"), user: user1) + create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-16 10:00:00"), user: user2) + + moods = Mood.where(user: user1) + result = MoodCalendarService.generate_calendar( + moods, + end_date: Date.parse("2025-02-28") + ) + + all_days = result[0][:weeks].flatten + mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") } + expect(mood_15[:mode]).to eq("heureux") + + # Le mood du user2 ne devrait pas apparaître + mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") } + expect(mood_16[:mode]).to be_nil + expect(mood_16[:guess]).to eq("heureux") + end + end + end +end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000..c7890e4 --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end