chore: init commit

in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
2026-08-11 14:44:09 -04:00
parent 0012380fd3
commit 2854619bc9
782 changed files with 65944 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class AnnouncementsControllerTest < ActionDispatch::IntegrationTest
setup do
@admin = create(:admin, admin: true)
sign_in(@admin)
@announcement = Announcement.create!(
title: "Test Announcement",
content: "Test content"
)
end
# Authorization tests
test "non-admin cannot access index" do
sign_out(@admin)
sign_in(create(:teacher))
get admin_announcements_path
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
test "unauthenticated user is redirected to sign in" do
sign_out(@admin)
get admin_announcements_path
assert_redirected_to new_user_session_path
end
# Index tests
test "should get index" do
get admin_announcements_path
assert_response :success
end
test "index lists all announcements" do
announcement2 = Announcement.create!(title: "Second Announcement", content: "More content")
get admin_announcements_path
assert_response :success
assert_match @announcement.title, response.body
assert_match announcement2.title, response.body
end
test "index sorts by created_at ascending by default" do
get admin_announcements_path
assert_response :success
end
test "index sorts by column when sort param is provided" do
get admin_announcements_path, params: { sort: "title", direction: "asc" }
assert_response :success
end
test "index sorts descending when direction is desc" do
get admin_announcements_path, params: { sort: "title", direction: "desc" }
assert_response :success
end
# Show tests
test "should show announcement" do
get admin_announcement_path(@announcement)
assert_response :success
assert_match @announcement.title, response.body
end
# New tests
test "should get new" do
get new_admin_announcement_path
assert_response :success
end
# Create tests
test "should create announcement" do
assert_difference("Announcement.count") do
post admin_announcements_path, params: {
announcement: {
title: "New Announcement",
content: "New content",
featured: false
}
}
end
assert_redirected_to admin_announcement_path(Announcement.last)
assert_equal "Announcement created successfully.", flash[:notice]
end
test "should create featured announcement" do
assert_difference("Announcement.count") do
post admin_announcements_path, params: {
announcement: {
title: "Featured Announcement",
content: "Important content",
featured: true
}
}
end
assert Announcement.last.featured?
end
test "should not create announcement without title" do
assert_no_difference("Announcement.count") do
post admin_announcements_path, params: {
announcement: {
title: "",
content: "Some content"
}
}
end
assert_response :unprocessable_content
end
test "should not create announcement without content" do
assert_no_difference("Announcement.count") do
post admin_announcements_path, params: {
announcement: {
title: "A title",
content: ""
}
}
end
assert_response :unprocessable_content
end
# Edit tests
test "should get edit" do
get edit_admin_announcement_path(@announcement)
assert_response :success
end
# Update tests
test "should update announcement" do
patch admin_announcement_path(@announcement), params: {
announcement: {
title: "Updated Title"
}
}
assert_redirected_to admin_announcement_path(@announcement)
assert_equal "Announcement updated successfully.", flash[:notice]
assert_equal "Updated Title", @announcement.reload.title
end
test "should not update announcement with invalid params" do
patch admin_announcement_path(@announcement), params: {
announcement: {
title: ""
}
}
assert_response :unprocessable_content
assert_equal "Test Announcement", @announcement.reload.title
end
# Destroy tests
test "should destroy announcement" do
assert_difference("Announcement.count", -1) do
delete admin_announcement_path(@announcement)
end
assert_redirected_to admin_announcements_path
assert_equal "Announcement deleted successfully.", flash[:notice]
end
test "non-admin cannot destroy announcement" do
sign_out(@admin)
sign_in(create(:teacher))
assert_no_difference("Announcement.count") do
delete admin_announcement_path(@announcement)
end
assert_redirected_to root_path
end
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class BaseControllerTest < ActionDispatch::IntegrationTest
test "admin can access admin v2 dashboard" do
admin = create(:admin, admin: true)
sign_in(admin)
get admin_root_path
assert_response :success
end
test "non-admin cannot access admin v2 dashboard" do
teacher = create(:teacher)
sign_in(teacher)
get admin_root_path
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
test "unauthenticated user redirected to sign in" do
get admin_root_path
assert_redirected_to new_user_session_path
end
end
end

View File

@@ -0,0 +1,165 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class ClassroomsControllerTest < ActionDispatch::IntegrationTest
test "index" do
classroom1 = create(:classroom, name: "Bravo")
classroom2 = create(:classroom, name: "Charlie")
classroom3 = create(:classroom, name: "Alpha")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_classrooms_path
rows = css_select("tbody tr[id^='classroom_']")
ordered_row_ids = rows.pluck("id")
assert_response :success
assert_select "h3", "Classrooms"
assert_equal(
[dom_id(classroom3), dom_id(classroom1), dom_id(classroom2)],
ordered_row_ids
)
end
test "show" do
grade9 = create(:grade, level: 9)
grade10 = create(:grade, level: 10)
classroom = create(:classroom, grades: [grade9, grade10])
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_classroom_path(classroom)
assert_response :success
assert_select "h2", classroom.name
assert_select "[data-testid='grades_display'] dd", text: "9th-10th"
end
test "should get new" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_classroom_path
assert_response :success
assert_select "h1", "New Classroom"
end
test "create" do
grade = create(:grade, level: 7)
school_year = create(:school_year)
params = {
classroom: {
name: "Abc123",
grade_ids: [grade.id],
school_year_id: school_year.id
}
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_difference("Classroom.count") do
post(admin_classrooms_path, params:)
end
assert_redirected_to admin_classroom_path(Classroom.last)
assert_equal "Classroom created successfully.", flash[:notice]
end
test "create with invalid params" do
params = { classroom: { name: "" } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("Classroom.count") do
post(admin_classrooms_path, params:)
end
assert_response :unprocessable_entity
end
test "edit" do
classroom = create(:classroom)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get edit_admin_classroom_path(classroom)
assert_response :success
assert_select "h1", "Edit Classroom"
end
test "update" do
name = "Abc123"
classroom = create(:classroom)
params = { classroom: { name: } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_classroom_path(classroom), params:)
assert_redirected_to admin_classroom_path(classroom)
assert_equal "Classroom updated successfully.", flash[:notice]
assert_equal name, classroom.reload.name
end
test "update with invalid params" do
classroom = create(:classroom)
params = { classroom: { name: "" } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_classroom_path(classroom), params:)
assert_response :unprocessable_entity
end
test "toggle_archive" do
classroom = create(:classroom, archived: false)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch toggle_archive_admin_classroom_path(classroom)
classroom.reload
assert_redirected_to admin_classroom_path(classroom)
assert_equal "Classroom has been archived.", flash[:notice]
assert classroom.archived?
end
test "activate via toggle_archive" do
classroom = create(:classroom, archived: true)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch toggle_archive_admin_classroom_path(classroom)
classroom.reload
assert_redirected_to admin_classroom_path(classroom)
assert_equal "Classroom has been activated.", flash[:notice]
assert_not classroom.archived?
end
test "index when teacher" do
teacher = create(:teacher)
sign_in(teacher)
get admin_classrooms_path
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
test "toggle archive when teacher" do
classroom = create(:classroom)
teacher = create(:teacher)
sign_in(teacher)
patch toggle_archive_admin_classroom_path(classroom)
assert_redirected_to root_path
end
end
end

View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class DashboardControllerTest < ActionDispatch::IntegrationTest
test "admin can access admin dashboard" do
admin = create(:admin, admin: true)
sign_in(admin)
get admin_root_path
assert_response :success
assert_select "h1", "Admin Dashboard"
end
test "non-admin cannot access admin dashboard" do
teacher = create(:teacher)
sign_in(teacher)
get admin_root_path
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
test "unauthenticated user redirected to sign in" do
get admin_root_path
assert_redirected_to new_user_session_path
end
end
end

View File

@@ -0,0 +1,133 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class PortfolioTransactionsControllerTest < ActionDispatch::IntegrationTest
test "show" do
portfolio = build(:portfolio)
create(:student, portfolio:)
portfolio_transaction = create(:portfolio_transaction, portfolio:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_portfolio_transaction_path(portfolio_transaction)
assert_response :success
end
test "new" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_portfolio_transaction_path
assert_response :success
end
test "create" do
portfolio = build(:portfolio)
create(:student, portfolio:)
params = {
portfolio_transaction: {
portfolio_id: portfolio.id,
transaction_type: "deposit",
reason: "awards",
description: "LSP's royal allowance",
amount_cents: 5000
}
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_difference("PortfolioTransaction.count") do
post(admin_portfolio_transactions_path, params:)
end
portfolio_transaction = PortfolioTransaction.last
assert_redirected_to(
admin_portfolio_transaction_path(portfolio_transaction)
)
assert_equal(
"Portfolio transaction created successfully.",
flash[:notice]
)
end
test "create with invalid params" do
params = { portfolio_transaction: { portfolio_id: "" } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("PortfolioTransaction.count") do
post(admin_portfolio_transactions_path, params:)
end
assert_response :unprocessable_content
end
test "edit" do
portfolio = build(:portfolio)
create(:student, portfolio:)
portfolio_transaction = create(:portfolio_transaction, portfolio:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get edit_admin_portfolio_transaction_path(portfolio_transaction)
assert_response :success
end
test "update" do
description = "Finn's dungeon loot"
amount_cents = 7_500
portfolio = build(:portfolio)
create(:student, portfolio:)
portfolio_transaction = create(:portfolio_transaction, portfolio:)
params = { portfolio_transaction: { description:, amount_cents: } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_portfolio_transaction_path(portfolio_transaction), params:)
portfolio_transaction.reload
assert_redirected_to(
admin_portfolio_transaction_path(portfolio_transaction)
)
assert_equal(
"Portfolio transaction updated successfully.",
flash[:notice]
)
assert_equal description, portfolio_transaction.description
assert_equal amount_cents, portfolio_transaction.amount_cents
end
test "update with invalid params" do
portfolio = build(:portfolio)
create(:student, portfolio:)
portfolio_transaction = create(:portfolio_transaction, portfolio:)
params = { portfolio_transaction: { portfolio_id: "" } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_portfolio_transaction_path(portfolio_transaction), params:)
assert_response :unprocessable_content
end
test "destroy" do
portfolio = build(:portfolio)
create(:student, portfolio:)
portfolio_transaction = create(:portfolio_transaction, portfolio:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_difference("PortfolioTransaction.count", -1) do
delete admin_portfolio_transaction_path(portfolio_transaction)
end
assert_redirected_to admin_root_path
assert_equal "Portfolio transaction deleted successfully.", flash[:notice]
end
end
end

View File

@@ -0,0 +1,158 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class SchoolYearsControllerTest < ActionDispatch::IntegrationTest
test "index" do
create(:school_year)
create(:school_year)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_school_years_path
assert_response :success
assert_select "h3", "School Years"
assert_select "tbody tr", count: 2
end
test "show" do
school_name = "Candy Kingdom"
year_name = "1000"
school = create(:school, name: school_name)
year = create(:year, name: year_name)
school_year = create(:school_year, school:, year:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_school_year_path(school_year)
assert_response :success
assert_select "h2", "#{school_name} (#{year_name})"
assert_select "h3", "Quarters"
end
test "new" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_school_year_path
assert_response :success
assert_select "h1", "New School Year"
end
test "create" do
school = create(:school)
year = create(:year)
params = { school_year: { school_id: school.id, year_id: year.id } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_difference("SchoolYear.count") do
post(admin_school_years_path, params:)
end
school_year = SchoolYear.last
assert_redirected_to admin_school_year_path(school_year)
assert_equal "School year created successfully.", flash[:notice]
assert_equal(
["Quarter 1", "Quarter 2", "Quarter 3", "Quarter 4"],
school_year.quarters.order(:number).pluck(:name)
)
end
test "create with invalid params" do
params = { school_year: { school_id: "" } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("SchoolYear.count") do
post(admin_school_years_path, params:)
end
assert_response :unprocessable_content
end
test "create with duplicate school year shows error" do
school = create(:school)
year = create(:year)
create(:school_year, school:, year:)
params = { school_year: { school_id: school.id, year_id: year.id } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("SchoolYear.count") do
post(admin_school_years_path, params:)
end
assert_response :unprocessable_content
assert_select "p.text-red-600", /already exists/
end
test "edit" do
school_year = create(:school_year)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get edit_admin_school_year_path(school_year)
assert_response :success
assert_select "h1", "Edit School Year"
end
test "update" do
school_year = create(:school_year)
school = create(:school)
params = { school_year: { school_id: school.id } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_school_year_path(school_year), params:)
school_year.reload
assert_redirected_to admin_school_year_path(school_year)
assert_equal "School year updated successfully.", flash[:notice]
assert_equal school.id, school_year.school_id
end
test "update with invalid params" do
school_year = create(:school_year)
params = { school_year: { school_id: nil } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_school_year_path(school_year), params:)
assert_response :unprocessable_content
end
test "destroy" do
school_year = create(:school_year)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_difference("SchoolYear.count", -1) do
delete admin_school_year_path(school_year)
end
assert_redirected_to admin_school_years_path
assert_equal "School year deleted successfully.", flash[:notice]
end
test "destroy with classrooms" do
school_year = create(:school_year)
create(:classroom, school_year:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("SchoolYear.count") do
delete admin_school_year_path(school_year)
end
assert_redirected_to admin_school_years_path
assert_match(/Cannot delete school/, flash[:alert])
end
end
end

View File

@@ -0,0 +1,203 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class SchoolsControllerTest < ActionDispatch::IntegrationTest
setup do
@admin = create(:admin, admin: true, classroom: nil)
sign_in(@admin)
end
test "index" do
create(:school, name: "Alpha School")
create(:school, name: "Beta School")
get admin_schools_path
assert_response :success
assert_select "h3", "Schools"
assert_select "tbody tr", count: 2
end
test "show" do
school = create(:school, name: "Test School")
get admin_school_path(school)
assert_response :success
assert_select "h2", school.name
end
test "show displays associated years" do
school = create(:school, name: "Test School")
year1 = create(:year, name: "2024 - 2025")
year2 = create(:year, name: "2025 - 2026")
school.years << [year1, year2]
get admin_school_path(school)
assert_response :success
assert_select "li", text: year1.name
assert_select "li", text: year2.name
end
test "new" do
get new_admin_school_path
assert_response :success
assert_select "h1", "New School"
end
test "new shows year checkboxes" do
year1 = create(:year, name: "2024 - 2025")
year2 = create(:year, name: "2025 - 2026")
get new_admin_school_path
assert_response :success
assert_select "input[type='checkbox'][name='school[year_ids][]']", count: 2
assert_select "label", text: year1.name
assert_select "label", text: year2.name
end
test "create" do
year1 = create(:year, name: "2024 - 2025")
year2 = create(:year, name: "2025 - 2026")
params = {
school: {
name: "New School",
year_ids: [year1.id, year2.id]
}
}
assert_difference("School.count") do
post(admin_schools_path, params:)
end
school = School.last
assert_redirected_to admin_school_path(school)
assert_equal "School created successfully.", flash[:notice]
assert_equal [year1, year2].sort_by(&:id), school.years.sort_by(&:id)
end
test "create without years" do
params = {
school: {
name: "School Without Years"
}
}
assert_difference("School.count") do
post(admin_schools_path, params:)
end
school = School.last
assert_redirected_to admin_school_path(school)
assert_empty school.years
end
test "create with invalid params" do
params = {
school: {
name: ""
}
}
assert_no_difference("School.count") do
post(admin_schools_path, params:)
end
assert_response :unprocessable_content
end
test "edit" do
school = create(:school, name: "Test School")
get edit_admin_school_path(school)
assert_response :success
assert_select "h1", "Edit School"
end
test "edit shows current year selections" do
year1 = create(:year, name: "2024 - 2025")
year2 = create(:year, name: "2025 - 2026")
school = create(:school, name: "Test School")
school.years << year1
get edit_admin_school_path(school)
assert_response :success
assert_select "input[type='checkbox'][name='school[year_ids][]'][value='#{year1.id}'][checked='checked']"
assert_select "input[type='checkbox'][name='school[year_ids][]'][value='#{year2.id}']:not([checked])"
end
test "update" do
year1 = create(:year, name: "2024 - 2025")
year2 = create(:year, name: "2025 - 2026")
school = create(:school, name: "Original Name")
school.years << year1
params = {
school: {
name: "Updated Name",
year_ids: [year2.id]
}
}
patch(admin_school_path(school), params:)
school.reload
assert_redirected_to admin_school_path(school)
assert_equal "School updated successfully.", flash[:notice]
assert_equal "Updated Name", school.name
assert_equal [year2], school.years
end
test "update can remove all years" do
year1 = create(:year, name: "2024 - 2025")
school = create(:school, name: "Test School")
school.years << year1
params = {
school: {
name: school.name,
year_ids: []
}
}
patch(admin_school_path(school), params:)
school.reload
assert_redirected_to admin_school_path(school)
assert_empty school.years
end
test "update with invalid params" do
school = create(:school, name: "Test School")
params = { school: { name: "" } }
patch(admin_school_path(school), params:)
assert_response :unprocessable_content
end
test "destroy" do
school = create(:school, name: "Test School")
assert_difference("School.count", -1) do
delete admin_school_path(school)
end
assert_redirected_to admin_schools_path
end
test "cannot destroy school with associated years" do
school = create(:school, name: "Test School")
school.years << create(:year, name: "2025 - 2026")
assert_no_difference("School.count") do
delete admin_school_path(school)
end
end
end
end

View File

@@ -0,0 +1,170 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class StocksControllerTest < ActionDispatch::IntegrationTest
setup do
@admin = create(:admin, admin: true)
sign_in(@admin)
@stock1 = Stock.create!(ticker: "AAPL", company_name: "Apple Inc.", price_cents: 15_000, archived: false)
@stock2 = Stock.create!(ticker: "GOOGL", company_name: "Alphabet Inc.", price_cents: 14_000, archived: false)
@stock3 = Stock.create!(ticker: "MSFT", company_name: "Microsoft Corp.", price_cents: 38_000, archived: true)
end
# Index tests
test "should get index" do
get admin_stocks_path
assert_response :success
assert_select "h3", "Stocks"
end
test "index shows all stocks" do
get admin_stocks_path
assert_response :success
assert_select "tbody tr", count: 3
end
# Show tests
test "should show stock" do
get admin_stock_path(@stock1)
assert_response :success
assert_select "h2", @stock1.ticker
end
test "should show stock price information" do
get admin_stock_path(@stock1)
assert_response :success
assert_select "h3", "Price Information"
assert_select "dt", text: "Current Price"
end
# New tests
test "should get new" do
get new_admin_stock_path
assert_response :success
assert_select "h1", "New Stock"
end
# Create tests
test "should create stock" do
assert_difference("Stock.count") do
post admin_stocks_path, params: {
stock: {
ticker: "TSLA",
company_name: "Tesla Inc.",
company_website: "https://www.tesla.com",
price_cents: 25_000,
yesterday_price_cents: 24_000,
archived: false
}
}
end
assert_redirected_to admin_stock_path(Stock.last)
assert_equal "Stock created successfully.", flash[:notice]
end
test "should not create stock with invalid params" do
assert_no_difference("Stock.count") do
post admin_stocks_path, params: {
stock: {
ticker: "",
company_name: ""
}
}
end
assert_response :unprocessable_content
end
test "should not create stock with invalid website URL" do
assert_no_difference("Stock.count") do
post admin_stocks_path, params: {
stock: {
ticker: "TEST",
company_name: "Test Company",
company_website: "not-a-valid-url"
}
}
end
assert_response :unprocessable_content
end
# Edit tests
test "should get edit" do
get edit_admin_stock_path(@stock1)
assert_response :success
assert_select "h1", "Edit Stock"
end
# Update tests
test "should update stock" do
patch admin_stock_path(@stock1), params: {
stock: {
company_name: "Apple Incorporated"
}
}
assert_redirected_to admin_stock_path(@stock1)
assert_equal "Stock updated successfully.", flash[:notice]
assert_equal "Apple Incorporated", @stock1.reload.company_name
end
test "should not update stock with invalid params" do
patch admin_stock_path(@stock1), params: {
stock: {
ticker: ""
}
}
assert_response :unprocessable_content
end
# Destroy tests
test "should destroy stock" do
assert_difference("Stock.count", -1) do
delete admin_stock_path(@stock1)
end
assert_redirected_to admin_stocks_path
assert_equal "Stock deleted successfully.", flash[:notice]
end
# Authorization tests
test "non-admin cannot access index" do
sign_out(@admin)
classroom = create(:classroom)
student = User.create!(username: "student", type: "Student", password: "password", classroom: classroom)
sign_in(student)
get admin_stocks_path
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
test "non-admin cannot create stock" do
sign_out(@admin)
teacher = create(:teacher)
sign_in(teacher)
post admin_stocks_path, params: {
stock: {
ticker: "TEST",
company_name: "Test"
}
}
assert_redirected_to root_path
end
end
end

View File

@@ -0,0 +1,482 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class StudentsControllerTest < ActionDispatch::IntegrationTest
setup do
@classroom1 = create(:classroom)
@classroom2 = create(:classroom)
@admin = create(:admin, admin: true, classroom: nil)
sign_in(@admin)
end
test "index" do
create(:student)
create(:student)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_students_path
assert_response :success
assert_select "h3", "Students"
assert_select "tbody tr", count: 2
end
test "index with discarded filter" do
student = create(:student, :discarded)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_students_path(discarded: true)
assert_response :success
assert_select "tbody tr", count: 1
assert_select "form[action=?]", restore_admin_student_path(student) do
assert_select "button", text: "Restore"
end
end
test "index with all filter" do
create(:student)
create(:student)
create(:student, :discarded)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_students_path(all: true)
assert_response :success
assert_select "tbody tr", count: 3
assert_select "a[href*='edit']", count: 2
end
test "show" do
username = "finn"
student = create(:student, username:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_student_path(student)
assert_response :success
assert_select "h2", username
assert_select "h3", text: "Portfolio Details"
assert_select(
"[data-testid='cash_balance_label']",
text: "Cash Balance"
)
assert_select(
"[data-testid='total_portfolio_worth_label']",
text: "Total Portfolio Worth"
)
end
test "show displays attendance records" do
student = create(:student)
quarter = student.classroom.school_year.quarters.find_by!(number: 1)
grade_book = student.classroom.grade_books.find_by!(quarter: quarter)
create(:grade_entry, grade_book: grade_book, user: student, attendance_days: 42, is_perfect_attendance: true)
get admin_student_path(student)
assert_response :success
assert_select "h3", text: "Attendance"
assert_select "tbody tr", minimum: 1
assert_select "td", text: "Q1"
assert_select "td", text: "42"
end
test "show displays empty attendance message when no records" do
student = create(:student)
get admin_student_path(student)
assert_response :success
assert_select "p", text: "No attendance records found."
end
test "show displays earnings summary" do
student = create(:student)
portfolio = student.portfolio
portfolio.portfolio_transactions.create!(
amount_cents: 500, transaction_type: :deposit,
reason: :attendance_earnings
)
portfolio.portfolio_transactions.create!(amount_cents: 300, transaction_type: :deposit, reason: :math_earnings)
portfolio.portfolio_transactions.create!(amount_cents: 200, transaction_type: :deposit, reason: :awards)
get admin_student_path(student)
assert_response :success
assert_select "h4", text: "Earnings Summary"
assert_select "td", text: "Attendance Earnings"
assert_select "td", text: "Math Earnings"
assert_select "td", text: "Rewards"
assert_select "td", text: "Total Earnings"
assert_select "td", text: "Transaction Fees"
end
test "new" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_student_path
assert_response :success
assert_select "h1", "New Student"
end
test "create" do
username = "jake"
classroom = create(:classroom, name: "Ice Kingdom")
params = {
student: {
username:,
classroom_id: classroom.id,
password: "password123",
password_confirmation: "password123"
}
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_difference(["Student.count", "Portfolio.count"]) do
post(admin_students_path, params:)
end
student = Student.last
assert_redirected_to admin_student_path(student)
assert_equal(
"Student #{username} created successfully. Password: password123",
flash[:notice]
)
assert_not_nil student.portfolio
end
test "create with invalid params" do
params = { student: { username: "", classroom_id: nil } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("Student.count") do
post(admin_students_path, params:)
end
assert_response :unprocessable_content
end
test "edit" do
student = create(:student)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get edit_admin_student_path(student)
assert_response :success
assert_select "h1", "Edit Student"
end
test "update" do
username = "marceline"
student = create(:student)
params = { student: { username: } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_student_path(student), params:)
student.reload
assert_redirected_to admin_student_path(student)
assert_equal "Student updated successfully.", flash[:notice]
assert_equal username, student.username
end
test "update with invalid params" do
student = create(:student)
params = { student: { username: "" } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_student_path(student), params:)
assert_response :unprocessable_content
end
test "destroy" do
username = "gunter"
student = create(:student, username:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("Student.count") do
delete admin_student_path(student)
end
student.reload
assert_redirected_to admin_students_path
assert_equal "Student #{username} archived successfully.", flash[:notice]
assert student.discarded?
end
test "restore" do
username = "lsp"
student = create(:student, :discarded, username:)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch restore_admin_student_path(student)
student.reload
assert_redirected_to admin_students_path(discarded: true)
assert_equal "Student #{username} restored successfully.", flash[:notice]
assert_not student.discarded?
end
test "add_transaction" do
portfolio = build(:portfolio, user: nil)
student = create(:student, portfolio:)
create(:portfolio_transaction, :deposit, portfolio:, amount_cents: 10_000)
params = {
transaction_type: "deposit",
add_fund_amount: "100.50",
transaction_reason: "awards",
transaction_description: "Test deposit"
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
post(add_transaction_admin_student_path(student), params:)
portfolio.reload
assert_redirected_to admin_student_path(student)
assert_equal "Transaction added successfully.", flash[:notice]
assert_equal 20_050, portfolio.cash_balance * 100
end
test "add_transaction debit" do
student = create(:student)
params = {
transaction_type: "debit",
add_fund_amount: "50.25",
transaction_reason: "administrative_adjustments",
transaction_description: "Test debit"
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
post(add_transaction_admin_student_path(student), params:)
transaction = student.portfolio.portfolio_transactions.last
assert_redirected_to admin_student_path(student)
assert_equal "debit", transaction.transaction_type
assert_equal 5_025, transaction.amount_cents
end
test "add_transaction invalid params" do
student = create(:student)
params = { transaction_type: "" }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
post(add_transaction_admin_student_path(student), params:)
expected_error_message =
"Transaction Type must be present, Amount must be present, " \
"Reason must be present"
assert_redirected_to edit_admin_student_path(student)
assert_equal expected_error_message, flash[:alert]
end
test "add_transaction params are not nested under student key" do
student = create(:student)
params = {
student: {
transaction_type: "deposit",
add_fund_amount: "50.00",
transaction_reason: "awards",
transaction_description: "Nested params should be ignored"
}
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("PortfolioTransaction.count") do
post(add_transaction_admin_student_path(student), params:)
end
assert_redirected_to edit_admin_student_path(student)
end
# Import/Template tests
test "template should download CSV template" do
get template_admin_students_path
assert_response :success
assert_equal "text/csv", response.media_type
assert_includes response.headers["Content-Disposition"], "attachment; filename=\"student_import_template.csv\""
assert_match(/classroom_id,username/, response.body)
end
test "import should create students from valid CSV" do
csv_content = "classroom_id,username\n#{@classroom1.id},import_student1\n#{@classroom2.id},import_student2"
csv_file = Tempfile.new(["test_import", ".csv"])
csv_file.write(csv_content)
csv_file.rewind
assert_difference("Student.count", 2) do
post import_admin_students_path, params: {
csv_file: fixture_file_upload(csv_file.path, "text/csv")
}
end
csv_file.close
csv_file.unlink
assert_redirected_to admin_students_path
assert_match(/Successfully created 2 students/, flash[:notice])
assert_match(/import_student1/, flash[:notice])
assert_match(/import_student2/, flash[:notice])
end
test "import should skip existing students" do
create(:student, username: "student1", classroom: @classroom1)
csv_content = "classroom_id,username\n#{@classroom1.id},student1\n#{@classroom2.id},new_student"
csv_file = Tempfile.new(["test_import", ".csv"])
csv_file.write(csv_content)
csv_file.rewind
assert_difference("Student.count", 1) do
post import_admin_students_path, params: {
csv_file: fixture_file_upload(csv_file.path, "text/csv")
}
end
csv_file.close
csv_file.unlink
assert_redirected_to admin_students_path
assert_match(/Successfully created 1 students/, flash[:notice])
assert_match(/Skipped 1 existing usernames/, flash[:notice])
end
test "import should handle errors and show line numbers" do
csv_content = "classroom_id,username\n999,invalid_student\n#{@classroom1.id},valid_student"
csv_file = Tempfile.new(["test_import", ".csv"])
csv_file.write(csv_content)
csv_file.rewind
post import_admin_students_path, params: {
csv_file: fixture_file_upload(csv_file.path, "text/csv")
}
csv_file.close
csv_file.unlink
assert_redirected_to admin_students_path
assert_match(/errors occurred/, flash[:alert])
assert_match(/Row 2:/, flash[:alert])
end
test "import should reject missing file" do
assert_no_difference("Student.count") do
post import_admin_students_path
end
assert_redirected_to admin_students_path
assert_equal I18n.t("admin.students.import.errors.no_file"), flash[:alert]
end
test "import should handle malformed CSV" do
csv_content = "classroom_id,username\n1,\"unclosed quote\n2,another_row"
csv_file = Tempfile.new(["test_import", ".csv"])
csv_file.write(csv_content)
csv_file.rewind
assert_no_difference("Student.count") do
post import_admin_students_path, params: {
csv_file: fixture_file_upload(csv_file.path, "text/csv")
}
end
csv_file.close
csv_file.unlink
assert_redirected_to admin_students_path
assert_match(/Invalid CSV format/, flash[:alert])
end
test "import should handle empty CSV" do
csv_content = "classroom_id,username\n"
csv_file = Tempfile.new(["test_import", ".csv"])
csv_file.write(csv_content)
csv_file.rewind
assert_no_difference("Student.count") do
post import_admin_students_path, params: {
csv_file: fixture_file_upload(csv_file.path, "text/csv")
}
end
csv_file.close
csv_file.unlink
assert_redirected_to admin_students_path
assert_equal I18n.t("admin.students.import.errors.no_students"), flash[:alert]
end
test "import should show both success and error messages" do
csv_content = "classroom_id,username\n" \
"#{@classroom1.id},import_success1\n999,import_fail\n#{@classroom2.id},import_success2"
csv_file = Tempfile.new(["test_import", ".csv"])
csv_file.write(csv_content)
csv_file.rewind
post import_admin_students_path, params: {
csv_file: fixture_file_upload(csv_file.path, "text/csv")
}
csv_file.close
csv_file.unlink
assert_redirected_to admin_students_path
assert_match(/Successfully created 2 students/, flash[:notice])
assert_match(/1 errors occurred/, flash[:alert])
end
test "non-admin cannot access template" do
sign_out(@admin)
teacher = create(:teacher)
sign_in(teacher)
get template_admin_students_path
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
test "non-admin cannot import students" do
sign_out(@admin)
teacher = create(:teacher)
sign_in(teacher)
csv_content = "classroom_id,username\n#{@classroom1.id},import_student"
csv_file = Tempfile.new(["test_import", ".csv"])
csv_file.write(csv_content)
csv_file.rewind
assert_no_difference("Student.count") do
post import_admin_students_path, params: {
csv_file: fixture_file_upload(csv_file.path, "text/csv")
}
end
csv_file.close
csv_file.unlink
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
end
end

View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true
require "test_helper"
module Admin
module Teachers
class DeactivationsControllerTest < ActionDispatch::IntegrationTest
setup do
@admin = create(:admin, admin: true)
sign_in(@admin)
@classroom1 = create(:classroom, name: "Math 101")
@teacher1 = create(:teacher, username: "teacher1", email: "teacher1@example.com")
@teacher1.classrooms << @classroom1
end
test "should deactivate teacher" do
assert_no_difference("Teacher.count") do
post admin_teacher_deactivation_path(@teacher1)
end
assert_redirected_to admin_teachers_path
assert_equal "Teacher teacher1@example.com deactivated successfully.", flash[:notice]
assert @teacher1.reload.discarded?
end
test "deactivate should soft delete teacher and remove from active list" do
post admin_teacher_deactivation_path(@teacher1)
# Teacher should still exist in database
assert_not_nil Teacher.with_discarded.find_by(id: @teacher1.id)
# But should not appear in kept scope
assert_nil Teacher.kept.find_by(id: @teacher1.id)
follow_redirect!
# Should not appear on the default (active) index
assert_select "##{dom_id(@teacher1)}", count: 0
end
end
end
end

View File

@@ -0,0 +1,45 @@
# frozen_string_literal: true
require "test_helper"
module Admin
module Teachers
class ReactivationsControllerTest < ActionDispatch::IntegrationTest
setup do
@admin = create(:admin, admin: true)
sign_in(@admin)
@classroom1 = create(:classroom, name: "Math 101")
@teacher1 = create(:teacher, username: "teacher1", email: "teacher1@example.com")
@teacher1.classrooms << @classroom1
end
test "should reactivate deactivated teacher" do
@teacher1.discard
assert @teacher1.discarded?
post admin_teacher_reactivation_path(@teacher1)
assert_redirected_to admin_teachers_path
assert_equal "Teacher teacher1@example.com reactivated successfully.", flash[:notice]
assert_not @teacher1.reload.discarded?
end
test "reactivate should restore teacher to active status and appear in active list" do
@teacher1.discard
assert_not_nil @teacher1.reload.discarded_at
post admin_teacher_reactivation_path(@teacher1)
# Should clear discarded_at timestamp
assert_nil @teacher1.reload.discarded_at
follow_redirect!
# Should appear on the default (active) index
assert_select "##{dom_id(@teacher1)}", count: 1
end
end
end
end

View File

@@ -0,0 +1,294 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class TeachersControllerTest < ActionDispatch::IntegrationTest
test "index" do
teacher1 = create(:teacher, email: "z@example.com")
teacher2 = create(:teacher, email: "a@example.com")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_teachers_path
rows = css_select("tbody tr[id^='teacher_']")
row_ids = rows.pluck("id")
assert_response :success
assert_select "h3", "Teachers"
assert_equal [dom_id(teacher2), dom_id(teacher1)], row_ids
end
test "index sorts by username by default" do
create(:teacher, email: "teacher1@example.com")
create(:teacher, email: "teacher2@example.com")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_teachers_path
assert_response :success
# Default sort should be by username ascending (username == email for teachers)
assert_select "tbody tr:nth-child(1)", text: /teacher1/
assert_select "tbody tr:nth-child(2)", text: /teacher2/
end
test "index shows only active teachers by default" do
teacher1 = create(:teacher, username: "teacher1")
create(:teacher, username: "teacher2")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
teacher1.discard
get admin_teachers_path
assert_response :success
assert_select "tbody tr", count: 1
assert_select "span.bg-green-50", text: "Active"
end
test "index shows both active and deactivated teachers with all filter" do
teacher1 = create(:teacher, username: "teacher1")
create(:teacher, username: "teacher2")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
teacher1.discard
get admin_teachers_path(all: true)
assert_response :success
assert_select "tbody tr", count: 2
assert_select "span.bg-red-50", text: "Deactivated"
assert_select "span.bg-green-50", text: "Active"
end
test "index shows only deactivated teachers with discarded filter" do
teacher1 = create(:teacher, username: "teacher1")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
teacher1.discard
get admin_teachers_path(discarded: true)
assert_response :success
assert_select "tbody tr", count: 1
assert_select "span.bg-red-50", text: "Deactivated"
end
# Show tests
test "should show teacher" do
teacher = create(:teacher)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_teacher_path(teacher)
assert_response :success
assert_select "h2", teacher.username
end
test "should show teacher classrooms" do
classroom = create(:classroom, name: "Math 101")
teacher = create(:teacher)
teacher.classrooms << classroom
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_teacher_path(teacher)
assert_response :success
assert_select "h2", teacher.username
assert_select "h3", "Classrooms"
assert_select "li", text: classroom.name
end
test "show when teacher has no classrooms" do
teacher = create(:teacher)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_teacher_path(teacher)
assert_response :success
assert_select "p", text: "No classrooms assigned to this teacher yet."
end
test "new" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_teacher_path
assert_response :success
assert_select "h1", "New Teacher"
end
test "new shows warning message when no classrooms available" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_teacher_path
assert_response :success
assert_select "h1", "New Teacher"
assert_select "h3", text: "No Classrooms Available"
assert_select "p", text: /No Classrooms associated with this school and active year/
assert_select "a[href='#{admin_classrooms_path}']", text: "update classrooms"
end
test "new shows classrooms when available" do
current_year = Year.find_or_create_by(name: "2025 - 2026")
school = create(:school, name: "Test School")
school_year = create(:school_year, school: school, year: current_year)
create(:classroom, name: "Test Classroom", archived: false, school_year: school_year)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_teacher_path
assert_response :success
assert_select "input[type='checkbox'][name='teacher[classroom_ids][]']"
assert_select "label", text: "Test Classroom"
assert_select "h3", text: "No Classrooms Available", count: 0
end
test "create" do
classroom1 = create(:classroom, name: "Ice Kingdom")
classroom2 = create(:classroom, name: "Candy Kingdom")
params = {
teacher: {
username: "lsp",
email: "lsp@lumpyspace.com",
name: "Lumpy Space Princess",
classroom_ids: [classroom1.id, classroom2.id]
}
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_difference("Teacher.count") do
post(admin_teachers_path, params:)
end
teacher = Teacher.last
assert_redirected_to admin_teacher_path(teacher)
assert_equal(
"Teacher created successfully. Password reset email has been sent.",
flash[:notice]
)
assert_equal [classroom1, classroom2], teacher.classrooms
assert_not_nil teacher.reset_password_token
end
test "create with invalid params" do
params = {
teacher: {
username: "",
email: "invalid"
}
}
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("Teacher.count") do
post(admin_teachers_path, params:)
end
assert_response :unprocessable_content
end
test "edit" do
teacher = create(:teacher)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get edit_admin_teacher_path(teacher)
assert_response :success
assert_select "h1", "Edit Teacher"
end
test "update" do
name = "Lumpy Space Princess"
classroom1 = create(:classroom, name: "Ice Kingdom")
classroom2 = create(:classroom, name: "Candy Kingdom")
teacher = create(:teacher, classrooms: [classroom1])
params = { teacher: { name:, classroom_ids: [classroom2.id] } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_teacher_path(teacher), params:)
teacher.reload
assert_redirected_to admin_teacher_path(teacher)
assert_equal "Teacher updated successfully.", flash[:notice]
assert_equal name, teacher.name
assert_equal [classroom2], teacher.classrooms
end
test "update with invalid params" do
teacher = create(:teacher)
params = { teacher: { email: "" } }
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch(admin_teacher_path(teacher), params:)
assert_response :unprocessable_content
end
# Hard delete (destroy) tests
test "should permanently delete deactivated teacher" do
teacher = create(:teacher, email: "teacher1@example.com")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
teacher.discard
assert_difference("Teacher.with_discarded.count", -1) do
delete admin_teacher_path(teacher)
end
assert_redirected_to admin_teachers_path
assert_equal "Teacher teacher1@example.com permanently deleted.", flash[:notice]
assert_nil Teacher.with_discarded.find_by(id: teacher.id)
end
test "should not permanently delete active teacher" do
teacher = create(:teacher, username: "teacher1")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
assert_no_difference("Teacher.with_discarded.count") do
delete admin_teacher_path(teacher)
end
assert_redirected_to edit_admin_teacher_path(teacher)
assert_equal "Teacher must be deactivated before permanent deletion.", flash[:alert]
assert_not teacher.reload.discarded?
end
test "permanent delete should remove teacher from database and DOM" do
teacher = create(:teacher, username: "teacher1")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
teacher.discard
assert_difference("Teacher.with_discarded.count", -1) do
delete admin_teacher_path(teacher)
end
follow_redirect!
assert_select "##{dom_id(teacher)}", count: 0
end
end
end

View File

@@ -0,0 +1,144 @@
# frozen_string_literal: true
require "test_helper"
module Admin
class UsersControllerTest < ActionDispatch::IntegrationTest
test "index" do
create(:admin, username: "marceline")
create(:teacher, username: "bonnibel")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_users_path
assert_response :success
assert_select "h3", "Users"
assert_select "tbody tr", count: User.count
end
test "index sorts by username by default" do
create(:admin, username: "zzz_last")
create(:admin, username: "aaa_first")
admin = create(:admin, username: "mmm_admin", admin: true, classroom: nil)
sign_in(admin)
get admin_users_path
rows = css_select("tbody tr td:nth-child(2)").map(&:text).map(&:strip)
assert_response :success
assert_equal rows, rows.sort
end
test "index sorts descending when direction is desc" do
create(:admin, username: "zzz_last")
create(:admin, username: "aaa_first")
admin = create(:admin, username: "mmm_admin", admin: true, classroom: nil)
sign_in(admin)
get admin_users_path, params: { sort: "username", direction: "desc" }
rows = css_select("tbody tr td:nth-child(2)").map(&:text).map(&:strip)
assert_response :success
assert_equal rows, rows.sort.reverse
end
test "show" do
user = create(:admin, username: "finn")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get admin_user_path(user)
assert_response :success
assert_select "h2", "finn"
end
test "new" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get new_admin_user_path
assert_response :success
assert_select "h1", "New User"
end
test "edit" do
user = create(:admin)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
get edit_admin_user_path(user)
assert_response :success
assert_select "h1", "Edit User"
end
test "create" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
params = {
user: {
username: "bmo",
email: "bmo@example.com",
type: "User",
admin: false,
password: "Passw0rd",
password_confirmation: "Passw0rd"
}
}
assert_difference("User.count") do
post admin_users_path, params: params
end
assert_redirected_to admin_user_path(User.last)
assert_equal "User created successfully.", flash[:notice]
end
test "create with invalid params" do
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
params = { user: { username: "", email: "bad@example.com" } }
assert_no_difference("User.count") do
post admin_users_path, params: params
end
assert_response :unprocessable_content
end
test "update" do
user = create(:admin, username: "original")
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch admin_user_path(user), params: { user: { username: "updated" } }
user.reload
assert_redirected_to admin_user_path(user)
assert_equal "User updated successfully.", flash[:notice]
assert_equal "updated", user.username
end
test "update with invalid params" do
user = create(:admin)
admin = create(:admin, admin: true, classroom: nil)
sign_in(admin)
patch admin_user_path(user), params: { user: { username: "" } }
assert_response :unprocessable_content
end
test "non-admin cannot access index" do
sign_in(create(:teacher))
get admin_users_path
assert_redirected_to root_path
assert_equal "Access denied. Admin privileges required.", flash[:alert]
end
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
require "test_helper"
class AnnouncementsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = create(:student)
sign_in @user
@announcement = Announcement.create!(
title: "Test Announcement",
content: "Test content"
)
end
test "should show announcement" do
get announcement_url(@announcement)
assert_response :success
end
test "students can view announcements" do
get announcement_url(@announcement)
assert_response :success
assert_match @announcement.title, response.body
end
test "teachers can view announcements" do
sign_out @user
teacher = create(:teacher)
sign_in teacher
get announcement_url(@announcement)
assert_response :success
end
end

View File

@@ -0,0 +1,59 @@
# frozen_string_literal: true
require "test_helper"
class ApplicationControllerTest < ActionDispatch::IntegrationTest
test "after_sign_in_path_for redirects students to root" do
create(:student, username: "teststudent", password: "password123")
post user_session_path, params: {
user: { username: "teststudent", password: "password123" }
}
assert_redirected_to root_path
end
test "after_sign_in_path_for redirects teachers to root" do
create(:teacher, email: "testteacher@example.com", password: "password123")
post user_session_path, params: {
user: { username: "testteacher@example.com", password: "password123" }
}
assert_redirected_to root_path
end
test "after_sign_in_path_for redirects admins to root" do
create(:admin, username: "testadmin", password: "password123")
post user_session_path, params: {
user: { username: "testadmin", password: "password123" }
}
assert_redirected_to root_path
end
test "ensure_teacher_or_admin allows teachers" do
teacher = create(:teacher)
sign_in teacher
get classrooms_path
assert_response :success
end
test "ensure_teacher_or_admin allows admins" do
admin = create(:admin)
sign_in admin
get classrooms_path
assert_response :success
end
test "ensure_teacher_or_admin blocks students" do
student = create(:student)
sign_in student
get new_classroom_path
assert_redirected_to student.portfolio_path
end
end

View File

@@ -0,0 +1,87 @@
# frozen_string_literal: true
require "test_helper"
class ClassroomEnrollmentsControllerTest < ActionDispatch::IntegrationTest
setup do
@classroom = create(:classroom)
@student = create(:student, classroom: @classroom)
@teacher = create(:teacher)
@teacher.classrooms << @classroom
end
# create
test "create enrolls student in classroom" do
other_classroom = create(:classroom)
sign_in(@teacher)
assert_difference("ClassroomEnrollment.count") do
post classroom_classroom_enrollments_path(other_classroom),
params: { classroom_enrollment: { student_id: @student.id, primary: "false" } }
end
assert_redirected_to classroom_path(other_classroom)
assert_match @student.username, flash[:notice]
assert_match other_classroom.name, flash[:notice]
end
test "create enrolls student as primary" do
other_classroom = create(:classroom)
sign_in(@teacher)
post classroom_classroom_enrollments_path(other_classroom),
params: { classroom_enrollment: { student_id: @student.id, primary: "true" } }
enrollment = ClassroomEnrollment.last
assert enrollment.primary?
assert_redirected_to classroom_path(other_classroom)
end
# destroy
test "destroy removes enrollment" do
enrollment = create(:classroom_enrollment, classroom: @classroom, student: @student)
sign_in(@teacher)
assert_difference("ClassroomEnrollment.count", -1) do
delete classroom_classroom_enrollment_path(@classroom, enrollment)
end
assert_redirected_to classroom_path(@classroom)
assert_match @student.username, flash[:notice]
assert_match @classroom.name, flash[:notice]
end
# unenroll
test "unenroll sets unenrolled_at on enrollment" do
enrollment = create(:classroom_enrollment, classroom: @classroom, student: @student)
sign_in(@teacher)
patch unenroll_classroom_classroom_enrollment_path(@classroom, enrollment)
assert enrollment.reload.historical?
assert_redirected_to classroom_path(@classroom)
assert_match @student.username, flash[:notice]
end
# authorization
test "student cannot create enrollment" do
other_classroom = create(:classroom)
sign_in(@student)
post classroom_classroom_enrollments_path(other_classroom),
params: { classroom_enrollment: { student_id: @student.id, primary: "false" } }
assert_equal "You do not have access to this page.", flash[:alert]
end
test "unauthenticated user is redirected to sign in" do
post classroom_classroom_enrollments_path(@classroom),
params: { classroom_enrollment: { student_id: @student.id, primary: "false" } }
assert_redirected_to new_user_session_path
end
end

View File

@@ -0,0 +1,405 @@
# frozen_string_literal: true
require "test_helper"
class ClassroomsControllerTest < ActionDispatch::IntegrationTest
def setup
@admin = create(:admin)
@classroom = create(:classroom)
@teacher = create(:teacher)
@teacher.classrooms << @classroom
@student = create(:student, classroom: @classroom)
end
test "index" do
sign_in(@admin)
get classrooms_path
assert_response :success
end
test "new" do
sign_in(@admin)
get new_classroom_path
assert_response :success
end
test "create" do
sign_in(@admin)
school = create(:school)
year = create(:year)
grade = create(:grade, level: 7)
params = {
classroom: {
name: "Test Class",
grade_ids: [grade.id],
school_id: school.id,
year_id: year.id
}
}
assert_difference("Classroom.count") do
post(classrooms_path, params:)
end
classroom = Classroom.last
assert_redirected_to classroom_path(classroom)
assert_includes classroom.grades, grade
assert_equal t("classrooms.create.notice"), flash[:notice]
end
test "create with multiple grades" do
sign_in(@admin)
school = create(:school)
year = create(:year)
grade1 = create(:grade)
grade2 = create(:grade)
grade3 = create(:grade)
params = {
classroom: {
name: "Combined Class",
grade_ids: [grade1.id, grade2.id, grade3.id],
school_id: school.id,
year_id: year.id
}
}
assert_difference("Classroom.count", 1) do
assert_difference("ClassroomGrade.count", 3) do
post classrooms_path, params: params
end
end
classroom = Classroom.last
assert_redirected_to classroom_path(classroom)
assert_equal [grade1, grade2, grade3].sort, classroom.grades.sort
end
test "show" do
sign_in(@admin)
get classroom_path(@classroom)
assert_response :success
end
test "edit" do
sign_in(@admin)
get edit_classroom_path(@classroom)
assert_response :success
end
test "update" do
school = create(:school)
year = create(:year)
grade = create(:grade, level: 6)
params = { classroom: { name: "Abc123", grade_ids: [grade.id], school_id: school.id, year_id: year.id } }
sign_in(@admin)
assert_changes "@classroom.reload.updated_at" do
patch(classroom_path(@classroom), params:)
end
assert_redirected_to classroom_path(@classroom)
assert_equal t("classrooms.update.notice"), flash[:notice]
@classroom.reload
assert_includes @classroom.grades, grade
end
test "update replaces grades correctly" do
old_grade = create(:grade)
create(:classroom_grade, classroom: @classroom, grade: old_grade)
new_grade1 = create(:grade)
new_grade2 = create(:grade)
sign_in(@admin)
params = {
classroom: {
name: "Updated Name",
grade_ids: [new_grade1.id, new_grade2.id]
}
}
patch classroom_path(@classroom), params: params
@classroom.reload
assert_equal [new_grade1, new_grade2].sort, @classroom.grades.sort
assert_not_includes @classroom.grades, old_grade
end
test "archive classroom" do
sign_in(@admin)
assert_no_difference("Classroom.count") do
patch toggle_archive_admin_classroom_path(@classroom)
end
assert_redirected_to admin_classroom_path(@classroom)
assert @classroom.reload.archived?
assert_equal "Classroom has been archived.", flash[:notice]
end
test "activate classroom" do
sign_in(@admin)
@classroom.update!(archived: true)
patch toggle_archive_admin_classroom_path(@classroom)
assert_redirected_to admin_classroom_path(@classroom)
assert_not @classroom.reload.archived?
assert_equal "Classroom has been activated.", flash[:notice]
end
test "admins can see all classrooms in index" do
classroom1 = create(:classroom, name: "Class 1")
classroom2 = create(:classroom, name: "Class 2")
sign_in(@admin)
get classrooms_path
assert_response :success
assert_includes response.body, classroom1.name
assert_includes response.body, classroom2.name
end
test "show includes student management for teachers" do
create(:portfolio, user: @student)
# @teacher.classrooms << @classroom
sign_in @teacher
get classroom_path(@classroom)
assert_response :success
assert_includes response.body, @student.username
end
test "teachers can only see their own classroom in index" do
classroom1 = create(:classroom, name: "Teacher 1 Class")
classroom2 = create(:classroom, name: "Teacher 2 Class")
teacher = create(:teacher, classroom: classroom1)
teacher.classrooms << classroom1
sign_in teacher
get classrooms_path
assert_response :success
assert_includes response.body, classroom1.name
assert_not_includes response.body, classroom2.name
end
test "teachers cannot see archived classrooms in index" do
classroom1 = create(:classroom, name: "Active Class")
classroom2 = create(:classroom, name: "Archived Class", archived: true)
teacher = create(:teacher)
teacher.classrooms << classroom1
teacher.classrooms << classroom2
sign_in teacher
get classrooms_path
assert_response :success
assert_includes response.body, classroom1.name
assert_not_includes response.body, classroom2.name
end
test "teachers can only access details of their own classroom" do
classroom1 = create(:classroom, name: "Teacher 1 Class")
classroom2 = create(:classroom, name: "Teacher 2 Class")
teacher = create(:teacher)
teacher.classrooms << classroom1
sign_in teacher
get classroom_path(classroom1)
assert_response :success
assert_includes response.body, classroom1.name
get classroom_path(classroom2)
assert_redirected_to root_path
end
test "students cannot see classrooms" do
sign_in @student
get classrooms_path
assert_redirected_to @student.portfolio_path
end
test "students cannot see details of a classroom" do
sign_in @student
classroom1 = create(:classroom, name: "Teacher 1 Class")
get classroom_path(id: classroom1.id)
assert_redirected_to @student.portfolio_path
end
test "students cannot create or edit classrooms" do
sign_in @student
get new_classroom_path
assert_redirected_to @student.portfolio_path
get edit_classroom_path(@classroom)
assert_redirected_to @student.portfolio_path
end
test "teachers cannot create classrooms" do
sign_in @teacher
get new_classroom_path
assert_redirected_to root_path
end
test "teachers can toggle trading for their own classrooms" do
sign_in @teacher
assert_not @classroom.trading_enabled
patch toggle_trading_classroom_path(@classroom)
assert_redirected_to classroom_path(@classroom)
assert @classroom.reload.trading_enabled
end
test "teachers cannot edit classrooms" do
sign_in @teacher
get edit_classroom_path(@classroom)
assert_redirected_to root_path
end
test "teachers cannot toggle trading for other teachers' classrooms" do
other_classroom = create(:classroom, name: "Other Class")
sign_in @teacher
patch toggle_trading_classroom_path(other_classroom)
assert_redirected_to root_path
end
test "teachers cannot archive classrooms" do
sign_in @teacher
# Teachers shouldn't be able to archive
patch toggle_archive_admin_classroom_path(@classroom)
assert_redirected_to root_path
end
# Tests for dropdown functionality
test "new action renders form with school and year dropdowns" do
school1 = create(:school, name: "Elementary School")
school2 = create(:school, name: "High School")
year1 = create(:year, name: "2023-2024")
year2 = create(:year, name: "2024-2025")
sign_in(@admin)
get new_classroom_path
assert_response :success
assert_select "select[name='classroom[school_id]']" do
assert_select "option[value='#{school1.id}']", text: school1.name
assert_select "option[value='#{school2.id}']", text: school2.name
end
assert_select "select[name='classroom[year_id]']" do
assert_select "option[value='#{year1.id}']", text: year1.name
assert_select "option[value='#{year2.id}']", text: year2.name
end
end
test "edit action renders form with school and year dropdowns" do
school1 = create(:school, name: "Elementary School")
school2 = create(:school, name: "High School")
year1 = create(:year, name: "2023-2024")
year2 = create(:year, name: "2024-2025")
sign_in(@admin)
get edit_classroom_path(@classroom)
assert_response :success
assert_select "select[name='classroom[school_id]']" do
assert_select "option[value='#{school1.id}']", text: school1.name
assert_select "option[value='#{school2.id}']", text: school2.name
end
assert_select "select[name='classroom[year_id]']" do
assert_select "option[value='#{year1.id}']", text: year1.name
assert_select "option[value='#{year2.id}']", text: year2.name
end
end
test "create with new school_year creates quarters and gradebooks" do
school = create(:school)
year = create(:year)
grade = create(:grade)
sign_in(@admin)
params = { classroom: { name: "New Class", grade_ids: [grade.id], school_id: school.id, year_id: year.id } }
assert_difference("Quarter.count", 4) do
assert_difference("GradeBook.count", 4) do
post(classrooms_path, params:)
end
end
classroom = Classroom.last
assert_equal 4, classroom.grade_books.count
end
test "create with existing school_year uses existing quarters for gradebooks" do
school = create(:school)
year = create(:year)
grade = create(:grade)
school_year = SchoolYear.create!(school: school, year: year)
sign_in(@admin)
params = { classroom: { name: "New Class", grade_ids: [grade.id], school_id: school.id, year_id: year.id } }
assert_no_difference("Quarter.count") do
assert_difference("GradeBook.count", 4) do
post(classrooms_path, params:)
end
end
classroom = Classroom.last
assert_equal school_year, classroom.school_year
assert_equal 4, classroom.grade_books.count
end
test "create with valid school_id and year_id creates classroom with correct school_year" do
school = create(:school, name: "Test School")
year = create(:year, name: "2024-2025")
grade = create(:grade)
sign_in(@admin)
params = { classroom: { name: "Test Class", grade_ids: [grade.id], school_id: school.id, year_id: year.id } }
assert_difference("Classroom.count") do
post(classrooms_path, params:)
end
classroom = Classroom.last
assert_equal school, classroom.school
assert_equal year, classroom.year
end
test "update with valid school_id and year_id updates classroom school_year" do
new_school = create(:school, name: "New School")
new_year = create(:year, name: "2025-2026")
sign_in(@admin)
params = { classroom: { name: "Updated Class", school_id: new_school.id, year_id: new_year.id } }
patch(classroom_path(@classroom), params:)
@classroom.reload
assert_equal new_school, @classroom.school
assert_equal new_year, @classroom.year
end
test "edit form shows current school and year selected" do
school = create(:school, name: "Current School")
year = create(:year, name: "Current Year")
school_year = create(:school_year, school: school, year: year)
classroom = create(:classroom, school_year: school_year)
sign_in(@admin)
get edit_classroom_path(classroom)
assert_response :success
assert_select "select[name='classroom[school_id]']" do
assert_select "option[value='#{school.id}'][selected='selected']", text: school.name
end
assert_select "select[name='classroom[year_id]']" do
assert_select "option[value='#{year.id}'][selected='selected']", text: year.name
end
end
test "index page includes link to classroom show page" do
sign_in(@admin)
classroom = create(:classroom, name: "Linked Class")
get classrooms_path
assert_response :success
assert_select "a[href='#{classroom_path(classroom)}']", text: "Linked Class"
end
end

View File

@@ -0,0 +1,209 @@
# frozen_string_literal: true
require "test_helper"
class GradeBooksControllerTest < ActionDispatch::IntegrationTest
def setup
@classroom = create(:classroom)
@teacher = create(:teacher)
@teacher.classrooms << @classroom
@student = create(:student, classroom: @classroom)
@first_quarter = @classroom.school_year.quarters.find_by!(number: 1)
@second_quarter = @classroom.school_year.quarters.find_by!(number: 2)
@grade_book = @classroom.grade_books.find_by!(quarter: @first_quarter)
create(:grade_entry, grade_book: @grade_book, user: @student)
end
test "show" do
sign_in(@teacher)
get classroom_grade_book_path(@classroom, @grade_book)
assert_response :success
end
test "update" do
sign_in(@teacher)
entry = @grade_book.grade_entries.first
params = {
grade_entries: {
entry.id => {
math_grade: "A",
reading_grade: "B",
attendance_days: 30
}
}
}
patch classroom_grade_book_path(@classroom, @grade_book), params: params
assert_redirected_to classroom_grade_book_path(@classroom, @grade_book)
entry.reload
assert_equal "A", entry.math_grade
assert_equal "B", entry.reading_grade
assert_equal 30, entry.attendance_days
end
test "students cannot access grade book" do
sign_in(@student)
get classroom_grade_book_path(@classroom, @grade_book)
assert_redirected_to @student.portfolio_path
end
test "teachers cannot finalize a grade book" do
DistributeEarnings.expects(:execute).never
sign_in(@teacher)
post finalize_classroom_grade_book_path(@classroom, @grade_book)
assert_redirected_to root_path
@grade_book.reload
assert_not @grade_book.verified?
end
test "finalize runs the DistributeFunds service" do
DistributeEarnings.expects(:execute).with(@grade_book).once
sign_in(create(:admin))
# Fill out all entries to make the grade book finalizable
@grade_book.grade_entries.each do |entry|
entry.update!(math_grade: "A", reading_grade: "B", attendance_days: 30)
end
post finalize_classroom_grade_book_path(@classroom, @grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, @grade_book)
@grade_book.reload
assert @grade_book.verified?
end
test "finalize grade book with incomplete grade entry and no previous quarter" do
sign_in(create(:admin))
@grade_book.grade_entries.first.update!(
math_grade: nil, reading_grade: nil, attendance_days: 30
)
post finalize_classroom_grade_book_path(@classroom, @grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, @grade_book)
assert_equal "Grade book finalized. Funds have been distributed.", flash[:notice]
@grade_book.reload
assert @grade_book.completed?
end
test "finalize grade book with incomplete grade entry (empty string) and no previous quarter" do
sign_in(create(:admin))
@grade_book.grade_entries.first.update!(
math_grade: "", reading_grade: "", attendance_days: 30
)
post finalize_classroom_grade_book_path(@classroom, @grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, @grade_book)
assert_equal "Grade book finalized. Funds have been distributed.", flash[:notice]
@grade_book.reload
assert @grade_book.completed?
end
test "finalize grade book with complete grade entry and no previous quarter" do
sign_in(create(:admin))
@grade_book.grade_entries.first.update!(
math_grade: "A", reading_grade: "B", attendance_days: 30
)
post finalize_classroom_grade_book_path(@classroom, @grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, @grade_book)
assert_equal "Grade book finalized. Funds have been distributed.", flash[:notice]
@grade_book.reload
assert @grade_book.completed?
end
# Scenario 2: Finalize grade book with previous quarter grade book
test "finalize grade book with incomplete grade entry and previous quarter exists" do
sign_in(create(:admin))
# Mark first quarter as completed (acts as previous quarter)
@grade_book.update!(status: :completed)
@grade_book.grade_entries.first.update!(
math_grade: "A", reading_grade: "B", attendance_days: 30
)
# Create new grade book for second quarter with incomplete grades
new_grade_book = @classroom.grade_books.find_by!(quarter: @second_quarter)
create(
:grade_entry, grade_book: new_grade_book, user: @student,
math_grade: nil, reading_grade: nil, attendance_days: 30
)
post finalize_classroom_grade_book_path(@classroom, new_grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, new_grade_book)
assert_equal "Grade book finalized. Funds have been distributed.", flash[:notice]
new_grade_book.reload
assert new_grade_book.completed?
end
test "finalize grade book with complete grade entry and previous quarter exists" do
sign_in(create(:admin))
# Mark first quarter as completed (acts as previous quarter)
@grade_book.update!(status: :completed)
@grade_book.grade_entries.first.update!(
math_grade: "B", reading_grade: nil, attendance_days: 30
)
# Create new grade book for second quarter with complete grades
new_grade_book = @classroom.grade_books.find_by!(quarter: @second_quarter)
create(
:grade_entry, grade_book: new_grade_book, user: @student,
math_grade: "A", reading_grade: "A", attendance_days: 30
)
post finalize_classroom_grade_book_path(@classroom, new_grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, new_grade_book)
assert_equal "Grade book finalized. Funds have been distributed.", flash[:notice]
new_grade_book.reload
assert new_grade_book.completed?
end
test "finalize grade book with complete grade entry and previous quarter exists (empty string)" do
sign_in(create(:admin))
# Mark first quarter as completed (acts as previous quarter)
@grade_book.update!(status: :completed)
@grade_book.grade_entries.first.update!(
math_grade: "", reading_grade: "", attendance_days: 30
)
# Create new grade book for second quarter with complete grades
new_grade_book = @classroom.grade_books.find_by!(quarter: @second_quarter)
create(
:grade_entry, grade_book: new_grade_book, user: @student,
math_grade: "A", reading_grade: "A", attendance_days: 30
)
post finalize_classroom_grade_book_path(@classroom, new_grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, new_grade_book)
assert_equal "Grade book finalized. Funds have been distributed.", flash[:notice]
new_grade_book.reload
assert new_grade_book.completed?
end
test "does not finalize already completed grade book" do
sign_in(create(:admin))
@grade_book.update!(status: :completed)
post finalize_classroom_grade_book_path(@classroom, @grade_book)
assert_redirected_to classroom_grade_book_path(@classroom, @grade_book)
assert_equal "Cannot finalize because it's already completed.", flash[:alert]
end
end

View File

@@ -0,0 +1,130 @@
# frozen_string_literal: true
require "test_helper"
class HomeControllerTest < ActionDispatch::IntegrationTest
test "should redirect to sign in when not authenticated" do
get root_url
assert_redirected_to new_user_session_url
end
test "should get index when authenticated" do
sign_in create(:student)
get root_url
assert_response :success
end
test "index page has expected content when authenticated" do
sign_in create(:student)
get root_url
assert_response :success
assert_select "title", /StocksInTheFuture/i
end
test "index responds with HTML when authenticated" do
sign_in create(:student)
get root_url
assert_response :success
assert_equal "text/html; charset=utf-8", response.content_type
end
test "should show current announcement on home page" do
sign_in create(:student)
announcement = Announcement.create!(
title: "Welcome Students!",
content: "This is an important announcement for all students.",
featured: true
)
get root_url
assert_response :success
assert_select "h3", text: "Welcome Students!"
assert_select "a[href=?]", announcement_path(announcement), text: "Read More", count: 0
end
test "should show featured announcement when multiple exist" do
sign_in create(:student)
Announcement.create!(
title: "Old Announcement",
content: "This is old news.",
created_at: 2.days.ago,
featured: false
)
Announcement.create!(
title: "Featured News",
content: "This is the featured announcement.",
featured: true
)
get root_url
assert_response :success
assert_select "h3", text: "Featured News"
assert_no_match(/Old Announcement/, response.body)
end
test "should show no announcements message when none exist" do
sign_in create(:student)
get root_url
assert_response :success
assert_select "p", text: "No announcements yet."
end
test "should show full announcement content in scrollable box" do
sign_in create(:student)
long_content = "This is a long announcement content that is displayed in full in a scrollable box. " * 5
Announcement.create!(
title: "Long Announcement",
content: long_content,
featured: true
)
get root_url
assert_response :success
assert_select "h3", text: "Long Announcement"
assert_select "a", text: "Read More", count: 0
end
test "should show positive balance message when student has money" do
student = create(:student)
portfolio = student.portfolio
create(:portfolio_transaction, :deposit, portfolio: portfolio, amount_cents: 5000) # $50.00
sign_in student
get root_url
assert_response :success
assert_select "span", text: /You have.*\$50\.00.*to invest! Lets Get Trading!/
assert_select "img[alt='Party popper celebration']"
end
test "should show no earnings message when student has zero balance" do
student = create(:student)
sign_in student
get root_url
assert_response :success
assert_select "span", text: "Sorry, you don't have any earnings to invest yet"
assert_no_match(/to invest! Lets Get Trading!/, response.body)
end
test "should not show balance message for non-student users" do
teacher = create(:teacher)
sign_in teacher
get root_url
assert_response :success
assert_no_match(/You have.*to invest/, response.body)
assert_no_match(/Sorry, you don't have any earnings/, response.body)
end
end

View File

@@ -0,0 +1,169 @@
# frozen_string_literal: true
require "test_helper"
class OrdersControllerTest < ActionDispatch::IntegrationTest
test "index" do
user = create(:teacher)
sign_in(user)
get orders_path
assert_response :success
end
test "index as guest" do
get orders_path
assert_redirected_to new_user_session_path
end
test "new" do
user = create(:student)
stock = create(:stock)
sign_in(user)
get new_order_path(stock_id: stock.id, transaction_type: "buy")
assert_response :success
end
test "create" do
student = create(:student)
student.portfolio.portfolio_transactions.create!(amount_cents: 10_000, transaction_type: :deposit) # $100
stock = create(:stock, price_cents: 1_000) # $10 per share
params = { order: { user_id: student.id, stock_id: stock.id, shares: 1, action: "buy" } }
sign_in(student)
assert_difference("Order.count", 1) do
post(orders_path, params:)
end
assert_redirected_to orders_path
end
test "create with invalid shares handles validation errors" do
student = create(:student)
student.portfolio.portfolio_transactions.create!(amount_cents: 10_000, transaction_type: :deposit)
stock = create(:stock, price_cents: 1_000)
sign_in(student)
[nil, "", "not_a_number"].each do |invalid_shares|
buy_params = { order: { stock_id: stock.id, shares: invalid_shares, action: :buy } }
sell_params = { order: { stock_id: stock.id, shares: invalid_shares, action: :sell } }
assert_no_difference("Order.count") do
assert_nothing_raised do
post(orders_path, params: buy_params)
assert_response :unprocessable_content
post(orders_path, params: sell_params)
assert_response :unprocessable_content
end
end
end
end
test "edit" do
user = create(:student)
stock = create(:stock)
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
order = create(:order, action: :sell, user: user, stock: stock, shares: 1)
sign_in(order.user)
get edit_order_path(order)
assert_response :success
end
test "update" do
user = create(:student)
stock = create(:stock)
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
params = { order: { shares: 3 } }
order = create(:order, :pending, action: :sell, user: user, stock: stock, shares: 1)
sign_in(user)
assert_changes "order.reload.updated_at" do
patch(order_path(order), params:)
end
assert_redirected_to orders_path
assert order.shares, 3
end
test "update with invalid shares handles validation errors" do
user = create(:student)
stock = create(:stock, price_cents: 1_000)
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
order = create(:order, :pending, action: :sell, user: user, stock: stock, shares: 5)
sign_in(user)
[nil, "", "not_a_number", -1, 0].each do |invalid_shares|
params = { order: { shares: invalid_shares } }
assert_no_changes "order.reload.shares" do
assert_nothing_raised do
patch(order_path(order), params: params)
assert_response :unprocessable_content
end
end
end
end
test "cancel" do
user = create(:student)
stock = create(:stock)
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
order = create(:order, :pending, action: :sell, user: user, stock: stock, shares: 1)
sign_in(order.user)
assert_difference("Order.pending.count", -1) do
assert_difference("Order.canceled.count", 1) do
patch(cancel_order_path(order))
end
end
assert_redirected_to orders_path
assert_equal "Order was successfully canceled", flash[:notice]
end
test "cancel route exists" do
assert_routing(
{ path: "orders/1/cancel", method: "patch" },
{ controller: "orders", action: "cancel", id: "1" }
)
end
test "cancel with unauthorized user" do
user = create(:student)
stock = create(:stock)
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
order = create(:order, :pending, action: :sell, user: user, stock: stock, shares: 1)
unauthorized_user = create(:student)
sign_in(unauthorized_user)
assert_no_difference("Order.count") do
patch(cancel_order_path(order))
end
assert_response :redirect
assert_equal "You do not have access to this page.", flash[:alert]
end
test "cancel with non-pending order" do
user = create(:student)
stock = create(:stock)
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
order = create(:order, :completed, action: :sell, user: user, stock: stock, shares: 1)
sign_in(order.user)
assert_no_difference("Order.count") do
patch(cancel_order_path(order))
end
assert_response :redirect
assert_equal "Only pending orders can be canceled", flash[:alert]
end
end

View File

@@ -0,0 +1,64 @@
# frozen_string_literal: true
require "test_helper"
class PortfoliosControllerTest < ActionDispatch::IntegrationTest
test "show with empty portfolio" do
portfolio = create(:portfolio)
sign_in(portfolio.user)
get portfolio_path(portfolio)
assert_response :success
end
test "show with portfolio positions renders" do
portfolio = create(:portfolio)
stock = create(:stock, price_cents: 15_000, yesterday_price_cents: 14_500)
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10)
sign_in(portfolio.user)
get portfolio_path(portfolio)
assert_response :success
end
test "trade button links to trading floor" do
portfolio = create(:portfolio)
stock = create(:stock)
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5)
sign_in(portfolio.user)
get portfolio_path(portfolio)
assert_response :success
assert_select "a[href='#{stocks_path}']", text: "Trade"
end
test "show displays empty state when less than 2 snapshots" do
portfolio = create(:portfolio)
create(:portfolio_snapshot, portfolio: portfolio, date: Date.current, worth_cents: 10_000)
sign_in(portfolio.user)
get portfolio_path(portfolio)
assert_response :success
assert_select ".text-gray-600", text: "Not Enough Data"
end
test "show displays chart when 2 or more snapshots exist" do
portfolio = create(:portfolio)
create(:portfolio_snapshot, portfolio: portfolio, date: 1.month.ago.to_date, worth_cents: 10_000)
create(:portfolio_snapshot, portfolio: portfolio, date: Date.current, worth_cents: 15_000)
sign_in(portfolio.user)
get portfolio_path(portfolio)
assert_response :success
assert_select "canvas[data-controller='portfolio-chart']"
end
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
require "test_helper"
class SchoolsControllerTest < ActionDispatch::IntegrationTest
setup do
@school = create(:school)
@admin_user = create(:admin)
sign_in @admin_user
end
test "should get index" do
get admin_schools_url
assert_response :success
assert_select "title", /Admin/i
end
test "should get new" do
get new_admin_school_url
assert_response :success
assert_select "form"
end
test "should create school" do
assert_difference("School.count") do
post admin_schools_url, params: { school: { name: "New Test School" } }
end
assert_redirected_to admin_school_url(School.last)
assert_equal "New Test School", School.last.name
end
test "should show school" do
get admin_school_url(@school)
assert_response :success
assert_select "h2", /#{@school.name}/
end
test "should get edit" do
get edit_admin_school_url(@school)
assert_response :success
assert_select "form"
end
test "should update school" do
patch admin_school_url(@school), params: { school: { name: "Updated School Name" } }
assert_redirected_to admin_school_url(@school)
@school.reload
assert_equal "Updated School Name", @school.name
end
test "should destroy school" do
assert_difference("School.count", -1) do
delete admin_school_url(@school)
end
assert_redirected_to admin_schools_url
end
test "requires authentication" do
sign_out @admin_user
get admin_schools_url
assert_redirected_to new_user_session_url
end
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
require "test_helper"
class StocksControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
admin = create(:admin)
sign_in admin
get stocks_url
assert_response :success
end
test "should get show" do
stock = create(:stock)
admin = create(:admin)
sign_in admin
get stock_url(stock)
assert_response :success
end
test "trade stock button links to trading floor for students" do
stock = create(:stock)
student = create(:student)
create(:portfolio, user: student)
sign_in student
get stock_url(stock)
assert_select "a[href='#{stocks_path}']", text: "Trade Stock"
end
end

View File

@@ -0,0 +1,131 @@
# frozen_string_literal: true
require "test_helper"
class StudentsControllerTest < ActionDispatch::IntegrationTest
setup do
@classroom = create(:classroom)
@teacher = create(:teacher, classroom: @classroom)
@student = create(:student, classroom: @classroom)
@other_classroom = create(:classroom)
@other_student = create(:student, classroom: @other_classroom)
end
test "students cannot access student management actions" do
sign_in @student
get new_classroom_student_path(@classroom)
assert_redirected_to @student.portfolio_path
get edit_classroom_student_path(@classroom, @student)
assert_redirected_to @student.portfolio_path
patch classroom_student_path(@classroom, @student), params: { student: { username: "hacked" } }
assert_redirected_to @student.portfolio_path
assert_no_difference("User.kept.count") do
delete classroom_student_path(@classroom, @student)
end
assert_redirected_to @student.portfolio_path
end
test "teacher can create student in their classroom" do
sign_in @teacher
assert_difference("User.count") do
assert_difference("Student.count") do
post classroom_students_path(@classroom), params: {
student: {
username: "newstudent",
email: "newstudent@example.com"
}
}
end
end
student = Student.last
assert_equal @classroom, student.classroom
assert_not_nil student.encrypted_password
assert_redirected_to classroom_path(@classroom)
assert_match(/created successfully/, flash[:notice])
end
test "student creation creates portfolio automatically" do
sign_in @teacher
assert_difference("Portfolio.count") do
post classroom_students_path(@classroom), params: {
student: {
username: "newstudent",
email: "newstudent@example.com"
}
}
end
student = Student.last
assert_not_nil student.portfolio
assert_equal 0, student.portfolio.calculate_total_value
end
test "student creation generates memorable password" do
sign_in @teacher
post classroom_students_path(@classroom), params: {
student: {
username: "newstudent",
email: "newstudent@example.com"
}
}
assert_match(/password:/, flash[:notice].downcase)
end
test "teacher can update student in their classroom" do
sign_in @teacher
patch classroom_student_path(@classroom, @student), params: {
student: {
username: "updatedname",
email: "updated@example.com"
}
}
@student.reload
assert_equal "updatedname", @student.username
assert_equal "updated@example.com", @student.email
assert_redirected_to classroom_path(@classroom)
end
test "teacher can delete student from their classroom" do
sign_in @teacher
assert_difference("User.kept.count", -1) do
delete classroom_student_path(@classroom, @student)
end
assert_redirected_to classroom_path(@classroom)
assert_match(/deleted successfully/, flash[:notice])
assert @student.reload.discarded?
end
test "teacher can reset student password" do
sign_in @teacher
original_password = @student.encrypted_password
patch reset_password_classroom_student_path(@classroom, @student)
@student.reload
assert_not_equal original_password, @student.encrypted_password
assert_redirected_to classroom_path(@classroom)
assert_match(/password reset/i, flash[:notice])
assert_match(/new password:/i, flash[:notice])
end
test "password reset generates memorable password" do
sign_in @teacher
patch reset_password_classroom_student_path(@classroom, @student)
assert_match(/new password: \w+\d+/i, flash[:notice])
end
end

View File

@@ -0,0 +1,176 @@
# frozen_string_literal: true
require "test_helper"
class TeachersControllerTest < ActionDispatch::IntegrationTest
test "index" do
admin = create(:admin)
sign_in(admin)
get admin_teachers_path
assert_response :success
end
test "new" do
admin = create(:admin)
sign_in(admin)
get new_admin_teacher_path
assert_response :success
end
test "show" do
teacher = create(:teacher)
admin = create(:admin)
sign_in(admin)
get admin_teacher_path(teacher)
assert_response :success
end
test "edit" do
teacher = create(:teacher)
admin = create(:admin)
sign_in(admin)
get edit_admin_teacher_path(teacher)
assert_response :success
end
test "update" do
teacher = create(:teacher)
admin = create(:admin)
sign_in(admin)
params = { teacher: { name: "Jane Smith" } }
assert_changes "teacher.reload.updated_at" do
patch admin_teacher_path(teacher), params: params
end
assert_redirected_to admin_teacher_path(teacher)
end
test "create creates a teacher, associates classroom, sends reset email, and redirects with notice" do
admin = create(:admin)
classroom = create(:classroom)
sign_in(admin)
ActionMailer::Base.deliveries.clear
params = {
teacher: {
email: "t1@example.com",
username: "teacher1",
classroom_ids: [classroom.id]
}
}
assert_difference %w[Teacher.count ActionMailer::Base.deliveries.size], 1 do
post admin_teachers_path, params: params
end
teacher = Teacher.find_by!(email: "t1@example.com")
assert_redirected_to admin_teacher_path(teacher)
assert_not_nil flash[:notice]
assert_includes teacher.classrooms, classroom
teacher.reload
assert_not_nil teacher.reset_password_sent_at, "reset_password_instructions should be sent"
assert teacher.encrypted_password.present?, "temp password should be set"
end
test "create with no classroom id still creates teacher, and does not associate classroom" do
admin = create(:admin)
sign_in(admin)
ActionMailer::Base.deliveries.clear
params = {
teacher: {
email: "t2@example.com",
username: "teacher2"
}
}
assert_difference %w[Teacher.count ActionMailer::Base.deliveries.size], 1 do
post admin_teachers_path, params: params
end
teacher = Teacher.find_by!(email: "t2@example.com")
assert_redirected_to admin_teacher_path(teacher)
assert_equal 0, teacher.classrooms.count, "no classroom should be associated"
end
test "create sends email with password reset link" do
admin = create(:admin)
sign_in(admin)
params = {
teacher: {
email: "newteacher@example.com",
username: "newteacher"
}
}
emails = capture_emails do
post admin_teachers_path, params: params
end
assert_equal 1, emails.size
email = emails.first
assert_equal ["newteacher@example.com"], email.to
assert_match(/reset_password_token=/, email.body.encoded)
assert_match(/Change my password/, email.body.encoded)
end
test "teacher can set password via reset link from creation email" do
admin = create(:admin)
sign_in(admin)
ActionMailer::Base.deliveries.clear
post admin_teachers_path, params: {
teacher: { email: "resettest@example.com", username: "resettest" }
}
email = ActionMailer::Base.deliveries.last
token = email.body.encoded.match(/reset_password_token=([^"]+)/)[1]
sign_out(admin)
# Teacher follows the link from the email
get edit_user_password_path(reset_password_token: token)
assert_response :success
# Teacher sets a new password
put user_password_path, params: {
user: {
reset_password_token: token,
password: "newpassword123",
password_confirmation: "newpassword123"
}
}
teacher = Teacher.find_by!(email: "resettest@example.com")
assert teacher.valid_password?("newpassword123")
end
test "create with invalid params renders unprocessable_entity and does not create teacher" do
admin = create(:admin)
sign_in(admin)
params = {
teacher: {
email: ""
}
}
assert_no_difference "Teacher.count" do
post admin_teachers_path, params: params
end
assert_response :unprocessable_entity
end
end