chore: save current Ruby implementation state before Next.js rewrite
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points,Winner
|
||||
1,1,Clubs,Derrick,Jesse C,5,Emma,Alissa,10,Evens
|
||||
1,1,Hearts,Kevin,Andy,8,Ellie,Jesse,6,Odds
|
||||
1,1,Diamonds,Sara M,Amelia,10,Mike G,AJ,4,Odds
|
||||
1,1,Spades,John,Dave B,7,Sara R,Emily,9,Evens
|
||||
1,1,Stars,Linden,Jim,1,David G,Lynn,10,Evens
|
||||
1,2,Clubs,John,Morgan,7,Alissa,Emma,14,Evens
|
||||
1,2,Hearts,Lynn,Mike G,6,Kevin,AJ,8,Evens
|
||||
1,2,Diamonds,Jesse F,Amelia,6,Linden,Jesse C,8,Evens
|
||||
1,2,Spades,Lucas,Andy,18,Derrick,Ellie,7,Odds
|
||||
1,2,Stars,Dave B,Emily,9,Jim,Sara R,12,Evens
|
||||
1,3,Hearts,Lynn,Derrick,10,Alissa,Emma,13,Evens
|
||||
1,3,Clubs,Kevin,AJ,2,Andy,Jesse C,11,Evens
|
||||
1,3,Diamonds,Jim,Mike G,8,Amelia,Kristen,4,Odds
|
||||
1,3,Spades,Ellie,John,11,Sara R,Dave B,12,Evens
|
||||
1,3,Stars,Lucas,Linden,7,Emily,Sara M,14,Evens
|
||||
1,4,Diamonds,John,Kevin,10,Alissa,Emma,7,Odds
|
||||
1,4,Hearts,Emily,Sara M,11,Jim,Ellie,6,Odds
|
||||
1,4,Clubs,Linden,AJ,2,Amelia,Mike G,8,Evens
|
||||
1,4,Spades,Andy,Derrick,10,Sara R,Jesse C,5,Odds
|
||||
1,4,Stars,Dave B,Lucas,9,David G,Lynn,7,Odds
|
||||
1,5,Clubs,Amelia,Alissa,11,Linden,Ellie,3,Odds
|
||||
1,5,Hearts,Emily,Sara R,10,Mike G,Andy,9,Odds
|
||||
1,5,Diamonds,Derrick,Jesse F,5,Sara M,Jim,5,Evens
|
||||
1,5,Spades,Kevin,Emma,11,Lynn,Dave B,11,Evens
|
||||
1,5,Stars,John,Lucas,9,AJ,Jesse C,6,Odds
|
||||
1,6,Diamonds,Alissa,Jim,11,John,Mike G,6,Odds
|
||||
1,6,Hearts,Kevin,AJ,8,Sara R,Emma,12,Evens
|
||||
1,6,Clubs,Derrick,Lucas,6,David G,Jesse C,5,Odds
|
||||
1,6,Spades,Lynn,Emily,9,Amelia,Dave B,7,Odds
|
||||
1,6,Stars,Sara M,Ellie,5,Linden,Andy,9,Evens
|
||||
|
+221
@@ -0,0 +1,221 @@
|
||||
# EuchreCamp User Stories
|
||||
|
||||
## Epic 1: Authentication & User Management
|
||||
|
||||
### As a new user, I want to register for an account so that I can participate in tournaments and track my games
|
||||
- Acceptance Criteria:
|
||||
- Registration form with name, email, password, password confirmation
|
||||
- Email validation and uniqueness check
|
||||
- Password strength requirements (8+ chars, uppercase, lowercase, number, special char)
|
||||
- Account confirmation via email
|
||||
- Auto-create player profile upon registration
|
||||
|
||||
### As a registered user, I want to log in to my account so that I can access my data
|
||||
- Acceptance Criteria:
|
||||
- Login form with email and password
|
||||
- Session management with secure cookies
|
||||
- "Remember me" functionality
|
||||
- Error handling for invalid credentials
|
||||
- Account lockout after 5 failed attempts
|
||||
|
||||
### As a logged-in user, I want to log out so that I can secure my account
|
||||
- Acceptance Criteria:
|
||||
- Logout button in navigation
|
||||
- Session cleared on logout
|
||||
- Redirect to home page
|
||||
|
||||
### As a user who forgot my password, I want to reset it so that I can regain access
|
||||
- Acceptance Criteria:
|
||||
- "Forgot password" link on login page
|
||||
- Email input for reset request
|
||||
- Unique token generation (expires in 1 hour)
|
||||
- Email with reset link
|
||||
- Password update form with validation
|
||||
|
||||
## Epic 2: Player Profile & Analytics
|
||||
|
||||
### As a player, I want to view my profile so that I can see my statistics
|
||||
- Acceptance Criteria:
|
||||
- Profile header with name and avatar
|
||||
- Current Elo rating with trend indicator
|
||||
- Total games played
|
||||
- Win rate percentage
|
||||
- Member since date
|
||||
|
||||
### As a player, I want to see my partnership performance so that I can identify my best partners
|
||||
- Acceptance Criteria:
|
||||
- Table showing all partners
|
||||
- Games played with each partner
|
||||
- Win rate with each partner
|
||||
- Total Elo change per partnership
|
||||
- Last played together date
|
||||
|
||||
### As a player, I want to see my recent games so that I can track my performance
|
||||
- Acceptance Criteria:
|
||||
- Timeline of recent matches
|
||||
- Date, opponents, scores, and results
|
||||
- Partner information for each game
|
||||
- Pagination for loading more games
|
||||
|
||||
### As a player, I want to see my tournament history so that I can review past performances
|
||||
- Acceptance Criteria:
|
||||
- List of tournaments participated in
|
||||
- Final standings in each tournament
|
||||
- Performance summary per tournament
|
||||
|
||||
## Epic 3: Rankings & Public Data
|
||||
|
||||
### As a visitor, I want to view player rankings so that I can see top players
|
||||
- Acceptance Criteria:
|
||||
- Sortable rankings table
|
||||
- Columns: Rank, Name, Elo, Win Rate, Games Played
|
||||
- Search/filter functionality
|
||||
- Pagination
|
||||
|
||||
### As a visitor, I want to view player profiles so that I can learn about other players
|
||||
- Acceptance Criteria:
|
||||
- Public profile pages (read-only for non-admins)
|
||||
- Basic stats: Elo, games played, win rate
|
||||
- Partnership performance (if public)
|
||||
- Recent games (limited)
|
||||
|
||||
## Epic 4: Tournament Management
|
||||
|
||||
### As a tournament admin, I want to create a new tournament so that I can organize events
|
||||
- Acceptance Criteria:
|
||||
- Tournament creation form
|
||||
- Fields: Name, Format (round-robin, single elim, double elim, Swiss), Date, Status
|
||||
- Default settings for each format
|
||||
- Validation for required fields
|
||||
|
||||
### As a tournament admin, I want to manage tournament participants so that I can control who plays
|
||||
- Acceptance Criteria:
|
||||
- Add participants from player list
|
||||
- Remove participants
|
||||
- Bulk import from CSV
|
||||
- View participant list with status
|
||||
|
||||
### As a tournament admin, I want to create teams so that I can organize players into pairs
|
||||
- Acceptance Criteria:
|
||||
- Team creation form
|
||||
- Select two players per team
|
||||
- Auto-generate team names
|
||||
- Edit/delete teams
|
||||
|
||||
### As a tournament admin, I want to generate a schedule so that matches are organized
|
||||
- Acceptance Criteria:
|
||||
- Round-robin schedule generation
|
||||
- Single/double elimination bracket generation
|
||||
- View schedule by round
|
||||
- Re-schedule matches if needed
|
||||
|
||||
### As a tournament admin, I want to record match results so that I can track tournament progress
|
||||
- Acceptance Criteria:
|
||||
- Match result entry form
|
||||
- Select teams and enter scores
|
||||
- Auto-determine winner from scores
|
||||
- Validation for score formats
|
||||
|
||||
### As a tournament admin, I want to upload results via CSV so that I can batch process matches
|
||||
- Acceptance Criteria:
|
||||
- CSV upload form with tournament selection
|
||||
- Parse CSV with specific format (Event, Round, Table, Seats, Scores, Winner)
|
||||
- Validate player names exist in system
|
||||
- Create teams automatically if needed
|
||||
- Import matches and update Elo ratings
|
||||
|
||||
### As a tournament admin, I want to view tournament analytics so that I can assess performance
|
||||
- Acceptance Criteria:
|
||||
- Tournament statistics (total matches, average score, etc.)
|
||||
- Partnership performance within tournament
|
||||
- Elo changes for participants
|
||||
- Visual charts for key metrics
|
||||
|
||||
## Epic 5: Match Recording & CSV Import
|
||||
|
||||
### As a user, I want to record a match result so that I can track my games
|
||||
- Acceptance Criteria:
|
||||
- Match creation form
|
||||
- Select Team 1 and Team 2 (each with 2 players)
|
||||
- Enter scores for each team
|
||||
- Auto-calculate winner
|
||||
- Update Elo ratings for all players
|
||||
- Update partnership statistics
|
||||
|
||||
### As a user, I want to upload match results via CSV so that I can process multiple matches at once
|
||||
- Acceptance Criteria:
|
||||
- CSV upload form
|
||||
- Support for Euchre format (Event, Round, Table, Seat 1-4, Odds/Evens Points, Winner)
|
||||
- Map table names (Clubs, Hearts, Diamonds, Spades, Stars) to numeric IDs
|
||||
- Handle player name variations
|
||||
- Create teams automatically
|
||||
- Import matches and calculate Elo
|
||||
- Show import summary with success/error count
|
||||
|
||||
### As a user, I want to edit match results so that I can correct mistakes
|
||||
- Acceptance Criteria:
|
||||
- Edit form for existing matches
|
||||
- Validation for time limits (5 minutes for players, unlimited for admins)
|
||||
- Update Elo ratings on change
|
||||
- Log changes in activity feed
|
||||
|
||||
## Epic 6: Club Administration
|
||||
|
||||
### As a club admin, I want to manage all players so that I can maintain the player database
|
||||
- Acceptance Criteria:
|
||||
- Player directory with search and filter
|
||||
- Create, edit, delete players
|
||||
- Bulk actions (export, update ratings)
|
||||
- View player activity
|
||||
|
||||
### As a club admin, I want to manage all tournaments so that I can oversee club events
|
||||
- Acceptance Criteria:
|
||||
- Tournament list with filters
|
||||
- Clone existing tournaments
|
||||
- Archive completed tournaments
|
||||
- View tournament analytics
|
||||
|
||||
### As a club admin, I want to configure club settings so that I can customize the system
|
||||
- Acceptance Criteria:
|
||||
- Elo rating parameters
|
||||
- Partnership tracking preferences
|
||||
- Notification settings
|
||||
- Default tournament settings
|
||||
|
||||
### As a club admin, I want to view club analytics so that I can assess overall performance
|
||||
- Acceptance Criteria:
|
||||
- Rating distribution charts
|
||||
- Activity heatmaps
|
||||
- Partnership network graph
|
||||
- Export reports (PDF/CSV)
|
||||
|
||||
## Epic 7: Mobile Responsiveness
|
||||
|
||||
### As a mobile user, I want to navigate the site so that I can access features on my phone
|
||||
- Acceptance Criteria:
|
||||
- Responsive navigation bar
|
||||
- Bottom navigation for mobile
|
||||
- Touch-friendly buttons and links
|
||||
- Optimized layouts for small screens
|
||||
|
||||
### As a mobile user, I want to record match results so that I can log games on the go
|
||||
- Acceptance Criteria:
|
||||
- Mobile-optimized forms
|
||||
- Quick entry for common actions
|
||||
- Offline support (optional)
|
||||
|
||||
## Epic 8: Data Management & Export
|
||||
|
||||
### As a club admin, I want to export data so that I can backup or analyze externally
|
||||
- Acceptance Criteria:
|
||||
- Export players to CSV
|
||||
- Export tournaments to CSV
|
||||
- Export matches to CSV
|
||||
- Export partnership data to CSV
|
||||
|
||||
### As a club admin, I want to import data so that I can migrate from other systems
|
||||
- Acceptance Criteria:
|
||||
- Import players from CSV
|
||||
- Import tournaments from CSV
|
||||
- Import matches from CSV
|
||||
- Data validation and error reporting
|
||||
@@ -1,83 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "csv"
|
||||
|
||||
module EuchreCamp
|
||||
module Actions
|
||||
module Admin
|
||||
module Matches
|
||||
class ProcessUpload < EuchreCamp::Action
|
||||
include Deps[repo: 'repositories.matches']
|
||||
include Deps[events_repo: 'repositories.events']
|
||||
|
||||
def handle(request, response)
|
||||
# In Hanami, file uploads are in request.params
|
||||
file = request.params[:csv_file]
|
||||
event_id = request.params[:event_id]&.to_i
|
||||
|
||||
unless file && file[:tempfile]
|
||||
response.flash[:error] = "No file uploaded"
|
||||
response.redirect_to("/admin/matches/upload")
|
||||
return
|
||||
end
|
||||
|
||||
matches_created = 0
|
||||
errors = []
|
||||
|
||||
# Parse CSV
|
||||
CSV.foreach(file[:tempfile].path, headers: true, header_converters: :symbol) do |row|
|
||||
begin
|
||||
# Map CSV columns to our database structure
|
||||
# Expected columns: played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score
|
||||
|
||||
# Find or create players by name
|
||||
player_1 = find_or_create_player(row[:team_1_p1_name])
|
||||
player_2 = find_or_create_player(row[:team_1_p2_name])
|
||||
player_3 = find_or_create_player(row[:team_2_p1_name])
|
||||
player_4 = find_or_create_player(row[:team_2_p2_name])
|
||||
|
||||
played_at = begin
|
||||
Time.parse(row[:played_at])
|
||||
rescue
|
||||
Time.now
|
||||
end
|
||||
|
||||
match_data = {
|
||||
played_at: played_at,
|
||||
team_1_p1_id: player_1[:id],
|
||||
team_1_p2_id: player_2[:id],
|
||||
team_1_score: row[:team_1_score].to_i,
|
||||
team_2_p1_id: player_3[:id],
|
||||
team_2_p2_id: player_4[:id],
|
||||
team_2_score: row[:team_2_score].to_i,
|
||||
status: 'completed'
|
||||
}
|
||||
|
||||
match = repo.create(match_data)
|
||||
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
|
||||
matches_created += 1
|
||||
|
||||
rescue => e
|
||||
errors << "Row #{row}: #{e.message}"
|
||||
end
|
||||
unless event_id
|
||||
response.flash[:error] = "Please select a tournament"
|
||||
response.redirect_to("/admin/matches/upload")
|
||||
return
|
||||
end
|
||||
|
||||
response.flash[:message] = "Created #{matches_created} matches.#{' Errors: ' + errors.join(', ') if errors.any?}"
|
||||
response.redirect_to("/admin/matches")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_or_create_player(name)
|
||||
rom = EuchreCamp::App["persistence.rom"]
|
||||
return nil unless name
|
||||
|
||||
name = name.strip
|
||||
player = rom.relations[:players].where(name: name).one
|
||||
|
||||
if player
|
||||
player
|
||||
else
|
||||
rom.relations[:players].insert(name: name, rating: 0, current_elo: 1000)
|
||||
rom.relations[:players].where(name: name).one
|
||||
# Verify event exists
|
||||
begin
|
||||
events_repo.by_id(event_id)
|
||||
rescue
|
||||
response.flash[:error] = "Invalid tournament ID"
|
||||
response.redirect_to("/admin/matches/upload")
|
||||
return
|
||||
end
|
||||
|
||||
# Use the specialized importer for Euchre tournament results
|
||||
importer = EuchreCamp::Services::TournamentCsvImporter.new(event_id: event_id)
|
||||
results = importer.import(file[:tempfile].path)
|
||||
|
||||
message = "Created #{results[:matches_created]} matches, #{results[:teams_created]} teams"
|
||||
message += ". Errors: #{results[:errors].join(', ')}" if results[:errors].any?
|
||||
|
||||
response.flash[:message] = message
|
||||
response.redirect_to("/admin/tournaments/#{event_id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "csv"
|
||||
|
||||
module EuchreCamp
|
||||
module Actions
|
||||
module Admin
|
||||
module Matches
|
||||
class Upload < EuchreCamp::Action
|
||||
include Deps[events_repo: 'repositories.events']
|
||||
|
||||
def handle(request, response)
|
||||
response.render(view)
|
||||
tournaments = events_repo.all
|
||||
response.render(view, tournaments: tournaments)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "csv"
|
||||
|
||||
module EuchreCamp
|
||||
module Services
|
||||
class TournamentCsvImporter
|
||||
# Map table names to numeric IDs
|
||||
TABLE_NAME_MAP = {
|
||||
"Clubs" => 1,
|
||||
"Hearts" => 2,
|
||||
"Diamonds" => 3,
|
||||
"Spades" => 4,
|
||||
"Stars" => 5
|
||||
}.freeze
|
||||
|
||||
def initialize(event_id:)
|
||||
@event_id = event_id
|
||||
@rom = EuchreCamp::App["persistence.rom"]
|
||||
@matches_repo = EuchreCamp::App["repositories.matches"]
|
||||
@teams_repo = EuchreCamp::App["repositories.teams"]
|
||||
@events_repo = EuchreCamp::App["repositories.events"]
|
||||
@rounds_repo = EuchreCamp::App["repositories.tournament_rounds"]
|
||||
@bracket_matchups_repo = EuchreCamp::App["repositories.bracket_matchups"]
|
||||
end
|
||||
|
||||
def import(csv_file_path)
|
||||
results = {
|
||||
matches_created: 0,
|
||||
teams_created: 0,
|
||||
rounds_created: 0,
|
||||
errors: []
|
||||
}
|
||||
|
||||
begin
|
||||
event = @events_repo.by_id(@event_id)
|
||||
|
||||
CSV.foreach(csv_file_path, headers: true, header_converters: :symbol) do |row|
|
||||
begin
|
||||
process_row(row, event, results)
|
||||
rescue => e
|
||||
results[:errors] << "Row #{row}: #{e.message}"
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
results[:errors] << "Fatal error: #{e.message}"
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_row(row, event, results)
|
||||
# Extract data from CSV row
|
||||
round_number = row[:round].to_i
|
||||
table_name = row[:table]
|
||||
seat_1_name = row[:seat_1]&.strip
|
||||
seat_3_name = row[:seat_3]&.strip
|
||||
odds_points = row[:odds_points].to_i
|
||||
seat_2_name = row[:seat_2]&.strip
|
||||
seat_4_name = row[:seat_4]&.strip
|
||||
evens_points = row[:evens_points].to_i
|
||||
winner = row[:winner]&.strip&.downcase
|
||||
|
||||
# Validate required fields
|
||||
unless seat_1_name && seat_3_name && seat_2_name && seat_4_name
|
||||
raise "Missing player names in row"
|
||||
end
|
||||
|
||||
# Find or create players
|
||||
player_1 = find_or_create_player(seat_1_name)
|
||||
player_2 = find_or_create_player(seat_3_name)
|
||||
player_3 = find_or_create_player(seat_2_name)
|
||||
player_4 = find_or_create_player(seat_4_name)
|
||||
|
||||
# Find or create teams
|
||||
team_1 = @teams_repo.find_or_create(@event_id, player_1[:id], player_2[:id], "#{seat_1_name} + #{seat_3_name}")
|
||||
team_2 = @teams_repo.find_or_create(@event_id, player_3[:id], player_4[:id], "#{seat_2_name} + #{seat_4_name}")
|
||||
|
||||
results[:teams_created] += 1 if team_1[:created_at] == team_2[:created_at] # Rough check for new teams
|
||||
|
||||
# Find or create round
|
||||
round = find_or_create_round(round_number)
|
||||
|
||||
# Map table name to number
|
||||
table_number = TABLE_NAME_MAP[table_name] || table_name.to_i
|
||||
if table_number == 0
|
||||
raise "Unknown table name: #{table_name}"
|
||||
end
|
||||
|
||||
# Determine winner based on scores or explicit winner column
|
||||
team_1_score = odds_points
|
||||
team_2_score = evens_points
|
||||
|
||||
# Verify winner matches scores if explicitly stated
|
||||
if winner
|
||||
if winner == "odds" && team_1_score <= team_2_score
|
||||
results[:errors] << "Warning: Row claims Odds won but score is #{team_1_score}-#{team_2_score}"
|
||||
elsif winner == "evens" && team_2_score <= team_1_score
|
||||
results[:errors] << "Warning: Row claims Evens won but score is #{team_1_score}-#{team_2_score}"
|
||||
end
|
||||
end
|
||||
|
||||
# Create match
|
||||
match_data = {
|
||||
team_1_p1_id: player_1[:id],
|
||||
team_1_p2_id: player_2[:id],
|
||||
team_1_score: team_1_score,
|
||||
team_2_p1_id: player_3[:id],
|
||||
team_2_p2_id: player_4[:id],
|
||||
team_2_score: team_2_score,
|
||||
played_at: Time.now,
|
||||
status: 'completed'
|
||||
}
|
||||
|
||||
match = @matches_repo.create(match_data)
|
||||
results[:matches_created] += 1
|
||||
|
||||
# Create bracket matchup entry
|
||||
bracket_data = {
|
||||
round_id: round[:id],
|
||||
event_id: @event_id,
|
||||
team_1_id: team_1[:id],
|
||||
team_2_id: team_2[:id],
|
||||
table_number: table_number,
|
||||
bracket_position: table_number,
|
||||
status: 'completed'
|
||||
}
|
||||
|
||||
@bracket_matchups_repo.create(bracket_data)
|
||||
|
||||
# Queue Elo calculation
|
||||
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
|
||||
|
||||
rescue => e
|
||||
raise "Error processing row: #{e.message}"
|
||||
end
|
||||
|
||||
def find_or_create_player(name)
|
||||
return nil unless name
|
||||
|
||||
player = @rom.relations[:players].where(name: name).one
|
||||
|
||||
if player
|
||||
player
|
||||
else
|
||||
# Create player with default rating
|
||||
@rom.relations[:players].insert(name: name, rating: 0, current_elo: 1000)
|
||||
@rom.relations[:players].where(name: name).one
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_round(round_number)
|
||||
round = @rom.relations[:tournament_rounds].where(event_id: @event_id, round_number: round_number).one
|
||||
|
||||
if round
|
||||
round
|
||||
else
|
||||
@rounds_repo.create(
|
||||
event_id: @event_id,
|
||||
round_number: round_number,
|
||||
status: 'completed'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +1,110 @@
|
||||
<h1>Upload Matches (CSV)</h1>
|
||||
<h1>Upload Tournament Results (CSV)</h1>
|
||||
|
||||
<p>Upload a CSV file with the following columns:</p>
|
||||
<pre>
|
||||
played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score
|
||||
</pre>
|
||||
<p>Upload a CSV file with Euchre tournament results. The format supports standard Euchre scoring with Odds/Evens teams.</p>
|
||||
|
||||
<form action="/admin/matches/process_upload" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label for="event_id">Tournament:</label>
|
||||
<select id="event_id" name="event_id" required>
|
||||
<option value="">Select a tournament...</option>
|
||||
<% if defined?(tournaments) && tournaments %>
|
||||
<% tournaments.each do |tournament| %>
|
||||
<option value="<%= tournament.id %>"><%= tournament.name %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="csv_file">CSV File:</label>
|
||||
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
|
||||
</div>
|
||||
|
||||
<button type="submit">Upload</button>
|
||||
<button type="submit" class="btn-primary">Upload Results</button>
|
||||
</form>
|
||||
|
||||
<p><a href="/admin/matches">Back to Matches</a></p>
|
||||
|
||||
<h2>Euchre Tournament CSV Format</h2>
|
||||
|
||||
<p>The CSV should have the following columns:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Event #</td>
|
||||
<td>Tournament ID (can be omitted if selected above)</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Round</td>
|
||||
<td>Round number</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Table</td>
|
||||
<td>Table name (Clubs, Hearts, Diamonds, Spades, Stars)</td>
|
||||
<td>Clubs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 1</td>
|
||||
<td>Player name (Odds team, position 1)</td>
|
||||
<td>Derrick</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 3</td>
|
||||
<td>Player name (Odds team, position 2)</td>
|
||||
<td>Jesse C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Odds Points</td>
|
||||
<td>Score for Odds team</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 2</td>
|
||||
<td>Player name (Evens team, position 1)</td>
|
||||
<td>Emma</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 4</td>
|
||||
<td>Player name (Evens team, position 2)</td>
|
||||
<td>Alissa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Evens Points</td>
|
||||
<td>Score for Evens team</td>
|
||||
<td>10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winner</td>
|
||||
<td>"Odds" or "Evens" (optional, for validation)</td>
|
||||
<td>Evens</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Example CSV:</h2>
|
||||
<pre>
|
||||
played_at,team_1_p1_name,team_1_p2_name,team_1_score,team_2_p1_name,team_2_p2_name,team_2_score
|
||||
2026-03-26 10:00,Alice,Bob,10,Charlie,David,7
|
||||
2026-03-26 10:30,Alice,Bob,10,Charlie,David,7
|
||||
<pre style="background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto;">
|
||||
Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points,Winner
|
||||
1,1,Clubs,Derrick,Jesse C,5,Emma,Alissa,10,Evens
|
||||
1,1,Hearts,Kevin,Andy,8,Ellie,Jesse,6,Odds
|
||||
1,1,Diamonds,Sara M,Amelia,10,Mike G,AJ,4,Odds
|
||||
</pre>
|
||||
|
||||
<h3>Notes:</h3>
|
||||
<ul>
|
||||
<li>Player names must match exactly (including middle initials if present)</li>
|
||||
<li>Table names are mapped to numeric IDs (Clubs=1, Hearts=2, etc.)</li>
|
||||
<li>New players will be created automatically if they don't exist</li>
|
||||
<li>Teams are automatically created for each player pair</li>
|
||||
<li>Elo ratings are calculated after import</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,8 +10,8 @@ ROM::SQL.migration do
|
||||
add_column :format, String, default: 'single_elim'
|
||||
add_column :status, String, default: 'planned'
|
||||
add_column :max_participants, Integer
|
||||
add_column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||
add_column :updated_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
|
||||
add_column :created_at, DateTime, null: false
|
||||
add_column :updated_at, DateTime, null: false
|
||||
end
|
||||
|
||||
# Remove the self-referential column that's not being used
|
||||
|
||||
@@ -4,16 +4,20 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class BracketMatchups < ROM::Relation[:sql]
|
||||
schema(:bracket_matchups, infer: true) do
|
||||
schema(:bracket_matchups) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :event_id, ROM::Types::Integer
|
||||
attribute :round_id, ROM::Types::Integer
|
||||
attribute :match_id, ROM::Types::Integer.optional
|
||||
attribute :team_1_id, ROM::Types::Integer.optional
|
||||
attribute :team_2_id, ROM::Types::Integer.optional
|
||||
attribute :table_number, ROM::Types::Integer.optional
|
||||
attribute :bracket_position, ROM::Types::Integer.optional
|
||||
attribute :winner_team_id, ROM::Types::Integer.optional
|
||||
attribute :loser_team_id, ROM::Types::Integer.optional
|
||||
attribute :status, ROM::Types::String.optional
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
belongs_to :event
|
||||
|
||||
@@ -4,14 +4,14 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class EloSnapshots < ROM::Relation[:sql]
|
||||
schema(:elo_snapshots, infer: true) do
|
||||
schema(:elo_snapshots) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :player_id, ROM::Types::Integer
|
||||
attribute :match_id, ROM::Types::Integer
|
||||
attribute :rating_before, ROM::Types::Integer
|
||||
attribute :rating_after, ROM::Types::Integer
|
||||
attribute :rating_change, ROM::Types::Integer
|
||||
attribute :created_at, ROM::Types::DateTime
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,14 +4,16 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class EventParticipants < ROM::Relation[:sql]
|
||||
schema(:event_participants, infer: true) do
|
||||
schema(:event_participants) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :event_id, ROM::Types::Integer
|
||||
attribute :player_id, ROM::Types::Integer
|
||||
attribute :team_id, ROM::Types::Integer.optional
|
||||
attribute :status, ROM::Types::String.default('registered')
|
||||
attribute :seed, ROM::Types::Integer.optional
|
||||
attribute :registration_date, ROM::Types::DateTime
|
||||
attribute :registration_date, ROM::Types::DateTime.optional
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
belongs_to :event
|
||||
|
||||
@@ -4,7 +4,7 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class Events < ROM::Relation[:sql]
|
||||
schema(:events, infer: true) do
|
||||
schema(:events) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :event_id, ROM::Types::Integer.optional
|
||||
attribute :name, ROM::Types::String
|
||||
@@ -14,8 +14,8 @@ module EuchreCamp
|
||||
attribute :format, ROM::Types::String.default('single_elim')
|
||||
attribute :status, ROM::Types::String.default('planned')
|
||||
attribute :max_participants, ROM::Types::Integer.optional
|
||||
attribute :created_at, ROM::Types::DateTime
|
||||
attribute :updated_at, ROM::Types::DateTime
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
has_many :event_participants
|
||||
|
||||
@@ -4,7 +4,7 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class GoodJobs < ROM::Relation[:sql]
|
||||
schema(:good_jobs, infer: true) do
|
||||
schema(:good_jobs) do
|
||||
attribute :id, ROM::Types::Integer
|
||||
attribute :queue_name, ROM::Types::String
|
||||
attribute :priority, ROM::Types::Integer
|
||||
@@ -13,8 +13,8 @@ module EuchreCamp
|
||||
attribute :performed_at, ROM::Types::DateTime.optional
|
||||
attribute :finished_at, ROM::Types::DateTime.optional
|
||||
attribute :error, ROM::Types::String.optional
|
||||
attribute :created_at, ROM::Types::DateTime
|
||||
attribute :updated_at, ROM::Types::DateTime
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
attribute :active_job_id, ROM::Types::String.optional
|
||||
attribute :concurrency_key, ROM::Types::String.optional
|
||||
attribute :sequence, ROM::Types::Integer.optional
|
||||
|
||||
@@ -4,7 +4,7 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class Matches < ROM::Relation[:sql]
|
||||
schema(:matches, infer: true) do
|
||||
schema(:matches) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :event_id, ROM::Types::Integer.optional
|
||||
attribute :played_at, ROM::Types::DateTime.optional
|
||||
@@ -15,6 +15,8 @@ module EuchreCamp
|
||||
attribute :team_1_score, ROM::Types::Integer
|
||||
attribute :team_2_score, ROM::Types::Integer
|
||||
attribute :status, ROM::Types::String.default('completed')
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
belongs_to :team_1_player, relation: :players, foreign_key: :team_1_p1_id
|
||||
|
||||
@@ -4,14 +4,16 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class PartnershipGames < ROM::Relation[:sql]
|
||||
schema(:partnership_games, infer: true) do
|
||||
schema(:partnership_games) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :match_id, ROM::Types::Integer
|
||||
attribute :player_1_id, ROM::Types::Integer
|
||||
attribute :player_2_id, ROM::Types::Integer
|
||||
attribute :team_number, ROM::Types::Integer
|
||||
attribute :won_match, ROM::Types::Integer
|
||||
attribute :created_at, ROM::Types::DateTime
|
||||
attribute :team_number, ROM::Types::Integer.optional
|
||||
attribute :won_match, ROM::Types::Integer.optional
|
||||
attribute :player_1_elo_change, ROM::Types::Integer.optional
|
||||
attribute :player_2_elo_change, ROM::Types::Integer.optional
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
belongs_to :match
|
||||
|
||||
@@ -4,7 +4,7 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class PartnershipStats < ROM::Relation[:sql]
|
||||
schema(:partnership_stats, infer: true) do
|
||||
schema(:partnership_stats) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :player_1_id, ROM::Types::Integer
|
||||
attribute :player_2_id, ROM::Types::Integer
|
||||
@@ -13,8 +13,8 @@ module EuchreCamp
|
||||
attribute :losses, ROM::Types::Integer.default(0)
|
||||
attribute :total_elo_change, ROM::Types::Integer.default(0)
|
||||
attribute :last_played, ROM::Types::DateTime.optional
|
||||
attribute :created_at, ROM::Types::DateTime
|
||||
attribute :updated_at, ROM::Types::DateTime
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
belongs_to :player_1, relation: :players, foreign_key: :player_1_id
|
||||
|
||||
@@ -4,11 +4,13 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class Players < ROM::Relation[:sql]
|
||||
schema(:players, infer: true) do
|
||||
schema(:players) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :name, ROM::Types::String
|
||||
attribute :rating, ROM::Types::Integer
|
||||
attribute :current_elo, ROM::Types::Integer.default(1000)
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,13 +4,14 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class Teams < ROM::Relation[:sql]
|
||||
schema(:teams, infer: true) do
|
||||
schema(:teams) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :event_id, ROM::Types::Integer
|
||||
attribute :team_name, ROM::Types::String.optional
|
||||
attribute :player_1_id, ROM::Types::Integer
|
||||
attribute :player_2_id, ROM::Types::Integer
|
||||
attribute :created_at, ROM::Types::DateTime
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
belongs_to :event
|
||||
|
||||
@@ -4,7 +4,7 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class TournamentRounds < ROM::Relation[:sql]
|
||||
schema(:tournament_rounds, infer: true) do
|
||||
schema(:tournament_rounds) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :event_id, ROM::Types::Integer
|
||||
attribute :round_number, ROM::Types::Integer
|
||||
@@ -12,6 +12,8 @@ module EuchreCamp
|
||||
attribute :scheduled_start, ROM::Types::DateTime.optional
|
||||
attribute :actual_start, ROM::Types::DateTime.optional
|
||||
attribute :completed_at, ROM::Types::DateTime.optional
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
|
||||
associations do
|
||||
belongs_to :event
|
||||
|
||||
@@ -4,7 +4,7 @@ module EuchreCamp
|
||||
module Persistence
|
||||
module Relations
|
||||
class Users < ROM::Relation[:sql]
|
||||
schema(:users, infer: true) do
|
||||
schema(:users) do
|
||||
attribute :id, ROM::Types::Integer, primary_key: true
|
||||
attribute :player_id, ROM::Types::Integer
|
||||
attribute :email, ROM::Types::String
|
||||
@@ -12,15 +12,15 @@ module EuchreCamp
|
||||
attribute :role, ROM::Types::String
|
||||
attribute :confirmed, ROM::Types::Bool
|
||||
attribute :confirmation_token, ROM::Types::String.optional
|
||||
attribute :confirmation_sent_at, ROM::Types::Time.optional
|
||||
attribute :confirmation_sent_at, ROM::Types::DateTime.optional
|
||||
attribute :reset_password_token, ROM::Types::String.optional
|
||||
attribute :reset_password_sent_at, ROM::Types::Time.optional
|
||||
attribute :reset_password_sent_at, ROM::Types::DateTime.optional
|
||||
attribute :failed_login_attempts, ROM::Types::Integer
|
||||
attribute :locked_until, ROM::Types::Time.optional
|
||||
attribute :last_login_at, ROM::Types::Time.optional
|
||||
attribute :locked_until, ROM::Types::DateTime.optional
|
||||
attribute :last_login_at, ROM::Types::DateTime.optional
|
||||
attribute :last_login_ip, ROM::Types::String.optional
|
||||
attribute :created_at, ROM::Types::Time
|
||||
attribute :updated_at, ROM::Types::Time
|
||||
attribute :created_at, ROM::Types::DateTime.optional
|
||||
attribute :updated_at, ROM::Types::DateTime.optional
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'playwright'
|
||||
|
||||
RSpec.describe 'Authentication', type: :playwright do
|
||||
# Use RSpec's around hook to manage Playwright lifecycle
|
||||
around do |example|
|
||||
Playwright.create(playwright_cli_executable_path: './node_modules/.bin/playwright') do |playwright|
|
||||
playwright.chromium.launch(headless: true) do |browser|
|
||||
@browser = browser
|
||||
example.run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
@context = @browser.new_context(viewport: { width: 1280, height: 800 })
|
||||
@page = @context.new_page
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
@context.close
|
||||
end
|
||||
|
||||
describe 'Login Form Display' do
|
||||
it 'displays login page at /login' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
expect(@page.locator('h1').text_content).to include('Login')
|
||||
end
|
||||
|
||||
it 'has email and password input fields' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
email_input = @page.locator('input[type="email"]')
|
||||
password_input = @page.locator('input[type="password"]')
|
||||
|
||||
expect(email_input.count).to be > 0
|
||||
expect(password_input.count).to be > 0
|
||||
end
|
||||
|
||||
it 'has a login form with POST method' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
form = @page.locator('form[method="POST"]')
|
||||
expect(form.count).to be > 0
|
||||
end
|
||||
|
||||
it 'includes CSRF token in login form' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
csrf_input = @page.locator('input[name="_csrf_token"]')
|
||||
expect(csrf_input.count).to be > 0
|
||||
expect(csrf_input.input_value).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Login Flow' do
|
||||
before(:all) do
|
||||
# Admin user credentials for testing
|
||||
@admin_email = 'david@dhg.lol'
|
||||
@admin_password = 'admin'
|
||||
end
|
||||
|
||||
it 'successfully logs in with valid credentials' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Fill in login form
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
|
||||
# Submit form
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Should redirect to rankings page
|
||||
expect(@page.url).to include('/')
|
||||
end
|
||||
|
||||
it 'shows success message after login' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Fill in login form
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
|
||||
# Submit form
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Should redirect to rankings page
|
||||
expect(@page.url).to include('/')
|
||||
end
|
||||
|
||||
it 'redirects to admin dashboard when logged in' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Login
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# After login, user is redirected to rankings
|
||||
# Then navigate to admin
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
|
||||
# Should see admin dashboard
|
||||
expect(@page.locator('h1').text_content).to include('Admin Dashboard')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Failed Login Scenarios' do
|
||||
it 'rejects invalid email' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
@page.locator('input[type="email"]').fill('invalid@example.com')
|
||||
@page.locator('input[type="password"]').fill('wrongpassword')
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Should show error message
|
||||
page_content = @page.content
|
||||
expect(page_content).to include('Invalid email or password')
|
||||
end
|
||||
|
||||
it 'rejects invalid password' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
@page.locator('input[type="email"]').fill('david@dhg.lol')
|
||||
@page.locator('input[type="password"]').fill('wrongpassword')
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Should show error message
|
||||
page_content = @page.content
|
||||
expect(page_content).to include('Invalid email or password')
|
||||
end
|
||||
|
||||
it 'requires email field' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
@page.locator('input[type="password"]').fill('somepassword')
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Browser validation should prevent submission
|
||||
# Or server should handle empty email
|
||||
page_content = @page.content
|
||||
# Either an error or the form should remain
|
||||
expect(page_content).to include('Login')
|
||||
end
|
||||
|
||||
it 'requires password field' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
@page.locator('input[type="email"]').fill('david@dhg.lol')
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Browser validation should prevent submission
|
||||
page_content = @page.content
|
||||
expect(page_content).to include('Login')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Session Management' do
|
||||
before(:all) do
|
||||
@admin_email = 'david@dhg.lol'
|
||||
@admin_password = 'admin'
|
||||
end
|
||||
|
||||
it 'creates session after successful login' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Login
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Check that session cookie is set
|
||||
cookies = @context.cookies
|
||||
session_cookie = cookies.find { |c| c['name'] == 'rack.session' }
|
||||
|
||||
expect(session_cookie).not_to be_nil
|
||||
expect(session_cookie['value']).not_to be_empty
|
||||
end
|
||||
|
||||
it 'persists session across page reloads' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Login
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Verify we're logged in
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
expect(@page.locator('h1').text_content).to include('Admin Dashboard')
|
||||
|
||||
# Reload page
|
||||
@page.reload
|
||||
|
||||
# Should still be logged in
|
||||
expect(@page.locator('h1').text_content).to include('Admin Dashboard')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Logout Functionality' do
|
||||
before(:all) do
|
||||
@admin_email = 'david@dhg.lol'
|
||||
@admin_password = 'admin'
|
||||
end
|
||||
|
||||
it 'logs out user when clicking logout link' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Login
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Verify logged in - check for Logout link in navigation
|
||||
# Navigate to admin to confirm login
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
expect(@page.locator('h1').text_content).to include('Admin Dashboard')
|
||||
|
||||
# Click logout - use first match since logout may appear in multiple places
|
||||
@page.locator('a[href="/logout"]').first.click
|
||||
|
||||
# After logout, check we're back at rankings
|
||||
expect(@page.url).to include('/')
|
||||
end
|
||||
|
||||
it 'clears session after logout' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Login
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Navigate to admin to verify logged in
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
expect(@page.locator('h1').text_content).to include('Admin Dashboard')
|
||||
|
||||
# Logout
|
||||
@page.locator('a[href="/logout"]').first.click
|
||||
|
||||
# Try to access admin page again - should redirect (not show admin dashboard)
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
|
||||
# Should not see admin dashboard
|
||||
expect(@page.locator('h1').text_content).not_to include('Admin Dashboard')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Authorization Redirects' do
|
||||
before(:all) do
|
||||
@admin_email = 'david@dhg.lol'
|
||||
@admin_password = 'admin'
|
||||
end
|
||||
|
||||
it 'redirects to rankings when accessing admin page without auth' do
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
|
||||
# Unauthenticated users are redirected to rankings page
|
||||
expect(@page.url).to include('/')
|
||||
expect(@page.locator('h1').text_content).to include('Rankings')
|
||||
end
|
||||
|
||||
it 'allows access to admin page after login' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Login
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Navigate to admin page
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
|
||||
# Should see admin dashboard
|
||||
expect(@page.locator('h1').text_content).to include('Admin Dashboard')
|
||||
end
|
||||
|
||||
it 'redirects to rankings after logout when accessing admin page' do
|
||||
@page.goto('http://localhost:2300/login')
|
||||
|
||||
# Login
|
||||
@page.locator('input[type="email"]').fill(@admin_email)
|
||||
@page.locator('input[type="password"]').fill(@admin_password)
|
||||
@page.locator('button[type="submit"]').click
|
||||
|
||||
# Go to admin page
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
expect(@page.url).to include('/admin')
|
||||
|
||||
# Click logout link in admin page
|
||||
# Note: logout link is in bottom nav on mobile, main nav on desktop
|
||||
@page.locator('a[href="/logout"]').first.click
|
||||
|
||||
# After logout, try to access admin page again
|
||||
@page.goto('http://localhost:2300/admin')
|
||||
|
||||
# Should redirect to rankings (unauthenticated users see rankings, not login)
|
||||
expect(@page.url).to include('/')
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user