CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing

From Expertiza_Wiki
Jump to navigation Jump to search

About This Project

Project Info
Course CSC/ECE 517 Spring 2026
Project E2610 — Teams Hierarchy Testing
Instructor Ed Gehringer
Mentor Vihar Manojkumar Shah
Collaborators Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh
Platform Expertiza (Ruby on Rails)
Test Framework RSpec
Root Class Team (STI superclass)
Subclasses CourseTeam, AssignmentTeam, MentoredTeam
Membership Join Model TeamsParticipant



The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the fundamental unit of participation: they allow students to reserve topics, collaborate on coursework, and submit assignments for evaluation.

This project focuses on ensuring the correctness of the Team hierarchy and strengthening automated test coverage. The work emphasises membership validity (enrolment-based participation), preventing duplicate membership across teams, enforcing capacity limits when configured, ensuring MentoredTeam uses duty-based mentor identification, and validating controller authorisation and HTTP response behaviour.

Background

The Team Hierarchy

The hierarchy is composed of exactly four classes:

  • Team — STI superclass providing shared associations and common behaviours
  • CourseTeam — course-scoped teams that persist across a course
  • AssignmentTeam — assignment-scoped teams created for a single assignment
  • MentoredTeam — subclass of AssignmentTeam with a designated mentor

Two broad kinds of team exist:

  • Course teams (CourseTeam) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.
  • Assignment teams (AssignmentTeam) exist only for a single assignment. When mentors are used, assignment teams are instantiated as MentoredTeam.

Class Hierarchy Diagram

                        ┌─────────────────┐
                        │      Team       │
                        │  (STI parent)   │
                        └────────┬────────┘
                                 │
               ┌─────────────────┴──────────────────┐
               │                                    │
      ┌────────┴────────┐                ┌──────────┴──────────┐
      │   CourseTeam    │                │   AssignmentTeam    │
      │ (course-scoped) │                │ (assignment-scoped) │
      └─────────────────┘                └──────────┬──────────┘
                                                    │
                                         ┌──────────┴──────────┐
                                         │    MentoredTeam     │
                                         │ (mentor via duty)   │
                                         └─────────────────────┘

Subclass Responsibilities

Class Scope Key Constraint Notes
Team Shared membership/associations Parent class for all team types (STI)
CourseTeam Course Member must be a course participant Can be converted to/from assignment teams
AssignmentTeam Assignment Member must be an assignment participant Capacity comes from Assignment#max_team_size
MentoredTeam Assignment Mentor identified by participant duty Uses Duty + participant duty_id

Duty vs. Role Distinction

A critical design point (and a core source of bugs in this area) is the separation between a user's role and a participant's duty.

Concept Definition Examples
Role System-level permission level assigned to a user account Instructor, Teaching Assistant, Student
Duty A function assigned to a participant within a specific team Submitter, Reviewer, Mentor

In Expertiza, mentors are not privileged system accounts; they are normal users assigned the duty of Mentor on one team. Therefore, mentor logic must use participant duty rather than user role.

Previous Implementation

1) MentoredTeam used role-based assumptions

The incorrect approach was to treat “mentor” as a system-level role check. This is incompatible with Expertiza’s domain model, where mentor is a team-level duty assigned to a participant.

What we changed: MentoredTeam mentor assignment and mentor discovery use Duty and the participant’s duty_id.

2) Capacity enforcement was inconsistent / bypassable

Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of TeamsParticipant could bypass capacity checks.

What we changed: Capacity is enforced at both:

  • the domain method level (Team#add_member)
  • the join model level (TeamsParticipant validation backstop)

Join-request acceptance also checks capacity inside a lock for race-safety.

3) TeamsController authorisation was not explicit

The authorisation framework existed (global before_action), but TeamsController did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.

What we changed: TeamsController now defines action_allowed?, and request specs verify correct 403 Forbidden behaviour.

4) CourseTeam capacity was assumed without schema support

The current schema does not provide a max_team_size attribute for courses. Attempting to enforce CourseTeam capacity via course.max_team_size results in runtime errors and breaks previously passing tests.

What we changed: Capacity enforcement is based on assignment configuration (Assignment#max_team_size). CourseTeams remain uncapped by default unless the schema is extended in the future.

Implementation Summary

1) Enrollment-based membership

Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.

# app/models/team.rb
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)

2) Prevent duplicate membership across teams

The join model prevents a participant from appearing on multiple teams:

# app/models/teams_participant.rb
validates :participant_id, uniqueness: true

A unique index migration reinforces this at the database level.

3) Capacity enforcement (configured via Assignment)

Capacity is enforced via the assignment’s max_team_size. When at capacity, attempts to add are rejected with an error.

# app/models/team.rb
def full?
  max = max_size
  return false if max.blank?
  participants.count >= max
end
# app/models/team.rb
return { success: false, error: "Unable to add participant: team is at full capacity." } if full?

4) Join-model capacity backstop

Even direct join-table creation is blocked when a team is at capacity.

# app/models/teams_participant.rb
validate :team_not_full, on: :create

def team_not_full
  return unless team
  max = team.max_size
  return if max.blank?

  if team.participants.count >= max
    errors.add(:base, "Team is at full capacity (max #{max}).")
  end
end

5) Race-safe join request acceptance

Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.

# app/controllers/join_team_requests_controller.rb (accept)
ActiveRecord::Base.transaction do
  team.with_lock do
    if team.full?
      # reject accept
    end
    result = team.add_member(participant)
    # reject if result failed
  end

  @join_team_request.update!(reply_status: ACCEPTED)
end

6) TeamsController authorisation and HTTP codes

TeamsController now declares an explicit policy:

  • Teaching staff (TA and above): allowed
  • Students: cannot list all teams, cannot manage membership, can only view teams they belong to

Request specs assert that restricted actions return 403 with an error payload.

RSpec Test Coverage

The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).

Model Specs

Area Spec File(s) What is validated
Team membership + validations spec/models/team_spec.rb Type/parent validation, membership checks, full?, add_member
Capacity behaviour spec/models/team_capacity_spec.rb AssignmentTeam capacity and error rejection
Association integrity spec/models/team_association_spec.rb Parent linkage and membership scoping
Conversion behaviour spec/models/team_conversion_spec.rb CourseTeam ⇄ AssignmentTeam conversions and member copying
MentoredTeam duty behaviour spec/models/mentored_team_spec.rb Mentor duty assignment/removal and mentor lookup by duty
Join model constraints spec/models/teams_participant_spec.rb Uniqueness, presence, and capacity backstop (no bypass)

Request Specs

Endpoint Area Spec File(s) What is validated
Teams API spec/requests/api/v1/teams_controller_spec.rb Index/show/members/add/remove + authorisation status codes
JoinTeamRequests API spec/requests/api/v1/join_team_requests_controller_spec.rb Authorisation, create/accept/decline, capacity rejection and rollback behaviour
TeamsParticipants API spec/requests/api/v1/teams_participants_controller_spec.rb Duty update authorisation, list/add/delete flows

Important Example Tests

Capacity and size limits

# spec/models/team_capacity_spec.rb
it 'rejects member when team is full' do
  2.times { |i| team.add_member(make_participant("full#{i}")) }
  extra  = make_participant('extra')

  result = team.add_member(extra)

  expect(result[:success]).to be false
  expect(result[:error]).to match(/capacity/i)
end

Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.

# spec/models/teams_participant_spec.rb
describe 'capacity validation (team_not_full)' do
  it 'raises when creating beyond capacity' do
    assignment.update!(max_team_size: 1)
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)

    u1 = make_user('cap2_u1')
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)
    TeamsParticipant.create!(team: team, participant: p1, user: u1)

    u2 = make_user('cap2_u2')
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)

    expect {
      TeamsParticipant.create!(team: team, participant: p2, user: u2)
    }.to raise_error(ActiveRecord::RecordInvalid)
  end
end

Purpose: ensures capacity cannot be bypassed by direct join-table creation.

Join request acceptance rollback correctness

# spec/requests/api/v1/join_team_requests_controller_spec.rb
it 'does not change reply_status or add participant when team is full' do
  assignment.update!(max_team_size: 1)

  patch "/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers

  expect(response).to have_http_status(:unprocessable_entity)
  expect(join_team_request.reload.reply_status).to eq('PENDING')
  expect(team1.participants.reload).not_to include(participant2)
end

Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.

TeamsController authorisation / HTTP status codes

# spec/requests/api/v1/teams_controller_spec.rb
it 'returns 403 for student on GET /teams' do
  student_token   = JsonWebToken.encode(id: other_user.id)
  student_headers = { Authorization: "Bearer #{student_token}" }

  get '/teams', headers: student_headers

  expect(response).to have_http_status(:forbidden)
  expect(JSON.parse(response.body)).to have_key('error')
end

Purpose: ensures a student cannot list teams and receives 403 with an error payload.

MentoredTeam duty-based mentor behaviour

# spec/models/mentored_team_spec.rb
it 'identifies mentor by duty not by role' do
  mentor_duty
  participant = make_participant('duty_mentor')
  team.add_member(participant)
  team.assign_mentor(participant.user)

  expect(team.send(:mentor)).to eq(participant.user)
  expect(participant.reload.duty).to eq(mentor_duty)
end

Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.

Demo Video

https://www.youtube.com/watch?v=KZh-Kwhduok

Summary

This project strengthens the Teams hierarchy by ensuring membership is grounded in enrolment, preventing duplicate membership, enforcing assignment team capacity with both domain-level checks and a join-table backstop, making join-request acceptance race-safe with locking, and implementing explicit TeamsController authorisation rules with request specs that validate correct HTTP response codes. MentoredTeam is tested to rely on participant duty (Mentor) rather than user role.

References

  • Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)
  • Rails Guides — Active Record Associations
  • RSpec Documentation