chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
include Devise::Test::IntegrationHelpers
|
||||
|
||||
# You can run the system test in an actual browser for visually validate the system test.
|
||||
# eg. TEST_DRIVER=chrome rails test test/system/student_trading_flow_test.rb
|
||||
# note: it does NOT work with Docker (needs standalone chrome + VNC)
|
||||
driven_by :selenium,
|
||||
using: ENV.fetch("TEST_DRIVER", "headless_chrome").to_sym,
|
||||
screen_size: [1400, 1400] do |driver_option|
|
||||
driver_option.add_argument("--no-sandbox")
|
||||
driver_option.add_argument("--disable-dev-shm-usage")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def auto_accept_confirmations
|
||||
page.execute_script("window.confirm = () => true")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
module ApplicationCable
|
||||
class ConnectionTest < ActionCable::Connection::TestCase
|
||||
# test "connects with cookies" do
|
||||
# cookies.signed[:user_id] = 42
|
||||
#
|
||||
# connect
|
||||
#
|
||||
# assert_equal connection.user_id, "42"
|
||||
# end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ScheduleTest < ActiveSupport::TestCase
|
||||
test "recurring configuration file exists and is valid" do
|
||||
recurring_file = Rails.root.join("config/recurring.yml")
|
||||
assert File.exist?(recurring_file), "config/recurring.yml should exist"
|
||||
|
||||
assert_nothing_raised do
|
||||
config = YAML.load_file(recurring_file, aliases: true)
|
||||
assert_not_nil config, "recurring.yml should contain valid YAML configuration"
|
||||
end
|
||||
end
|
||||
|
||||
test "recurring configuration includes required jobs" do
|
||||
recurring_file = Rails.root.join("config/recurring.yml")
|
||||
content = File.read(recurring_file)
|
||||
|
||||
assert_match(/OrderExecutionJob/, content)
|
||||
assert_match(/MonthlyPortfolioSnapshotJob/, content)
|
||||
|
||||
config = YAML.load_file(recurring_file, aliases: true)
|
||||
default_config = config["development"] || config["test"] || config["production"]
|
||||
|
||||
assert_not_nil default_config, "Should have configuration for at least one environment"
|
||||
assert default_config.key?("daily_order_execution"), "Should include daily_order_execution job"
|
||||
assert default_config.key?("monthly_portfolio_snapshot"), "Should include monthly_portfolio_snapshot job"
|
||||
end
|
||||
|
||||
test "scheduled job classes exist and can be loaded" do
|
||||
assert_nothing_raised do
|
||||
StockPricesUpdateJob
|
||||
end
|
||||
|
||||
assert_equal ApplicationJob, OrderExecutionJob.superclass
|
||||
assert_equal ApplicationJob, MonthlyPortfolioSnapshotJob.superclass
|
||||
assert_equal ApplicationJob, StockPricesUpdateJob.superclass
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "DIS",
|
||||
"02. open": "101.5200",
|
||||
"03. high": "104.0800",
|
||||
"04. low": "101.4100",
|
||||
"05. price": "103.9100",
|
||||
"06. volume": "14735791",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "101.7000",
|
||||
"09. change": "2.2100",
|
||||
"10. change percent": "2.1731%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "EA",
|
||||
"02. open": "131.2100",
|
||||
"03. high": "133.0200",
|
||||
"04. low": "129.0800",
|
||||
"05. price": "132.8800",
|
||||
"06. volume": "4525269",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "131.2300",
|
||||
"09. change": "1.6500",
|
||||
"10. change percent": "1.2573%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "F",
|
||||
"02. open": "11.7700",
|
||||
"03. high": "12.1500",
|
||||
"04. low": "11.6600",
|
||||
"05. price": "12.1300",
|
||||
"06. volume": "57205750",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "11.7700",
|
||||
"09. change": "0.3600",
|
||||
"10. change percent": "3.0586%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "FB",
|
||||
"02. open": "n/a",
|
||||
"03. high": "n/a",
|
||||
"04. low": "n/a" ,
|
||||
"05. price": "467.29",
|
||||
"06. volume": "n/a",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "n/a",
|
||||
"09. change": "n/a",
|
||||
"10. change percent": "n/a"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "GPS",
|
||||
"02. open": "n/a",
|
||||
"03. high": "n/a",
|
||||
"04. low": "n/a" ,
|
||||
"05. price": "28.96",
|
||||
"06. volume": "n/a",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "n/a",
|
||||
"09. change": "n/a",
|
||||
"10. change percent": "n/a"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "IBM",
|
||||
"02. open": "165.5600",
|
||||
"03. high": "166.7300",
|
||||
"04. low": "164.2300",
|
||||
"05. price": "165.6300",
|
||||
"06. volume": "3852963",
|
||||
"07. latest trading day": "2024-05-30",
|
||||
"08. previous close": "167.0500",
|
||||
"09. change": "-1.4200",
|
||||
"10. change percent": "-0.8500%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "KO",
|
||||
"02. open": "61.9900",
|
||||
"03. high": "63.0250",
|
||||
"04. low": "61.6900",
|
||||
"05. price": "62.9300",
|
||||
"06. volume": "19816630",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "61.9700",
|
||||
"09. change": "0.9600",
|
||||
"10. change percent": "1.5491%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "LUV",
|
||||
"02. open": "n/a",
|
||||
"03. high": "n/a",
|
||||
"04. low": "n/a" ,
|
||||
"05. price": "26.84",
|
||||
"06. volume": "n/a",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "n/a",
|
||||
"09. change": "n/a",
|
||||
"10. change percent": "n/a"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "300135.SHZ",
|
||||
"02. open": "2.4700",
|
||||
"03. high": "2.5100",
|
||||
"04. low": "2.3900",
|
||||
"05. price": "2.4400",
|
||||
"06. volume": "38891270",
|
||||
"07. latest trading day": "2024-03-01",
|
||||
"08. previous close": "2.4500",
|
||||
"09. change": "-0.0100",
|
||||
"10. change percent": "-0.4082%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "SIRI",
|
||||
"02. open": "2.7350",
|
||||
"03. high": "2.9300",
|
||||
"04. low": "2.7200",
|
||||
"05. price": "2.8200",
|
||||
"06. volume": "101093349",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "2.7400",
|
||||
"09. change": "0.0800",
|
||||
"10. change percent": "2.9197%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "SNE",
|
||||
"02. open": "81.7500",
|
||||
"03. high": "82.3750",
|
||||
"04. low": "81.4700",
|
||||
"05. price": "82.3400",
|
||||
"06. volume": "968344",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "80.7900",
|
||||
"09. change": "1.5500",
|
||||
"10. change percent": "1.9186%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "TWX",
|
||||
"02. open": "98.7700",
|
||||
"03. high": "98.7700",
|
||||
"04. low": "98.7700",
|
||||
"05. price": "98.7700",
|
||||
"06. volume": "8475",
|
||||
"07. latest trading day": "2018-06-15",
|
||||
"08. previous close": "98.7700",
|
||||
"09. change": "0.0000",
|
||||
"10. change percent": "0.0000%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Global Quote": {
|
||||
"01. symbol": "UA",
|
||||
"02. open": "6.7800",
|
||||
"03. high": "6.9700",
|
||||
"04. low": "6.6900",
|
||||
"05. price": "6.9600",
|
||||
"06. volume": "3699250",
|
||||
"07. latest trading day": "2024-05-31",
|
||||
"08. previous close": "6.7400",
|
||||
"09. change": "0.2200",
|
||||
"10. change percent": "3.2641%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
["KO", "SNE", "TWX", "DIS", "SIRI", "F", "EA", "FB", "UA", "LUV", "GPS"]
|
||||
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ClassroomFacadeTest < ActiveSupport::TestCase
|
||||
test "students returns students enrolled via enrollments" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student)
|
||||
create(:classroom_enrollment, student: student, classroom: classroom)
|
||||
|
||||
facade = ClassroomFacade.new(classroom)
|
||||
|
||||
assert_includes facade.students, student
|
||||
end
|
||||
|
||||
test "students returns students assigned via legacy classroom_id" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student, classroom: classroom)
|
||||
|
||||
facade = ClassroomFacade.new(classroom)
|
||||
|
||||
assert_includes facade.students, student
|
||||
end
|
||||
|
||||
test "students returns both enrollment and legacy students without duplicates" do
|
||||
classroom = create(:classroom)
|
||||
|
||||
# Student with enrollment
|
||||
student_with_enrollment = create(:student)
|
||||
create(:classroom_enrollment, student: student_with_enrollment, classroom: classroom)
|
||||
|
||||
# Student with legacy classroom_id
|
||||
student_with_legacy = create(:student, classroom: classroom)
|
||||
|
||||
facade = ClassroomFacade.new(classroom)
|
||||
students = facade.students
|
||||
|
||||
assert_equal 2, students.count
|
||||
assert_includes students, student_with_enrollment
|
||||
assert_includes students, student_with_legacy
|
||||
end
|
||||
|
||||
test "students does not return duplicate if student has both enrollment and classroom_id" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student, classroom: classroom)
|
||||
create(:classroom_enrollment, student: student, classroom: classroom)
|
||||
|
||||
facade = ClassroomFacade.new(classroom)
|
||||
students = facade.students
|
||||
|
||||
assert_equal 1, students.count
|
||||
assert_includes students, student
|
||||
end
|
||||
|
||||
test "students only returns current enrollments, not historical" do
|
||||
classroom = create(:classroom)
|
||||
current_student = create(:student, :without_enrollment)
|
||||
historical_student = create(:student, :without_enrollment)
|
||||
|
||||
create(
|
||||
:classroom_enrollment,
|
||||
student: current_student,
|
||||
classroom: classroom,
|
||||
unenrolled_at: nil
|
||||
)
|
||||
create(
|
||||
:classroom_enrollment,
|
||||
student: historical_student,
|
||||
classroom: classroom,
|
||||
enrolled_at: 2.weeks.ago,
|
||||
unenrolled_at: 1.week.ago
|
||||
)
|
||||
|
||||
facade = ClassroomFacade.new(classroom)
|
||||
students = facade.students
|
||||
|
||||
assert_includes students, current_student
|
||||
assert_not_includes students, historical_student
|
||||
end
|
||||
|
||||
test "students excludes discarded students" do
|
||||
classroom = create(:classroom)
|
||||
active_student = create(:student, classroom: classroom)
|
||||
discarded_student = create(:student, classroom: classroom)
|
||||
discarded_student.discard
|
||||
|
||||
facade = ClassroomFacade.new(classroom)
|
||||
students = facade.students
|
||||
|
||||
assert_includes students, active_student
|
||||
assert_not_includes students, discarded_student
|
||||
end
|
||||
|
||||
test "students includes necessary associations for performance" do
|
||||
classroom = create(:classroom, :with_trading)
|
||||
student = create(:student, classroom: classroom)
|
||||
portfolio = create(:portfolio, user: student)
|
||||
create(:portfolio_transaction, :credit, portfolio: portfolio, amount_cents: 15_000)
|
||||
create(:order, user: student, action: :buy)
|
||||
|
||||
facade = ClassroomFacade.new(classroom)
|
||||
|
||||
# Should not raise N+1 queries when accessing associations
|
||||
assert_nothing_raised do
|
||||
facade.students.each do |s|
|
||||
s.portfolio
|
||||
s.orders.to_a
|
||||
s.portfolio&.portfolio_transactions&.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :classroom_enrollment do
|
||||
student { association :student }
|
||||
classroom { association :classroom }
|
||||
enrolled_at { Time.current }
|
||||
unenrolled_at { nil }
|
||||
primary { false }
|
||||
|
||||
trait :current do
|
||||
unenrolled_at { nil }
|
||||
end
|
||||
|
||||
trait :historical do
|
||||
enrolled_at { 1.year.ago }
|
||||
unenrolled_at { 6.months.ago }
|
||||
end
|
||||
|
||||
trait :primary do
|
||||
primary { true }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :classroom_grade do
|
||||
association :classroom
|
||||
association :grade
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :classroom do
|
||||
sequence(:name) { |n| "Classroom #{n}" }
|
||||
trading_enabled { false }
|
||||
association :school_year
|
||||
|
||||
after(:build) do |classroom|
|
||||
classroom.grades << build(:grade) if classroom.grades.empty?
|
||||
end
|
||||
|
||||
trait :with_trading do
|
||||
trading_enabled { true }
|
||||
end
|
||||
|
||||
trait :with_grade do
|
||||
after(:create) do |classroom|
|
||||
create(:classroom_grade, classroom: classroom)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_grades do
|
||||
transient do
|
||||
grades_count { 3 }
|
||||
end
|
||||
|
||||
after(:create) do |classroom, evaluator|
|
||||
create_list(:classroom_grade, evaluator.grades_count, classroom: classroom)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :grade_book do
|
||||
quarter { create(:school_year).quarters.first }
|
||||
association :classroom
|
||||
|
||||
status { "draft" }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# test/factories/grade_entries.rb
|
||||
FactoryBot.define do
|
||||
factory :grade_entry do
|
||||
association :grade_book
|
||||
association :user, factory: :student
|
||||
|
||||
math_grade { nil }
|
||||
reading_grade { nil }
|
||||
attendance_days { 45 }
|
||||
is_perfect_attendance { false }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :grade do
|
||||
sequence(:level) { |n| n }
|
||||
sequence(:name) { |n| "Grade #{n}" }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :order do
|
||||
stock
|
||||
shares { 1 }
|
||||
user { create(:student) }
|
||||
action { :sell } # Default to sell to avoid validation issues
|
||||
end
|
||||
|
||||
trait :pending do
|
||||
status { :pending }
|
||||
end
|
||||
|
||||
trait :completed do
|
||||
status { :completed }
|
||||
end
|
||||
|
||||
trait :canceled do
|
||||
status { :canceled }
|
||||
end
|
||||
|
||||
trait :buy do
|
||||
action { :buy }
|
||||
end
|
||||
|
||||
trait :sell do
|
||||
action { :sell }
|
||||
end
|
||||
|
||||
trait :with_sufficient_funds do
|
||||
after(:create) do |order|
|
||||
order.user.portfolio.portfolio_transactions.create!(
|
||||
amount_cents: 100_000, # $1000 - plenty for testing
|
||||
transaction_type: :deposit
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_sufficient_shares do
|
||||
after(:create) do |order|
|
||||
create(
|
||||
:portfolio_stock,
|
||||
portfolio: order.user.portfolio,
|
||||
stock: order.stock,
|
||||
shares: 100, # Plenty of shares for testing
|
||||
purchase_price: order.stock.price_cents
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :portfolio_snapshot do
|
||||
portfolio { create(:portfolio) }
|
||||
date { Date.current }
|
||||
worth_cents { 50_000 }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :portfolio_stock do
|
||||
association :portfolio
|
||||
association :stock
|
||||
shares { 10.0 }
|
||||
purchase_price { 100.0 }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :portfolio_transaction do
|
||||
portfolio
|
||||
amount_cents { 1_000 }
|
||||
transaction_type { :credit }
|
||||
end
|
||||
|
||||
trait :debit do
|
||||
transaction_type { :debit }
|
||||
end
|
||||
|
||||
trait :credit do
|
||||
transaction_type { :credit }
|
||||
end
|
||||
|
||||
trait :deposit do
|
||||
transaction_type { :deposit }
|
||||
end
|
||||
|
||||
trait :withdrawal do
|
||||
transaction_type { :withdrawal }
|
||||
end
|
||||
|
||||
trait :fee do
|
||||
transaction_type { :fee }
|
||||
amount_cents { PortfolioTransaction::TRANSACTION_FEE_CENTS }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :portfolio do
|
||||
user { create(:student) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# test/factories/quarters.rb
|
||||
# SchoolYear auto-creates 4 quarters via after_create, so create(:quarter) finds
|
||||
# the existing quarter rather than inserting a new one (which would violate the
|
||||
# uniqueness constraint on number scoped to school_year).
|
||||
FactoryBot.define do
|
||||
factory :quarter do
|
||||
association :school_year
|
||||
number { 1 }
|
||||
name { "Quarter #{number}" }
|
||||
|
||||
to_create do |instance|
|
||||
instance.id = instance.school_year.quarters.find_by!(number: instance.number).id
|
||||
instance.reload
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# test/factories/school_years.rb
|
||||
FactoryBot.define do
|
||||
factory :school_year do
|
||||
association :school
|
||||
association :year
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :school do
|
||||
sequence(:name) { |n| "School #{n}" }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :stock do
|
||||
sequence(:ticker) { |n| "STOCK#{n}" }
|
||||
stock_exchange { "NYSE" }
|
||||
sequence(:company_name) { |n| "Sample Company#{n} Inc." }
|
||||
company_website { "https://example.com" }
|
||||
description { "A sample stock for testing" }
|
||||
industry { "Technology" }
|
||||
management { "CEO: John Doe" }
|
||||
employees { 1000 }
|
||||
sequence(:competitor_names) { |n| "Competitor #{n}, Competitor #{n + 1}" }
|
||||
sales_growth { 5.5 }
|
||||
industry_avg_sales_growth { 4.2 }
|
||||
debt_to_equity { 0.3 }
|
||||
industry_avg_debt_to_equity { 0.4 }
|
||||
profit_margin { 15.2 }
|
||||
industry_avg_profit_margin { 12.8 }
|
||||
cash_flow { 1_000_000.00 }
|
||||
debt { 500_000.00 }
|
||||
price_cents { 12_500 } # $125.00
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :teacher_classroom do
|
||||
teacher
|
||||
classroom
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :admin, class: "User" do
|
||||
password { "Passw0rd" }
|
||||
sequence(:username) { |n| "admin_#{n}" }
|
||||
sequence(:email) { |n| "admin_#{n}@example.com" }
|
||||
admin { true }
|
||||
end
|
||||
|
||||
factory :student, class: "Student" do
|
||||
type { "Student" }
|
||||
password { "Passw0rd" }
|
||||
classroom { create(:classroom, :with_trading) }
|
||||
sequence(:username) { |n| "student_#{n}" }
|
||||
sequence(:email) { |n| "student_#{n}@example.com" }
|
||||
|
||||
trait :with_portfolio do
|
||||
after(:create) do |student|
|
||||
create(:portfolio, user: student)
|
||||
end
|
||||
end
|
||||
|
||||
trait :without_enrollment do
|
||||
classroom { nil }
|
||||
end
|
||||
|
||||
trait :discarded do
|
||||
discarded_at { Time.current }
|
||||
end
|
||||
end
|
||||
|
||||
factory :teacher, class: "Teacher" do
|
||||
type { "Teacher" }
|
||||
password { "Passw0rd" }
|
||||
classroom { create(:classroom) }
|
||||
sequence(:email) { |n| "teacher_#{n}@example.com" }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :year do
|
||||
sequence(:name) { |n| "#{200 + n} - #{2000 + n + 1}" }
|
||||
end
|
||||
end
|
||||
4
worker-toolkit-stocks-in-the-future/repo/test/fixtures/action_text/rich_texts.yml
vendored
Normal file
4
worker-toolkit-stocks-in-the-future/repo/test/fixtures/action_text/rich_texts.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# one:
|
||||
# record: name_of_fixture (ClassOfFixture)
|
||||
# name: content
|
||||
# body: <p>In a <i>million</i> stars!</p>
|
||||
7
worker-toolkit-stocks-in-the-future/repo/test/fixtures/announcements.yml
vendored
Normal file
7
worker-toolkit-stocks-in-the-future/repo/test/fixtures/announcements.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
title: "Important School Announcement"
|
||||
|
||||
two:
|
||||
title: "Draft Announcement"
|
||||
@@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class AdminHelperTest < ActionView::TestCase
|
||||
test "format_attribute formats boolean true" do
|
||||
user = build(:admin)
|
||||
result = format_attribute(user, :admin)
|
||||
assert_match(/Yes/, result)
|
||||
assert_match(/bg-green-100/, result)
|
||||
end
|
||||
|
||||
test "format_attribute formats boolean false" do
|
||||
user = build(:student)
|
||||
result = format_attribute(user, :admin)
|
||||
assert_match(/No/, result)
|
||||
assert_match(/bg-gray-100/, result)
|
||||
end
|
||||
|
||||
test "format_attribute formats date" do
|
||||
date = Date.new(2025, 12, 22)
|
||||
user = build(:student, created_at: date)
|
||||
result = format_attribute(user, :created_at)
|
||||
assert_equal "December 22, 2025", result
|
||||
end
|
||||
|
||||
test "format_attribute formats nil value" do
|
||||
user = build(:student, name: nil)
|
||||
result = format_attribute(user, :name)
|
||||
assert_match(/—/, result)
|
||||
assert_match(/text-gray-400/, result)
|
||||
end
|
||||
|
||||
test "format_attribute formats string" do
|
||||
user = build(:student, email: "test@example.com")
|
||||
result = format_attribute(user, :email)
|
||||
assert_equal "test@example.com", result
|
||||
end
|
||||
|
||||
test "boolean_badge renders yes badge" do
|
||||
result = boolean_badge(true)
|
||||
assert_match(/Yes/, result)
|
||||
assert_match(/bg-green-100/, result)
|
||||
assert_match(/text-green-800/, result)
|
||||
end
|
||||
|
||||
test "boolean_badge renders no badge" do
|
||||
result = boolean_badge(false)
|
||||
assert_match(/No/, result)
|
||||
assert_match(/bg-gray-100/, result)
|
||||
assert_match(/text-gray-800/, result)
|
||||
end
|
||||
|
||||
test "sort_icon returns up arrow for asc sort" do
|
||||
params[:sort] = "name"
|
||||
params[:direction] = "asc"
|
||||
assert_equal "↑", sort_icon(:name)
|
||||
end
|
||||
|
||||
test "sort_icon returns down arrow for desc sort" do
|
||||
params[:sort] = "name"
|
||||
params[:direction] = "desc"
|
||||
assert_equal "↓", sort_icon(:name)
|
||||
end
|
||||
|
||||
test "sort_icon returns both arrows for unsorted column" do
|
||||
params[:sort] = "email"
|
||||
assert_equal "⇅", sort_icon(:name)
|
||||
end
|
||||
|
||||
# TODO: Fix sort_link routing issues in Admin - create separate ticket
|
||||
# Error: No route matches {direction: "desc", sort: :name}
|
||||
test "sort_link generates correct direction toggle" do
|
||||
skip "Broken due to routing issues"
|
||||
# params[:sort] = "name"
|
||||
# params[:direction] = "asc"
|
||||
# result = sort_link(:name, "Name")
|
||||
# assert_match(/direction=desc/, result)
|
||||
# assert_match(/Name/, result)
|
||||
end
|
||||
|
||||
# TODO: Fix sort_link routing issues in Admin - create separate ticket
|
||||
# Error: No route matches {direction: "asc", sort: :name}
|
||||
test "sort_link defaults to asc for new sort" do
|
||||
skip "Broken due to routing issues"
|
||||
# params[:sort] = nil
|
||||
# params[:direction] = nil
|
||||
# result = sort_link(:name, "Name")
|
||||
# assert_match(/direction=asc/, result)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ApplicationHelperTest < ActionView::TestCase
|
||||
test "safe_url returns valid http URL" do
|
||||
url = "http://example.com"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_equal url, result
|
||||
end
|
||||
|
||||
test "safe_url returns valid https URL" do
|
||||
url = "https://example.com"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_equal url, result
|
||||
end
|
||||
|
||||
test "safe_url returns valid https URL with path and query" do
|
||||
url = "https://example.com/path?query=value"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_equal url, result
|
||||
end
|
||||
|
||||
test "safe_url returns nil for javascript URL" do
|
||||
url = "javascript:alert('XSS')"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "safe_url returns nil for data URL" do
|
||||
url = "data:text/html,<script>alert('XSS')</script>"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "safe_url returns nil for invalid URL" do
|
||||
url = "not a valid url"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "safe_url returns nil for blank string" do
|
||||
result = safe_url("")
|
||||
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "safe_url returns nil for nil" do
|
||||
result = safe_url(nil)
|
||||
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "safe_url returns nil for ftp URL" do
|
||||
url = "ftp://example.com"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "safe_url returns nil for file URL" do
|
||||
url = "file:///etc/passwd"
|
||||
|
||||
result = safe_url(url)
|
||||
|
||||
assert_nil result
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
def assert_permit(user, record, action)
|
||||
msg = "User #{user.inspect} should be permitted to #{action} #{record}, but isn't permitted"
|
||||
assert permit(user, record, action), msg
|
||||
end
|
||||
|
||||
def refute_permit(user, record, action)
|
||||
msg = "User #{user.inspect} should NOT be permitted to #{action} #{record}, but is permitted"
|
||||
assert_not permit(user, record, action), msg
|
||||
end
|
||||
|
||||
def permit(user, record, action)
|
||||
test_name = self.class.ancestors.select { |a| a.to_s.match(/PolicyTest/) }.first
|
||||
klass = test_name.to_s.gsub("Test", "")
|
||||
klass.constantize.new(user, record).public_send("#{action}?")
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class NavbarPolicyVisibilityTest < ActionDispatch::IntegrationTest
|
||||
include Devise::Test::IntegrationHelpers
|
||||
|
||||
def setup
|
||||
classroom = create(:classroom)
|
||||
@student = User.create!(username: "student", type: "Student", password: "password", classroom: classroom)
|
||||
@teacher = User.create!(username: "teacher", type: "Teacher", password: "password", email: "teacher@test.com")
|
||||
@admin = User.create!(
|
||||
username: "admin", type: "Teacher", admin: true, password: "password",
|
||||
email: "admin@test.com"
|
||||
)
|
||||
@portfolio = Portfolio.create!(user: @student)
|
||||
end
|
||||
|
||||
test "student sees My Portfolio if permitted" do
|
||||
sign_in @student
|
||||
get root_path
|
||||
assert_select "a", text: "My Portfolio"
|
||||
end
|
||||
|
||||
test "teacher sees Classes if permitted" do
|
||||
sign_in @teacher
|
||||
get root_path
|
||||
assert_select "a", text: "Classes"
|
||||
end
|
||||
|
||||
test "admin sees Admin if permitted" do
|
||||
sign_in @admin
|
||||
get root_path
|
||||
assert_select "a", text: "Admin"
|
||||
end
|
||||
|
||||
test "navbar shows only active stocks" do
|
||||
active_stock = create(:stock, archived: false, ticker: "AAPL")
|
||||
archived_stock = create(:stock, archived: true, ticker: "DEAD")
|
||||
|
||||
sign_in @student
|
||||
get root_path
|
||||
|
||||
assert_select "p", text: active_stock.ticker
|
||||
assert_select "p", text: archived_stock.ticker, count: 0
|
||||
end
|
||||
|
||||
test "navbar stocks are displayed in trading floor dropdown" do
|
||||
active_stock1 = create(:stock, archived: false, ticker: "AAPL")
|
||||
active_stock2 = create(:stock, archived: false, ticker: "GOOGL")
|
||||
archived_stock = create(:stock, archived: true, ticker: "DEAD")
|
||||
|
||||
sign_in @student
|
||||
get root_path
|
||||
|
||||
assert_select "details[data-controller='stock-navbar-toggle']" do
|
||||
assert_select "p", text: active_stock1.ticker
|
||||
assert_select "p", text: active_stock2.ticker
|
||||
end
|
||||
|
||||
assert_select "details[data-controller='stock-navbar-toggle']" do
|
||||
assert_select "p", text: archived_stock.ticker, count: 0
|
||||
end
|
||||
end
|
||||
|
||||
test "navbar stocks link to correct stock paths" do
|
||||
active_stock = create(:stock, archived: false, ticker: "AAPL")
|
||||
|
||||
sign_in @student
|
||||
get root_path
|
||||
|
||||
assert_select "a[href='#{stock_path(active_stock)}']" do
|
||||
assert_select "p", text: active_stock.ticker
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,169 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class OrderExecutionWorkflowTest < ActionDispatch::IntegrationTest
|
||||
test "buy order execution updates portfolio correctly" do
|
||||
classroom = create(:classroom, :with_trading)
|
||||
student = create(:student, classroom: classroom)
|
||||
portfolio = student.portfolio
|
||||
initial_deposit_cents = 100_000
|
||||
create(:portfolio_transaction, :deposit, portfolio: portfolio, amount_cents: initial_deposit_cents)
|
||||
|
||||
stock = create(:stock, ticker: "AAPL", price_cents: 15_000)
|
||||
shares_to_buy = 3
|
||||
order = create(:order, :pending, :buy, user: student, stock: stock, shares: shares_to_buy)
|
||||
purchase_cost_cents = stock.price_cents * shares_to_buy
|
||||
|
||||
# Sanity check: order is pending
|
||||
assert order.pending?, "order should be pending before job execution"
|
||||
|
||||
OrderExecutionJob.perform_now
|
||||
|
||||
order.reload
|
||||
portfolio.reload
|
||||
|
||||
assert order.completed?, "order should be marked as completed"
|
||||
assert_not_nil order.portfolio_transaction, "order should have linked transaction"
|
||||
assert_not_nil order.portfolio_stock, "order should have linked portfolio stock"
|
||||
|
||||
assert order.portfolio_transaction.debit?, "buy order should create debit transaction"
|
||||
assert_equal purchase_cost_cents, order.portfolio_transaction.amount_cents
|
||||
|
||||
assert_equal shares_to_buy, order.portfolio_stock.shares
|
||||
assert_equal stock.id, order.portfolio_stock.stock_id
|
||||
|
||||
# Verify cash balance (cash_balance returns dollars)
|
||||
expected_cash_cents = initial_deposit_cents - purchase_cost_cents - PortfolioTransaction::TRANSACTION_FEE_CENTS
|
||||
actual_cash_cents = (portfolio.cash_balance * 100).to_i
|
||||
assert_equal expected_cash_cents, actual_cash_cents
|
||||
end
|
||||
|
||||
test "sell order execution updates portfolio correctly" do
|
||||
classroom = create(:classroom, :with_trading)
|
||||
student = create(:student, classroom: classroom)
|
||||
portfolio = student.portfolio
|
||||
|
||||
stock = create(:stock, ticker: "GOOGL", price_cents: 20_000)
|
||||
shares_owned = 10
|
||||
shares_to_sell = 4
|
||||
create(
|
||||
:portfolio_stock, portfolio: portfolio, stock: stock, shares: shares_owned,
|
||||
purchase_price: stock.current_price
|
||||
)
|
||||
|
||||
order = create(:order, :pending, :sell, user: student, stock: stock, shares: shares_to_sell)
|
||||
sale_proceeds_cents = stock.price_cents * shares_to_sell
|
||||
|
||||
# Sanity check
|
||||
assert order.pending?, "order should be pending before job execution"
|
||||
|
||||
OrderExecutionJob.perform_now
|
||||
|
||||
order.reload
|
||||
portfolio.reload
|
||||
|
||||
assert order.completed?, "order should be marked as completed"
|
||||
assert order.portfolio_transaction.credit?, "sell order should create credit transaction"
|
||||
assert_equal sale_proceeds_cents, order.portfolio_transaction.amount_cents
|
||||
assert_equal(-shares_to_sell, order.portfolio_stock.shares, "sell order should create negative shares record")
|
||||
|
||||
remaining_shares = portfolio.shares_owned(stock.id).to_i
|
||||
assert_equal shares_owned - shares_to_sell, remaining_shares
|
||||
end
|
||||
|
||||
test "multiple orders in single job run with single fee per user" do
|
||||
classroom = create(:classroom, :with_trading)
|
||||
|
||||
student1 = create(:student, classroom: classroom)
|
||||
create(:portfolio_transaction, :deposit, portfolio: student1.portfolio, amount_cents: 200_000)
|
||||
|
||||
student2 = create(:student, classroom: classroom)
|
||||
create(:portfolio_transaction, :deposit, portfolio: student2.portfolio, amount_cents: 200_000)
|
||||
|
||||
stock = create(:stock, ticker: "AAPL", price_cents: 10_000)
|
||||
|
||||
order1_s1 = create(:order, :pending, :buy, user: student1, stock: stock, shares: 2)
|
||||
order2_s1 = create(:order, :pending, :buy, user: student1, stock: stock, shares: 1)
|
||||
order1_s2 = create(:order, :pending, :buy, user: student2, stock: stock, shares: 3)
|
||||
|
||||
# Sanity checks
|
||||
assert order1_s1.pending?
|
||||
assert order2_s1.pending?
|
||||
assert order1_s2.pending?
|
||||
assert_equal 0, student1.portfolio.portfolio_transactions.fees.count
|
||||
assert_equal 0, student2.portfolio.portfolio_transactions.fees.count
|
||||
|
||||
OrderExecutionJob.perform_now
|
||||
|
||||
[order1_s1, order2_s1, order1_s2].each do |order|
|
||||
order.reload
|
||||
assert order.completed?, "all orders should be completed"
|
||||
end
|
||||
|
||||
student1.portfolio.reload
|
||||
student2.portfolio.reload
|
||||
|
||||
# Each user should have exactly 1 fee transaction, regardless of order count
|
||||
assert_equal 1, student1.portfolio.portfolio_transactions.fees.count, "student1 should have exactly 1 fee"
|
||||
assert_equal 1, student2.portfolio.portfolio_transactions.fees.count, "student2 should have exactly 1 fee"
|
||||
assert_equal PortfolioTransaction::TRANSACTION_FEE_CENTS,
|
||||
student1.portfolio.portfolio_transactions.fees.sum(:amount_cents)
|
||||
|
||||
s1_purchase_cost_cents = stock.price_cents * 3 # 2 + 1 shares
|
||||
s1_expected_cash_cents = 200_000 - s1_purchase_cost_cents - PortfolioTransaction::TRANSACTION_FEE_CENTS
|
||||
s1_actual_cash_cents = (student1.portfolio.cash_balance * 100).to_i
|
||||
assert_equal s1_expected_cash_cents, s1_actual_cash_cents
|
||||
|
||||
s2_purchase_cost_cents = stock.price_cents * 3
|
||||
s2_expected_cash_cents = 200_000 - s2_purchase_cost_cents - PortfolioTransaction::TRANSACTION_FEE_CENTS
|
||||
s2_actual_cash_cents = (student2.portfolio.cash_balance * 100).to_i
|
||||
assert_equal s2_expected_cash_cents, s2_actual_cash_cents
|
||||
end
|
||||
|
||||
test "mixed buy and sell orders execute correctly" do
|
||||
classroom = create(:classroom, :with_trading)
|
||||
student = create(:student, classroom: classroom)
|
||||
portfolio = student.portfolio
|
||||
initial_deposit_cents = 100_000
|
||||
create(:portfolio_transaction, :deposit, portfolio: portfolio, amount_cents: initial_deposit_cents)
|
||||
|
||||
stock1 = create(:stock, ticker: "AAPL", price_cents: 10_000)
|
||||
stock2 = create(:stock, ticker: "GOOGL", price_cents: 20_000)
|
||||
|
||||
initial_stock2_shares = 5
|
||||
create(
|
||||
:portfolio_stock, portfolio: portfolio, stock: stock2, shares: initial_stock2_shares,
|
||||
purchase_price: stock2.current_price
|
||||
)
|
||||
|
||||
buy_shares = 2
|
||||
sell_shares = 3
|
||||
buy_order = create(:order, :pending, :buy, user: student, stock: stock1, shares: buy_shares)
|
||||
sell_order = create(:order, :pending, :sell, user: student, stock: stock2, shares: sell_shares)
|
||||
|
||||
# Sanity checks
|
||||
assert buy_order.pending?
|
||||
assert sell_order.pending?
|
||||
|
||||
buy_cost_cents = stock1.price_cents * buy_shares
|
||||
sell_proceeds_cents = stock2.price_cents * sell_shares
|
||||
|
||||
OrderExecutionJob.perform_now
|
||||
|
||||
buy_order.reload
|
||||
sell_order.reload
|
||||
portfolio.reload
|
||||
|
||||
assert buy_order.completed?, "buy order should be completed"
|
||||
assert sell_order.completed?, "sell order should be completed"
|
||||
|
||||
assert_equal buy_shares, portfolio.shares_owned(stock1.id).to_i
|
||||
assert_equal initial_stock2_shares - sell_shares, portfolio.shares_owned(stock2.id).to_i
|
||||
|
||||
# Cash = initial + sell proceeds - buy cost - fee
|
||||
expected_cash_cents = initial_deposit_cents + sell_proceeds_cents - buy_cost_cents - PortfolioTransaction::TRANSACTION_FEE_CENTS
|
||||
actual_cash_cents = (portfolio.cash_balance * 100).to_i
|
||||
assert_equal expected_cash_cents, actual_cash_cents
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class OrdersScopingTest < ActionDispatch::IntegrationTest
|
||||
test "student sees only their own orders" do
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
classroom = create(:classroom, :with_trading)
|
||||
student1, order1 = create_student_and_order(stock, classroom)
|
||||
_student2, order2 = create_student_and_order(stock, classroom)
|
||||
sign_in(student1)
|
||||
|
||||
get orders_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "tr##{dom_id(order1)}"
|
||||
assert_select "tr##{dom_id(order2)}", count: 0
|
||||
end
|
||||
|
||||
test "teacher sees orders from students in their classrooms" do
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
classroom1, order1 = create_classroom_and_student_order(stock)
|
||||
_classroom2, order2 = create_classroom_and_student_order(stock)
|
||||
classroom3, order3 = create_classroom_and_student_order(stock)
|
||||
teacher = create(:teacher, classrooms: [classroom1, classroom3])
|
||||
sign_in(teacher)
|
||||
|
||||
get orders_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "tr##{dom_id(order1)}"
|
||||
assert_select "tr##{dom_id(order3)}"
|
||||
assert_select "tr##{dom_id(order2)}", count: 0
|
||||
end
|
||||
|
||||
test "admin sees all orders" do
|
||||
admin = create(:admin)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
_classroom1, order1 = create_classroom_and_student_order(stock)
|
||||
_classroom2, order2 = create_classroom_and_student_order(stock)
|
||||
sign_in(admin)
|
||||
|
||||
get orders_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "tr##{dom_id(order1)}"
|
||||
assert_select "tr##{dom_id(order2)}"
|
||||
end
|
||||
|
||||
test "user with no orders sees empty list" do
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
create_classroom_and_student_order(stock)
|
||||
student = create(:student)
|
||||
sign_in(student)
|
||||
|
||||
get orders_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "tr[data-testid='no-orders']"
|
||||
end
|
||||
|
||||
test "teacher with no classrooms sees no orders" do
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
create_classroom_and_student_order(stock)
|
||||
teacher = create(:teacher, classrooms: [])
|
||||
sign_in(teacher)
|
||||
|
||||
get orders_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "tr[data-testid='no-orders']"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_classroom_and_student_order(stock)
|
||||
classroom = create(:classroom, :with_trading)
|
||||
_student, order = create_student_and_order(stock, classroom)
|
||||
|
||||
[classroom, order]
|
||||
end
|
||||
|
||||
def create_student_and_order(stock, classroom)
|
||||
student = create(:student, classroom:)
|
||||
portfolio = create(:portfolio, user: student)
|
||||
create(:portfolio_transaction, :deposit, portfolio:, amount_cents: 10_000)
|
||||
order = create(:order, :buy, user: student, stock:)
|
||||
|
||||
[student, order]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class MonthlyPortfolioSnapshotJobTest < ActiveJob::TestCase
|
||||
setup do
|
||||
@user = create(:student)
|
||||
@portfolio = @user.portfolio
|
||||
@target_date = Date.new(2024, 12, 1)
|
||||
end
|
||||
|
||||
test "creates snapshot calculating cash and stock holdings" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 50_000)
|
||||
stock = create(:stock, price_cents: 10_000)
|
||||
create(:portfolio_stock, portfolio: @portfolio, stock: stock, shares: 10)
|
||||
|
||||
assert_difference("PortfolioSnapshot.count", 1) do
|
||||
MonthlyPortfolioSnapshotJob.perform_now(@target_date)
|
||||
end
|
||||
|
||||
snapshot = @portfolio.portfolio_snapshots.find_by(date: @target_date)
|
||||
expected_value = @portfolio.calculate_total_value_cents
|
||||
assert_equal expected_value, snapshot.worth_cents
|
||||
end
|
||||
|
||||
test "handles empty portfolio database" do
|
||||
Portfolio.delete_all
|
||||
|
||||
assert_no_difference("PortfolioSnapshot.count") do
|
||||
MonthlyPortfolioSnapshotJob.perform_now(@target_date)
|
||||
end
|
||||
end
|
||||
|
||||
test "skips existing snapshots" do
|
||||
create(:portfolio_snapshot, portfolio: @portfolio, date: @target_date)
|
||||
|
||||
assert_no_difference("PortfolioSnapshot.count") do
|
||||
MonthlyPortfolioSnapshotJob.perform_now(@target_date)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class OrderExecutionJobTest < ActiveJob::TestCase
|
||||
test "with pending orders" do
|
||||
portfolio1 = create(:portfolio)
|
||||
student1 = create(:student, portfolio: portfolio1)
|
||||
create(
|
||||
:portfolio_transaction,
|
||||
:deposit,
|
||||
portfolio: portfolio1,
|
||||
amount_cents: 10_000
|
||||
)
|
||||
portfolio2 = create(:portfolio)
|
||||
student2 = create(:student, portfolio: portfolio2)
|
||||
create(
|
||||
:portfolio_transaction,
|
||||
:deposit,
|
||||
portfolio: portfolio2,
|
||||
amount_cents: 10_000
|
||||
)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
order1 = create(:order, :pending, :buy, user: student1, stock:, shares: 1)
|
||||
order2 = create(:order, :pending, :buy, user: student2, stock:, shares: 2)
|
||||
|
||||
ExecuteOrder.expects(:execute).with(order1).once
|
||||
ExecuteOrder.expects(:execute).with(order2).once
|
||||
TransactionFeeProcessor.expects(:execute).once
|
||||
|
||||
OrderExecutionJob.perform_now
|
||||
end
|
||||
|
||||
test "with no pending orders" do
|
||||
ExecuteOrder.expects(:execute).never
|
||||
TransactionFeeProcessor.expects(:execute).never
|
||||
|
||||
OrderExecutionJob.perform_now
|
||||
end
|
||||
|
||||
test "when an error occurs" do
|
||||
portfolio = create(:portfolio)
|
||||
student = create(:student, portfolio:)
|
||||
create(:portfolio_transaction, :deposit, portfolio:, amount_cents: 10_000)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
create(:order, :pending, :buy, user: student, stock:, shares: 1)
|
||||
ExecuteOrder.stubs(:execute).raises(StandardError.new("Test error"))
|
||||
|
||||
assert_raises(StandardError) do
|
||||
OrderExecutionJob.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class StockAttributeUpdateJobTest < ActiveJob::TestCase
|
||||
test "processes all stocks in the database" do
|
||||
stock1 = create(:stock, ticker: "STOCK1")
|
||||
stock2 = create(:stock, ticker: "STOCK2")
|
||||
stock3 = create(:stock, ticker: "STOCK3")
|
||||
|
||||
Net::HTTP.expects(:get).times(3).returns(
|
||||
{ "Symbol" => "STOCK1", "Name" => "Company 1" }.to_json,
|
||||
{ "Symbol" => "STOCK2", "Name" => "Company 2" }.to_json,
|
||||
{ "Symbol" => "STOCK3", "Name" => "Company 3" }.to_json
|
||||
)
|
||||
|
||||
StockAttributeUpdateJob.perform_now
|
||||
|
||||
stock1.reload
|
||||
stock2.reload
|
||||
stock3.reload
|
||||
|
||||
assert_equal "Company 1", stock1.company_name
|
||||
assert_equal "Company 2", stock2.company_name
|
||||
assert_equal "Company 3", stock3.company_name
|
||||
end
|
||||
|
||||
test "handles stocks with invalid API responses" do
|
||||
stock1 = create(:stock, ticker: "VALID")
|
||||
stock2 = create(:stock, ticker: "INVALID")
|
||||
|
||||
original_name = stock2.company_name
|
||||
|
||||
Net::HTTP.expects(:get).times(2).returns(
|
||||
{ "Symbol" => "VALID", "Name" => "Valid Company" }.to_json,
|
||||
"{}"
|
||||
)
|
||||
|
||||
StockAttributeUpdateJob.perform_now
|
||||
|
||||
stock1.reload
|
||||
stock2.reload
|
||||
|
||||
assert_equal "Valid Company", stock1.company_name
|
||||
assert_equal original_name, stock2.company_name
|
||||
end
|
||||
|
||||
test "skips stocks without ticker" do
|
||||
stock_with_ticker = create(:stock, ticker: "HAS_TICKER")
|
||||
stock_without_ticker = Stock.new(description: "Test stock")
|
||||
stock_without_ticker.save(validate: false)
|
||||
|
||||
Net::HTTP.expects(:get).once.returns(
|
||||
{ "Symbol" => "HAS_TICKER", "Name" => "Has Ticker Company" }.to_json
|
||||
)
|
||||
|
||||
StockAttributeUpdateJob.perform_now
|
||||
|
||||
stock_with_ticker.reload
|
||||
stock_without_ticker.reload
|
||||
|
||||
assert_equal "Has Ticker Company", stock_with_ticker.company_name
|
||||
assert_nil stock_without_ticker.company_name
|
||||
end
|
||||
|
||||
test "handles empty stock table" do
|
||||
Stock.delete_all
|
||||
Net::HTTP.expects(:get).never
|
||||
|
||||
assert_nothing_raised do
|
||||
StockAttributeUpdateJob.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class StockPricesUpdateJobTest < ActiveJob::TestCase
|
||||
STOCK_URL_MATCHER = %r{https://www\.alphavantage\.co/query\?apikey=[^&]+&function=GLOBAL_QUOTE&symbol=[A-Z]+}
|
||||
|
||||
setup do
|
||||
ENV["ALPHA_VANTAGE_API_KEY"] = "test_api_key"
|
||||
@last_trading_date = Date.parse("2024-05-31")
|
||||
|
||||
stub_request(:get, STOCK_URL_MATCHER)
|
||||
.with(
|
||||
headers: {
|
||||
"Accept" => "*/*",
|
||||
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
|
||||
"Host" => "www.alphavantage.co",
|
||||
"User-Agent" => "Ruby"
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: Rails.root.join("./test/data/global_quote_f.json").open)
|
||||
end
|
||||
|
||||
test "makes API calls" do
|
||||
create(:stock, ticker: "AAPL")
|
||||
create(:stock, ticker: "KO")
|
||||
|
||||
StockPricesUpdateJob.perform_now
|
||||
assert_requested :get, STOCK_URL_MATCHER, times: 2
|
||||
end
|
||||
|
||||
test "updates existing Stock records" do
|
||||
stock1 = create(:stock, ticker: "AAPL", price_cents: 0)
|
||||
stock2 = create(:stock, ticker: "KO", price_cents: 0)
|
||||
|
||||
assert_no_difference("Stock.count") do
|
||||
StockPricesUpdateJob.perform_now
|
||||
end
|
||||
|
||||
stock1.reload
|
||||
stock2.reload
|
||||
assert stock1.price_cents.positive?
|
||||
assert stock2.price_cents.positive?
|
||||
end
|
||||
|
||||
test "sets Stock ticker and price" do
|
||||
test_tickers = %w[AAPL KO DIS]
|
||||
test_tickers.each { |ticker| create(:stock, ticker: ticker, price_cents: 0) }
|
||||
|
||||
StockPricesUpdateJob.perform_now
|
||||
test_tickers.each do |ticker|
|
||||
stock = Stock.find_by(ticker: ticker)
|
||||
assert stock.present?
|
||||
assert stock.price_cents.present?
|
||||
end
|
||||
end
|
||||
|
||||
test "handles empty stock database gracefully" do
|
||||
# Ensure no stocks exist
|
||||
Stock.delete_all
|
||||
|
||||
assert_no_difference("Stock.count") do
|
||||
assert_nothing_raised do
|
||||
StockPricesUpdateJob.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
assert_not_requested :get, STOCK_URL_MATCHER
|
||||
end
|
||||
|
||||
test "stores current price as yesterday price before updating" do
|
||||
initial_price = 12_345
|
||||
stock = create(:stock, ticker: "AAPL", price_cents: initial_price, yesterday_price_cents: nil)
|
||||
|
||||
StockPricesUpdateJob.perform_now
|
||||
|
||||
stock.reload
|
||||
assert_equal initial_price, stock.yesterday_price_cents
|
||||
assert_not_equal initial_price, stock.price_cents
|
||||
end
|
||||
|
||||
test "preserves yesterday price when API call fails" do
|
||||
initial_price = 12_345
|
||||
stock = create(:stock, ticker: "AAPL", price_cents: initial_price, yesterday_price_cents: nil)
|
||||
|
||||
stub_request(:get, STOCK_URL_MATCHER).to_return(status: 200, body: "{}")
|
||||
|
||||
StockPricesUpdateJob.perform_now
|
||||
|
||||
stock.reload
|
||||
assert_equal initial_price, stock.yesterday_price_cents
|
||||
assert_equal initial_price, stock.price_cents
|
||||
end
|
||||
|
||||
test "stores trading day when updating stock price" do
|
||||
stock = create(:stock, ticker: "AAPL", price_cents: 10_000, last_trading_day: nil)
|
||||
|
||||
StockPricesUpdateJob.perform_now
|
||||
|
||||
stock.reload
|
||||
assert_equal @last_trading_date, stock.last_trading_day
|
||||
end
|
||||
|
||||
test "skips update when trading day hasn't changed" do
|
||||
initial_price = 10_000
|
||||
stock = create(:stock, ticker: "AAPL", price_cents: initial_price, last_trading_day: @last_trading_date)
|
||||
|
||||
StockPricesUpdateJob.perform_now
|
||||
|
||||
stock.reload
|
||||
assert_equal initial_price, stock.price_cents
|
||||
assert_equal @last_trading_date, stock.last_trading_day
|
||||
end
|
||||
|
||||
test "updates when trading day is newer" do
|
||||
prev_trading_date = @last_trading_date - 1.day
|
||||
stock = create(:stock, ticker: "AAPL", price_cents: 10_000, last_trading_day: prev_trading_date)
|
||||
|
||||
StockPricesUpdateJob.perform_now
|
||||
|
||||
stock.reload
|
||||
assert_equal @last_trading_date, stock.last_trading_day
|
||||
assert stock.price_cents.positive?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# test/mailers/previews/devise_mailer_preview.rb
|
||||
|
||||
class DeviseMailerPreview < ActionMailer::Preview
|
||||
def confirmation_instructions
|
||||
Devise::Mailer.confirmation_instructions(User.first || User.new(email: "test@example.com"), "faketoken")
|
||||
end
|
||||
|
||||
def reset_password_instructions
|
||||
Devise::Mailer.reset_password_instructions(User.first || User.new(email: "test@example.com"), "faketoken")
|
||||
end
|
||||
|
||||
def unlock_instructions
|
||||
Devise::Mailer.unlock_instructions(User.first || User.new(email: "test@example.com"), "faketoken")
|
||||
end
|
||||
|
||||
def email_changed
|
||||
Devise::Mailer.email_changed(User.first || User.new(email: "test@example.com"))
|
||||
end
|
||||
|
||||
def password_change
|
||||
Devise::Mailer.password_change(User.first || User.new(email: "test@example.com"))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class AnnouncementTest < ActiveSupport::TestCase
|
||||
test "should be valid with valid attributes" do
|
||||
announcement = Announcement.new(title: "Test Title", content: "Test content")
|
||||
assert announcement.valid?
|
||||
end
|
||||
|
||||
test "should require title" do
|
||||
announcement = Announcement.new(content: "Test content")
|
||||
assert_not announcement.valid?
|
||||
assert_includes announcement.errors[:title], "can't be blank"
|
||||
end
|
||||
|
||||
test "should require content" do
|
||||
announcement = Announcement.new(title: "Test Title")
|
||||
assert_not announcement.valid?
|
||||
assert_includes announcement.errors[:content], "can't be blank"
|
||||
end
|
||||
|
||||
test "should validate content length" do
|
||||
long_title = "a" * 256
|
||||
announcement = Announcement.new(title: long_title, content: "Test content")
|
||||
assert_not announcement.valid?
|
||||
assert_includes announcement.errors[:title], "is too long (maximum is 255 characters)"
|
||||
end
|
||||
|
||||
test "should have rich text content" do
|
||||
announcement = Announcement.new(title: "Test", content: "Test content")
|
||||
assert_respond_to announcement, :content
|
||||
# ActionText creates a rich_text association
|
||||
assert_respond_to announcement, :rich_text_content
|
||||
end
|
||||
|
||||
test "should create announcement with ActionText content" do
|
||||
announcement = Announcement.create!(
|
||||
title: "Test with Rich Text",
|
||||
content: "<p>This is <strong>bold</strong> text</p>"
|
||||
)
|
||||
assert_not_nil announcement.content
|
||||
assert_equal "<div class=\"trix-content\">\n <p>This is <strong>bold</strong> text</p>\n</div>\n",
|
||||
announcement.content.to_s
|
||||
end
|
||||
|
||||
test "excerpt method should return truncated plain text content" do
|
||||
long_content = "This is a very long announcement content that should be truncated when excerpted. " * 5
|
||||
announcement = Announcement.create!(title: "Long Content", content: long_content)
|
||||
excerpt = announcement.excerpt(limit: 100)
|
||||
assert excerpt.length <= 100
|
||||
assert excerpt.end_with?("...")
|
||||
assert_not_includes excerpt, "<p>"
|
||||
assert_includes excerpt, "This is a very long announcement content"
|
||||
end
|
||||
|
||||
test "published_at should return created_at" do
|
||||
announcement = Announcement.create!(title: "Test", content: "Test content")
|
||||
assert_equal announcement.created_at, announcement.published_at
|
||||
end
|
||||
|
||||
test "latest scope should return announcements in reverse chronological order" do
|
||||
old_announcement = Announcement.create!(title: "Old", content: "Old content", created_at: 1.day.ago)
|
||||
new_announcement = Announcement.create!(title: "New", content: "New content")
|
||||
|
||||
latest_announcements = Announcement.latest
|
||||
assert_equal new_announcement, latest_announcements.first
|
||||
assert_equal old_announcement, latest_announcements.last
|
||||
end
|
||||
|
||||
test "current should return the featured announcement" do
|
||||
Announcement.create!(title: "Old", content: "Old content", featured: false)
|
||||
featured_announcement = Announcement.create!(title: "Featured", content: "Featured content", featured: true)
|
||||
Announcement.create!(title: "New", content: "New content", featured: false)
|
||||
|
||||
assert_equal featured_announcement, Announcement.current
|
||||
end
|
||||
|
||||
test "current should return nil when no announcement is featured" do
|
||||
Announcement.create!(title: "Old", content: "Old content", featured: false)
|
||||
assert_nil Announcement.current
|
||||
end
|
||||
|
||||
test "current should return nil when no announcements exist" do
|
||||
assert_nil Announcement.current
|
||||
end
|
||||
|
||||
test "should default featured to false" do
|
||||
announcement = Announcement.create!(title: "Test", content: "Test content")
|
||||
assert_equal false, announcement.featured
|
||||
end
|
||||
|
||||
test "should allow setting featured to true" do
|
||||
announcement = Announcement.create!(title: "Test", content: "Test content", featured: true)
|
||||
assert_equal true, announcement.featured
|
||||
end
|
||||
|
||||
test "should unfeature other announcements when one is featured" do
|
||||
announcement1 = Announcement.create!(title: "First", content: "First content", featured: true)
|
||||
announcement2 = Announcement.create!(title: "Second", content: "Second content", featured: true)
|
||||
|
||||
announcement1.reload
|
||||
assert_equal false, announcement1.featured
|
||||
assert_equal true, announcement2.featured
|
||||
end
|
||||
|
||||
test "should allow multiple announcements with featured false" do
|
||||
announcement1 = Announcement.create!(title: "First", content: "First content", featured: false)
|
||||
announcement2 = Announcement.create!(title: "Second", content: "Second content", featured: false)
|
||||
|
||||
assert_equal false, announcement1.featured
|
||||
assert_equal false, announcement2.featured
|
||||
end
|
||||
|
||||
test "should unfeature announcement when updating another to featured" do
|
||||
announcement1 = Announcement.create!(title: "First", content: "First content", featured: true)
|
||||
announcement2 = Announcement.create!(title: "Second", content: "Second content", featured: false)
|
||||
|
||||
announcement2.update!(featured: true)
|
||||
|
||||
announcement1.reload
|
||||
assert_equal false, announcement1.featured
|
||||
assert_equal true, announcement2.featured
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,183 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ClassroomEnrollmentTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:classroom_enrollment).valid?
|
||||
end
|
||||
|
||||
test "belongs to student" do
|
||||
enrollment = build(:classroom_enrollment)
|
||||
assert enrollment.respond_to?(:student)
|
||||
end
|
||||
|
||||
test "belongs to classroom" do
|
||||
enrollment = build(:classroom_enrollment)
|
||||
assert enrollment.respond_to?(:classroom)
|
||||
end
|
||||
|
||||
test "requires student_id" do
|
||||
enrollment = build(:classroom_enrollment, student: nil)
|
||||
assert_not enrollment.valid?
|
||||
assert_includes enrollment.errors[:student], "must exist"
|
||||
end
|
||||
|
||||
test "requires classroom_id" do
|
||||
enrollment = build(:classroom_enrollment, classroom: nil)
|
||||
assert_not enrollment.valid?
|
||||
assert_includes enrollment.errors[:classroom], "must exist"
|
||||
end
|
||||
|
||||
test "requires enrolled_at" do
|
||||
enrollment = build(:classroom_enrollment, enrolled_at: nil)
|
||||
assert_not enrollment.valid?
|
||||
assert_includes enrollment.errors[:enrolled_at], "can't be blank"
|
||||
end
|
||||
|
||||
test "unenrolled_at must be after enrolled_at" do
|
||||
enrollment = build(
|
||||
:classroom_enrollment,
|
||||
enrolled_at: 1.day.ago,
|
||||
unenrolled_at: 2.days.ago
|
||||
)
|
||||
assert_not enrollment.valid?
|
||||
assert_includes enrollment.errors[:unenrolled_at], "must be after enrolled_at"
|
||||
end
|
||||
|
||||
test "unenrolled_at can be equal to enrolled_at" do
|
||||
time = Time.current
|
||||
enrollment = build(
|
||||
:classroom_enrollment,
|
||||
enrolled_at: time,
|
||||
unenrolled_at: time
|
||||
)
|
||||
assert enrollment.valid?
|
||||
end
|
||||
|
||||
test "student can only have one primary enrollment" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
|
||||
create(:classroom_enrollment, student: student, classroom: classroom1, primary: true)
|
||||
enrollment2 = build(:classroom_enrollment, student: student, classroom: classroom2, primary: true)
|
||||
|
||||
assert_not enrollment2.valid?
|
||||
assert_includes enrollment2.errors[:primary], "student can only have one primary enrollment"
|
||||
end
|
||||
|
||||
test "current scope returns enrollments with nil unenrolled_at" do
|
||||
student = create(:student, :without_enrollment)
|
||||
current = create(:classroom_enrollment, student: student, unenrolled_at: nil)
|
||||
historical = create(:classroom_enrollment, student: student, enrolled_at: 2.days.ago, unenrolled_at: 1.day.ago)
|
||||
|
||||
assert_includes ClassroomEnrollment.current, current
|
||||
assert_not_includes ClassroomEnrollment.current, historical
|
||||
end
|
||||
|
||||
test "historical scope returns enrollments with unenrolled_at set" do
|
||||
student = create(:student, :without_enrollment)
|
||||
current = create(:classroom_enrollment, student: student, unenrolled_at: nil)
|
||||
historical = create(:classroom_enrollment, student: student, enrolled_at: 2.days.ago, unenrolled_at: 1.day.ago)
|
||||
|
||||
assert_includes ClassroomEnrollment.historical, historical
|
||||
assert_not_includes ClassroomEnrollment.historical, current
|
||||
end
|
||||
|
||||
test "primary_enrollment scope returns only primary enrollments" do
|
||||
student1 = create(:student, :without_enrollment)
|
||||
student2 = create(:student, :without_enrollment)
|
||||
primary = create(:classroom_enrollment, student: student1, primary: true)
|
||||
non_primary = create(:classroom_enrollment, student: student2, primary: false)
|
||||
|
||||
assert_includes ClassroomEnrollment.primary_enrollment, primary
|
||||
assert_not_includes ClassroomEnrollment.primary_enrollment, non_primary
|
||||
end
|
||||
|
||||
test "make_primary! marks enrollment as primary" do
|
||||
student = create(:student)
|
||||
enrollment = create(:classroom_enrollment, student: student, primary: false)
|
||||
|
||||
enrollment.make_primary!
|
||||
|
||||
assert enrollment.reload.primary
|
||||
end
|
||||
|
||||
test "make_primary! demotes other primary enrollments for same student" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
old_primary = create(:classroom_enrollment, student: student, classroom: classroom1, primary: true)
|
||||
new_enrollment = create(:classroom_enrollment, student: student, classroom: classroom2, primary: false)
|
||||
|
||||
new_enrollment.make_primary!
|
||||
|
||||
assert_not old_primary.reload.primary
|
||||
assert new_enrollment.reload.primary
|
||||
end
|
||||
|
||||
test "make_primary! returns self" do
|
||||
enrollment = create(:classroom_enrollment)
|
||||
result = enrollment.make_primary!
|
||||
assert_equal enrollment, result
|
||||
end
|
||||
|
||||
test "unenroll! sets unenrolled_at to current time by default" do
|
||||
student = create(:student, :without_enrollment)
|
||||
enrollment = create(
|
||||
:classroom_enrollment, student: student, enrolled_at: 1.hour.ago, unenrolled_at: nil,
|
||||
primary: true
|
||||
)
|
||||
|
||||
freeze_time do
|
||||
enrollment.unenroll!
|
||||
assert_in_delta Time.current, enrollment.reload.unenrolled_at, 1.second
|
||||
end
|
||||
end
|
||||
|
||||
test "unenroll! accepts custom unenrollment time" do
|
||||
student = create(:student, :without_enrollment)
|
||||
enrollment = create(:classroom_enrollment, student: student, enrolled_at: 2.weeks.ago, unenrolled_at: nil)
|
||||
custom_time = 1.week.ago
|
||||
|
||||
enrollment.unenroll!(at: custom_time)
|
||||
|
||||
assert_in_delta custom_time, enrollment.reload.unenrolled_at, 1.second
|
||||
end
|
||||
|
||||
test "unenroll! sets primary to false" do
|
||||
student = create(:student, :without_enrollment)
|
||||
enrollment = create(:classroom_enrollment, student: student, primary: true)
|
||||
|
||||
enrollment.unenroll!
|
||||
|
||||
assert_not enrollment.reload.primary
|
||||
end
|
||||
|
||||
test "unenroll! returns self" do
|
||||
enrollment = create(:classroom_enrollment)
|
||||
result = enrollment.unenroll!
|
||||
assert_equal enrollment, result
|
||||
end
|
||||
|
||||
test "current? returns true when unenrolled_at is nil" do
|
||||
enrollment = build(:classroom_enrollment, unenrolled_at: nil)
|
||||
assert enrollment.current?
|
||||
end
|
||||
|
||||
test "current? returns false when unenrolled_at is set" do
|
||||
enrollment = build(:classroom_enrollment, unenrolled_at: 1.day.ago)
|
||||
assert_not enrollment.current?
|
||||
end
|
||||
|
||||
test "historical? returns false when unenrolled_at is nil" do
|
||||
enrollment = build(:classroom_enrollment, unenrolled_at: nil)
|
||||
assert_not enrollment.historical?
|
||||
end
|
||||
|
||||
test "historical? returns true when unenrolled_at is set" do
|
||||
enrollment = build(:classroom_enrollment, unenrolled_at: 1.day.ago)
|
||||
assert enrollment.historical?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ClassroomGradeTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:classroom_grade).validate!
|
||||
end
|
||||
|
||||
test "belongs to classroom" do
|
||||
classroom = create(:classroom)
|
||||
cg = create(:classroom_grade, classroom: classroom)
|
||||
|
||||
assert_equal classroom, cg.classroom
|
||||
end
|
||||
|
||||
test "belongs to grade" do
|
||||
grade = create(:grade)
|
||||
cg = create(:classroom_grade, grade: grade)
|
||||
|
||||
assert_equal grade, cg.grade
|
||||
end
|
||||
|
||||
test "validates uniqueness of grade scoped to classroom" do
|
||||
classroom = create(:classroom)
|
||||
grade = create(:grade)
|
||||
create(:classroom_grade, classroom: classroom, grade: grade)
|
||||
|
||||
duplicate = build(:classroom_grade, classroom: classroom, grade: grade)
|
||||
assert_not duplicate.valid?
|
||||
assert_includes duplicate.errors[:grade_id], "has already been taken"
|
||||
end
|
||||
|
||||
test "destroying classroom destroys classroom_grade" do
|
||||
classroom = create(:classroom)
|
||||
create(:classroom_grade, classroom: classroom)
|
||||
|
||||
assert_difference "ClassroomGrade.count", -2 do
|
||||
classroom.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,167 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ClassroomTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:classroom).validate!
|
||||
end
|
||||
|
||||
test "name is required" do
|
||||
classroom = build(:classroom, name: "")
|
||||
assert_not classroom.valid?
|
||||
assert_includes classroom.errors[:name], "can't be blank"
|
||||
end
|
||||
|
||||
test "destroying classroom nullifies user classroom_id instead of destroying users" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student, classroom: classroom)
|
||||
teacher = create(:teacher)
|
||||
teacher.classrooms << classroom
|
||||
admin = create(:admin, classroom: classroom)
|
||||
|
||||
user_ids = [student.id, teacher.id, admin.id]
|
||||
|
||||
classroom.destroy!
|
||||
|
||||
user_ids.each do |user_id|
|
||||
assert User.exists?(user_id), "User #{user_id} should still exist after classroom destruction"
|
||||
end
|
||||
|
||||
# Students and admins should have their classroom_id nullified
|
||||
[student, admin].each do |user|
|
||||
user.reload
|
||||
assert_nil user.classroom_id, "User #{user.id} classroom_id should be null after classroom destruction"
|
||||
end
|
||||
|
||||
# Teachers should still exist but have no classrooms
|
||||
teacher.reload
|
||||
assert_equal 0, teacher.classrooms.count, "Teacher should have no classrooms after classroom destruction"
|
||||
end
|
||||
|
||||
test "teachers can belong to multiple classrooms" do
|
||||
teacher = create(:teacher)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
|
||||
teacher.classrooms << classroom1
|
||||
teacher.classrooms << classroom2
|
||||
|
||||
assert_equal 2, teacher.classrooms.count
|
||||
assert_includes teacher.classrooms, classroom1
|
||||
assert_includes teacher.classrooms, classroom2
|
||||
end
|
||||
|
||||
test "has many students through classrooms association" do
|
||||
classroom = create(:classroom)
|
||||
student1 = create(:student, classroom: classroom)
|
||||
student2 = create(:student, classroom: classroom)
|
||||
|
||||
assert_includes classroom.students, student1
|
||||
assert_includes classroom.students, student2
|
||||
assert_equal 2, classroom.students.count
|
||||
end
|
||||
|
||||
test "has many teachers through teacher_classrooms association" do
|
||||
classroom = create(:classroom)
|
||||
teacher1 = create(:teacher, classrooms: [classroom])
|
||||
teacher2 = create(:teacher, classrooms: [classroom])
|
||||
|
||||
assert_includes classroom.teachers, teacher1
|
||||
assert_includes classroom.teachers, teacher2
|
||||
assert_equal 2, classroom.teachers.count
|
||||
end
|
||||
|
||||
test "students association only includes Student type users" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student, classroom: classroom)
|
||||
teacher = create(:teacher, classroom: classroom)
|
||||
admin = create(:admin, classroom: classroom)
|
||||
|
||||
assert_includes classroom.students, student
|
||||
assert_not_includes classroom.students, teacher
|
||||
assert_not_includes classroom.students, admin
|
||||
end
|
||||
|
||||
test "teachers association only includes Teacher type users" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student)
|
||||
admin = create(:admin)
|
||||
|
||||
assert_raises(ActiveRecord::AssociationTypeMismatch) { classroom.teachers << student }
|
||||
assert_raises(ActiveRecord::AssociationTypeMismatch) { classroom.teachers << admin }
|
||||
end
|
||||
|
||||
test "students association excludes discarded students" do
|
||||
classroom = create(:classroom)
|
||||
kept_student = create(:student, classroom: classroom)
|
||||
discarded_student = create(:student, classroom: classroom)
|
||||
discarded_student.discard
|
||||
|
||||
assert_includes classroom.students, kept_student
|
||||
assert_not_includes classroom.students, discarded_student
|
||||
assert_equal [kept_student.id], classroom.students.pluck(:id)
|
||||
end
|
||||
|
||||
test "creates gradebooks for all quarters when classroom is created" do
|
||||
school_year = create(:school_year)
|
||||
|
||||
assert_difference("GradeBook.count", 4) do
|
||||
classroom = create(:classroom, school_year: school_year)
|
||||
|
||||
school_year.quarters.each do |quarter|
|
||||
assert GradeBook.exists?(classroom: classroom, quarter: quarter)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Tests for sorting scopes
|
||||
test "order_by_name sorts by name ascending" do
|
||||
create(:classroom, name: "Zebra")
|
||||
create(:classroom, name: "Alpha")
|
||||
create(:classroom, name: "Middle")
|
||||
|
||||
classrooms = Classroom.order_by_name(:asc)
|
||||
assert_equal %w[Alpha Middle Zebra], classrooms.pluck(:name)
|
||||
end
|
||||
|
||||
test "order_by_name sorts by name descending" do
|
||||
create(:classroom, name: "Zebra")
|
||||
create(:classroom, name: "Alpha")
|
||||
create(:classroom, name: "Middle")
|
||||
|
||||
classrooms = Classroom.order_by_name(:desc)
|
||||
assert_equal %w[Zebra Middle Alpha], classrooms.pluck(:name)
|
||||
end
|
||||
|
||||
test "order_by_student_count sorts by student count" do
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
create(:student, classroom: classroom1)
|
||||
create(:student, classroom: classroom1)
|
||||
create(:student, classroom: classroom2)
|
||||
|
||||
classrooms = Classroom.order_by_student_count(:desc)
|
||||
assert_equal classroom1.id, classrooms.first.id
|
||||
assert_equal classroom2.id, classrooms.last.id
|
||||
end
|
||||
|
||||
test "apply_sorting defaults to name ascending" do
|
||||
create(:classroom, name: "Zebra")
|
||||
create(:classroom, name: "Alpha")
|
||||
|
||||
classrooms = Classroom.apply_sorting(nil, nil, nil)
|
||||
assert_equal %w[Alpha Zebra], classrooms.pluck(:name)
|
||||
end
|
||||
|
||||
test "apply_sorting with sort_column applies correct scope" do
|
||||
classroom1 = create(:classroom, name: "Zebra")
|
||||
classroom2 = create(:classroom, name: "Alpha")
|
||||
create(:student, classroom: classroom1)
|
||||
create(:student, classroom: classroom2)
|
||||
create(:student, classroom: classroom1)
|
||||
|
||||
classrooms = Classroom.apply_sorting(nil, "student_count", :desc)
|
||||
assert_equal classroom1.id, classrooms.first.id
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class EarningsSummaryTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@student = create(:student)
|
||||
@portfolio = @student.portfolio
|
||||
@earnings_summary = EarningsSummary.new(@portfolio)
|
||||
end
|
||||
|
||||
test "should calculate attendance earnings" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 500, reason: :attendance_earnings)
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 300, reason: :attendance_earnings)
|
||||
|
||||
assert_equal 800, @earnings_summary.attendance_earnings_cents
|
||||
end
|
||||
|
||||
test "should calculate reading earnings" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 600, reason: :reading_earnings)
|
||||
|
||||
assert_equal 600, @earnings_summary.reading_earnings_cents
|
||||
end
|
||||
|
||||
test "should calculate math earnings" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 400, reason: :math_earnings)
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 100, reason: :math_earnings)
|
||||
|
||||
assert_equal 500, @earnings_summary.math_earnings_cents
|
||||
end
|
||||
|
||||
test "should calculate awards" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 1000, reason: :awards)
|
||||
|
||||
assert_equal 1000, @earnings_summary.awards_cents
|
||||
end
|
||||
|
||||
test "should calculate total earnings from all sources" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 500, reason: :attendance_earnings)
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 600, reason: :reading_earnings)
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 400, reason: :math_earnings)
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 1000, reason: :awards)
|
||||
|
||||
assert_equal 2500, @earnings_summary.total_earnings_cents
|
||||
end
|
||||
|
||||
test "should calculate transaction fees" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 100, reason: :transaction_fees)
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 50, reason: :transaction_fees)
|
||||
|
||||
assert_equal 150, @earnings_summary.transaction_fees_cents
|
||||
end
|
||||
|
||||
test "should return zero for earnings with no transactions" do
|
||||
assert_equal 0, @earnings_summary.attendance_earnings_cents
|
||||
assert_equal 0, @earnings_summary.reading_earnings_cents
|
||||
assert_equal 0, @earnings_summary.math_earnings_cents
|
||||
assert_equal 0, @earnings_summary.awards_cents
|
||||
assert_equal 0, @earnings_summary.total_earnings_cents
|
||||
assert_equal 0, @earnings_summary.transaction_fees_cents
|
||||
end
|
||||
|
||||
test "should only sum deposits not debits" do
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 500, reason: :math_earnings)
|
||||
create(:portfolio_transaction, :debit, portfolio: @portfolio, amount_cents: 200, reason: :math_earnings)
|
||||
|
||||
assert_equal 500, @earnings_summary.math_earnings_cents
|
||||
end
|
||||
|
||||
test "should only sum transactions for the specific portfolio" do
|
||||
other_student = create(:student)
|
||||
other_portfolio = other_student.portfolio
|
||||
|
||||
create(:portfolio_transaction, :deposit, portfolio: @portfolio, amount_cents: 500, reason: :math_earnings)
|
||||
create(:portfolio_transaction, :deposit, portfolio: other_portfolio, amount_cents: 1000, reason: :math_earnings)
|
||||
|
||||
assert_equal 500, @earnings_summary.math_earnings_cents
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# test/models/grade_book_test.rb
|
||||
require "test_helper"
|
||||
|
||||
class GradeBookTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:grade_book).validate!
|
||||
end
|
||||
|
||||
test "default status is draft" do
|
||||
book = create(:grade_book)
|
||||
assert_equal "draft", book.status
|
||||
assert_predicate book, :draft?
|
||||
end
|
||||
|
||||
test "status enum transitions work" do
|
||||
book = create(:grade_book)
|
||||
book.verified!
|
||||
assert_equal "verified", book.status
|
||||
assert_predicate book, :verified?
|
||||
|
||||
book.completed!
|
||||
assert_equal "completed", book.status
|
||||
assert_predicate book, :completed?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,101 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# test/models/grade_entry_test.rb
|
||||
require "test_helper"
|
||||
|
||||
class GradeEntryTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:grade_entry).validate!
|
||||
end
|
||||
|
||||
test "math_grade, reading_grade, and attendance_days accept any string or nil" do
|
||||
entry = build(
|
||||
:grade_entry,
|
||||
math_grade: "A+",
|
||||
reading_grade: "B-",
|
||||
attendance_days: 45
|
||||
)
|
||||
assert entry.valid?
|
||||
|
||||
entry.math_grade = nil
|
||||
entry.reading_grade = nil
|
||||
entry.attendance_days = nil
|
||||
assert entry.valid?
|
||||
end
|
||||
|
||||
test "#earnings_for_attendance is 20 cents per day" do
|
||||
entry = build(:grade_entry, attendance_days: 0)
|
||||
assert_equal 0, entry.earnings_for_attendance
|
||||
|
||||
entry.attendance_days = 5
|
||||
assert_equal 100, entry.earnings_for_attendance
|
||||
|
||||
entry.attendance_days = 12
|
||||
assert_equal 240, entry.earnings_for_attendance
|
||||
end
|
||||
|
||||
test "#attendance_perfect_earnings returns $1 only for perfect attendance" do
|
||||
entry = build(:grade_entry, attendance_days: 5, is_perfect_attendance: true)
|
||||
assert_equal 1_00, entry.attendance_perfect_earnings
|
||||
|
||||
entry.is_perfect_attendance = false
|
||||
assert_equal 0, entry.attendance_perfect_earnings
|
||||
end
|
||||
|
||||
test "#earnings_for_reading grade is $2 for B, $3 for an A" do
|
||||
entry = build(:grade_entry, reading_grade: nil)
|
||||
|
||||
["F", "D", "C-", "C", "C+"].each do |grade|
|
||||
entry.reading_grade = grade
|
||||
assert_equal 0, entry.earnings_for_reading
|
||||
end
|
||||
|
||||
["B-", "B", "B+"].each do |grade|
|
||||
entry.reading_grade = grade
|
||||
assert_equal 2_00, entry.earnings_for_reading
|
||||
end
|
||||
|
||||
["A-", "A", "A+"].each do |grade|
|
||||
entry.reading_grade = grade
|
||||
assert_equal 3_00, entry.earnings_for_reading
|
||||
end
|
||||
end
|
||||
|
||||
test "#earnings_for_math grade is $2 for B, $3 for an A" do
|
||||
entry = build(:grade_entry, math_grade: nil)
|
||||
|
||||
["F", "D", "C-", "C", "C+"].each do |grade|
|
||||
entry.math_grade = grade
|
||||
assert_equal 0, entry.earnings_for_math
|
||||
end
|
||||
|
||||
["B-", "B", "B+"].each do |grade|
|
||||
entry.math_grade = grade
|
||||
assert_equal 2_00, entry.earnings_for_math
|
||||
end
|
||||
|
||||
["A-", "A", "A+"].each do |grade|
|
||||
entry.math_grade = grade
|
||||
assert_equal 3_00, entry.earnings_for_math
|
||||
end
|
||||
end
|
||||
|
||||
test "subject improvement earnings returns 0 when no previous entry or no improvement" do
|
||||
entry = build(:grade_entry, math_grade: "A", reading_grade: "A")
|
||||
assert_equal 0, entry.math_improvement_earnings(nil)
|
||||
assert_equal 0, entry.reading_improvement_earnings(nil)
|
||||
|
||||
previous_entry = build(:grade_entry, math_grade: "B", reading_grade: "B")
|
||||
worse_entry = build(:grade_entry, math_grade: "C", reading_grade: "C")
|
||||
assert_equal 0, worse_entry.math_improvement_earnings(previous_entry)
|
||||
assert_equal 0, worse_entry.reading_improvement_earnings(previous_entry)
|
||||
end
|
||||
|
||||
test "subject improvement earnings returns $2 when grade has improved" do
|
||||
previous_entry = build(:grade_entry, math_grade: "C", reading_grade: "C")
|
||||
improved_entry = build(:grade_entry, math_grade: "B", reading_grade: "B")
|
||||
|
||||
assert_equal 2_00, improved_entry.math_improvement_earnings(previous_entry)
|
||||
assert_equal 2_00, improved_entry.reading_improvement_earnings(previous_entry)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class GradeTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:grade).validate!
|
||||
end
|
||||
|
||||
test "has many classroom_grades" do
|
||||
grade = create(:grade)
|
||||
cg1 = create(:classroom_grade, grade: grade)
|
||||
cg2 = create(:classroom_grade, grade: grade)
|
||||
|
||||
assert_equal 2, grade.classroom_grades.count
|
||||
assert_includes grade.classroom_grades, cg1
|
||||
assert_includes grade.classroom_grades, cg2
|
||||
end
|
||||
|
||||
test "has many classrooms through classroom_grades" do
|
||||
grade = create(:grade)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
create(:classroom_grade, grade: grade, classroom: classroom1)
|
||||
create(:classroom_grade, grade: grade, classroom: classroom2)
|
||||
|
||||
assert_equal 2, grade.classrooms.count
|
||||
assert_includes grade.classrooms, classroom1
|
||||
assert_includes grade.classrooms, classroom2
|
||||
end
|
||||
|
||||
test "can destroy a grade with no classrooms" do
|
||||
grade = create(:grade)
|
||||
|
||||
assert_difference "Grade.count", -1 do
|
||||
grade.destroy
|
||||
end
|
||||
end
|
||||
|
||||
test "cannot destroy a grade that is used by classrooms" do
|
||||
grade = create(:grade)
|
||||
create(:classroom_grade, grade: grade)
|
||||
|
||||
assert_not grade.destroy
|
||||
assert grade.errors.any?
|
||||
assert_includes grade.errors[:base], "Cannot delete record because dependent classroom grades exist"
|
||||
end
|
||||
|
||||
test "name must be present" do
|
||||
grade = build(:grade, name: nil)
|
||||
assert_not grade.valid?
|
||||
assert_includes grade.errors[:name], "can't be blank"
|
||||
end
|
||||
|
||||
test "level must be present" do
|
||||
grade = build(:grade, level: nil)
|
||||
assert_not grade.valid?
|
||||
assert_includes grade.errors[:level], "can't be blank"
|
||||
end
|
||||
|
||||
test "name is unique and case-insensitively" do
|
||||
create(:grade, name: "5th Grade", level: 6)
|
||||
|
||||
duplicate = build(:grade, name: "5TH grade", level: 6)
|
||||
assert_not duplicate.valid?
|
||||
assert_includes duplicate.errors[:name], "has already been taken"
|
||||
end
|
||||
|
||||
test "level is unique" do
|
||||
create(:grade, name: "5th Grade", level: 6)
|
||||
|
||||
duplicate = build(:grade, name: "5th Grade", level: 6)
|
||||
assert_not duplicate.valid?
|
||||
assert_includes duplicate.errors[:level], "has already been taken"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,456 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class OrderTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order = create(:order, user: user, stock: stock, shares: 1, action: :sell)
|
||||
assert order.valid?
|
||||
end
|
||||
|
||||
test "defaults to pending status when created" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order = create(:order, user: user, stock: stock, shares: 1, action: :sell)
|
||||
|
||||
assert_equal "pending", order.status
|
||||
end
|
||||
|
||||
test ".pending" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order1 = create(:order, status: :pending, action: :sell, user: user, stock: stock, shares: 1)
|
||||
create(:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1)
|
||||
create(:order, status: :canceled, action: :sell, user: user, stock: stock, shares: 1)
|
||||
order4 = create(:order, status: :pending, action: :sell, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert_equal [order1, order4], Order.pending
|
||||
end
|
||||
|
||||
test ".completed" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order1 = create(:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1)
|
||||
create(:order, status: :pending, action: :sell, user: user, stock: stock, shares: 1)
|
||||
create(:order, status: :canceled, action: :sell, user: user, stock: stock, shares: 1)
|
||||
order4 = create(:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert_equal [order1, order4], Order.completed
|
||||
end
|
||||
|
||||
test ".canceled" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order1 = create(:order, status: :canceled, action: :sell, user: user, stock: stock, shares: 1)
|
||||
create(:order, status: :pending, action: :sell, user: user, stock: stock, shares: 1)
|
||||
create(:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1)
|
||||
order4 = create(:order, status: :canceled, action: :sell, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert_equal [order1, order4], Order.canceled
|
||||
end
|
||||
|
||||
test "cannot buy archived stocks" do
|
||||
user = create(:student)
|
||||
stock = create(:stock, archived: true)
|
||||
|
||||
order = build(:order, action: :buy, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert_not order.valid?
|
||||
assert_includes order.errors[:stock], "Cannot purchase shares of archived stocks"
|
||||
end
|
||||
|
||||
test "can sell archived stocks" do
|
||||
user = create(:student)
|
||||
stock = create(:stock, archived: true)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert order.valid?
|
||||
end
|
||||
|
||||
test "#purchase_cost" do
|
||||
user = create(:student)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
# Add funds for buy order
|
||||
user.portfolio.portfolio_transactions.create!(amount_cents: 10_000, transaction_type: :deposit)
|
||||
|
||||
order = create(:order, action: :buy, user: user, stock: stock, shares: 5.1)
|
||||
|
||||
assert_equal 5_100, order.purchase_cost
|
||||
end
|
||||
|
||||
test "creates a buy order without portfolio transaction" do
|
||||
user = create(:student)
|
||||
user.portfolio.portfolio_transactions.create!(amount_cents: 5000, transaction_type: :deposit) # $50.00
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
order = build(:order, action: :buy, user: user, stock: stock, shares: 2.5)
|
||||
|
||||
assert_difference "PortfolioTransaction.count", 0 do
|
||||
order.save!
|
||||
end
|
||||
|
||||
assert_equal "buy", order.action
|
||||
assert order.buy?
|
||||
end
|
||||
|
||||
test "sell order validation allows selling when sufficient shares owned" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10, purchase_price: 200.0)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 5)
|
||||
|
||||
assert order.valid?
|
||||
end
|
||||
|
||||
test "sell order validation prevents selling more shares than owned" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5, purchase_price: 200.0)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 10)
|
||||
|
||||
assert_not order.valid?
|
||||
assert_includes order.errors[:base], "Cannot sell more shares than you own (5 available)"
|
||||
end
|
||||
|
||||
test "sell order validation prevents selling when no shares owned" do
|
||||
user = create(:student)
|
||||
create(:portfolio, user: user)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert_not order.valid?
|
||||
assert_includes order.errors[:base], "Cannot sell more shares than you own (0 available)"
|
||||
end
|
||||
|
||||
test "sell order validation with multiple portfolio_stock records" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10, purchase_price: 200.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5, purchase_price: 250.0)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 12)
|
||||
assert order.valid?
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 20)
|
||||
assert_not order.valid?
|
||||
assert_includes order.errors[:base], "Cannot sell more shares than you own (15 available)"
|
||||
end
|
||||
|
||||
test "buy order validation is not affected by sell validation" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
portfolio.portfolio_transactions.create!(amount_cents: 15_000, transaction_type: :deposit) # $150.00
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
|
||||
order = build(:order, :buy, user: user, stock: stock, shares: 10)
|
||||
|
||||
assert order.valid?
|
||||
end
|
||||
|
||||
test "creates a sell order without portfolio transaction" do
|
||||
user = create(:student)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 5, purchase_price: 800)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 2.5)
|
||||
|
||||
assert_difference "PortfolioTransaction.count", 0 do
|
||||
order.save!
|
||||
end
|
||||
|
||||
assert_equal "sell", order.action
|
||||
assert order.sell?
|
||||
end
|
||||
|
||||
test "buy order validation prevents buying when insufficient funds" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
portfolio.portfolio_transactions.create!(amount_cents: 100, transaction_type: :deposit)
|
||||
|
||||
stock = create(:stock, price_cents: 1000)
|
||||
order = build(:order, action: :buy, user: user, stock: stock, shares: 5)
|
||||
|
||||
assert_not order.valid?
|
||||
assert_includes order.errors[:shares], "Insufficient funds. You have $1.00 but need $51.00"
|
||||
end
|
||||
|
||||
test "buy order validation prevents buying if not enough funds to cover stocks and transaction fee" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
portfolio.portfolio_transactions.create!(amount_cents: 10_00, transaction_type: :deposit)
|
||||
|
||||
stock = create(:stock, price_cents: 10_00)
|
||||
order = build(:order, action: :buy, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert_not order.valid?(:create)
|
||||
assert_includes order.errors[:shares], "Insufficient funds. You have $10.00 but need $11.00"
|
||||
end
|
||||
|
||||
test "buy order validation allows buying if enough funds when multiple orders share transaction fees" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
portfolio.portfolio_transactions.create!(amount_cents: 21_00, transaction_type: :deposit)
|
||||
|
||||
stock = create(:stock, price_cents: 10_00)
|
||||
pending_order = build(:order, :pending, action: :buy, user: user, stock: stock, shares: 1)
|
||||
assert_nothing_raised do
|
||||
pending_order.save!
|
||||
end
|
||||
|
||||
order = build(:order, action: :buy, user: user, stock: stock, shares: 1)
|
||||
|
||||
assert order.valid?(:create)
|
||||
end
|
||||
|
||||
test "buy order validation allows buying when sufficient funds" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
portfolio.portfolio_transactions.create!(amount_cents: 100_00, transaction_type: :deposit)
|
||||
|
||||
stock = create(:stock, price_cents: 10_00)
|
||||
order = build(:order, action: :buy, user: user, stock: stock, shares: 5)
|
||||
|
||||
assert order.valid?(:create)
|
||||
end
|
||||
|
||||
test "update order allows user to update pending buy order when transaction amount less than portfolio value" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
portfolio.portfolio_transactions.create!(amount_cents: 100_00, transaction_type: :deposit)
|
||||
|
||||
stock = create(:stock, price_cents: 10_00)
|
||||
order = build(:order, action: :buy, user: user, stock: stock, shares: 5)
|
||||
|
||||
assert_difference "PortfolioTransaction.count", 0 do
|
||||
order.save!
|
||||
end
|
||||
|
||||
assert order.valid?
|
||||
|
||||
# NOTE: PortfolioTransaction updates are handled by ExecuteOrder service
|
||||
order.update!(shares: 6)
|
||||
assert_equal 6, order.shares
|
||||
end
|
||||
|
||||
test "update order does not allow user to update pending buy order when transaction amount exceeds portfolio value" do
|
||||
user = create(:student)
|
||||
portfolio = create(:portfolio, user: user)
|
||||
portfolio.portfolio_transactions.create!(amount_cents: 10_00, transaction_type: :deposit)
|
||||
|
||||
stock = create(:stock, price_cents: 1_00)
|
||||
order = build(:order, :pending, action: :buy, user: user, stock: stock, shares: 5)
|
||||
|
||||
assert_no_changes "PortfolioTransaction.count" do
|
||||
order.save!
|
||||
end
|
||||
|
||||
order.shares = 10
|
||||
|
||||
assert_not order.valid?(:update)
|
||||
assert_includes order.errors[:shares], "Insufficient funds. You have $10.00 but need $11.00"
|
||||
end
|
||||
|
||||
test "update order allows user to update pending sell order when shares less than portfolio value" do
|
||||
user = create(:student)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 5, purchase_price: 800)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 2.5)
|
||||
|
||||
assert_difference "PortfolioTransaction.count", 0 do
|
||||
order.save!
|
||||
end
|
||||
|
||||
order.update!(shares: 3)
|
||||
assert_equal 3, order.shares
|
||||
end
|
||||
|
||||
test "update order prevents overselling shares when updating sell order" do
|
||||
user = create(:student)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 5, purchase_price: 1_000)
|
||||
|
||||
order = build(:order, action: :sell, user: user, stock: stock, shares: 2)
|
||||
order.save!
|
||||
|
||||
order.shares = 6
|
||||
|
||||
assert_not order.valid?
|
||||
assert_includes order.errors[:base], "Cannot sell more shares than you own (5 available)"
|
||||
end
|
||||
|
||||
test "validations handle invalid shares safely without exceptions" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
user.portfolio.portfolio_transactions.create!(amount_cents: 10_000, transaction_type: :deposit)
|
||||
|
||||
[nil, "", "not_a_number"].each do |invalid_shares|
|
||||
buy_order = build(:order, action: :buy, user: user, stock: stock, shares: invalid_shares)
|
||||
sell_order = build(:order, action: :sell, user: user, stock: stock, shares: invalid_shares)
|
||||
|
||||
assert_nothing_raised do
|
||||
assert_equal false, buy_order.valid?
|
||||
assert_equal false, sell_order.valid?
|
||||
end
|
||||
assert buy_order.errors[:shares].any?
|
||||
assert sell_order.errors[:shares].any?
|
||||
end
|
||||
end
|
||||
|
||||
test ".for_student" do
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
classroom = create(:classroom, :with_trading)
|
||||
student1, order1 = create_student_and_order(stock, classroom)
|
||||
_student2, _order2 = create_student_and_order(stock, classroom)
|
||||
|
||||
result = Order.for_student(student1)
|
||||
|
||||
assert_equal [order1], result
|
||||
end
|
||||
|
||||
test ".for_teacher" do
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
classroom1, order1 = create_classroom_and_student_order(stock)
|
||||
_classroom2, _order2 = create_classroom_and_student_order(stock)
|
||||
classroom3, order3 = create_classroom_and_student_order(stock)
|
||||
teacher = create(:teacher, classrooms: [classroom1, classroom3])
|
||||
|
||||
result = Order.for_teacher(teacher)
|
||||
|
||||
assert_equal [order1.id, order3.id], result.pluck(:id).sort
|
||||
end
|
||||
|
||||
test ".for_teacher with no classrooms" do
|
||||
teacher = create(:teacher)
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
_classroom, _order = create_classroom_and_student_order(stock)
|
||||
|
||||
result = Order.for_teacher(teacher)
|
||||
|
||||
assert_empty result
|
||||
end
|
||||
|
||||
# Tests for sorting scopes
|
||||
test "order_by_created_at sorts ascending" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order1 = create(
|
||||
:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1,
|
||||
created_at: 1.day.ago
|
||||
)
|
||||
order2 = create(
|
||||
:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1,
|
||||
created_at: Time.current
|
||||
)
|
||||
|
||||
result = Order.order_by_created_at(:asc)
|
||||
assert_equal [order1.id, order2.id], result.pluck(:id)
|
||||
end
|
||||
|
||||
test "order_by_created_at sorts descending" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
|
||||
order1 = create(
|
||||
:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1,
|
||||
created_at: 1.day.ago
|
||||
)
|
||||
order2 = create(
|
||||
:order, status: :completed, action: :sell, user: user, stock: stock, shares: 1,
|
||||
created_at: Time.current
|
||||
)
|
||||
|
||||
result = Order.order_by_created_at(:desc)
|
||||
assert_equal [order2.id, order1.id], result.pluck(:id)
|
||||
end
|
||||
|
||||
test "order_by_stock sorts by stock ticker" do
|
||||
user = create(:student)
|
||||
stock1 = create(:stock, ticker: "ZZZ")
|
||||
stock2 = create(:stock, ticker: "AAA")
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock1, shares: 10)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock2, shares: 10)
|
||||
|
||||
create(
|
||||
:order, status: :completed, action: :sell, user: user, stock: stock1, shares: 1,
|
||||
created_at: 1.day.ago
|
||||
)
|
||||
create(
|
||||
:order, status: :completed, action: :sell, user: user, stock: stock2, shares: 1,
|
||||
created_at: Time.current
|
||||
)
|
||||
|
||||
result = Order.order_by_stock(:asc)
|
||||
assert_equal "AAA", result.first.stock.ticker
|
||||
end
|
||||
|
||||
test "apply_sorting defaults to created_at descending" do
|
||||
user = create(:student)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 10)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock, shares: 5)
|
||||
|
||||
create(:order, status: :completed, action: :sell, user: user, stock: stock, created_at: 1.day.ago)
|
||||
create(:order, status: :completed, action: :sell, user: user, stock: stock, created_at: Time.current)
|
||||
|
||||
result = Order.apply_sorting(nil, nil, nil)
|
||||
assert_equal :desc, result.order_values.first.direction
|
||||
end
|
||||
|
||||
test "apply_sorting with sort_column applies correct scope" do
|
||||
user = create(:student)
|
||||
stock1 = create(:stock, ticker: "ZZZ")
|
||||
stock2 = create(:stock, ticker: "AAA")
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock1, shares: 10)
|
||||
create(:portfolio_stock, portfolio: user.portfolio, stock: stock2, shares: 5)
|
||||
|
||||
create(:order, status: :completed, action: :sell, user: user, stock: stock1, created_at: 1.day.ago)
|
||||
create(:order, status: :completed, action: :sell, user: user, stock: stock2, created_at: Time.current)
|
||||
|
||||
result = Order.apply_sorting(nil, "stock", :asc)
|
||||
assert_equal "AAA", result.first.stock.ticker
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_classroom_and_student_order(stock)
|
||||
classroom = create(:classroom, :with_trading)
|
||||
_student, order = create_student_and_order(stock, classroom)
|
||||
|
||||
[classroom, order]
|
||||
end
|
||||
|
||||
def create_student_and_order(stock, classroom)
|
||||
student = create(:student, classroom:)
|
||||
portfolio = create(:portfolio, user: student)
|
||||
create(:portfolio_transaction, :deposit, portfolio:, amount_cents: 10_000)
|
||||
order = create(:order, :buy, user: student, stock:)
|
||||
|
||||
[student, order]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class PortfolioPositionTest < ActiveSupport::TestCase
|
||||
test "current_value and current_value_cents calculation" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock, price_cents: 12_000)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 8, purchase_price: 100.0)
|
||||
|
||||
position = PortfolioPosition.for_portfolio(portfolio).first
|
||||
|
||||
assert_equal 960.0, position.current_value
|
||||
assert_equal 96_000, position.current_value_cents
|
||||
end
|
||||
|
||||
test "for_portfolio aggregates buy/sell transactions correctly" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock, ticker: "AAPL")
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: -3)
|
||||
|
||||
positions = PortfolioPosition.for_portfolio(portfolio)
|
||||
|
||||
assert_equal 1, positions.length
|
||||
position = positions.first
|
||||
assert_equal 12, position.shares
|
||||
assert_equal stock, position.stock
|
||||
end
|
||||
|
||||
test "for_portfolio excludes stocks with zero net holdings" do
|
||||
portfolio = create(:portfolio)
|
||||
stock1 = create(:stock, ticker: "ZERO")
|
||||
stock2 = create(:stock, ticker: "POSITIVE")
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock1, shares: 10)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock1, shares: -10)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock2, shares: 5)
|
||||
|
||||
positions = PortfolioPosition.for_portfolio(portfolio)
|
||||
|
||||
assert_equal 1, positions.length
|
||||
assert_equal "POSITIVE", positions.first.stock_ticker
|
||||
assert_equal 5, positions.first.shares
|
||||
end
|
||||
|
||||
test "change_amount with multiple purchases and sells" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock, price_cents: 15_000)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10, purchase_price: 100.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5, purchase_price: 120.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: -3, purchase_price: 130.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 8, purchase_price: 140.0)
|
||||
|
||||
positions = PortfolioPosition.for_portfolio(portfolio)
|
||||
position = positions.first
|
||||
|
||||
cost_basis = (100.0 * 10) + (120.0 * 5) + (-3 * 130.0) + (140.0 * 8)
|
||||
expected_change = (150.0 * 20) - cost_basis
|
||||
assert_equal expected_change, position.change_amount
|
||||
end
|
||||
|
||||
test "change_amount calculates profit correctly" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock, price_cents: 700)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 4, purchase_price: 5.0)
|
||||
|
||||
position = PortfolioPosition.for_portfolio(portfolio).first
|
||||
|
||||
assert_equal 4, position.shares
|
||||
assert_in_delta 8.0, position.change_amount, 0.001
|
||||
end
|
||||
|
||||
test "change_amount calculates loss correctly" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock, price_cents: 100)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 2, purchase_price: 5.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 3, purchase_price: 7.0)
|
||||
|
||||
position = PortfolioPosition.for_portfolio(portfolio).first
|
||||
|
||||
assert_equal 5, position.shares
|
||||
assert_in_delta(-26.0, position.change_amount, 0.001)
|
||||
end
|
||||
|
||||
test "total_return_amount with multiple purchases and sells" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock, price_cents: 15_000)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10, purchase_price: 100.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5, purchase_price: 120.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: -3, purchase_price: 130.0)
|
||||
|
||||
positions = PortfolioPosition.for_portfolio(portfolio)
|
||||
position = positions.first
|
||||
|
||||
expected_value = 150.0 * 12
|
||||
assert_equal expected_value, position.total_return_amount
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class PortfolioSnapshotTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:portfolio_snapshot).validate!
|
||||
end
|
||||
|
||||
test "#current_worth converts cents to dollars" do
|
||||
portfolio_snapshot = build(:portfolio_snapshot, worth_cents: 50_000)
|
||||
assert_equal 500.0, portfolio_snapshot.current_worth
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class PortfolioStockTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:portfolio_stock).validate!
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,208 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class PortfolioTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:portfolio).validate!
|
||||
end
|
||||
|
||||
test "#cash_balance" do
|
||||
portfolio = create(:portfolio)
|
||||
user = portfolio.user
|
||||
|
||||
# should increase cash_balance
|
||||
create(:portfolio_transaction, :deposit, portfolio: portfolio, amount_cents: 10_00)
|
||||
|
||||
stock = create(:stock, price_cents: 100)
|
||||
|
||||
# This simulates previous completed purchases
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 20, purchase_price: 100)
|
||||
|
||||
# pending debit for stock purchase, should decrease cash_balance
|
||||
# -$2.00 - $1.00 fee = -$3.00
|
||||
create(:order, :pending, :buy, stock: stock, shares: 2, user: user)
|
||||
|
||||
# pending credit for stock sale, should NOT affect cash_balance
|
||||
create(:order, :pending, :sell, stock:, shares: 3, user:)
|
||||
|
||||
# canceled debit for stock purchase, should NOT affect cash_balance
|
||||
create(:order, :canceled, :buy, stock:, shares: 1, user:)
|
||||
|
||||
# canceled credit for stock sale, should NOT affect cash_balance
|
||||
create(:order, :canceled, :sell, stock:, shares: 4, user:)
|
||||
|
||||
# successful completed stock purchase, should decrease cash_balance
|
||||
# -$5.00
|
||||
buy_order = create(:order, :completed, :buy, stock: stock, shares: 5, user: user)
|
||||
create(
|
||||
:portfolio_transaction, :debit, portfolio: portfolio, amount_cents: 5_00,
|
||||
order: buy_order
|
||||
)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5, purchase_price: 100)
|
||||
|
||||
# successful completed stock sale, should increase cash_balance
|
||||
# # +$6.00
|
||||
sell_order = create(:order, :completed, :sell, stock: stock, shares: 6, user: user)
|
||||
create(
|
||||
:portfolio_transaction, :credit, portfolio: portfolio, amount_cents: 6_00,
|
||||
order: sell_order
|
||||
)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: -6, purchase_price: 100)
|
||||
|
||||
# withdrawal from the account, should decrease cash_balance
|
||||
create(:portfolio_transaction, :withdrawal, portfolio:, amount_cents: 2_00) # -$2.00
|
||||
|
||||
# fee, should decrease cash_balance
|
||||
# -$1.00
|
||||
create(:portfolio_transaction, :fee, portfolio:, amount_cents: 1_00)
|
||||
|
||||
# expected balance = (10.00 - 3.00 - 5.00 + 6.00 - 2.00 - 1.00) = 5.00
|
||||
result = portfolio.cash_balance
|
||||
assert_equal 5.0, result
|
||||
end
|
||||
|
||||
test "#shares_owned with single purchase" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 15, purchase_price: 200.0)
|
||||
|
||||
result = portfolio.shares_owned(stock.id)
|
||||
assert_equal 15, result
|
||||
end
|
||||
|
||||
test "#shares_owned with multiple purchases aggregates correctly" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10, purchase_price: 200.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 5, purchase_price: 250.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 3, purchase_price: 300.0)
|
||||
|
||||
result = portfolio.shares_owned(stock.id)
|
||||
assert_equal 18, result # 10 + 5 + 3
|
||||
end
|
||||
|
||||
test "#cash_balance with transactions without orders" do
|
||||
portfolio = create(:portfolio)
|
||||
create(:portfolio_transaction, :credit, portfolio: portfolio, amount_cents: 10_00)
|
||||
create(:portfolio_transaction, :debit, portfolio: portfolio, amount_cents: 5_00)
|
||||
|
||||
create(:portfolio_transaction, :deposit, portfolio: portfolio, amount_cents: 20_00)
|
||||
# expected balance = (2000 + 1000 - 500) / 100.0 = 25.0
|
||||
result = portfolio.cash_balance
|
||||
assert_equal 25.0, result
|
||||
end
|
||||
|
||||
test "#shares_owned with buy and sell orders" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 20, purchase_price: 200.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: -7, purchase_price: 250.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 3, purchase_price: 300.0)
|
||||
|
||||
result = portfolio.shares_owned(stock.id)
|
||||
assert_equal 16, result # 20 - 7 + 3
|
||||
end
|
||||
|
||||
test "#shares_owned returns 0 when no shares owned" do
|
||||
portfolio = create(:portfolio)
|
||||
stock = create(:stock)
|
||||
|
||||
result = portfolio.shares_owned(stock.id)
|
||||
assert_equal 0, result
|
||||
end
|
||||
|
||||
test "#shares_owned with different stocks" do
|
||||
portfolio = create(:portfolio)
|
||||
stock1 = create(:stock)
|
||||
stock2 = create(:stock)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock1, shares: 10, purchase_price: 200.0)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock2, shares: 5, purchase_price: 300.0)
|
||||
|
||||
assert_equal 10, portfolio.shares_owned(stock1.id)
|
||||
assert_equal 5, portfolio.shares_owned(stock2.id)
|
||||
end
|
||||
|
||||
test "#holdings_value_cents calculates stock holdings value" do
|
||||
portfolio = create(:portfolio)
|
||||
stock1 = create(:stock, price_cents: 10_000)
|
||||
stock2 = create(:stock, price_cents: 20_000)
|
||||
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock1, shares: 5)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock2, shares: 3)
|
||||
|
||||
expected_value = (5 * 10_000) + (3 * 20_000)
|
||||
assert_equal expected_value, portfolio.holdings_value_cents
|
||||
end
|
||||
|
||||
test "#calculate_total_value_cents includes cash and stock holdings" do
|
||||
portfolio = create(:portfolio)
|
||||
create(:portfolio_transaction, :deposit, portfolio: portfolio, amount_cents: 50_000)
|
||||
|
||||
stock = create(:stock, price_cents: 10_000)
|
||||
create(:portfolio_stock, portfolio: portfolio, stock: stock, shares: 10)
|
||||
|
||||
expected_value = 50_000 + (10 * 10_000)
|
||||
assert_equal expected_value, portfolio.calculate_total_value_cents
|
||||
end
|
||||
|
||||
test "#calculate_total_value converts cents to dollars" do
|
||||
portfolio = create(:portfolio)
|
||||
create(:portfolio_transaction, :deposit, portfolio: portfolio, amount_cents: 50_000)
|
||||
|
||||
assert_equal 500.0, portfolio.calculate_total_value
|
||||
end
|
||||
|
||||
test "#chart_data returns empty array when no snapshots exist" do
|
||||
portfolio = create(:portfolio)
|
||||
|
||||
result = portfolio.chart_data
|
||||
assert_equal [], result
|
||||
end
|
||||
|
||||
test "#chart_data returns formatted data for single snapshot" do
|
||||
portfolio = create(:portfolio)
|
||||
create(:portfolio_snapshot, portfolio: portfolio, date: Date.new(2025, 1, 1), worth_cents: 10_000)
|
||||
|
||||
result = portfolio.chart_data
|
||||
assert_equal 1, result.length
|
||||
assert_equal "Jan 2025", result.first[:label]
|
||||
assert_equal 100.0, result.first[:value]
|
||||
end
|
||||
|
||||
test "#chart_data returns last 12 snapshots in chronological order" do
|
||||
portfolio = create(:portfolio)
|
||||
|
||||
15.times do |i|
|
||||
create(
|
||||
:portfolio_snapshot,
|
||||
portfolio: portfolio,
|
||||
date: (14 - i).months.ago.beginning_of_month.to_date,
|
||||
worth_cents: (100 + (i * 10)) * 100
|
||||
)
|
||||
end
|
||||
|
||||
result = portfolio.chart_data
|
||||
assert_equal 12, result.length
|
||||
end
|
||||
|
||||
test "#chart_data formats dates as 'Mon YYYY'" do
|
||||
portfolio = create(:portfolio)
|
||||
create(:portfolio_snapshot, portfolio: portfolio, date: Date.new(2025, 3, 15), worth_cents: 15_000)
|
||||
create(:portfolio_snapshot, portfolio: portfolio, date: Date.new(2024, 12, 1), worth_cents: 20_000)
|
||||
|
||||
result = portfolio.chart_data
|
||||
assert_equal(["Dec 2024", "Mar 2025"], result.pluck(:label))
|
||||
end
|
||||
|
||||
test "#chart_data converts worth_cents to dollars" do
|
||||
portfolio = create(:portfolio)
|
||||
create(:portfolio_snapshot, portfolio: portfolio, date: Date.current, worth_cents: 123_456)
|
||||
|
||||
result = portfolio.chart_data
|
||||
assert_equal 1234.56, result.first[:value]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class PortfolioTransactionTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:portfolio_transaction).validate!
|
||||
end
|
||||
|
||||
test ".deposits" do
|
||||
transaction1 = create(:portfolio_transaction, :deposit)
|
||||
create(:portfolio_transaction, :debit)
|
||||
create(:portfolio_transaction, :credit)
|
||||
transaction4 = create(:portfolio_transaction, :deposit)
|
||||
|
||||
assert_equal [transaction1, transaction4], PortfolioTransaction.deposits
|
||||
end
|
||||
|
||||
test ".debits" do
|
||||
transaction1 = create(:portfolio_transaction, :debit)
|
||||
create(:portfolio_transaction, :deposit)
|
||||
create(:portfolio_transaction, :credit)
|
||||
transaction4 = create(:portfolio_transaction, :debit)
|
||||
|
||||
assert_equal [transaction1, transaction4], PortfolioTransaction.debits
|
||||
end
|
||||
|
||||
test ".credits" do
|
||||
transaction1 = create(:portfolio_transaction, :credit)
|
||||
create(:portfolio_transaction, :deposit)
|
||||
create(:portfolio_transaction, :debit)
|
||||
transaction4 = create(:portfolio_transaction, :credit)
|
||||
|
||||
assert_equal [transaction1, transaction4], PortfolioTransaction.credits
|
||||
end
|
||||
|
||||
test ".withdrawals" do
|
||||
transaction1 = create(:portfolio_transaction, :withdrawal)
|
||||
create(:portfolio_transaction, :deposit)
|
||||
create(:portfolio_transaction, :debit)
|
||||
transaction4 = create(:portfolio_transaction, :withdrawal)
|
||||
|
||||
assert_equal [transaction1, transaction4], PortfolioTransaction.withdrawals
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# test/models/quarter_test.rb
|
||||
require "test_helper"
|
||||
|
||||
class QuarterTest < ActiveSupport::TestCase
|
||||
test "can be destroyed when no grade books exist" do
|
||||
school_year = create(:school_year)
|
||||
quarter = school_year.quarters.first
|
||||
|
||||
assert quarter.destroy
|
||||
end
|
||||
|
||||
test "cannot be destroyed when grade books exist" do
|
||||
school_year = create(:school_year)
|
||||
quarter = school_year.quarters.first
|
||||
classroom = create(:classroom, school_year: school_year)
|
||||
grade_book = classroom.grade_books.find_by!(quarter: quarter)
|
||||
|
||||
assert_not quarter.destroy
|
||||
assert quarter.errors[:base].any?
|
||||
assert GradeBook.exists?(grade_book.id)
|
||||
end
|
||||
|
||||
test "factory is valid and assigns sequential numbers" do
|
||||
school_year = build(:school_year)
|
||||
q1 = build_stubbed(:quarter, school_year: school_year, number: 1)
|
||||
q2 = build_stubbed(:quarter, school_year: school_year, number: 2)
|
||||
assert q1.valid?
|
||||
assert q2.valid?
|
||||
assert_equal 1, q1.number
|
||||
assert_equal 2, q2.number
|
||||
end
|
||||
|
||||
test "validates presence of number" do
|
||||
quarter = build_stubbed(:quarter, number: nil)
|
||||
assert_not quarter.valid?
|
||||
assert_includes quarter.errors[:number], "can't be blank"
|
||||
end
|
||||
|
||||
test "validates inclusion of number in 1..4" do
|
||||
%w[0 5 10].each do |bad|
|
||||
quarter = build_stubbed(:quarter, number: bad.to_i)
|
||||
assert_not quarter.valid?, "#{bad} should be invalid"
|
||||
assert_includes quarter.errors[:number], "is not included in the list"
|
||||
end
|
||||
end
|
||||
|
||||
test "validates uniqueness of number per school_year" do
|
||||
school_year = create(:school_year)
|
||||
dup = build(:quarter, school_year: school_year, number: 1)
|
||||
assert_not dup.valid?
|
||||
assert_includes dup.errors[:number], "has already been taken"
|
||||
end
|
||||
|
||||
test "default scope orders by number" do
|
||||
school_year = create(:school_year)
|
||||
assert_equal [1, 2, 3, 4], school_year.quarters.ordered.pluck(:number)
|
||||
end
|
||||
|
||||
test "next and previous helpers" do
|
||||
school_year = create(:school_year)
|
||||
q1 = school_year.quarters.find_by!(number: 1)
|
||||
q2 = school_year.quarters.find_by!(number: 2)
|
||||
q4 = school_year.quarters.find_by!(number: 4)
|
||||
assert_nil q1.previous
|
||||
assert_equal q2, q1.next
|
||||
assert_equal q1, q2.previous
|
||||
assert_nil q4.next
|
||||
end
|
||||
|
||||
test "next and previous across school years" do
|
||||
school = create(:school)
|
||||
year1 = create(:year, name: "2023 - 2024")
|
||||
year2 = create(:year, name: "2024 - 2025")
|
||||
sy1 = SchoolYear.create!(school: school, year: year1)
|
||||
sy2 = SchoolYear.create!(school: school, year: year2)
|
||||
|
||||
q4_y1 = sy1.quarters.find_by!(number: 4)
|
||||
q1_y2 = sy2.quarters.find_by!(number: 1)
|
||||
|
||||
assert_equal q1_y2, q4_y1.next
|
||||
assert_equal q4_y1, q1_y2.previous
|
||||
|
||||
# Edge cases where there is no next/previous year
|
||||
year4 = create(:year, name: "2026 - 2027")
|
||||
sy4 = SchoolYear.create!(school: school, year: year4)
|
||||
q1_y4 = sy4.quarters.find_by!(number: 1)
|
||||
q4_y4 = sy4.quarters.find_by!(number: 4)
|
||||
|
||||
assert_nil q1_y4.previous
|
||||
assert_nil q4_y4.next
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,129 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class SchoolTest < ActiveSupport::TestCase
|
||||
# Factory validation
|
||||
test "factory is valid" do
|
||||
school = build(:school)
|
||||
assert school.valid?
|
||||
end
|
||||
|
||||
# Validation tests
|
||||
test "is valid with valid attributes" do
|
||||
school = build(:school, name: "Test School")
|
||||
assert school.valid?
|
||||
end
|
||||
|
||||
test "is invalid without a name" do
|
||||
school = build(:school, name: nil)
|
||||
assert_not school.valid?
|
||||
assert_includes school.errors[:name], "can't be blank"
|
||||
end
|
||||
|
||||
test "is invalid with an empty name" do
|
||||
school = build(:school, name: "")
|
||||
assert_not school.valid?
|
||||
assert_includes school.errors[:name], "can't be blank"
|
||||
end
|
||||
|
||||
test "is invalid with a name containing only whitespace" do
|
||||
school = build(:school, name: " ")
|
||||
assert_not school.valid?
|
||||
assert_includes school.errors[:name], "can't be blank"
|
||||
end
|
||||
|
||||
# Association tests
|
||||
test "has many school_years" do
|
||||
school = create(:school)
|
||||
assert_respond_to school, :school_years
|
||||
end
|
||||
|
||||
test "has many years through school_years" do
|
||||
school = create(:school)
|
||||
assert_respond_to school, :years
|
||||
end
|
||||
|
||||
test "can have multiple school_years" do
|
||||
school = create(:school)
|
||||
year1 = create(:year, name: "2024")
|
||||
year2 = create(:year, name: "2025")
|
||||
|
||||
school_year1 = create(:school_year, school: school, year: year1)
|
||||
school_year2 = create(:school_year, school: school, year: year2)
|
||||
|
||||
assert_equal 2, school.school_years.count
|
||||
assert_includes school.school_years, school_year1
|
||||
assert_includes school.school_years, school_year2
|
||||
end
|
||||
|
||||
test "can access years through school_years association" do
|
||||
school = create(:school)
|
||||
year1 = create(:year, name: "2024")
|
||||
year2 = create(:year, name: "2025")
|
||||
|
||||
create(:school_year, school: school, year: year1)
|
||||
create(:school_year, school: school, year: year2)
|
||||
|
||||
assert_equal 2, school.years.count
|
||||
assert_includes school.years, year1
|
||||
assert_includes school.years, year2
|
||||
end
|
||||
|
||||
# Dependent restrict_with_error tests
|
||||
test "cannot be destroyed when it has associated school_years" do
|
||||
school = create(:school)
|
||||
year = create(:year)
|
||||
create(:school_year, school: school, year: year)
|
||||
|
||||
assert_not school.destroy
|
||||
assert_includes school.errors[:base], "Cannot delete record because dependent school years exist"
|
||||
end
|
||||
|
||||
test "can be destroyed when it has no associated school_years" do
|
||||
school = create(:school)
|
||||
|
||||
assert_difference("School.count", -1) do
|
||||
school.destroy!
|
||||
end
|
||||
end
|
||||
|
||||
# Edge cases
|
||||
test "allows duplicate names" do
|
||||
create(:school, name: "Test School")
|
||||
duplicate_school = build(:school, name: "Test School")
|
||||
|
||||
assert duplicate_school.valid?
|
||||
end
|
||||
|
||||
test "name can contain special characters" do
|
||||
school = build(:school, name: "St. Mary's School & Academy")
|
||||
assert school.valid?
|
||||
end
|
||||
|
||||
test "name can contain numbers" do
|
||||
school = build(:school, name: "PS 123")
|
||||
assert school.valid?
|
||||
end
|
||||
|
||||
test "name can be very long" do
|
||||
long_name = "A" * 255
|
||||
school = build(:school, name: long_name)
|
||||
assert school.valid?
|
||||
end
|
||||
|
||||
# Persistence tests
|
||||
test "persists name correctly" do
|
||||
school = create(:school, name: "Persisted School")
|
||||
reloaded_school = School.find(school.id)
|
||||
|
||||
assert_equal "Persisted School", reloaded_school.name
|
||||
end
|
||||
|
||||
test "updates name correctly" do
|
||||
school = create(:school, name: "Original Name")
|
||||
school.update!(name: "Updated Name")
|
||||
|
||||
assert_equal "Updated Name", school.reload.name
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class SchoolYearTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:school_year).validate!
|
||||
end
|
||||
|
||||
test "name returns school name and year name" do
|
||||
school_year = build(:school_year)
|
||||
assert_equal "#{school_year.school_name} (#{school_year.year_name})", school_year.name
|
||||
end
|
||||
|
||||
test "can be destroyed and cascades quarters" do
|
||||
school_year = create(:school_year)
|
||||
quarter_ids = school_year.quarters.pluck(:id)
|
||||
assert_equal 4, quarter_ids.size
|
||||
|
||||
assert school_year.destroy
|
||||
assert Quarter.where(id: quarter_ids).empty?
|
||||
end
|
||||
|
||||
test "cannot be destroyed when classrooms exist" do
|
||||
school_year = create(:school_year)
|
||||
create(:classroom, school_year: school_year)
|
||||
|
||||
assert_not school_year.destroy
|
||||
assert school_year.errors.added?(:base, "Cannot delete record because dependent classrooms exist")
|
||||
end
|
||||
|
||||
test "cannot be destroyed when grade books exist" do
|
||||
school_year = create(:school_year)
|
||||
create(:classroom, school_year: school_year)
|
||||
|
||||
assert_not school_year.destroy
|
||||
end
|
||||
|
||||
test "automatically creates 4 quarters when created" do
|
||||
school = create(:school)
|
||||
year = create(:year)
|
||||
|
||||
school_year = SchoolYear.create!(school: school, year: year)
|
||||
|
||||
assert_equal 4, school_year.quarters.count
|
||||
assert_equal [1, 2, 3, 4], school_year.quarters.order(:number).pluck(:number)
|
||||
assert_equal ["Quarter 1", "Quarter 2", "Quarter 3", "Quarter 4"],
|
||||
school_year.quarters.order(:number).pluck(:name)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class StiTest < ActiveSupport::TestCase
|
||||
def test_sti_implementation
|
||||
classroom = create(:classroom)
|
||||
|
||||
admin = create(:admin, classroom: classroom)
|
||||
assert_equal "User", admin.type
|
||||
|
||||
student = create(:student, classroom: classroom)
|
||||
assert_equal "Student", student.type
|
||||
|
||||
teacher = create(:teacher, classroom: classroom)
|
||||
assert_equal "Teacher", teacher.type
|
||||
|
||||
admin.update(type: "Student")
|
||||
assert_equal "Student", admin.type
|
||||
|
||||
assert_equal 3, User.count
|
||||
assert_equal 2, Student.count
|
||||
assert_equal 1, Teacher.count
|
||||
|
||||
assert_equal "Student", admin.reload.type
|
||||
assert_equal "Student", student.reload.type
|
||||
assert_equal "Teacher", teacher.reload.type
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class StockTest < ActiveSupport::TestCase
|
||||
test "factory" do
|
||||
assert build(:stock).validate!
|
||||
end
|
||||
|
||||
test "#current_price" do
|
||||
stock = create(:stock, price_cents: 1_000)
|
||||
|
||||
result = stock.current_price
|
||||
|
||||
assert_equal 10.0, result
|
||||
end
|
||||
|
||||
test "archived defaults to false" do
|
||||
stock = create(:stock)
|
||||
|
||||
assert_equal false, stock.archived
|
||||
end
|
||||
|
||||
test "active and archived scopes filter correctly" do
|
||||
active_stock = create(:stock, archived: false)
|
||||
archived_stock = create(:stock, archived: true)
|
||||
|
||||
active_stocks = Stock.active
|
||||
archived_stocks = Stock.archived
|
||||
|
||||
assert_includes active_stocks, active_stock
|
||||
assert_not_includes active_stocks, archived_stock
|
||||
assert_includes archived_stocks, archived_stock
|
||||
assert_not_includes archived_stocks, active_stock
|
||||
end
|
||||
|
||||
test "validates company_website format accepts valid http URLs" do
|
||||
stock = build(:stock, company_website: "http://example.com")
|
||||
|
||||
assert stock.valid?
|
||||
end
|
||||
|
||||
test "validates company_website format accepts valid https URLs" do
|
||||
stock = build(:stock, company_website: "https://example.com")
|
||||
|
||||
assert stock.valid?
|
||||
end
|
||||
|
||||
test "validates company_website format accepts blank URLs" do
|
||||
stock = build(:stock, company_website: "")
|
||||
|
||||
assert stock.valid?
|
||||
end
|
||||
|
||||
test "validates company_website format rejects javascript URLs" do
|
||||
stock = build(:stock, company_website: "javascript:alert('XSS')")
|
||||
|
||||
assert_not stock.valid?
|
||||
assert stock.errors.added?(
|
||||
:company_website,
|
||||
"must be a valid HTTP or HTTPS URL"
|
||||
)
|
||||
end
|
||||
|
||||
test "validates company_website format rejects invalid URLs" do
|
||||
stock = build(:stock, company_website: "not a url")
|
||||
|
||||
assert_not stock.valid?
|
||||
assert stock.errors.added?(
|
||||
:company_website,
|
||||
"must be a valid HTTP or HTTPS URL"
|
||||
)
|
||||
end
|
||||
|
||||
test "validates company_website format rejects data URLs" do
|
||||
stock = build(
|
||||
:stock,
|
||||
company_website: "data:text/html,<script>alert('XSS')</script>"
|
||||
)
|
||||
|
||||
assert_not stock.valid?
|
||||
assert stock.errors.added?(
|
||||
:company_website,
|
||||
"must be a valid HTTP or HTTPS URL"
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,294 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class StudentTest < ActiveSupport::TestCase
|
||||
test "inherits from User" do
|
||||
student = create(:student)
|
||||
assert_kind_of User, student
|
||||
assert_equal "Student", student.type
|
||||
end
|
||||
|
||||
test "belongs to classroom" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student, classroom: classroom)
|
||||
assert_equal classroom, student.classroom
|
||||
end
|
||||
|
||||
test "has portfolio association" do
|
||||
student = create(:student)
|
||||
portfolio = create(:portfolio, user: student)
|
||||
assert_equal portfolio, student.portfolio
|
||||
end
|
||||
|
||||
test "email can be blank for students" do
|
||||
student = build(:student, email: "")
|
||||
assert student.valid?
|
||||
end
|
||||
|
||||
test "password generation works" do
|
||||
student = create(:student)
|
||||
assert_not_nil student.encrypted_password
|
||||
end
|
||||
|
||||
test "destroy raises and does not delete the row" do
|
||||
student = create(:student)
|
||||
assert_raises(RuntimeError) { student.destroy }
|
||||
assert student.reload.persisted?
|
||||
assert_not student.discarded?
|
||||
end
|
||||
|
||||
test "has many classroom_enrollments" do
|
||||
student = create(:student)
|
||||
assert student.respond_to?(:classroom_enrollments)
|
||||
end
|
||||
|
||||
test "has many classrooms through classroom_enrollments" do
|
||||
student = create(:student)
|
||||
assert student.respond_to?(:classrooms)
|
||||
end
|
||||
|
||||
test "current_enrollments returns only enrollments with nil unenrolled_at" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
|
||||
current = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom1,
|
||||
unenrolled_at: nil
|
||||
)
|
||||
historical = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom2,
|
||||
enrolled_at: 2.days.ago,
|
||||
unenrolled_at: 1.day.ago
|
||||
)
|
||||
|
||||
assert_includes student.current_enrollments, current
|
||||
assert_not_includes student.current_enrollments, historical
|
||||
end
|
||||
|
||||
test "current_classrooms returns classrooms with active enrollments" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
classroom3 = create(:classroom)
|
||||
|
||||
create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom1,
|
||||
unenrolled_at: nil
|
||||
)
|
||||
create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom2,
|
||||
unenrolled_at: nil
|
||||
)
|
||||
create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom3,
|
||||
enrolled_at: 2.days.ago,
|
||||
unenrolled_at: 1.day.ago
|
||||
)
|
||||
|
||||
assert_includes student.current_classrooms, classroom1
|
||||
assert_includes student.current_classrooms, classroom2
|
||||
assert_not_includes student.current_classrooms, classroom3
|
||||
end
|
||||
|
||||
test "primary_enrollment returns the primary enrollment" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
|
||||
primary = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom1,
|
||||
primary: true
|
||||
)
|
||||
create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom2,
|
||||
primary: false
|
||||
)
|
||||
|
||||
assert_equal primary, student.primary_enrollment
|
||||
end
|
||||
|
||||
test "primary_classroom returns primary enrollment classroom" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom = create(:classroom)
|
||||
|
||||
create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom,
|
||||
primary: true
|
||||
)
|
||||
|
||||
assert_equal classroom, student.primary_classroom
|
||||
end
|
||||
|
||||
test "primary_classroom falls back to classroom_id when no primary enrollment" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student, classroom: classroom)
|
||||
|
||||
assert_equal classroom, student.primary_classroom
|
||||
end
|
||||
|
||||
test "enroll_in! creates a new enrollment" do
|
||||
student = create(:student)
|
||||
classroom = create(:classroom)
|
||||
|
||||
assert_difference -> { student.classroom_enrollments.count }, 1 do
|
||||
student.enroll_in!(classroom)
|
||||
end
|
||||
end
|
||||
|
||||
test "enroll_in! sets enrolled_at to current time by default" do
|
||||
student = create(:student)
|
||||
classroom = create(:classroom)
|
||||
|
||||
freeze_time do
|
||||
enrollment = student.enroll_in!(classroom)
|
||||
assert_in_delta Time.current, enrollment.enrolled_at, 1.second
|
||||
end
|
||||
end
|
||||
|
||||
test "enroll_in! accepts custom enrolled_at time" do
|
||||
student = create(:student)
|
||||
classroom = create(:classroom)
|
||||
custom_time = 1.week.ago
|
||||
|
||||
enrollment = student.enroll_in!(classroom, enrolled_at: custom_time)
|
||||
|
||||
assert_in_delta custom_time, enrollment.enrolled_at, 1.second
|
||||
end
|
||||
|
||||
test "enroll_in! sets primary flag when requested" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom = create(:classroom)
|
||||
|
||||
enrollment = student.enroll_in!(classroom, primary: true)
|
||||
|
||||
assert enrollment.primary
|
||||
end
|
||||
|
||||
test "enroll_in! demotes other primary enrollments when creating new primary" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
|
||||
old_primary = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom1,
|
||||
primary: true
|
||||
)
|
||||
|
||||
student.enroll_in!(classroom2, primary: true)
|
||||
|
||||
assert_not old_primary.reload.primary
|
||||
end
|
||||
|
||||
test "enroll_in! returns the enrollment" do
|
||||
student = create(:student)
|
||||
classroom = create(:classroom)
|
||||
|
||||
enrollment = student.enroll_in!(classroom)
|
||||
|
||||
assert_kind_of ClassroomEnrollment, enrollment
|
||||
assert enrollment.persisted?
|
||||
end
|
||||
|
||||
test "unenroll_from! sets unenrolled_at on enrollment" do
|
||||
student = create(:student)
|
||||
classroom = create(:classroom)
|
||||
enrollment = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom
|
||||
)
|
||||
|
||||
student.unenroll_from!(classroom)
|
||||
|
||||
assert_not_nil enrollment.reload.unenrolled_at
|
||||
end
|
||||
|
||||
test "unenroll_from! sets unenrolled_at to current time by default" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom = create(:classroom)
|
||||
enrollment = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom,
|
||||
enrolled_at: 1.hour.ago
|
||||
)
|
||||
|
||||
freeze_time do
|
||||
student.unenroll_from!(classroom)
|
||||
assert_in_delta Time.current, enrollment.reload.unenrolled_at, 1.second
|
||||
end
|
||||
end
|
||||
|
||||
test "unenroll_from! accepts custom unenrollment time" do
|
||||
student = create(:student, :without_enrollment)
|
||||
classroom = create(:classroom)
|
||||
enrollment = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom,
|
||||
enrolled_at: 2.weeks.ago
|
||||
)
|
||||
custom_time = 1.week.ago
|
||||
|
||||
student.unenroll_from!(classroom, unenrolled_at: custom_time)
|
||||
|
||||
assert_in_delta custom_time, enrollment.reload.unenrolled_at, 1.second
|
||||
end
|
||||
|
||||
test "unenroll_from! handles multiple enrollments for same classroom" do
|
||||
student = create(:student)
|
||||
classroom = create(:classroom)
|
||||
enrollment1 = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom,
|
||||
enrolled_at: 1.month.ago
|
||||
)
|
||||
enrollment2 = create(
|
||||
:classroom_enrollment,
|
||||
student: student,
|
||||
classroom: classroom,
|
||||
enrolled_at: 2.weeks.ago
|
||||
)
|
||||
|
||||
student.unenroll_from!(classroom)
|
||||
|
||||
assert_not_nil enrollment1.reload.unenrolled_at
|
||||
assert_not_nil enrollment2.reload.unenrolled_at
|
||||
end
|
||||
|
||||
test "automatically creates enrollment when student is created with classroom_id" do
|
||||
classroom = create(:classroom)
|
||||
student = create(:student, classroom: classroom)
|
||||
|
||||
assert_equal 1, student.classroom_enrollments.count
|
||||
assert_equal classroom, student.classroom_enrollments.first.classroom
|
||||
assert student.classroom_enrollments.first.primary
|
||||
end
|
||||
|
||||
test "does not create enrollment when student is created without classroom_id" do
|
||||
student = Student.create!(username: "test_#{rand(10_000)}", password: "Test1234")
|
||||
|
||||
assert_equal 0, student.classroom_enrollments.count
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class TeacherClassroomTest < ActiveSupport::TestCase
|
||||
test "valid teacher_classroom" do
|
||||
teacher = create(:teacher)
|
||||
classroom = create(:classroom)
|
||||
teacher_classroom = TeacherClassroom.new(teacher: teacher, classroom: classroom)
|
||||
|
||||
assert teacher_classroom.valid?
|
||||
end
|
||||
|
||||
test "requires teacher" do
|
||||
classroom = create(:classroom)
|
||||
teacher_classroom = TeacherClassroom.new(classroom: classroom)
|
||||
|
||||
assert_not teacher_classroom.valid?
|
||||
assert_includes teacher_classroom.errors[:teacher], "must exist"
|
||||
end
|
||||
|
||||
test "requires classroom" do
|
||||
teacher = create(:teacher)
|
||||
teacher_classroom = TeacherClassroom.new(teacher: teacher)
|
||||
|
||||
assert_not teacher_classroom.valid?
|
||||
assert_includes teacher_classroom.errors[:classroom], "must exist"
|
||||
end
|
||||
|
||||
test "validates uniqueness of teacher_id scoped to classroom_id" do
|
||||
teacher = create(:teacher)
|
||||
classroom = create(:classroom)
|
||||
|
||||
TeacherClassroom.create!(teacher: teacher, classroom: classroom)
|
||||
|
||||
duplicate = TeacherClassroom.new(teacher: teacher, classroom: classroom)
|
||||
|
||||
assert_not duplicate.valid?
|
||||
assert_includes duplicate.errors[:teacher_id], "has already been taken"
|
||||
end
|
||||
|
||||
test "allows same teacher in different classrooms" do
|
||||
teacher = create(:teacher)
|
||||
classroom1 = create(:classroom)
|
||||
classroom2 = create(:classroom)
|
||||
|
||||
assert TeacherClassroom.create(teacher: teacher, classroom: classroom1)
|
||||
assert TeacherClassroom.create(teacher: teacher, classroom: classroom2)
|
||||
end
|
||||
|
||||
test "allows different teachers in same classroom" do
|
||||
teacher1 = create(:teacher)
|
||||
teacher2 = create(:teacher)
|
||||
classroom = create(:classroom)
|
||||
|
||||
assert TeacherClassroom.create(teacher: teacher1, classroom: classroom)
|
||||
assert TeacherClassroom.create(teacher: teacher2, classroom: classroom)
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user