From 4accedbb4364509171dcda9b32e953d586720d65 Mon Sep 17 00:00:00 2001 From: Jacob Cody Wimer Date: Wed, 7 May 2025 16:01:48 -0400 Subject: [PATCH] Added a daily recurring job to cleanup tournaments. Fixed final score fields not loading without page refresh on mat stats page and added a cypress test for it. --- .DS_Store | Bin 8196 -> 8196 bytes .cursorrules | 4 +- app/jobs/tournament_cleanup_job.rb | 36 ++++ .../_matchstats_variable_score_input.html.erb | 95 +++++----- bin/rails-dev-run.sh | 2 + config/recurring.yml | 21 +-- cypress-tests/.DS_Store | Bin 6148 -> 6148 bytes cypress-tests/cypress/.DS_Store | Bin 6148 -> 6148 bytes .../cypress/e2e/02-create_tournament.cy.js | 8 +- .../e2e/04-matstats_functionality.cy.js | 162 +++++++++++++----- .../e2e/05-matstats_realtime_updates.cy.js | 3 + db/seeds.rb | 13 +- deploy/deploy-test.sh | 2 +- deploy/docker-compose-test.yml | 7 + test/fixtures/tournaments.yml | 11 ++ test/jobs/tournament_cleanup_job_test.rb | 83 +++++++++ 16 files changed, 343 insertions(+), 104 deletions(-) create mode 100644 app/jobs/tournament_cleanup_job.rb create mode 100644 test/jobs/tournament_cleanup_job_test.rb diff --git a/.DS_Store b/.DS_Store index eb2b89368b17e78be12e6294eac336761614c748..6d2c6989ee5df14d7d0efced02aa68db5beecd5e 100644 GIT binary patch delta 74 zcmZp1XmQveDag2EvXo$1J3;-IG7ApV% delta 102 zcmZp1XmQveDag2OvXo$D%gG_CtZy9@pPiGNm*2qv j28@glnt>Ne!>G>59|Yt!e-IRB+srEQgJtu75q4$(xe^vM diff --git a/.cursorrules b/.cursorrules index 8769c3c..7780223 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,4 +1,6 @@ - If rails isn't installed use docker: docker run -it -v $(pwd):/rails wrestlingdev-dev - If the docker image doesn't exist, use the build command: docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile . - If the Gemfile changes, you need to rebuild the docker image: docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile. -- Do not add unnecessary comments to the code where you remove things. \ No newline at end of file +- Do not add unnecessary comments to the code where you remove things. +- Cypress tests are created for js tests. They can be found in cypress-tests/cypress +- Cypress tests can be run with docker: bash cypress-tests/run-cypress-tests.sh \ No newline at end of file diff --git a/app/jobs/tournament_cleanup_job.rb b/app/jobs/tournament_cleanup_job.rb new file mode 100644 index 0000000..769650e --- /dev/null +++ b/app/jobs/tournament_cleanup_job.rb @@ -0,0 +1,36 @@ +class TournamentCleanupJob < ApplicationJob + queue_as :default + + def perform + # Remove or clean up tournaments based on age and match status + process_old_tournaments + end + + private + + def process_old_tournaments + # Get all tournaments older than 1 week that have a user_id + old_tournaments = Tournament.where('date < ? AND user_id IS NOT NULL', 1.week.ago.to_date) + + old_tournaments.each do |tournament| + # Check if it has any non-BYE finished matches + has_real_matches = tournament.matches.where(finished: 1).where.not(win_type: 'BYE').exists? + + if has_real_matches + + # 1. Remove all school delegates + tournament.schools.each do |school| + school.delegates.destroy_all + end + + # 2. Remove all tournament delegates + tournament.delegates.destroy_all + + # 3. Set user_id to null + tournament.update(user_id: nil) + else + tournament.destroy + end + end + end +end \ No newline at end of file diff --git a/app/views/matches/_matchstats_variable_score_input.html.erb b/app/views/matches/_matchstats_variable_score_input.html.erb index 513f68d..3d1b171 100644 --- a/app/views/matches/_matchstats_variable_score_input.html.erb +++ b/app/views/matches/_matchstats_variable_score_input.html.erb @@ -1,6 +1,6 @@ diff --git a/bin/rails-dev-run.sh b/bin/rails-dev-run.sh index 98109be..2eb0b78 100755 --- a/bin/rails-dev-run.sh +++ b/bin/rails-dev-run.sh @@ -17,4 +17,6 @@ docker build -t $1 -f ${project_dir}/deploy/rails-dev-Dockerfile \ docker run --rm -it -p 3000:3000 \ -v ${project_dir}:/rails \ + -v /etc/localtime:/etc/localtime:ro \ + -v /etc/timezone:/etc/timezone:ro \ $1 /bin/bash \ No newline at end of file diff --git a/config/recurring.yml b/config/recurring.yml index d045b19..3a6845a 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -1,10 +1,11 @@ -# production: -# periodic_cleanup: -# class: CleanSoftDeletedRecordsJob -# queue: background -# args: [ 1000, { batch_size: 500 } ] -# schedule: every hour -# periodic_command: -# command: "SoftDeletedRecord.due.delete_all" -# priority: 2 -# schedule: at 5am every day +production: + tournament_cleanup_job: + class: TournamentCleanupJob + schedule: every day at 3am + queue: default + +development: + tournament_cleanup_job: + class: TournamentCleanupJob + schedule: every day at 3am + queue: default \ No newline at end of file diff --git a/cypress-tests/.DS_Store b/cypress-tests/.DS_Store index 0d8f35b0ef452408338581411bcfbe02e7506670..4c90eb8aa7a3bd17694fe68103e0ac5254d8bda9 100644 GIT binary patch delta 69 zcmZoMXffCj&cwK5as*SMxD%gG_CtZy9@pPiGNm)|k@ U1Cu;s=Vk%s?<|{HIsWql0GsI)SpWb4 diff --git a/cypress-tests/cypress/.DS_Store b/cypress-tests/cypress/.DS_Store index 5d587a8c370a777b72916728a3e6f275f5a437b5..1aed8cb41f5c61517480ce233e897ac118c72736 100644 GIT binary patch delta 47 zcmZoMXffE(%fh&0@&ndFZi(t@Qv(AX1!Dt)$x&?5j9rt<*yI_zH@mPNWZKNm@s}R} DZY>VE delta 21 dcmZoMXffE(%QE>P>#oh7toxWYvvU0B2LNF22y6fV diff --git a/cypress-tests/cypress/e2e/02-create_tournament.cy.js b/cypress-tests/cypress/e2e/02-create_tournament.cy.js index 15dc4de..eb33501 100644 --- a/cypress-tests/cypress/e2e/02-create_tournament.cy.js +++ b/cypress-tests/cypress/e2e/02-create_tournament.cy.js @@ -27,7 +27,13 @@ describe('Create a tournament', () => { cy.get('input[name="tournament[address]"]').type('123 Wrestling Way'); cy.get('input[name="tournament[director]"]').type('John Doe'); cy.get('input[name="tournament[director_email]"]').type('john.doe@example.com'); - cy.get('input[name="tournament[date]"]').type('2024-12-31'); + + // Set date to 1 month from today dynamically + const futureDate = new Date(); + futureDate.setMonth(futureDate.getMonth() + 1); + const formattedDate = futureDate.toISOString().split('T')[0]; // Format as YYYY-MM-DD + cy.get('input[name="tournament[date]"]').type(formattedDate); + cy.get('select[name="tournament[tournament_type]"]').select('Pool to bracket'); // cy.get('input[name="tournament[is_public]"]').check(); diff --git a/cypress-tests/cypress/e2e/04-matstats_functionality.cy.js b/cypress-tests/cypress/e2e/04-matstats_functionality.cy.js index 08cd459..8e13510 100644 --- a/cypress-tests/cypress/e2e/04-matstats_functionality.cy.js +++ b/cypress-tests/cypress/e2e/04-matstats_functionality.cy.js @@ -148,55 +148,125 @@ describe('Matstats Page Functionality', () => { // Only attempt this test if the match_win_type element exists cy.get('body').then(($body) => { if ($body.find('#match_win_type').length) { - // Select win type as Decision + // 1. Test Decision win type with no winner selected cy.get('#match_win_type').select('Decision'); - // Check if there are input fields visible in the form - const hasScoreInputs = $body.find('input[type="number"]').length > 0 || - $body.find('input[type="text"]').length > 0; + // Wait for dynamic fields to update + cy.wait(300); - if (hasScoreInputs) { - // Try to find score inputs using a more generic approach - cy.get('input[type="number"], input[type="text"]').then($inputs => { - if ($inputs.length >= 2) { - // Use the first two inputs for winner and loser scores - cy.wrap($inputs).first().as('winnerScore'); - cy.wrap($inputs).eq(1).as('loserScore'); - - // Try invalid input (loser score > winner score) - cy.get('@winnerScore').clear().type('2'); - cy.get('@loserScore').clear().type('5'); - - // Should show validation error - cy.get('#validation-alerts').should('be.visible'); - - // Update to valid scores for Decision - cy.get('@winnerScore').clear().type('5'); - cy.get('@loserScore').clear().type('2'); - - // Error should be gone after valid input - cy.get('#validation-alerts').should('not.exist').should('not.be.visible'); - - // Test Major validation (score difference 8+) - cy.get('@winnerScore').clear().type('10'); - cy.get('@loserScore').clear().type('2'); - - // Should show type validation error for needing Major - cy.get('#validation-alerts').should('be.visible'); - - // Change to Major - cy.get('#match_win_type').select('Major'); - - // Error should be gone after changing win type - cy.wait(500); // Give validation time to update - cy.get('#validation-alerts').should('not.exist').should('not.be.visible'); - } else { - cy.log('Found fewer than 2 score input fields - test conditionally passed'); - } - }); - } else { - cy.log('No score input fields found - test conditionally passed'); - } + // Verify correct form fields appear + cy.get('#winner-score').should('exist'); + cy.get('#loser-score').should('exist'); + + // Enter valid scores for Decision + cy.get('#winner-score').clear().type('5'); + cy.get('#loser-score').clear().type('2'); + + // Without a winner, form should show validation error + cy.get('#validation-alerts').should('be.visible') + .and('contain.text', 'Please select a winner'); + cy.get('#update-match-btn').should('be.disabled'); + + // 2. Test invalid score scenario (loser score > winner score) + cy.get('#winner-score').clear().type('2'); + cy.get('#loser-score').clear().type('5'); + + cy.get('#validation-alerts').should('be.visible') + .and('contain.text', 'Winner\'s score must be higher than loser\'s score'); + cy.get('#update-match-btn').should('be.disabled'); + + // 3. Fix scores and select a winner + cy.get('#winner-score').clear().type('5'); + cy.get('#loser-score').clear().type('2'); + cy.get('#match_winner_id').select(1); + + // Now validation should pass for Decision + cy.get('#validation-alerts').should('not.be.visible'); + cy.get('#update-match-btn').should('not.be.disabled'); + + // 4. Test Major score range validation + cy.get('#winner-score').clear().type('10'); + cy.get('#loser-score').clear().type('2'); + // Score difference is 8, should require Major + cy.get('#validation-alerts').should('be.visible') + .and('contain.text', 'Win type should be'); + cy.get('#update-match-btn').should('be.disabled'); + + // 5. Fix by changing win type to Major + cy.get('#match_win_type').select('Major'); + cy.wait(300); + + // Validation should now pass + cy.get('#validation-alerts').should('not.be.visible'); + cy.get('#update-match-btn').should('not.be.disabled'); + + // 6. Test Tech Fall score range validation + cy.get('#winner-score').clear().type('17'); + cy.get('#loser-score').clear().type('2'); + // Score difference is 15, should require Tech Fall + cy.get('#validation-alerts').should('be.visible') + .and('contain.text', 'Win type should be'); + cy.get('#update-match-btn').should('be.disabled'); + + // 7. Fix by changing win type to Tech Fall + cy.get('#match_win_type').select('Tech Fall'); + cy.wait(300); + + // Validation should now pass + cy.get('#validation-alerts').should('not.be.visible'); + cy.get('#update-match-btn').should('not.be.disabled'); + + // 8. Test Pin win type form + cy.get('#match_win_type').select('Pin'); + cy.wait(300); + + // Should show pin time inputs + cy.get('#minutes').should('exist'); + cy.get('#seconds').should('exist'); + cy.get('#pin-time-tip').should('be.visible'); + + // Winner still required + cy.get('#validation-alerts').should('not.be.visible'); // Previous winner selection should still be valid + + // 9. Test other win types (no score input) + cy.get('#match_win_type').select('Forfeit'); + cy.wait(300); + + // Should not show score inputs + cy.get('#dynamic-score-input').should('be.empty'); + + // Winner still required + cy.get('#validation-alerts').should('not.be.visible'); // Previous winner selection should still be valid + } else { + cy.log('Match form not present - test conditionally passed'); + } + }); + }); + + it('should display final score fields without requiring page refresh', () => { + // Check if we're on a mat page with match form + cy.get('body').then(($body) => { + if ($body.find('#match_win_type').length) { + // Test Decision type first + cy.get('#match_win_type').select('Decision'); + cy.wait(300); + cy.get('#dynamic-score-input').should('exist'); + cy.get('#winner-score').should('exist'); + cy.get('#loser-score').should('exist'); + + // Test Pin type + cy.get('#match_win_type').select('Pin'); + cy.wait(300); + cy.get('#minutes').should('exist'); + cy.get('#seconds').should('exist'); + cy.get('#pin-time-tip').should('be.visible'); + + // Test other types + cy.get('#match_win_type').select('Forfeit'); + cy.wait(300); + cy.get('#dynamic-score-input').should('be.empty'); + + cy.log('Final score fields load correctly without page refresh'); } else { cy.log('Match form not present - test conditionally passed'); } diff --git a/cypress-tests/cypress/e2e/05-matstats_realtime_updates.cy.js b/cypress-tests/cypress/e2e/05-matstats_realtime_updates.cy.js index a281371..0d16ca4 100644 --- a/cypress-tests/cypress/e2e/05-matstats_realtime_updates.cy.js +++ b/cypress-tests/cypress/e2e/05-matstats_realtime_updates.cy.js @@ -161,6 +161,9 @@ describe('Matstats Real-time Updates', () => { cy.get('#cable-status-indicator', { timeout: 10000 }) .should('contain.text', 'Connected'); + // Select a winner + cy.get('#match_winner_id').select(1); + // Check if match form and inputs still exist after reload cy.get('body').then(($reloadedBody) => { if ($reloadedBody.find('#match_win_type').length && diff --git a/db/seeds.rb b/db/seeds.rb index fadf44c..3cb76de 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -30,8 +30,11 @@ User.create(id: 1, email: 'test@test.com', password: 'password', password_confirmation: 'password') + # Set tournament date to a month from today + future_date = 1.month.from_now.to_date + # Pool to bracket - tournament = Tournament.create(id: 200, name: 'Pool to bracket', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Pool to bracket', user_id: 1, date: Date.today, is_public: true) + tournament = Tournament.create(id: 200, name: 'Pool to bracket', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Pool to bracket', user_id: 1, date: future_date, is_public: true) create_schools(tournament, 24) weight_classes=Weight::HS_WEIGHT_CLASSES.split(",") tournament.create_pre_defined_weights(weight_classes) @@ -58,7 +61,7 @@ end # Modified 16 Man Double Elimination 1-6 - tournament = Tournament.create(id: 201, name: 'Modified 16 Man Double Elimination 1-6', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Modified 16 Man Double Elimination 1-6', user_id: 1, date: Date.today, is_public: true) + tournament = Tournament.create(id: 201, name: 'Modified 16 Man Double Elimination 1-6', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Modified 16 Man Double Elimination 1-6', user_id: 1, date: future_date, is_public: true) create_schools(tournament, 16) weight_classes=Weight::HS_WEIGHT_CLASSES.split(",") tournament.create_pre_defined_weights(weight_classes) @@ -69,7 +72,7 @@ end # Modified 16 Man Double Elimination 1-8 - tournament = Tournament.create(id: 202, name: 'Modified 16 Man Double Elimination 1-8', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Modified 16 Man Double Elimination 1-8', user_id: 1, date: Date.today, is_public: true) + tournament = Tournament.create(id: 202, name: 'Modified 16 Man Double Elimination 1-8', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Modified 16 Man Double Elimination 1-8', user_id: 1, date: future_date, is_public: true) create_schools(tournament, 16) weight_classes=Weight::HS_WEIGHT_CLASSES.split(",") tournament.create_pre_defined_weights(weight_classes) @@ -86,7 +89,7 @@ end # Regular Double Elimination 1-6 - tournament = Tournament.create(id: 203, name: 'Regular Double Elimination 1-6', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Regular Double Elimination 1-6', user_id: 1, date: Date.today, is_public: true) + tournament = Tournament.create(id: 203, name: 'Regular Double Elimination 1-6', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Regular Double Elimination 1-6', user_id: 1, date: future_date, is_public: true) create_schools(tournament, 32) weight_classes=Weight::HS_WEIGHT_CLASSES.split(",") tournament.create_pre_defined_weights(weight_classes) @@ -109,7 +112,7 @@ end # Regular Double Elimination 1-8 - tournament = Tournament.create(id: 204, name: 'Regular Double Elimination 1-8', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Regular Double Elimination 1-8', user_id: 1, date: Date.today, is_public: true) + tournament = Tournament.create(id: 204, name: 'Regular Double Elimination 1-8', address: 'some place', director: 'some guy', director_email: 'their@email.com', tournament_type: 'Regular Double Elimination 1-8', user_id: 1, date: future_date, is_public: true) create_schools(tournament, 32) weight_classes=Weight::HS_WEIGHT_CLASSES.split(",") tournament.create_pre_defined_weights(weight_classes) diff --git a/deploy/deploy-test.sh b/deploy/deploy-test.sh index c00dbe6..fd6529d 100755 --- a/deploy/deploy-test.sh +++ b/deploy/deploy-test.sh @@ -31,7 +31,7 @@ docker-compose -f ${project_dir}/deploy/docker-compose-test.yml up -d # DISABLE_DATABASE_ENVIRONMENT_CHECK=1 is needed because this is "destructive" action on production echo Resetting the db with seed data -docker-compose -f ${project_dir}/deploy/docker-compose-test.yml exec -T app bash -c "DISABLE_DATABASE_ENVIRONMENT_CHECK=1 rake db:reset" +docker-compose -f ${project_dir}/deploy/docker-compose-test.yml run --rm app bash -c "DISABLE_DATABASE_ENVIRONMENT_CHECK=1 rake db:reset" # echo Simulating tournament 204 # docker-compose -f ${project_dir}/deploy/docker-compose-test.yml exec -T app rails tournament:assign_random_wins diff --git a/deploy/docker-compose-test.yml b/deploy/docker-compose-test.yml index d806380..04b21b8 100644 --- a/deploy/docker-compose-test.yml +++ b/deploy/docker-compose-test.yml @@ -36,6 +36,9 @@ services: - "443:443" healthcheck: test: curl http://127.0.0.1/ + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro db: image: mariadb:10.10 @@ -43,6 +46,8 @@ services: - "3306:3306" volumes: - mysql:/var/lib/mysql + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro environment: - MYSQL_ROOT_PASSWORD=password restart: always @@ -65,3 +70,5 @@ services: metrics: volumes: - influxdb:/var/lib/influxdb + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro diff --git a/test/fixtures/tournaments.yml b/test/fixtures/tournaments.yml index 2287e6d..9f1175a 100644 --- a/test/fixtures/tournaments.yml +++ b/test/fixtures/tournaments.yml @@ -9,4 +9,15 @@ one: tournament_type: Pool to bracket user_id: 1 date: 2015-12-30 + is_public: true + +two: + id: 2 + name: Test Tournament 2 + address: Some Place + director: Jacob Cody Wimer + director_email: jacob.wimer@gmail.com + tournament_type: Pool to bracket + user_id: 1 + date: 2015-12-30 is_public: true \ No newline at end of file diff --git a/test/jobs/tournament_cleanup_job_test.rb b/test/jobs/tournament_cleanup_job_test.rb new file mode 100644 index 0000000..1f6d370 --- /dev/null +++ b/test/jobs/tournament_cleanup_job_test.rb @@ -0,0 +1,83 @@ +require 'test_helper' + +class TournamentCleanupJobTest < ActiveJob::TestCase + setup do + # Create an old empty tournament (1 week old, 0 finished matches) + @old_empty_tournament = tournaments(:one) + @old_empty_tournament.update(date: 2.weeks.ago.to_date) + + # Create an old active tournament (1 week old, with finished matches) + @old_active_tournament = tournaments(:two) + @old_active_tournament.update(date: 2.weeks.ago.to_date) + + # Add a finished match to the active tournament using create instead of fixtures + weight = Weight.create(max: 120, tournament: @old_active_tournament) + wrestler = Wrestler.create(name: "Test Wrestler", weight: weight, school: schools(:one)) + + @match = Match.create( + tournament: @old_active_tournament, + weight: weight, + bracket_position: "Pool", + round: 1, + finished: 1, + win_type: "Decision", + score: "10-5", + w1: wrestler.id, + winner_id: wrestler.id + ) + + # Add delegates to test removal + tournament_delegate = TournamentDelegate.create(tournament: @old_active_tournament, user: users(:one)) + school = schools(:one) + school.update(tournament_id: @old_active_tournament.id) + school_delegate = SchoolDelegate.create(school: school, user: users(:one)) + end + + test "removes old empty tournaments" do + # In this test, only the empty tournament should be deleted + @match.update!(win_type: "Decision") # Ensure this tournament has a non-BYE match + + assert_difference 'Tournament.count', -1 do + TournamentCleanupJob.perform_now + end + + assert_raises(ActiveRecord::RecordNotFound) { @old_empty_tournament.reload } + assert_nothing_raised { @old_active_tournament.reload } + end + + test "removes old empty tournaments with only a bye finished match" do + # Update the win_type to BYE and score to empty as required by validation + @match.update!(win_type: "BYE", score: "") + assert_equal "BYE", @match.reload.win_type + assert_equal "", @match.score + + # Both tournaments should be deleted (the empty one and the one with only BYE matches) + assert_difference 'Tournament.count', -2 do + TournamentCleanupJob.perform_now + end + + assert_raises(ActiveRecord::RecordNotFound) { @old_empty_tournament.reload } + assert_raises(ActiveRecord::RecordNotFound) { @old_active_tournament.reload } + end + + test "cleans up old active tournaments" do + # Ensure this tournament has a non-BYE match + @match.update!(win_type: "Decision") + + TournamentCleanupJob.perform_now + + # Tournament should still exist + @old_active_tournament.reload + + # User association should be removed + assert_nil @old_active_tournament.user_id + + # Tournament delegates should be removed + assert_equal 0, @old_active_tournament.delegates.count + + # School delegates should be removed + @old_active_tournament.schools.each do |school| + assert_equal 0, school.delegates.count + end + end +end \ No newline at end of file