From 49fbf6735dbc1fa6380dd78ce89bef6b57859b25 Mon Sep 17 00:00:00 2001 From: Jacob Cody Wimer Date: Sat, 4 Jan 2025 16:27:52 -0500 Subject: [PATCH] Added tournament backups to the database and added pages to restore and create backups --- .../tournament_backups_controller.rb | 78 +++++++ app/controllers/tournaments_controller.rb | 18 +- app/models/tournament.rb | 5 + app/models/tournament_backup.rb | 5 + .../tournament_backup_service.rb | 63 +++++ .../wrestlingdev_importer.rb | 37 ++- app/views/layouts/_tournament-navbar.html.erb | 1 + app/views/tournament_backups/index.html.erb | 38 +++ app/views/tournament_backups/show.html.erb | 1 + app/views/tournaments/_import_form.html.erb | 11 - app/views/tournaments/show.html.erb | 9 +- config/routes.rb | 7 +- ...0241224132705_create_tournament_backups.rb | 16 ++ db/schema.rb | 11 +- .../tournament_backups_controller_test.rb | 220 ++++++++++++++++++ test/fixtures/tournament_backups.yml | 11 + test/models/tournament_backup_test.rb | 7 + 17 files changed, 497 insertions(+), 41 deletions(-) create mode 100644 app/controllers/tournament_backups_controller.rb create mode 100644 app/models/tournament_backup.rb create mode 100644 app/services/tournament_services/tournament_backup_service.rb create mode 100644 app/views/tournament_backups/index.html.erb create mode 100644 app/views/tournament_backups/show.html.erb delete mode 100644 app/views/tournaments/_import_form.html.erb create mode 100644 db/migrate/20241224132705_create_tournament_backups.rb create mode 100644 test/controllers/tournament_backups_controller_test.rb create mode 100644 test/fixtures/tournament_backups.yml create mode 100644 test/models/tournament_backup_test.rb diff --git a/app/controllers/tournament_backups_controller.rb b/app/controllers/tournament_backups_controller.rb new file mode 100644 index 0000000..0a9992b --- /dev/null +++ b/app/controllers/tournament_backups_controller.rb @@ -0,0 +1,78 @@ +class TournamentBackupsController < ApplicationController + before_action :set_tournament + before_action :set_tournament_backup, only: [:show, :destroy, :restore] + before_action :check_access_manage + + # GET /tournament/:tournament_id/tournament_backups + def index + @tournament_backups = @tournament.tournament_backups.order(created_at: :desc) + end + + # GET /tournament/:tournament_id/tournament_backups/:id + def show + end + + # DELETE /tournament/:tournament_id/tournament_backups/:id + def destroy + if @tournament_backup.destroy + redirect_to tournament_tournament_backups_path(@tournament), notice: 'Backup was successfully deleted.' + else + redirect_to tournament_tournament_backups_path(@tournament), alert: 'Failed to delete the backup.' + end + end + + # POST /tournament/:tournament_id/tournament_backups/create + def create + TournamentBackupService.new(@tournament, 'Manual backup').create_backup + redirect_to tournament_tournament_backups_path(@tournament), notice: 'Backup was successfully created. It will show up soon, check your background jobs for status.' + end + + # POST /tournament/:tournament_id/tournament_backups/:id/restore + def restore + WrestlingdevImporter.new(@tournament, @tournament_backup).import + redirect_to tournament_path(@tournament), notice: 'Restore has successfully been submitted, please check your background jobs to see if it has finished.' + end + + # POST /tournament/:tournament_id/tournament_backups/import_manual + def import_manual + import_text = params[:tournament][:import_text] + if import_text.blank? + redirect_to tournament_tournament_backups_path(@tournament), alert: 'Import text cannot be blank.' + return + end + + begin + + # Create a temporary backup object + backup = TournamentBackup.new( + tournament: @tournament, + backup_data: Base64.encode64(import_text), + backup_reason: 'Manual Import' + ) + + # Pass the backup object to the importer + WrestlingdevImporter.new(@tournament, backup).import + + redirect_to tournament_path(@tournament), notice: 'Restore has successfully been submitted, please check your background jobs to see if it has finished.' + rescue JSON::ParserError => e + redirect_to tournament_tournament_backups_path(@tournament), alert: "Failed to parse JSON: #{e.message}" + rescue StandardError => e + redirect_to tournament_tournament_backups_path(@tournament), alert: "An error occurred: #{e.message}" + end + end + + private + + def set_tournament + @tournament = Tournament.find(params[:tournament_id]) + end + + def set_tournament_backup + @tournament_backup = @tournament.tournament_backups.find(params[:id]) + end + + def check_access_manage + authorize! :manage, @tournament + end + end + \ No newline at end of file diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index c07ec52..6332e53 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -1,6 +1,6 @@ class TournamentsController < ApplicationController - before_action :set_tournament, only: [:reset_bout_board,:calculate_team_scores, :import,:export,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:brackets,:generate_matches,:bracket,:all_brackets] - before_action :check_access_manage, only: [:reset_bout_board,:calculate_team_scores, :import,:export,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches] + before_action :set_tournament, only: [:reset_bout_board,:calculate_team_scores,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:brackets,:generate_matches,:bracket,:all_brackets] + before_action :check_access_manage, only: [:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches] before_action :check_access_destroy, only: [:destroy,:delegate,:remove_delegate] before_action :check_tournament_errors, only: [:generate_matches] before_action :check_for_matches, only: [:up_matches,:bracket,:all_brackets] @@ -10,10 +10,6 @@ class TournamentsController < ApplicationController end - def export - - end - def calculate_team_scores respond_to do |format| if @tournament.calculate_all_team_scores @@ -23,16 +19,6 @@ class TournamentsController < ApplicationController end end - def import - import_text = params[:tournament][:import_text] - respond_to do |format| - if WrestlingdevImporter.new(@tournament,import_text).import - format.html { redirect_to "/tournaments/#{@tournament.id}", notice: 'Import is on-going. This will take 1-5 minutes.' } - format.json { head :no_content } - end - end - end - def swap @wrestler = Wrestler.find(params[:wrestler][:originalId]) respond_to do |format| diff --git a/app/models/tournament.rb b/app/models/tournament.rb index 0c06115..119fcca 100644 --- a/app/models/tournament.rb +++ b/app/models/tournament.rb @@ -8,6 +8,7 @@ class Tournament < ApplicationRecord has_many :matches, dependent: :destroy has_many :delegates, class_name: "TournamentDelegate" has_many :mat_assignment_rules, dependent: :destroy + has_many :tournament_backups, dependent: :destroy validates :date, :name, :tournament_type, :address, :director, :director_email , presence: true @@ -227,5 +228,9 @@ class Tournament < ApplicationRecord end end end + + def create_backup() + TournamentBackupService.new(self, "Manual backup").create_backup + end end \ No newline at end of file diff --git a/app/models/tournament_backup.rb b/app/models/tournament_backup.rb new file mode 100644 index 0000000..72098a2 --- /dev/null +++ b/app/models/tournament_backup.rb @@ -0,0 +1,5 @@ +class TournamentBackup < ApplicationRecord + belongs_to :tournament + + validates :backup_data, presence: true +end \ No newline at end of file diff --git a/app/services/tournament_services/tournament_backup_service.rb b/app/services/tournament_services/tournament_backup_service.rb new file mode 100644 index 0000000..eb94fc9 --- /dev/null +++ b/app/services/tournament_services/tournament_backup_service.rb @@ -0,0 +1,63 @@ +class TournamentBackupService + def initialize(tournament, reason) + @tournament = tournament + @reason = reason + end + + def create_backup + if Rails.env.production? + self.delay(:job_owner_id => @tournament.id, :job_owner_type => "Create a backup").create_backup_raw + else + self.create_backup_raw + end + end + + def create_backup_raw + # Generate the JSON directly in Ruby and encode it + backup_data = Base64.encode64(generate_json.to_json) + + begin + # Save the backup with encoded data + TournamentBackup.create!(tournament: @tournament, backup_data: backup_data, backup_reason: @reason) + Rails.logger.info("Backup created successfully for tournament ##{@tournament.id}") + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error("Failed to save backup: #{e.message}") + end + end + + private + + def generate_json + { + tournament: { + attributes: @tournament.attributes, + schools: @tournament.schools.map(&:attributes), + weights: @tournament.weights.map(&:attributes), + mats: @tournament.mats.map(&:attributes), + mat_assignment_rules: @tournament.mat_assignment_rules.map do |rule| + rule.attributes.merge( + mat: Mat.find_by(id: rule.mat_id)&.attributes.slice("name"), + weight_classes: rule.weight_classes.map do |weight_id| + Weight.find_by(id: weight_id)&.max + end + ) + end, + wrestlers: @tournament.wrestlers.map do |wrestler| + wrestler.attributes.merge( + school: wrestler.school&.attributes, + weight: wrestler.weight&.attributes + ) + end, + matches: @tournament.matches.sort_by(&:bout_number).map do |match| + match.attributes.merge( + w1_name: Wrestler.find_by(id: match.w1)&.name, + w2_name: Wrestler.find_by(id: match.w2)&.name, + winner_name: Wrestler.find_by(id: match.winner_id)&.name, + weight: Weight.find_by(id: match.weight_id)&.attributes, + mat: Mat.find_by(id: match.mat_id)&.attributes + ) + end + } + } + end +end diff --git a/app/services/tournament_services/wrestlingdev_importer.rb b/app/services/tournament_services/wrestlingdev_importer.rb index e5d06cd..02d963b 100644 --- a/app/services/tournament_services/wrestlingdev_importer.rb +++ b/app/services/tournament_services/wrestlingdev_importer.rb @@ -1,11 +1,11 @@ class WrestlingdevImporter - ##### Note, the json contains id's for each row in the tables as well as it's associations + ##### Note, the json contains id's for each row in the tables as well as its associations ##### this ignores those ids and uses this tournament id and then looks up associations based on name ##### and this tournament id - def initialize(tournament, import_json) + def initialize(tournament, backup) @tournament = tournament - @import_data = JSON.parse(import_json) + @import_data = JSON.parse(Base64.decode64(backup.backup_data)) end def import @@ -26,6 +26,7 @@ class WrestlingdevImporter end def destroy_all + @tournament.mat_assignment_rules.destroy_all @tournament.mats.destroy_all @tournament.matches.destroy_all @tournament.schools.each do |school| @@ -42,6 +43,8 @@ class WrestlingdevImporter parse_mats(@import_data["tournament"]["mats"]) parse_wrestlers(@import_data["tournament"]["wrestlers"]) parse_matches(@import_data["tournament"]["matches"]) + puts "Parsing mat assignment rules" + parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"]) end def parse_tournament(attributes) @@ -70,6 +73,34 @@ class WrestlingdevImporter end end + def parse_mat_assignment_rules(mat_assignment_rules) + mat_assignment_rules.each do |rule_attributes| + mat_name = rule_attributes.dig("mat", "name") + mat = Mat.find_by(name: mat_name, tournament_id: @tournament.id) + + # Map max values of weight_classes to their new IDs + new_weight_classes = rule_attributes["weight_classes"].map do |max_value| + Weight.find_by(max: max_value, tournament_id: @tournament.id)&.id + end.compact + + # Extract bracket_positions and rounds + bracket_positions = rule_attributes["bracket_positions"] + rounds = rule_attributes["rounds"] + + rule_attributes.except!("id", "mat", "tournament_id", "weight_classes") + + MatAssignmentRule.create( + rule_attributes.merge( + tournament_id: @tournament.id, + mat_id: mat&.id, + weight_classes: new_weight_classes, + bracket_positions: bracket_positions, + rounds: rounds + ) + ) + end + end + def parse_wrestlers(wrestlers) wrestlers.each do |wrestler_attributes| school = School.find_by(name: wrestler_attributes["school"]["name"], tournament_id: @tournament.id) diff --git a/app/views/layouts/_tournament-navbar.html.erb b/app/views/layouts/_tournament-navbar.html.erb index 1d2a6d6..6c1abd6 100644 --- a/app/views/layouts/_tournament-navbar.html.erb +++ b/app/views/layouts/_tournament-navbar.html.erb @@ -33,6 +33,7 @@
  • <%= link_to "Full Screen Bout Board" , "/tournaments/#{@tournament.id}/up_matches?print=true" , target: :_blank %>
  • <%= link_to "Deduct Team Points" , "/tournaments/#{@tournament.id}/teampointadjust" %>
  • <%= link_to "View All Mat Assignment Rules", tournament_mat_assignment_rules_path(@tournament) %>
  • +
  • <%= link_to 'Manage Backups', tournament_tournament_backups_path(@tournament) %>
  • <%= link_to "Reset Bout Board", reset_bout_board_tournament_path(@tournament), method: :post, data: { confirm: "Are you sure you want to reset the bout board?" } %>
  • <% if can? :destroy, @tournament %>
  • <%= link_to "Tournament Delegation" , "/tournaments/#{@tournament.id}/delegate" %>
  • diff --git a/app/views/tournament_backups/index.html.erb b/app/views/tournament_backups/index.html.erb new file mode 100644 index 0000000..c3fb464 --- /dev/null +++ b/app/views/tournament_backups/index.html.erb @@ -0,0 +1,38 @@ +

    Tournament Backups

    +

    You can click on the timestamp to view the backup text. You can manually store this anywhere you'd like and then paste it into the manual import field below. +Doing this is risky, if the text is formatted incorrectly (like Microsoft Word changing the quotation marks), it will not import properly +and will also delete all of your current data. It's best to use the create backup function.

    +
    + + + + + + + + + + <% @tournament_backups.each do |backup| %> + + + + + + <% end %> + +
    Backup Created AtBackup Reason<%= link_to ' Create New Backup', tournament_tournament_backups_path(@tournament), method: :post, class: 'fas fa-plus'%>
    + <%= link_to backup.created_at.strftime('%Y-%m-%d %H:%M:%S'), tournament_tournament_backup_path(@tournament, backup, print: true), target: '_blank', class: 'text-decoration-none' %> + <%= backup.backup_reason.presence || 'No reason provided' %> + <%= link_to '', restore_tournament_tournament_backup_path(@tournament, backup), method: :post, data: { confirm: "This will restore the backup from #{backup.created_at.strftime('%Y-%m-%d %H:%M:%S')}. It will delete all current data from the tournament in order to restore the backup." }, class: 'fas fa-undo-alt text-warning', title: 'Restore Backup' %> + <%= link_to '', tournament_tournament_backup_path(@tournament, backup), method: :delete, data: { confirm: 'Are you sure you want to delete this backup?' }, class: 'fas fa-trash-alt', title: 'Delete Backup' %> +
    +

    +

    Import Manual Backup

    +

    Paste the backup text here. Note, if this is formatted wrong, you'll need to restore a backup from above to fix it and you'll see an error in your background jobs.

    +<%= form_for(:tournament, url: import_manual_tournament_tournament_backups_path(@tournament)) do |f| %> +
    + <%= f.label 'Import text' %>
    + <%= f.text_area :import_text, cols: "30", rows: "20" %> +
    + <%= submit_tag "Import", class: "btn btn-success", data: { confirm: 'Are you sure? This will delete everything for the current tournament and restore it with the backup text pasted below.' } %> +<% end %> diff --git a/app/views/tournament_backups/show.html.erb b/app/views/tournament_backups/show.html.erb new file mode 100644 index 0000000..4c491e1 --- /dev/null +++ b/app/views/tournament_backups/show.html.erb @@ -0,0 +1 @@ +<%= JSON.pretty_generate(JSON.parse(Base64.decode64(@tournament_backup.backup_data))) %> \ No newline at end of file diff --git a/app/views/tournaments/_import_form.html.erb b/app/views/tournaments/_import_form.html.erb deleted file mode 100644 index 954b8c6..0000000 --- a/app/views/tournaments/_import_form.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% if can? :manage, @tournament %> -

    -

    Import Data

    - <%= form_for(Tournament.new, url: import_url(@tournament)) do |f| %> -
    - <%= f.label 'Import text' %>
    - <%= f.text_area :import_text, cols: "30", rows: "20" %> -
    - <%= submit_tag "Import", :class=>"btn btn-success", data: { confirm: 'Are you sure? This will delete everything for the current tournament and restore it with the backup text pasted below.' }%> - <% end %> -<% end %> \ No newline at end of file diff --git a/app/views/tournaments/show.html.erb b/app/views/tournaments/show.html.erb index 0a742a3..3acaed0 100644 --- a/app/views/tournaments/show.html.erb +++ b/app/views/tournaments/show.html.erb @@ -119,7 +119,7 @@ <% end %> <% if can? :manage, @tournament %> -
    +

    Background Jobs

    This is a list of queued or running background jobs. Match generation, bracket advancement, team score calculation, etc.

    @@ -148,9 +148,4 @@
    <% end %> -
    -
    -<%= render 'import_form' %> - - - +

    diff --git a/config/routes.rb b/config/routes.rb index 91fc00c..a68edd0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,11 @@ Wrestling::Application.routes.draw do member do post :reset_bout_board end + resources :tournament_backups, only: [:index, :show, :destroy] do + post :create, on: :collection + post :restore, on: :member + post :import_manual, on: :collection + end end resources :schools @@ -57,8 +62,6 @@ Wrestling::Application.routes.draw do delete 'tournaments/:id/:teampointadjust/remove_teampointadjust' => 'tournaments#remove_teampointadjust' get 'tournaments/:id/error' => 'tournaments#error' post "/tournaments/:id/swap" => "tournaments#swap", :as => :swap_wrestlers - get 'tournaments/:id/export' => "tournaments#export" - post "/tournaments/:id/import" => "tournaments#import", :as => :import get "/tournaments/:id/brackets" => "tournaments#show" put "/tournaments/:id/calculate_team_scores", :to => "tournaments#calculate_team_scores" diff --git a/db/migrate/20241224132705_create_tournament_backups.rb b/db/migrate/20241224132705_create_tournament_backups.rb new file mode 100644 index 0000000..a0bbe21 --- /dev/null +++ b/db/migrate/20241224132705_create_tournament_backups.rb @@ -0,0 +1,16 @@ +class CreateTournamentBackups < ActiveRecord::Migration[7.0] + def up + create_table :tournament_backups do |t| + t.integer :tournament_id, null: false, foreign_key: true + t.text :backup_data, null: false, limit: 4294967295 # Use LONGTEXT for large backups + t.string :backup_reason + t.timestamps + end + end + + def down + # Drop the table + drop_table :tournament_backups + end +end + diff --git a/db/schema.rb b/db/schema.rb index ee2d16b..aca546e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_10_27_203209) do +ActiveRecord::Schema[7.2].define(version: 2024_12_24_132705) do create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false @@ -99,6 +99,14 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_27_203209) do t.index ["wrestler_id"], name: "index_teampointadjusts_on_wrestler_id" end + create_table "tournament_backups", force: :cascade do |t| + t.integer "tournament_id", null: false + t.text "backup_data", limit: 4294967295, null: false + t.string "backup_reason" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "tournament_delegates", force: :cascade do |t| t.integer "user_id" t.integer "tournament_id" @@ -166,5 +174,4 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_27_203209) do t.index ["school_id"], name: "index_wrestlers_on_school_id" t.index ["weight_id"], name: "index_wrestlers_on_weight_id" end - end diff --git a/test/controllers/tournament_backups_controller_test.rb b/test/controllers/tournament_backups_controller_test.rb new file mode 100644 index 0000000..0c2f04c --- /dev/null +++ b/test/controllers/tournament_backups_controller_test.rb @@ -0,0 +1,220 @@ +require "test_helper" + +class TournamentBackupsControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + setup do + @tournament = Tournament.find(1) + TournamentBackupService.new(@tournament, 'Manual backup').create_backup + @backup = @tournament.tournament_backups.first + end + + def sign_in_owner + sign_in users(:one) + end + + def sign_in_non_owner + sign_in users(:two) + end + + def sign_in_delegate + sign_in users(:three) + end + + def sign_in_school_delegate + sign_in users(:four) + end + + def success + assert_response :success + end + + def redirect + assert_redirected_to '/static_pages/not_allowed' + end + + # Index endpoint tests + test "logged in tournament owner can access index" do + sign_in_owner + get :index, params: { tournament_id: @tournament.id } + assert_response :success + end + + test "logged in delegate can access index" do + sign_in_delegate + get :index, params: { tournament_id: @tournament.id } + assert_response :success + end + + test "non-tournament owner cannot access index" do + sign_in_non_owner + get :index, params: { tournament_id: @tournament.id } + redirect + end + + test "school delegate cannot access index" do + sign_in_school_delegate + get :index, params: { tournament_id: @tournament.id } + redirect + end + + # Show endpoint tests + test "tournament owner can view a backup" do + sign_in_owner + get :show, params: { tournament_id: @tournament.id, id: @backup.id } + assert_response :success + end + + test "delegate can view a backup" do + sign_in_delegate + get :show, params: { tournament_id: @tournament.id, id: @backup.id } + assert_response :success + end + + test "non-tournament owner cannot view a backup" do + sign_in_non_owner + get :show, params: { tournament_id: @tournament.id, id: @backup.id } + redirect + end + + test "school delegate cannot view a backup" do + sign_in_school_delegate + get :show, params: { tournament_id: @tournament.id, id: @backup.id } + redirect + end + + # Destroy endpoint tests + test "tournament owner can delete a backup" do + sign_in_owner + assert_difference("TournamentBackup.count", -1) do + delete :destroy, params: { tournament_id: @tournament.id, id: @backup.id } + end + assert_redirected_to tournament_tournament_backups_path(@tournament) + end + + test "delegate can delete a backup" do + sign_in_delegate + assert_difference("TournamentBackup.count", -1) do + delete :destroy, params: { tournament_id: @tournament.id, id: @backup.id } + end + assert_redirected_to tournament_tournament_backups_path(@tournament) + end + + test "non-tournament owner cannot delete a backup" do + sign_in_non_owner + assert_no_difference("TournamentBackup.count") do + delete :destroy, params: { tournament_id: @tournament.id, id: @backup.id } + end + redirect + end + + test "school delegate cannot delete a backup" do + sign_in_school_delegate + assert_no_difference("TournamentBackup.count") do + delete :destroy, params: { tournament_id: @tournament.id, id: @backup.id } + end + redirect + end + + # Restore endpoint tests + test "tournament owner can restore a backup" do + sign_in_owner + post :restore, params: { tournament_id: @tournament.id, id: @backup.id } + assert_redirected_to tournament_path(@tournament) + end + + test "delegate can restore a backup" do + sign_in_delegate + post :restore, params: { tournament_id: @tournament.id, id: @backup.id } + assert_redirected_to tournament_path(@tournament) + end + + test "non-tournament owner cannot restore a backup" do + sign_in_non_owner + post :restore, params: { tournament_id: @tournament.id, id: @backup.id } + redirect + end + + test "school delegate cannot restore a backup" do + sign_in_school_delegate + post :restore, params: { tournament_id: @tournament.id, id: @backup.id } + redirect + end + + # Import manual tests + test "tournament owner can manually import a backup" do + sign_in_owner + post :import_manual, params: { tournament_id: @tournament.id, tournament: { import_text: Base64.decode64(@backup.backup_data) } } + assert_redirected_to tournament_path(@tournament) + end + + test "delegate can manually import a backup" do + sign_in_delegate + post :import_manual, params: { tournament_id: @tournament.id, tournament: { import_text: Base64.decode64(@backup.backup_data) } } + assert_redirected_to tournament_path(@tournament) + end + + test "non-tournament owner cannot manually import a backup" do + sign_in_non_owner + post :import_manual, params: { tournament_id: @tournament.id, tournament: { import_text: Base64.decode64(@backup.backup_data) } } + redirect + end + + test "school delegate cannot manually import a backup" do + sign_in_school_delegate + post :import_manual, params: { tournament_id: @tournament.id, tournament: { import_text: Base64.decode64(@backup.backup_data) } } + redirect + end + + test "index shows empty list when no backups exist" do + @tournament.tournament_backups.destroy_all + sign_in_owner + get :index, params: { tournament_id: @tournament.id } + assert_response :success + assert_select 'tbody tr', 0 # Ensure no rows are rendered in the table + end + + test "show action for non-existent backup" do + sign_in_owner + assert_raises(ActiveRecord::RecordNotFound) do + get :show, params: { tournament_id: @tournament.id, id: 9999 } # Use a non-existent backup ID + end + end + + test "destroy action for non-existent backup" do + sign_in_owner + assert_no_difference("TournamentBackup.count") do + assert_raises(ActiveRecord::RecordNotFound) do + delete :destroy, params: { tournament_id: @tournament.id, id: 9999 } # Use a non-existent backup ID + end + end + end + + test "restore action for non-existent backup" do + sign_in_owner + assert_raises(ActiveRecord::RecordNotFound) do + post :restore, params: { tournament_id: @tournament.id, id: 9999 } # Use a non-existent backup ID + end + end + + test "manual import with blank input" do + sign_in_owner + post :import_manual, params: { tournament_id: @tournament.id, tournament: { import_text: '' } } + assert_redirected_to tournament_tournament_backups_path(@tournament) + assert_equal 'Import text cannot be blank.', flash[:alert] + end + + test "manual import restores associations" do + schools_count = @tournament.schools.count + wrestlers_count = @tournament.wrestlers.count + matches_count = @tournament.matches.count + sign_in_owner + valid_backup_data = Base64.decode64(@backup.backup_data) + post :import_manual, params: { tournament_id: @tournament.id, tournament: { import_text: valid_backup_data } } + + @tournament.reload + assert_equal schools_count, @tournament.schools.count + assert_equal wrestlers_count, @tournament.wrestlers.count + assert_equal matches_count, @tournament.matches.count + end +end diff --git a/test/fixtures/tournament_backups.yml b/test/fixtures/tournament_backups.yml new file mode 100644 index 0000000..1f0df1d --- /dev/null +++ b/test/fixtures/tournament_backups.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +# one: {} +# column: value +# +# two: {} +# column: value diff --git a/test/models/tournament_backup_test.rb b/test/models/tournament_backup_test.rb new file mode 100644 index 0000000..91fdf35 --- /dev/null +++ b/test/models/tournament_backup_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TournamentBackupTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end