chore: init commit

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,47 @@
# frozen_string_literal: true
require "test_helper"
class TeacherTest < ActiveSupport::TestCase
test "inherits from User" do
teacher = create(:teacher)
assert_kind_of User, teacher
assert_equal "Teacher", teacher.type
end
test "email is required" do
teacher = build(:teacher, email: nil)
assert_not teacher.valid?
assert_includes teacher.errors[:email], "can't be blank"
end
test "username is automatically set to email" do
teacher = create(:teacher, email: "jane@school.com")
assert_equal "jane@school.com", teacher.username
end
test "username updates when email changes" do
teacher = create(:teacher, email: "jane@school.com")
teacher.update!(email: "jane.new@school.com")
assert_equal "jane.new@school.com", teacher.username
end
test "display_name returns name field" do
teacher = create(:teacher, name: "Jane Smith")
assert_equal "Jane Smith", teacher.display_name
end
test "display_name falls back to email prefix when name is blank" do
teacher = create(:teacher, name: nil, email: "jane@school.com")
assert_equal "jane", teacher.display_name
end
test "can manage students in same classroom" do
classroom = create(:classroom)
teacher = create(:teacher, classroom: classroom)
student = create(:student, classroom: classroom)
assert_equal classroom, teacher.classroom
assert_equal classroom, student.classroom
end
end

View File

@@ -0,0 +1,112 @@
# frozen_string_literal: true
require "test_helper"
class UserTest < ActiveSupport::TestCase
test "returns true when the user holds the stock" do
student = create(:student, :with_portfolio)
stock = create(:stock)
create(:portfolio_stock, portfolio: student.portfolio, stock: stock)
assert student.holding?(stock)
end
test "returns false when the user does not hold the stock" do
student = create(:student, :with_portfolio)
stock = create(:stock)
create(:portfolio_stock, portfolio: student.portfolio, stock: stock, shares: 0)
assert_not student.holding?(stock)
end
test "factory" do
assert build(:student).validate!
end
test "validate uniqueness of email" do
create(:student, email: "test@example.com")
new_student = build(:student, email: "test@example.com")
assert_not new_student.valid?
assert_includes new_student.errors[:email], "has already been taken"
end
test "student? returns true for Student type" do
student = create(:student)
assert student.student?
assert_not student.teacher?
assert_not student.teacher_or_admin?
end
test "teacher? returns true for Teacher type" do
teacher = create(:teacher)
assert teacher.teacher?
assert teacher.teacher_or_admin?
assert_not teacher.student?
end
test "teacher_or_admin? returns true for admin users" do
admin = create(:admin)
assert admin.teacher_or_admin?
assert_not admin.student?
end
test "email is optional for students" do
student = build(:student, email: nil)
assert student.valid?
end
test "email uniqueness constraint allows nil values" do
create(:student, email: nil)
student2 = build(:student, email: nil)
assert student2.valid?
end
test "email uniqueness constraint prevents duplicate emails" do
create(:student, email: "test@example.com")
student2 = build(:student, email: "test@example.com")
assert_not student2.valid?
assert_includes student2.errors[:email], "has already been taken"
end
test "username is required for all students" do
user = build(:student, username: nil)
assert_not user.valid?
assert_includes user.errors[:username], "can't be blank"
end
test "username must be unique" do
create(:student, username: "testuser")
user2 = build(:student, username: "testuser")
assert_not user2.valid?
assert_includes user2.errors[:username], "has already been taken"
end
test "type must be valid STI class" do
user = build(:student, type: "InvalidType")
assert_not user.valid?
assert_includes user.errors[:type], "is not included in the list"
end
test "students and teacher scopes returns only correct type users" do
student = create(:student)
teacher = create(:teacher)
students = User.students
assert_includes students, student
assert_not_includes students, teacher
teachers = User.teachers
assert_includes teachers, teacher
assert_not_includes teachers, student
end
test "display_name returns username when present" do
user = create(:student, username: "testuser")
assert_equal "testuser", user.display_name
end
test "display_name returns email prefix when username blank" do
user = build(:student, username: "", email: "test@example.com")
user.save(validate: false) # Skip validation for this test
assert_equal "test", user.display_name
end
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
require "test_helper"
class YearTest < ActiveSupport::TestCase
test "factory" do
assert build(:year).validate!
end
test "year presence" do
year = build(:year, name: nil)
assert_not year.valid?
assert year.errors.added?(:name, :blank)
end
test "year uniqueness" do
duplicate_year = "2025 - 2026"
create(:year, name: duplicate_year)
year = build(:year, name: duplicate_year)
assert_not year.valid?
assert year.errors.added?(:name, :taken, value: duplicate_year)
end
test "previous_year and next_year" do
year2022_to2023 = create(:year, name: "2022 - 2023")
year2023_to2024 = create(:year, name: "2023 - 2024")
year2024_to2025 = create(:year, name: "2024 - 2025")
assert_nil year2022_to2023.previous_year
assert_equal year2023_to2024, year2022_to2023.next_year
assert_equal year2022_to2023, year2023_to2024.previous_year
assert_equal year2024_to2025, year2023_to2024.next_year
assert_equal year2023_to2024, year2024_to2025.previous_year
assert_nil year2024_to2025.next_year
end
test "current_school_year" do
create(:year, name: "2025 - 2026")
create(:year, name: "2026 - 2027")
feb_date = Date.new(2026, 2, 28)
assert_equal "2025 - 2026", Year.current_school_year(feb_date).first.name
sep_date = Date.new(2026, 9, 1)
assert_equal "2026 - 2027", Year.current_school_year(sep_date).first.name
end
end