chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user