<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Awainga</id>
	<title>Expertiza_Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Awainga"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Awainga"/>
	<updated>2026-09-12T14:51:55Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=168168</id>
		<title>CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=168168"/>
		<updated>2026-04-28T23:09:14Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== E2615: Full Implementation Wiki for User Model and UsersController Testing ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Information&lt;br /&gt;
|-&lt;br /&gt;
| Course || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| Project || E2615 - Testing User Model and UsersController&lt;br /&gt;
|-&lt;br /&gt;
| Instructor || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| Mentor || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| Team Members || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| Platform || Expertiza reimplementation back-end (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| Primary Testing Framework || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| Target Components || User model and UsersController&lt;br /&gt;
|-&lt;br /&gt;
| Final Deliverables || Comprehensive model specs, comprehensive request specs, and targeted defect fixes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Why This Project Matters ==&lt;br /&gt;
&lt;br /&gt;
The User domain is the backbone of Expertiza. It controls account identity, role-driven behavior, hierarchical relationships between users, and authentication-related workflows. If this layer breaks, many workflows break.&lt;br /&gt;
&lt;br /&gt;
This project addressed a critical testing gap by replacing placeholder coverage with real, scenario-driven tests that validate both normal behavior and failure behavior for:&lt;br /&gt;
* Model rules and role logic&lt;br /&gt;
* API contracts and error handling&lt;br /&gt;
* Authentication-sensitive endpoints&lt;br /&gt;
* Regression-prone edge cases&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
* Build broad and meaningful tests for User model behavior.&lt;br /&gt;
* Build comprehensive request tests for all UsersController actions.&lt;br /&gt;
* Validate both happy paths and failure paths with explicit status and payload checks.&lt;br /&gt;
* Fix defects discovered during implementation and lock them with regression tests.&lt;br /&gt;
* Keep changes minimal, focused, and consistent with repository conventions.&lt;br /&gt;
&lt;br /&gt;
== Final Scope Delivered ==&lt;br /&gt;
&lt;br /&gt;
=== In Scope (Completed) ===&lt;br /&gt;
* spec/models/user_spec.rb&lt;br /&gt;
* spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
* Targeted production changes tied to test evidence in:&lt;br /&gt;
* app/models/user.rb&lt;br /&gt;
* app/models/ta.rb&lt;br /&gt;
* app/controllers/users_controller.rb&lt;br /&gt;
* db/migrate/20260428000000_update_teams_participants_unique_index.rb&lt;br /&gt;
* db/schema.rb&lt;br /&gt;
&lt;br /&gt;
=== Out of Scope ===&lt;br /&gt;
* Broad refactors unrelated to User and UsersController&lt;br /&gt;
* Frontend and UI work&lt;br /&gt;
* Unrelated controller rewrites&lt;br /&gt;
&lt;br /&gt;
== User Role Structure ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                   ┌───────────────────────┐&lt;br /&gt;
                   │         User          │&lt;br /&gt;
                   │   (core account)      │&lt;br /&gt;
                   └───────────┬───────────┘&lt;br /&gt;
                               │ belongs_to&lt;br /&gt;
                               ▼&lt;br /&gt;
                        ┌─────────────┐&lt;br /&gt;
                        │    Role     │&lt;br /&gt;
                        └──────┬──────┘&lt;br /&gt;
                               │ hierarchy&lt;br /&gt;
      ┌────────────────────────┼────────────────────────┐&lt;br /&gt;
      ▼                        ▼                        ▼&lt;br /&gt;
  Student              Teaching Assistant           Instructor&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                  Administrator&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                Super Administrator&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Design Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Model Spec Plan (spec/models/user_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! What Will Be Tested !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| Validations || presence, format, and length checks for name, email, password, full_name || Invalid data is rejected with clear errors&lt;br /&gt;
|-&lt;br /&gt;
| Associations || role, institution, parent, child users, invitations, assignments, teams, participants || Associations are correctly defined and functional&lt;br /&gt;
|-&lt;br /&gt;
| Scopes || students, tas, instructors, administrators, superadministrators || Each scope returns users with matching role&lt;br /&gt;
|-&lt;br /&gt;
| Delegated role checks || student?, ta?, instructor?, administrator?, super_administrator? || Role query methods behave consistently&lt;br /&gt;
|-&lt;br /&gt;
| Defaults || initialization flags and email preference defaults || Defaults are set correctly on initialization&lt;br /&gt;
|-&lt;br /&gt;
| Authentication behavior || has_secure_password behavior, password digest creation, minimum length || Password workflows behave securely and predictably&lt;br /&gt;
|-&lt;br /&gt;
| Class methods || login_user, from_params, instantiate || Lookup and STI conversion logic works as intended&lt;br /&gt;
|-&lt;br /&gt;
| Instance methods || instructor_id, can_impersonate?, recursively_parent_of, teaching_assistant_for?, as_json, generate_jwt || Methods return correct values and enforce role rules&lt;br /&gt;
|-&lt;br /&gt;
| Regression checks || edge cases and known fragile areas discovered during implementation || Defects are detected and fixed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Spec Plan (spec/requests/api/v1/users_controller_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint !! Scenarios !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| GET /users || Authenticated request || 200 with user list payload&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id || Existing and missing id || 200 for existing, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| POST /users || Valid payload, invalid payload, missing user param || 201 for valid, 422 for invalid or missing params&lt;br /&gt;
|-&lt;br /&gt;
| PATCH/PUT /users/:id || Valid update, invalid update, missing id || 200 for valid, 422 or 404 for failures&lt;br /&gt;
|-&lt;br /&gt;
| DELETE /users/:id || Existing and missing id || No-content success path, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/institution/:id || Valid institution and missing institution || 200 with users, 404 when not found&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id/managed || Student parent versus non-student parent || 422 for student parent, 200 for allowed roles&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/role/:name || Valid role name and missing role || 200 for valid role, graceful failure behavior for invalid role&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== How the Plan Was Converted into Real Results ==&lt;br /&gt;
&lt;br /&gt;
=== Phase 1: Build Baseline Scaffolding ===&lt;br /&gt;
&lt;br /&gt;
Both target files were organized with repository-consistent patterns:&lt;br /&gt;
* Request specs include swagger helper and JWT helper.&lt;br /&gt;
* Role hierarchy is initialized with create_roles_hierarchy.&lt;br /&gt;
* Request specs use bearer token setup and structured response blocks.&lt;br /&gt;
* Model specs use factory-driven setup and behavior-oriented describe blocks.&lt;br /&gt;
&lt;br /&gt;
=== Phase 2: Implement User Model Scenarios ===&lt;br /&gt;
&lt;br /&gt;
The model suite was written category by category:&lt;br /&gt;
* Validation boundaries first&lt;br /&gt;
* Association definitions next&lt;br /&gt;
* Scope and role delegation behavior&lt;br /&gt;
* Password and default flag behavior&lt;br /&gt;
* Class method contracts&lt;br /&gt;
* Domain-level methods with role-aware and hierarchy-aware cases&lt;br /&gt;
&lt;br /&gt;
This ensured early detection of behavior regressions in core user logic.&lt;br /&gt;
&lt;br /&gt;
=== Phase 3: Implement UsersController Scenarios ===&lt;br /&gt;
&lt;br /&gt;
The request suite was written route by route:&lt;br /&gt;
* Full CRUD route coverage&lt;br /&gt;
* Custom route coverage for institution, managed users, and role filtering&lt;br /&gt;
* Authorization failure checks with invalid token&lt;br /&gt;
* Missing parameter and missing record checks&lt;br /&gt;
* JSON contract checks for both success and failure responses&lt;br /&gt;
&lt;br /&gt;
=== Phase 4: Apply Defect Fixes Found During Test Implementation ===&lt;br /&gt;
&lt;br /&gt;
When scenario coverage exposed issues, targeted fixes were applied:&lt;br /&gt;
* Missing-role lookup now returns explicit 404 in role_users.&lt;br /&gt;
* teaching_assistant? now returns strict boolean values.&lt;br /&gt;
* TA model has parent_id presence validation and improved course query pattern.&lt;br /&gt;
* teams_participants unique index now aligns with scoped uniqueness intent.&lt;br /&gt;
&lt;br /&gt;
=== Phase 5: Regression Lock-In ===&lt;br /&gt;
&lt;br /&gt;
New or updated tests were added for discovered fragile paths so behavior remains stable after future changes.&lt;br /&gt;
&lt;br /&gt;
== Code Snippets from Implemented Work ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Model Validation Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/models/user_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe 'validations' do&lt;br /&gt;
  it 'is invalid when name is not unique' do&lt;br /&gt;
    create(:user, name: 'duplicate', role: role)&lt;br /&gt;
    user = User.new(name: 'duplicate', email: 'other@example.com', full_name: 'Other User', role: role, password: 'password')&lt;br /&gt;
    expect(user).not_to be_valid&lt;br /&gt;
    expect(user.errors[:name]).to include('has already been taken')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  it 'is invalid with a malformed email' do&lt;br /&gt;
    user = User.new(name: 'testuser', email: 'not-an-email', full_name: 'Test User', role: role, password: 'password')&lt;br /&gt;
    expect(user).not_to be_valid&lt;br /&gt;
    expect(user.errors[:email]).to be_present&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* Invalid input is rejected with explicit errors.&lt;br /&gt;
* Validation expectations in the model plan are fully executed.&lt;br /&gt;
&lt;br /&gt;
=== 2) Model Scope Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/models/user_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '.tas' do&lt;br /&gt;
  it 'includes users with teaching assistant role and excludes others' do&lt;br /&gt;
    ta = create(:user, role: ta_role)&lt;br /&gt;
    student = create(:user, role: student_role)&lt;br /&gt;
&lt;br /&gt;
    expect(User.tas).to include(ta)&lt;br /&gt;
    expect(User.tas).not_to include(student)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* Role scopes are tested with inclusion and exclusion checks, not only one-sided checks.&lt;br /&gt;
&lt;br /&gt;
=== 3) Model Instance Method Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/models/user_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#instructor_id' do&lt;br /&gt;
  it 'returns own id for instructor' do&lt;br /&gt;
    instructor = create(:user, role: instructor_role, institution: institution)&lt;br /&gt;
    expect(instructor.instructor_id).to eq(instructor.id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  it 'raises NotImplementedError for student role' do&lt;br /&gt;
    student = create(:user, role: student_role, institution: institution)&lt;br /&gt;
    expect { student.instructor_id }.to raise_error(NotImplementedError, /Unknown role/)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* Role-dependent instance behavior is enforced.&lt;br /&gt;
* Error behavior for unsupported roles is explicit and tested.&lt;br /&gt;
&lt;br /&gt;
=== 4) Request Authorization Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response '401', 'Unauthorized when token is invalid' do&lt;br /&gt;
  let(:Authorization) { 'Bearer invalid_token' }&lt;br /&gt;
&lt;br /&gt;
  run_test! do |response|&lt;br /&gt;
    body = JSON.parse(response.body)&lt;br /&gt;
    expect(body['error']).to eq('Not Authorized')&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* JWT-protected routes fail safely and consistently with invalid tokens.&lt;br /&gt;
&lt;br /&gt;
=== 5) Request Missing Parameter Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response '422', 'Rejects request when user param is missing' do&lt;br /&gt;
  let(:payload) { {} }&lt;br /&gt;
&lt;br /&gt;
  run_test! do |response|&lt;br /&gt;
    body = JSON.parse(response.body)&lt;br /&gt;
    expect(body['error']).to eq('Parameter missing')&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* ParameterMissing handling is validated through API tests.&lt;br /&gt;
&lt;br /&gt;
=== 6) Request Invalid Role Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response '404', 'Returns not found for invalid role name' do&lt;br /&gt;
  let(:name) { 'role_that_does_not_exist' }&lt;br /&gt;
&lt;br /&gt;
  run_test! do |response|&lt;br /&gt;
    body = JSON.parse(response.body)&lt;br /&gt;
    expect(body['error']).to eq(&amp;quot;Role 'Role That Does Not Exist' not found&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* The role filter endpoint now has a stable, graceful failure contract.&lt;br /&gt;
&lt;br /&gt;
== Production Fix Snippets that Back the Tested Results ==&lt;br /&gt;
&lt;br /&gt;
=== users_controller: role lookup guard ===&lt;br /&gt;
&lt;br /&gt;
Source: app/controllers/users_controller.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def role_users&lt;br /&gt;
  name = params[:name].split('_').map(&amp;amp;:capitalize).join(' ')&lt;br /&gt;
  role = Role.find_by(name:)&lt;br /&gt;
  unless role&lt;br /&gt;
    render json: { error: &amp;quot;Role '#{name}' not found&amp;quot; }, status: :not_found&lt;br /&gt;
    return&lt;br /&gt;
  end&lt;br /&gt;
  users = role.users&lt;br /&gt;
  render json: users, status: :ok&lt;br /&gt;
rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
  render json: { error: e.message }, status: :not_found&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== user: strict boolean predicate for role check ===&lt;br /&gt;
&lt;br /&gt;
Source: app/models/user.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def teaching_assistant?&lt;br /&gt;
  !!role.ta?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Defects Found and Fixed ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Defect Observed During Implementation !! Fix Applied !! Linked Test Outcome&lt;br /&gt;
|-&lt;br /&gt;
| Role-filter endpoint || Missing role lookup path was not safely handled || Added explicit 404 JSON guard in controller || Invalid role request now asserted as 404 with message&lt;br /&gt;
|-&lt;br /&gt;
| Role predicate return type || Predicate behavior needed strict true/false consistency || Enforced boolean coercion in User model || teaching_assistant? expectations are stable&lt;br /&gt;
|-&lt;br /&gt;
| TA support path || Model integrity and query pattern needed tightening || Added validation and improved query path || Supporting logic stabilized for tested user flows&lt;br /&gt;
|-&lt;br /&gt;
| teams_participants uniqueness || DB constraint mismatched scoped uniqueness expectation || Added composite unique index migration || Constraint behavior aligned with model intent&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Test Commands ==&lt;br /&gt;
&lt;br /&gt;
Primary command:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Demo Video Link ==&lt;br /&gt;
&lt;br /&gt;
* Demo video: https://youtu.be/lwW9BuSOeUk&lt;br /&gt;
&lt;br /&gt;
== Definition of Done Status ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Completion Criteria !! Status&lt;br /&gt;
|-&lt;br /&gt;
| User model spec implemented with broad category coverage || Completed&lt;br /&gt;
|-&lt;br /&gt;
| UsersController request spec implemented for all target routes || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Success and failure scenarios explicitly asserted || Completed&lt;br /&gt;
|-&lt;br /&gt;
| JWT-related authorization behavior covered || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Defects discovered during testing fixed with targeted patches || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Regression checks added for fragile paths || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Reproducible verification command documented || Completed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Final Summary ==&lt;br /&gt;
&lt;br /&gt;
E2615 is now a high-value quality layer for Expertiza user workflows. The project moved from negligible practical coverage to comprehensive, scenario-driven tests with concrete production hardening. The resulting test suites validate core contracts, enforce predictable API behavior, and provide a durable safety net for future development.&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=168163</id>
		<title>CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=168163"/>
		<updated>2026-04-28T17:09:28Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== E2615: Full Implementation Wiki for User Model and UsersController Testing ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Information&lt;br /&gt;
|-&lt;br /&gt;
| Course || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| Project || E2615 - Testing User Model and UsersController&lt;br /&gt;
|-&lt;br /&gt;
| Instructor || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| Mentor || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| Team Members || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| Platform || Expertiza reimplementation back-end (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| Primary Testing Framework || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| Target Components || User model and UsersController&lt;br /&gt;
|-&lt;br /&gt;
| Final Deliverables || Comprehensive model specs, comprehensive request specs, and targeted defect fixes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Demo Video Link ==&lt;br /&gt;
&lt;br /&gt;
* Demo video: INSERT_DEMO_VIDEO_LINK_HERE&lt;br /&gt;
&lt;br /&gt;
== Why This Project Matters ==&lt;br /&gt;
&lt;br /&gt;
The User domain is the backbone of Expertiza. It controls account identity, role-driven behavior, hierarchical relationships between users, and authentication-related workflows. If this layer breaks, many workflows break.&lt;br /&gt;
&lt;br /&gt;
This project addressed a critical testing gap by replacing placeholder coverage with real, scenario-driven tests that validate both normal behavior and failure behavior for:&lt;br /&gt;
* Model rules and role logic&lt;br /&gt;
* API contracts and error handling&lt;br /&gt;
* Authentication-sensitive endpoints&lt;br /&gt;
* Regression-prone edge cases&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
* Build broad and meaningful tests for User model behavior.&lt;br /&gt;
* Build comprehensive request tests for all UsersController actions.&lt;br /&gt;
* Validate both happy paths and failure paths with explicit status and payload checks.&lt;br /&gt;
* Fix defects discovered during implementation and lock them with regression tests.&lt;br /&gt;
* Keep changes minimal, focused, and consistent with repository conventions.&lt;br /&gt;
&lt;br /&gt;
== Final Scope Delivered ==&lt;br /&gt;
&lt;br /&gt;
=== In Scope (Completed) ===&lt;br /&gt;
* spec/models/user_spec.rb&lt;br /&gt;
* spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
* Targeted production changes tied to test evidence in:&lt;br /&gt;
* app/models/user.rb&lt;br /&gt;
* app/models/ta.rb&lt;br /&gt;
* app/controllers/users_controller.rb&lt;br /&gt;
* db/migrate/20260428000000_update_teams_participants_unique_index.rb&lt;br /&gt;
* db/schema.rb&lt;br /&gt;
&lt;br /&gt;
=== Out of Scope ===&lt;br /&gt;
* Broad refactors unrelated to User and UsersController&lt;br /&gt;
* Frontend and UI work&lt;br /&gt;
* Unrelated controller rewrites&lt;br /&gt;
&lt;br /&gt;
== User Role Structure ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                   ┌───────────────────────┐&lt;br /&gt;
                   │         User          │&lt;br /&gt;
                   │   (core account)      │&lt;br /&gt;
                   └───────────┬───────────┘&lt;br /&gt;
                               │ belongs_to&lt;br /&gt;
                               ▼&lt;br /&gt;
                        ┌─────────────┐&lt;br /&gt;
                        │    Role     │&lt;br /&gt;
                        └──────┬──────┘&lt;br /&gt;
                               │ hierarchy&lt;br /&gt;
      ┌────────────────────────┼────────────────────────┐&lt;br /&gt;
      ▼                        ▼                        ▼&lt;br /&gt;
  Student              Teaching Assistant           Instructor&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                  Administrator&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                Super Administrator&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Design Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Model Spec Plan (spec/models/user_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! What Will Be Tested !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| Validations || presence, format, and length checks for name, email, password, full_name || Invalid data is rejected with clear errors&lt;br /&gt;
|-&lt;br /&gt;
| Associations || role, institution, parent, child users, invitations, assignments, teams, participants || Associations are correctly defined and functional&lt;br /&gt;
|-&lt;br /&gt;
| Scopes || students, tas, instructors, administrators, superadministrators || Each scope returns users with matching role&lt;br /&gt;
|-&lt;br /&gt;
| Delegated role checks || student?, ta?, instructor?, administrator?, super_administrator? || Role query methods behave consistently&lt;br /&gt;
|-&lt;br /&gt;
| Defaults || initialization flags and email preference defaults || Defaults are set correctly on initialization&lt;br /&gt;
|-&lt;br /&gt;
| Authentication behavior || has_secure_password behavior, password digest creation, minimum length || Password workflows behave securely and predictably&lt;br /&gt;
|-&lt;br /&gt;
| Class methods || login_user, from_params, instantiate || Lookup and STI conversion logic works as intended&lt;br /&gt;
|-&lt;br /&gt;
| Instance methods || instructor_id, can_impersonate?, recursively_parent_of, teaching_assistant_for?, as_json, generate_jwt || Methods return correct values and enforce role rules&lt;br /&gt;
|-&lt;br /&gt;
| Regression checks || edge cases and known fragile areas discovered during implementation || Defects are detected and fixed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Spec Plan (spec/requests/api/v1/users_controller_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint !! Scenarios !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| GET /users || Authenticated request || 200 with user list payload&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id || Existing and missing id || 200 for existing, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| POST /users || Valid payload, invalid payload, missing user param || 201 for valid, 422 for invalid or missing params&lt;br /&gt;
|-&lt;br /&gt;
| PATCH/PUT /users/:id || Valid update, invalid update, missing id || 200 for valid, 422 or 404 for failures&lt;br /&gt;
|-&lt;br /&gt;
| DELETE /users/:id || Existing and missing id || No-content success path, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/institution/:id || Valid institution and missing institution || 200 with users, 404 when not found&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id/managed || Student parent versus non-student parent || 422 for student parent, 200 for allowed roles&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/role/:name || Valid role name and missing role || 200 for valid role, graceful failure behavior for invalid role&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== How the Plan Was Converted into Real Results ==&lt;br /&gt;
&lt;br /&gt;
=== Phase 1: Build Baseline Scaffolding ===&lt;br /&gt;
&lt;br /&gt;
Both target files were organized with repository-consistent patterns:&lt;br /&gt;
* Request specs include swagger helper and JWT helper.&lt;br /&gt;
* Role hierarchy is initialized with create_roles_hierarchy.&lt;br /&gt;
* Request specs use bearer token setup and structured response blocks.&lt;br /&gt;
* Model specs use factory-driven setup and behavior-oriented describe blocks.&lt;br /&gt;
&lt;br /&gt;
=== Phase 2: Implement User Model Scenarios ===&lt;br /&gt;
&lt;br /&gt;
The model suite was written category by category:&lt;br /&gt;
* Validation boundaries first&lt;br /&gt;
* Association definitions next&lt;br /&gt;
* Scope and role delegation behavior&lt;br /&gt;
* Password and default flag behavior&lt;br /&gt;
* Class method contracts&lt;br /&gt;
* Domain-level methods with role-aware and hierarchy-aware cases&lt;br /&gt;
&lt;br /&gt;
This ensured early detection of behavior regressions in core user logic.&lt;br /&gt;
&lt;br /&gt;
=== Phase 3: Implement UsersController Scenarios ===&lt;br /&gt;
&lt;br /&gt;
The request suite was written route by route:&lt;br /&gt;
* Full CRUD route coverage&lt;br /&gt;
* Custom route coverage for institution, managed users, and role filtering&lt;br /&gt;
* Authorization failure checks with invalid token&lt;br /&gt;
* Missing parameter and missing record checks&lt;br /&gt;
* JSON contract checks for both success and failure responses&lt;br /&gt;
&lt;br /&gt;
=== Phase 4: Apply Defect Fixes Found During Test Implementation ===&lt;br /&gt;
&lt;br /&gt;
When scenario coverage exposed issues, targeted fixes were applied:&lt;br /&gt;
* Missing-role lookup now returns explicit 404 in role_users.&lt;br /&gt;
* teaching_assistant? now returns strict boolean values.&lt;br /&gt;
* TA model has parent_id presence validation and improved course query pattern.&lt;br /&gt;
* teams_participants unique index now aligns with scoped uniqueness intent.&lt;br /&gt;
&lt;br /&gt;
=== Phase 5: Regression Lock-In ===&lt;br /&gt;
&lt;br /&gt;
New or updated tests were added for discovered fragile paths so behavior remains stable after future changes.&lt;br /&gt;
&lt;br /&gt;
== Code Snippets from Implemented Work ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Model Validation Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/models/user_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe 'validations' do&lt;br /&gt;
  it 'is invalid when name is not unique' do&lt;br /&gt;
    create(:user, name: 'duplicate', role: role)&lt;br /&gt;
    user = User.new(name: 'duplicate', email: 'other@example.com', full_name: 'Other User', role: role, password: 'password')&lt;br /&gt;
    expect(user).not_to be_valid&lt;br /&gt;
    expect(user.errors[:name]).to include('has already been taken')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  it 'is invalid with a malformed email' do&lt;br /&gt;
    user = User.new(name: 'testuser', email: 'not-an-email', full_name: 'Test User', role: role, password: 'password')&lt;br /&gt;
    expect(user).not_to be_valid&lt;br /&gt;
    expect(user.errors[:email]).to be_present&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* Invalid input is rejected with explicit errors.&lt;br /&gt;
* Validation expectations in the model plan are fully executed.&lt;br /&gt;
&lt;br /&gt;
=== 2) Model Scope Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/models/user_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '.tas' do&lt;br /&gt;
  it 'includes users with teaching assistant role and excludes others' do&lt;br /&gt;
    ta = create(:user, role: ta_role)&lt;br /&gt;
    student = create(:user, role: student_role)&lt;br /&gt;
&lt;br /&gt;
    expect(User.tas).to include(ta)&lt;br /&gt;
    expect(User.tas).not_to include(student)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* Role scopes are tested with inclusion and exclusion checks, not only one-sided checks.&lt;br /&gt;
&lt;br /&gt;
=== 3) Model Instance Method Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/models/user_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#instructor_id' do&lt;br /&gt;
  it 'returns own id for instructor' do&lt;br /&gt;
    instructor = create(:user, role: instructor_role, institution: institution)&lt;br /&gt;
    expect(instructor.instructor_id).to eq(instructor.id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  it 'raises NotImplementedError for student role' do&lt;br /&gt;
    student = create(:user, role: student_role, institution: institution)&lt;br /&gt;
    expect { student.instructor_id }.to raise_error(NotImplementedError, /Unknown role/)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* Role-dependent instance behavior is enforced.&lt;br /&gt;
* Error behavior for unsupported roles is explicit and tested.&lt;br /&gt;
&lt;br /&gt;
=== 4) Request Authorization Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response '401', 'Unauthorized when token is invalid' do&lt;br /&gt;
  let(:Authorization) { 'Bearer invalid_token' }&lt;br /&gt;
&lt;br /&gt;
  run_test! do |response|&lt;br /&gt;
    body = JSON.parse(response.body)&lt;br /&gt;
    expect(body['error']).to eq('Not Authorized')&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* JWT-protected routes fail safely and consistently with invalid tokens.&lt;br /&gt;
&lt;br /&gt;
=== 5) Request Missing Parameter Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response '422', 'Rejects request when user param is missing' do&lt;br /&gt;
  let(:payload) { {} }&lt;br /&gt;
&lt;br /&gt;
  run_test! do |response|&lt;br /&gt;
    body = JSON.parse(response.body)&lt;br /&gt;
    expect(body['error']).to eq('Parameter missing')&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* ParameterMissing handling is validated through API tests.&lt;br /&gt;
&lt;br /&gt;
=== 6) Request Invalid Role Scenario ===&lt;br /&gt;
&lt;br /&gt;
Source: spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response '404', 'Returns not found for invalid role name' do&lt;br /&gt;
  let(:name) { 'role_that_does_not_exist' }&lt;br /&gt;
&lt;br /&gt;
  run_test! do |response|&lt;br /&gt;
    body = JSON.parse(response.body)&lt;br /&gt;
    expect(body['error']).to eq(&amp;quot;Role 'Role That Does Not Exist' not found&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What this proves:&lt;br /&gt;
* The role filter endpoint now has a stable, graceful failure contract.&lt;br /&gt;
&lt;br /&gt;
== Production Fix Snippets that Back the Tested Results ==&lt;br /&gt;
&lt;br /&gt;
=== users_controller: role lookup guard ===&lt;br /&gt;
&lt;br /&gt;
Source: app/controllers/users_controller.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def role_users&lt;br /&gt;
  name = params[:name].split('_').map(&amp;amp;:capitalize).join(' ')&lt;br /&gt;
  role = Role.find_by(name:)&lt;br /&gt;
  unless role&lt;br /&gt;
    render json: { error: &amp;quot;Role '#{name}' not found&amp;quot; }, status: :not_found&lt;br /&gt;
    return&lt;br /&gt;
  end&lt;br /&gt;
  users = role.users&lt;br /&gt;
  render json: users, status: :ok&lt;br /&gt;
rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
  render json: { error: e.message }, status: :not_found&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== user: strict boolean predicate for role check ===&lt;br /&gt;
&lt;br /&gt;
Source: app/models/user.rb&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def teaching_assistant?&lt;br /&gt;
  !!role.ta?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Defects Found and Fixed ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Defect Observed During Implementation !! Fix Applied !! Linked Test Outcome&lt;br /&gt;
|-&lt;br /&gt;
| Role-filter endpoint || Missing role lookup path was not safely handled || Added explicit 404 JSON guard in controller || Invalid role request now asserted as 404 with message&lt;br /&gt;
|-&lt;br /&gt;
| Role predicate return type || Predicate behavior needed strict true/false consistency || Enforced boolean coercion in User model || teaching_assistant? expectations are stable&lt;br /&gt;
|-&lt;br /&gt;
| TA support path || Model integrity and query pattern needed tightening || Added validation and improved query path || Supporting logic stabilized for tested user flows&lt;br /&gt;
|-&lt;br /&gt;
| teams_participants uniqueness || DB constraint mismatched scoped uniqueness expectation || Added composite unique index migration || Constraint behavior aligned with model intent&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Test Commands ==&lt;br /&gt;
&lt;br /&gt;
Primary command:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Definition of Done Status ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Completion Criteria !! Status&lt;br /&gt;
|-&lt;br /&gt;
| User model spec implemented with broad category coverage || Completed&lt;br /&gt;
|-&lt;br /&gt;
| UsersController request spec implemented for all target routes || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Success and failure scenarios explicitly asserted || Completed&lt;br /&gt;
|-&lt;br /&gt;
| JWT-related authorization behavior covered || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Defects discovered during testing fixed with targeted patches || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Regression checks added for fragile paths || Completed&lt;br /&gt;
|-&lt;br /&gt;
| Reproducible verification command documented || Completed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Final Summary ==&lt;br /&gt;
&lt;br /&gt;
E2615 is now a high-value quality layer for Expertiza user workflows. The project moved from negligible practical coverage to comprehensive, scenario-driven tests with concrete production hardening. The resulting test suites validate core contracts, enforce predictable API behavior, and provide a durable safety net for future development.&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167810</id>
		<title>CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167810"/>
		<updated>2026-04-11T01:33:34Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:380px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2615 — Testing User Model and UsersController&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Team Members''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Primary Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Target Components''' || User model, UsersController&lt;br /&gt;
|-&lt;br /&gt;
| '''Current Coverage''' || No meaningful tests (stub files only)&lt;br /&gt;
|-&lt;br /&gt;
| '''Deliverables''' || Comprehensive model + request specs&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Why This Project Matters ==&lt;br /&gt;
&lt;br /&gt;
The User entity is the backbone of Expertiza. It controls identity, authentication, role-based behavior, and account management relationships.  &lt;br /&gt;
The User model and UsersController currently have no meaningful tests in scope (legacy placeholders only), so regressions in critical user workflows can slip through unnoticed.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
&lt;br /&gt;
The current codebase has essentially zero useful automated test coverage for:&lt;br /&gt;
&lt;br /&gt;
* User model behavior&lt;br /&gt;
* UsersController API behavior&lt;br /&gt;
&lt;br /&gt;
Legacy minitest placeholders exist but are commented out and should be treated as empty. Without coverage for user creation, updates, role handling, and authorization-sensitive endpoints, regressions in core identity flows can go undetected.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== What the User Class Represents ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model stores core account information such as:&lt;br /&gt;
&lt;br /&gt;
* username (&amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;)&lt;br /&gt;
* &amp;lt;code&amp;gt;full_name&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;email&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;password_digest&amp;lt;/code&amp;gt; (bcrypt-backed)&lt;br /&gt;
* role association (&amp;lt;code&amp;gt;role_id&amp;lt;/code&amp;gt;)&lt;br /&gt;
* institution association (&amp;lt;code&amp;gt;institution_id&amp;lt;/code&amp;gt;)&lt;br /&gt;
* self-referential manager relationship (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;)&lt;br /&gt;
* notification and preference flags&lt;br /&gt;
&lt;br /&gt;
It also includes business logic for:&lt;br /&gt;
&lt;br /&gt;
* role delegation helpers (student, TA, instructor, admin, super admin checks)&lt;br /&gt;
* role-scoped queries (students, tas, instructors, administrators, superadministrators)&lt;br /&gt;
* identity lookup (&amp;lt;code&amp;gt;login_user&amp;lt;/code&amp;gt;)&lt;br /&gt;
* JWT creation (&amp;lt;code&amp;gt;generate_jwt&amp;lt;/code&amp;gt;)&lt;br /&gt;
* impersonation and parent-chain checks&lt;br /&gt;
* JSON serialization contract for API responses&lt;br /&gt;
&lt;br /&gt;
=== User Role Structure ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                   ┌───────────────────────┐&lt;br /&gt;
                   │         User          │&lt;br /&gt;
                   │   (core account)      │&lt;br /&gt;
                   └───────────┬───────────┘&lt;br /&gt;
                               │ belongs_to&lt;br /&gt;
                               ▼&lt;br /&gt;
                        ┌─────────────┐&lt;br /&gt;
                        │    Role     │&lt;br /&gt;
                        └──────┬──────┘&lt;br /&gt;
                               │ hierarchy&lt;br /&gt;
      ┌────────────────────────┼────────────────────────┐&lt;br /&gt;
      ▼                        ▼                        ▼&lt;br /&gt;
  Student              Teaching Assistant           Instructor&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                  Administrator&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                Super Administrator&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== UsersController Functions ===&lt;br /&gt;
&lt;br /&gt;
UsersController is a RESTful controller with standard CRUD and three custom collection routes.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Category !! Action !! Route !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| CRUD || index || GET /users || List all users&lt;br /&gt;
|-&lt;br /&gt;
| CRUD || show || GET /users/:id || Show one user&lt;br /&gt;
|-&lt;br /&gt;
| CRUD || create || POST /users || Create a user (applies default password when omitted)&lt;br /&gt;
|-&lt;br /&gt;
| CRUD || update || PATCH/PUT /users/:id || Update user attributes&lt;br /&gt;
|-&lt;br /&gt;
| CRUD || destroy || DELETE /users/:id || Delete a user&lt;br /&gt;
|-&lt;br /&gt;
| Custom || institution_users || GET /users/institution/:id || List users belonging to an institution&lt;br /&gt;
|-&lt;br /&gt;
| Custom || managed_users || GET /users/:id/managed || List users managed by a given user&lt;br /&gt;
|-&lt;br /&gt;
| Custom || role_users || GET /users/role/:name || List users by role name&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The controller also includes explicit rescue handlers for missing records and missing required parameters, which should be validated in request specs.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
* Create comprehensive RSpec tests for User model validations, associations, scopes, authentication behavior, and domain methods.&lt;br /&gt;
* Create comprehensive request specs for all UsersController RESTful endpoints and custom collection routes.&lt;br /&gt;
* Verify HTTP status codes, response payload shape, and failure behavior for invalid input and missing records.&lt;br /&gt;
* Confirm role hierarchy setup and JWT-authenticated access patterns using repository conventions.&lt;br /&gt;
* Identify and fix defects uncovered by tests so the new suite is stable and passing.&lt;br /&gt;
* Ensure all work uses American English spelling and matches existing repository testing patterns.&lt;br /&gt;
&lt;br /&gt;
== Scope ==&lt;br /&gt;
&lt;br /&gt;
=== In Scope ===&lt;br /&gt;
&lt;br /&gt;
* New model spec file for User behavior&lt;br /&gt;
* New request spec file for UsersController endpoints&lt;br /&gt;
* Necessary factory updates for reliable setup&lt;br /&gt;
* Small, targeted production-code fixes if tests expose defects&lt;br /&gt;
&lt;br /&gt;
=== Out of Scope ===&lt;br /&gt;
&lt;br /&gt;
* Broad refactors unrelated to User and UsersController&lt;br /&gt;
* Rewriting unrelated controllers or models&lt;br /&gt;
* UI or frontend behavior&lt;br /&gt;
&lt;br /&gt;
== Files To Create ==&lt;br /&gt;
&lt;br /&gt;
Following repository conventions under &amp;lt;code&amp;gt;spec/&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;spec/models/user_spec.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;spec/requests/api/v1/users_controller_spec.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Potential supporting update:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;spec/factories.rb&amp;lt;/code&amp;gt; (extend existing user factory only if needed)&lt;br /&gt;
&lt;br /&gt;
== Test Design Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Model Spec Plan (&amp;lt;code&amp;gt;spec/models/user_spec.rb&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! What Will Be Tested !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| Validations || presence, format, and length checks for &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;email&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;password&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;full_name&amp;lt;/code&amp;gt; || Invalid data is rejected with clear errors&lt;br /&gt;
|-&lt;br /&gt;
| Associations || role, institution, parent, child users, invitations, assignments, teams, participants || Associations are correctly defined and functional&lt;br /&gt;
|-&lt;br /&gt;
| Scopes || students, tas, instructors, administrators, superadministrators || Each scope returns users with matching role&lt;br /&gt;
|-&lt;br /&gt;
| Delegated role checks || &amp;lt;code&amp;gt;student?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ta?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;instructor?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;administrator?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;super_administrator?&amp;lt;/code&amp;gt; || Role query methods behave consistently&lt;br /&gt;
|-&lt;br /&gt;
| Defaults || initialization flags and email preference defaults || Defaults are set correctly on initialization&lt;br /&gt;
|-&lt;br /&gt;
| Authentication behavior || &amp;lt;code&amp;gt;has_secure_password&amp;lt;/code&amp;gt; behavior, password digest creation, minimum length || Password workflows behave securely and predictably&lt;br /&gt;
|-&lt;br /&gt;
| Class methods || &amp;lt;code&amp;gt;login_user&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;from_params&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;instantiate&amp;lt;/code&amp;gt; || Lookup and STI conversion logic works as intended&lt;br /&gt;
|-&lt;br /&gt;
| Instance methods || &amp;lt;code&amp;gt;instructor_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_impersonate?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;recursively_parent_of&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;teaching_assistant_for?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;as_json&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;generate_jwt&amp;lt;/code&amp;gt; || Methods return correct values and enforce role rules&lt;br /&gt;
|-&lt;br /&gt;
| Regression checks || edge cases and known fragile areas discovered during implementation || Defects are detected and fixed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Spec Plan (&amp;lt;code&amp;gt;spec/requests/api/v1/users_controller_spec.rb&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint !! Scenarios !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| GET /users || Authenticated request || 200 with user list payload&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id || Existing and missing id || 200 for existing, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| POST /users || Valid payload, invalid payload, missing user param || 201 for valid, 422 for invalid or missing params&lt;br /&gt;
|-&lt;br /&gt;
| PATCH/PUT /users/:id || Valid update, invalid update, missing id || 200 for valid, 422 or 404 for failures&lt;br /&gt;
|-&lt;br /&gt;
| DELETE /users/:id || Existing and missing id || No-content success path, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/institution/:id || Valid institution and missing institution || 200 with users, 404 when not found&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id/managed || Student parent versus non-student parent || 422 for student parent, 200 for allowed roles&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/role/:name || Valid role name and missing role || 200 for valid role, graceful failure behavior for invalid role&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Test Setup Conventions ==&lt;br /&gt;
&lt;br /&gt;
Both new spec files will follow established repository patterns:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;require 'swagger_helper'&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;require 'json_web_token'&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;create_roles_hierarchy&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;spec/support/roles_helper.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
* FactoryBot for test data&lt;br /&gt;
* JWT bearer token in Authorization header for request specs&lt;br /&gt;
* DatabaseCleaner behavior from &amp;lt;code&amp;gt;spec/rails_helper.rb&amp;lt;/code&amp;gt; (already configured)&lt;br /&gt;
&lt;br /&gt;
== Planned Work Breakdown ==&lt;br /&gt;
&lt;br /&gt;
=== Task 1: Baseline and Test Scaffolding ===&lt;br /&gt;
&lt;br /&gt;
* Create both target spec files with proper requires and role setup helpers&lt;br /&gt;
* Add shared setup helpers for authenticated requests and role seeds&lt;br /&gt;
&lt;br /&gt;
=== Task 2: User Model Coverage ===&lt;br /&gt;
&lt;br /&gt;
* Implement validation, association, scope, and method tests&lt;br /&gt;
* Add edge-case and regression-focused examples&lt;br /&gt;
&lt;br /&gt;
=== Task 3: UsersController Coverage ===&lt;br /&gt;
&lt;br /&gt;
* Implement request specs for all REST and custom routes&lt;br /&gt;
* Assert status codes and response body contracts&lt;br /&gt;
&lt;br /&gt;
=== Task 4: Bug Fixes Uncovered by Tests ===&lt;br /&gt;
&lt;br /&gt;
* Patch defects exposed by new tests (only where needed)&lt;br /&gt;
* Keep fixes minimal, targeted, and documented in commit history&lt;br /&gt;
&lt;br /&gt;
=== Task 5: Stabilization and Pass Verification ===&lt;br /&gt;
&lt;br /&gt;
* Run:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Ensure both files pass cleanly and do not introduce regressions in touched behavior&lt;br /&gt;
&lt;br /&gt;
== Risks and Likely Defect Areas ==&lt;br /&gt;
&lt;br /&gt;
Based on code review before implementation, the following areas are high risk and should be validated early:&lt;br /&gt;
&lt;br /&gt;
* Role predicate behavior and delegated role checks&lt;br /&gt;
* STI conversion in &amp;lt;code&amp;gt;User.instantiate&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;reset_password&amp;lt;/code&amp;gt; implementation correctness&lt;br /&gt;
* Managed users logic across role hierarchy&lt;br /&gt;
* Error behavior when role lookup returns nil in role-filtered endpoint&lt;br /&gt;
* Authorization and authentication interactions on request endpoints&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
&lt;br /&gt;
By project completion, we expect:&lt;br /&gt;
&lt;br /&gt;
* Two comprehensive, maintainable RSpec files covering the User model and UsersController&lt;br /&gt;
* Meaningful coverage of normal flows, invalid input, and edge cases for core user operations&lt;br /&gt;
* Verified behavior for JWT-authenticated requests and role-driven responses&lt;br /&gt;
* Any discovered defects in tested paths fixed and covered by regression tests&lt;br /&gt;
* A reproducible command that passes:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Definition of Done ==&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;spec/models/user_spec.rb&amp;lt;/code&amp;gt; exists and covers core model behavior comprehensively&lt;br /&gt;
* &amp;lt;code&amp;gt;spec/requests/api/v1/users_controller_spec.rb&amp;lt;/code&amp;gt; exists and covers all controller routes comprehensively&lt;br /&gt;
* Tests follow repository conventions and use role hierarchy helpers plus FactoryBot&lt;br /&gt;
* All tests in the project scope pass locally&lt;br /&gt;
* Documented expected behaviors align with Expertiza domain rules for users and roles&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
E2615 establishes confidence in one of Expertiza’s most critical areas by introducing full automated coverage for user-domain logic and user API endpoints. The project focuses on correctness, authorization-aware behavior, robust error handling, and long-term maintainability through high-value RSpec tests and targeted defect fixes.&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167809</id>
		<title>CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167809"/>
		<updated>2026-04-11T00:56:06Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:380px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2615 — Testing User Model and Users Controller&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Team Members''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Primary Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Target Components''' || User model, UsersController&lt;br /&gt;
|-&lt;br /&gt;
| '''Current Coverage''' || No meaningful tests (stub files only)&lt;br /&gt;
|-&lt;br /&gt;
| '''Deliverables''' || Comprehensive model + request specs&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Why This Project Matters ==&lt;br /&gt;
&lt;br /&gt;
The User entity is the backbone of Expertiza. It controls identity, authentication, role-based behavior, and account management relationships.  &lt;br /&gt;
The User model and UsersController currently have no meaningful tests in scope (legacy placeholders only), so regressions in critical user workflows can slip through unnoticed.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
&lt;br /&gt;
The current codebase has essentially zero useful automated test coverage for:&lt;br /&gt;
* User model behavior&lt;br /&gt;
* UsersController API behavior&lt;br /&gt;
&lt;br /&gt;
Legacy minitest placeholders exist but are commented out and should be treated as empty. Without coverage for user creation, updates, role handling, and authorization-sensitive endpoints, regressions in core identity flows can go undetected.&lt;br /&gt;
&lt;br /&gt;
== Background: User Class ==&lt;br /&gt;
&lt;br /&gt;
=== What User Represents ===&lt;br /&gt;
The `User` model stores core account information such as:&lt;br /&gt;
&lt;br /&gt;
* username (`name`)&lt;br /&gt;
* `full_name`&lt;br /&gt;
* `email`&lt;br /&gt;
* `password_digest` (bcrypt-backed)&lt;br /&gt;
* role association (`role_id`)&lt;br /&gt;
* institution association (`institution_id`)&lt;br /&gt;
* self-referential manager relationship (`parent_id`)&lt;br /&gt;
* notification and preference flags&lt;br /&gt;
&lt;br /&gt;
It also includes business logic for:&lt;br /&gt;
&lt;br /&gt;
* role delegation helpers (student, TA, instructor, admin, super admin checks)&lt;br /&gt;
* role-scoped queries (students, tas, instructors, administrators, superadministrators)&lt;br /&gt;
* identity lookup (`login_user`)&lt;br /&gt;
* JWT creation (`generate_jwt`)&lt;br /&gt;
* impersonation and parent-chain checks&lt;br /&gt;
* JSON serialization contract for API responses&lt;br /&gt;
&lt;br /&gt;
=== User Role Structure ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                   ┌───────────────────────┐&lt;br /&gt;
                   │         User          │&lt;br /&gt;
                   │   (core account)      │&lt;br /&gt;
                   └───────────┬───────────┘&lt;br /&gt;
                               │ belongs_to&lt;br /&gt;
                               ▼&lt;br /&gt;
                        ┌─────────────┐&lt;br /&gt;
                        │    Role     │&lt;br /&gt;
                        └──────┬──────┘&lt;br /&gt;
                               │ hierarchy&lt;br /&gt;
      ┌────────────────────────┼────────────────────────┐&lt;br /&gt;
      ▼                        ▼                        ▼&lt;br /&gt;
  Student              Teaching Assistant           Instructor&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                  Administrator&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                Super Administrator&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
* Create comprehensive RSpec tests for User model validations, associations, scopes, authentication behavior, and domain methods.&lt;br /&gt;
* Create comprehensive request specs for all UsersController RESTful endpoints and custom collection routes.&lt;br /&gt;
* Verify HTTP status codes, response payload shape, and failure behavior for invalid input and missing records.&lt;br /&gt;
* Confirm role hierarchy setup and JWT-authenticated access patterns using repository conventions.&lt;br /&gt;
* Identify and fix defects uncovered by tests so the new suite is stable and passing.&lt;br /&gt;
* Ensure all work uses American English spelling and matches existing repository testing patterns.&lt;br /&gt;
&lt;br /&gt;
== Scope ==&lt;br /&gt;
&lt;br /&gt;
=== In Scope ===&lt;br /&gt;
* New model spec file for User behavior.&lt;br /&gt;
* New request spec file for UsersController endpoints.&lt;br /&gt;
* Necessary factory updates for reliable setup.&lt;br /&gt;
* Small, targeted production-code fixes if tests expose defects.&lt;br /&gt;
&lt;br /&gt;
=== Out of Scope ===&lt;br /&gt;
* Broad refactors unrelated to User and UsersController.&lt;br /&gt;
* Rewriting unrelated controllers or models.&lt;br /&gt;
* UI/frontend behavior.&lt;br /&gt;
&lt;br /&gt;
== Files To Create ==&lt;br /&gt;
&lt;br /&gt;
Following repository conventions under spec/:&lt;br /&gt;
&lt;br /&gt;
* spec/models/user_spec.rb&lt;br /&gt;
* spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
Potential supporting update:&lt;br /&gt;
&lt;br /&gt;
* spec/factories.rb (extend existing user factory only if needed)&lt;br /&gt;
&lt;br /&gt;
== Background: User Domain Responsibilities ==&lt;br /&gt;
&lt;br /&gt;
The User model is responsible for:&lt;br /&gt;
* Password hashing and authentication support (bcrypt / has_secure_password)&lt;br /&gt;
* Role linkage and role-based behavior&lt;br /&gt;
* Parent-child user relationships for managed accounts&lt;br /&gt;
* JWT generation support&lt;br /&gt;
* Participant/team/invitation associations&lt;br /&gt;
* Serialization of core user fields and related role/parent/institution data&lt;br /&gt;
&lt;br /&gt;
UsersController is responsible for:&lt;br /&gt;
* Standard REST actions: index, show, create, update, destroy&lt;br /&gt;
* Custom collection endpoints:&lt;br /&gt;
** institution users listing&lt;br /&gt;
** managed users listing&lt;br /&gt;
** role-filtered users listing&lt;br /&gt;
* Error handling for missing records and missing parameters&lt;br /&gt;
&lt;br /&gt;
== Test Design Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Model Spec Plan (spec/models/user_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! What Will Be Tested !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| Validations || presence/format/length checks for name, email, password, full_name || invalid data is rejected with clear errors&lt;br /&gt;
|-&lt;br /&gt;
| Associations || role, institution, parent, child users, invitations, assignments, teams, participants || associations are correctly defined and functional&lt;br /&gt;
|-&lt;br /&gt;
| Scopes || students, tas, instructors, administrators, superadministrators || each scope returns users with matching role&lt;br /&gt;
|-&lt;br /&gt;
| Delegated role checks || student?/ta?/instructor?/administrator?/super_administrator? || role query methods behave consistently&lt;br /&gt;
|-&lt;br /&gt;
| Defaults || initialization flags and email preference defaults || defaults are set correctly on initialization&lt;br /&gt;
|-&lt;br /&gt;
| Authentication behavior || has_secure_password behavior, password digest creation, minimum length || password workflows behave securely and predictably&lt;br /&gt;
|-&lt;br /&gt;
| Class methods || login_user, from_params, instantiate || lookup and STI conversion logic works as intended&lt;br /&gt;
|-&lt;br /&gt;
| Instance methods || instructor_id, can_impersonate?, recursively_parent_of, teaching_assistant_for?, as_json, generate_jwt || methods return correct values and enforce role rules&lt;br /&gt;
|-&lt;br /&gt;
| Regression checks || edge cases and known fragile areas discovered during implementation || defects are detected and fixed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Spec Plan (spec/requests/api/v1/users_controller_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint !! Scenarios !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| GET /users || authenticated request || 200 with user list payload&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id || existing and missing id || 200 for existing, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| POST /users || valid payload, invalid payload, missing user param || 201 for valid, 422 for invalid/missing params&lt;br /&gt;
|-&lt;br /&gt;
| PATCH/PUT /users/:id || valid update, invalid update, missing id || 200 for valid, 422/404 for failures&lt;br /&gt;
|-&lt;br /&gt;
| DELETE /users/:id || existing and missing id || no-content success path, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/institution/:id || valid institution and missing institution || 200 with users, 404 when not found&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id/managed || student parent vs non-student parent || 422 for student parent, 200 for allowed roles&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/role/:name || valid role name and missing role || 200 for valid role, graceful failure behavior for invalid role&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Test Setup Conventions ==&lt;br /&gt;
&lt;br /&gt;
Both new spec files will follow established repository patterns:&lt;br /&gt;
* require 'swagger_helper'&lt;br /&gt;
* require 'json_web_token'&lt;br /&gt;
* create_roles_hierarchy from spec/support/roles_helper.rb&lt;br /&gt;
* FactoryBot for test data&lt;br /&gt;
* JWT bearer token in Authorization header for request specs&lt;br /&gt;
* DatabaseCleaner behavior from spec/rails_helper.rb (already configured)&lt;br /&gt;
&lt;br /&gt;
== Planned Work Breakdown ==&lt;br /&gt;
&lt;br /&gt;
=== Task 1: Baseline and Test Scaffolding ===&lt;br /&gt;
* Create both target spec files with proper requires and role setup helpers.&lt;br /&gt;
* Add shared setup helpers for authenticated requests and role seeds.&lt;br /&gt;
&lt;br /&gt;
=== Task 2: User Model Coverage ===&lt;br /&gt;
* Implement validation, association, scope, and method tests.&lt;br /&gt;
* Add edge-case and regression-focused examples.&lt;br /&gt;
&lt;br /&gt;
=== Task 3: UsersController Coverage ===&lt;br /&gt;
* Implement request specs for all REST and custom routes.&lt;br /&gt;
* Assert status codes and response body contracts.&lt;br /&gt;
&lt;br /&gt;
=== Task 4: Bug Fixes Uncovered by Tests ===&lt;br /&gt;
* Patch defects exposed by new tests (only where needed).&lt;br /&gt;
* Keep fixes minimal, targeted, and documented in commit history.&lt;br /&gt;
&lt;br /&gt;
=== Task 5: Stabilization and Pass Verification ===&lt;br /&gt;
* Run:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Ensure both files pass cleanly and do not introduce regressions in touched behavior.&lt;br /&gt;
&lt;br /&gt;
== Risks and Likely Defect Areas ==&lt;br /&gt;
&lt;br /&gt;
Based on code review before implementation, the following areas are high risk and should be validated early:&lt;br /&gt;
&lt;br /&gt;
* Role predicate behavior and delegated role checks&lt;br /&gt;
* STI conversion in User.instantiate&lt;br /&gt;
* reset_password implementation correctness&lt;br /&gt;
* managed users logic across role hierarchy&lt;br /&gt;
* Error behavior when role lookup returns nil in role-filtered endpoint&lt;br /&gt;
* Authorization and authentication interactions on request endpoints&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
&lt;br /&gt;
By project completion, we expect:&lt;br /&gt;
&lt;br /&gt;
* Two comprehensive, maintainable RSpec files covering the User model and UsersController.&lt;br /&gt;
* Meaningful coverage of normal flows, invalid input, and edge cases for core user operations.&lt;br /&gt;
* Verified behavior for JWT-authenticated requests and role-driven responses.&lt;br /&gt;
* Any discovered defects in tested paths fixed and covered by regression tests.&lt;br /&gt;
* A reproducible command that passes:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Definition of Done ==&lt;br /&gt;
&lt;br /&gt;
* spec/models/user_spec.rb exists and covers core model behavior comprehensively.&lt;br /&gt;
* spec/requests/api/v1/users_controller_spec.rb exists and covers all controller routes comprehensively.&lt;br /&gt;
* Tests follow repository conventions and use role hierarchy helpers plus FactoryBot.&lt;br /&gt;
* All tests in the project scope pass locally.&lt;br /&gt;
* Documented expected behaviors align with Expertiza domain rules for users and roles.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
E2615 establishes confidence in one of Expertiza’s most critical areas by introducing full automated coverage for user-domain logic and user API endpoints. The project focuses on correctness, authorization-aware behavior, robust error handling, and long-term maintainability through high-value RSpec tests and targeted defect fixes.&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167808</id>
		<title>CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167808"/>
		<updated>2026-04-11T00:43:59Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:380px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2615 — Testing User Model and Users Controller&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Primary Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Target Components''' || User model, UsersController&lt;br /&gt;
|-&lt;br /&gt;
| '''Current Coverage''' || No meaningful tests (stub files only)&lt;br /&gt;
|-&lt;br /&gt;
| '''Deliverables''' || Comprehensive model + request specs&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Why This Project Matters ==&lt;br /&gt;
&lt;br /&gt;
The User entity is the backbone of Expertiza. It controls identity, authentication, role-based behavior, and account management relationships.  &lt;br /&gt;
The User model and UsersController currently have no meaningful tests in scope (legacy placeholders only), so regressions in critical user workflows can slip through unnoticed.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
&lt;br /&gt;
The current codebase has essentially zero useful automated test coverage for:&lt;br /&gt;
* User model behavior&lt;br /&gt;
* UsersController API behavior&lt;br /&gt;
&lt;br /&gt;
Legacy minitest placeholders exist but are commented out and should be treated as empty. Without coverage for user creation, updates, role handling, and authorization-sensitive endpoints, regressions in core identity flows can go undetected.&lt;br /&gt;
&lt;br /&gt;
== Background: User Class ==&lt;br /&gt;
&lt;br /&gt;
=== What User Represents ===&lt;br /&gt;
The `User` model stores core account information such as:&lt;br /&gt;
&lt;br /&gt;
* username (`name`)&lt;br /&gt;
* `full_name`&lt;br /&gt;
* `email`&lt;br /&gt;
* `password_digest` (bcrypt-backed)&lt;br /&gt;
* role association (`role_id`)&lt;br /&gt;
* institution association (`institution_id`)&lt;br /&gt;
* self-referential manager relationship (`parent_id`)&lt;br /&gt;
* notification and preference flags&lt;br /&gt;
&lt;br /&gt;
It also includes business logic for:&lt;br /&gt;
&lt;br /&gt;
* role delegation helpers (student, TA, instructor, admin, super admin checks)&lt;br /&gt;
* role-scoped queries (students, tas, instructors, administrators, superadministrators)&lt;br /&gt;
* identity lookup (`login_user`)&lt;br /&gt;
* JWT creation (`generate_jwt`)&lt;br /&gt;
* impersonation and parent-chain checks&lt;br /&gt;
* JSON serialization contract for API responses&lt;br /&gt;
&lt;br /&gt;
=== User Role Structure ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                   ┌───────────────────────┐&lt;br /&gt;
                   │         User          │&lt;br /&gt;
                   │   (core account)      │&lt;br /&gt;
                   └───────────┬───────────┘&lt;br /&gt;
                               │ belongs_to&lt;br /&gt;
                               ▼&lt;br /&gt;
                        ┌─────────────┐&lt;br /&gt;
                        │    Role     │&lt;br /&gt;
                        └──────┬──────┘&lt;br /&gt;
                               │ hierarchy&lt;br /&gt;
      ┌────────────────────────┼────────────────────────┐&lt;br /&gt;
      ▼                        ▼                        ▼&lt;br /&gt;
  Student              Teaching Assistant           Instructor&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                  Administrator&lt;br /&gt;
                                                        │&lt;br /&gt;
                                                        ▼&lt;br /&gt;
                                                Super Administrator&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
* Create comprehensive RSpec tests for User model validations, associations, scopes, authentication behavior, and domain methods.&lt;br /&gt;
* Create comprehensive request specs for all UsersController RESTful endpoints and custom collection routes.&lt;br /&gt;
* Verify HTTP status codes, response payload shape, and failure behavior for invalid input and missing records.&lt;br /&gt;
* Confirm role hierarchy setup and JWT-authenticated access patterns using repository conventions.&lt;br /&gt;
* Identify and fix defects uncovered by tests so the new suite is stable and passing.&lt;br /&gt;
* Ensure all work uses American English spelling and matches existing repository testing patterns.&lt;br /&gt;
&lt;br /&gt;
== Scope ==&lt;br /&gt;
&lt;br /&gt;
=== In Scope ===&lt;br /&gt;
* New model spec file for User behavior.&lt;br /&gt;
* New request spec file for UsersController endpoints.&lt;br /&gt;
* Necessary factory updates for reliable setup.&lt;br /&gt;
* Small, targeted production-code fixes if tests expose defects.&lt;br /&gt;
&lt;br /&gt;
=== Out of Scope ===&lt;br /&gt;
* Broad refactors unrelated to User and UsersController.&lt;br /&gt;
* Rewriting unrelated controllers or models.&lt;br /&gt;
* UI/frontend behavior.&lt;br /&gt;
&lt;br /&gt;
== Files To Create ==&lt;br /&gt;
&lt;br /&gt;
Following repository conventions under spec/:&lt;br /&gt;
&lt;br /&gt;
* spec/models/user_spec.rb&lt;br /&gt;
* spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&lt;br /&gt;
Potential supporting update:&lt;br /&gt;
&lt;br /&gt;
* spec/factories.rb (extend existing user factory only if needed)&lt;br /&gt;
&lt;br /&gt;
== Background: User Domain Responsibilities ==&lt;br /&gt;
&lt;br /&gt;
The User model is responsible for:&lt;br /&gt;
* Password hashing and authentication support (bcrypt / has_secure_password)&lt;br /&gt;
* Role linkage and role-based behavior&lt;br /&gt;
* Parent-child user relationships for managed accounts&lt;br /&gt;
* JWT generation support&lt;br /&gt;
* Participant/team/invitation associations&lt;br /&gt;
* Serialization of core user fields and related role/parent/institution data&lt;br /&gt;
&lt;br /&gt;
UsersController is responsible for:&lt;br /&gt;
* Standard REST actions: index, show, create, update, destroy&lt;br /&gt;
* Custom collection endpoints:&lt;br /&gt;
** institution users listing&lt;br /&gt;
** managed users listing&lt;br /&gt;
** role-filtered users listing&lt;br /&gt;
* Error handling for missing records and missing parameters&lt;br /&gt;
&lt;br /&gt;
== Test Design Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Model Spec Plan (spec/models/user_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! What Will Be Tested !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| Validations || presence/format/length checks for name, email, password, full_name || invalid data is rejected with clear errors&lt;br /&gt;
|-&lt;br /&gt;
| Associations || role, institution, parent, child users, invitations, assignments, teams, participants || associations are correctly defined and functional&lt;br /&gt;
|-&lt;br /&gt;
| Scopes || students, tas, instructors, administrators, superadministrators || each scope returns users with matching role&lt;br /&gt;
|-&lt;br /&gt;
| Delegated role checks || student?/ta?/instructor?/administrator?/super_administrator? || role query methods behave consistently&lt;br /&gt;
|-&lt;br /&gt;
| Defaults || initialization flags and email preference defaults || defaults are set correctly on initialization&lt;br /&gt;
|-&lt;br /&gt;
| Authentication behavior || has_secure_password behavior, password digest creation, minimum length || password workflows behave securely and predictably&lt;br /&gt;
|-&lt;br /&gt;
| Class methods || login_user, from_params, instantiate || lookup and STI conversion logic works as intended&lt;br /&gt;
|-&lt;br /&gt;
| Instance methods || instructor_id, can_impersonate?, recursively_parent_of, teaching_assistant_for?, as_json, generate_jwt || methods return correct values and enforce role rules&lt;br /&gt;
|-&lt;br /&gt;
| Regression checks || edge cases and known fragile areas discovered during implementation || defects are detected and fixed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Spec Plan (spec/requests/api/v1/users_controller_spec.rb) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint !! Scenarios !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| GET /users || authenticated request || 200 with user list payload&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id || existing and missing id || 200 for existing, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| POST /users || valid payload, invalid payload, missing user param || 201 for valid, 422 for invalid/missing params&lt;br /&gt;
|-&lt;br /&gt;
| PATCH/PUT /users/:id || valid update, invalid update, missing id || 200 for valid, 422/404 for failures&lt;br /&gt;
|-&lt;br /&gt;
| DELETE /users/:id || existing and missing id || no-content success path, 404 for missing&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/institution/:id || valid institution and missing institution || 200 with users, 404 when not found&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/:id/managed || student parent vs non-student parent || 422 for student parent, 200 for allowed roles&lt;br /&gt;
|-&lt;br /&gt;
| GET /users/role/:name || valid role name and missing role || 200 for valid role, graceful failure behavior for invalid role&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Test Setup Conventions ==&lt;br /&gt;
&lt;br /&gt;
Both new spec files will follow established repository patterns:&lt;br /&gt;
* require 'swagger_helper'&lt;br /&gt;
* require 'json_web_token'&lt;br /&gt;
* create_roles_hierarchy from spec/support/roles_helper.rb&lt;br /&gt;
* FactoryBot for test data&lt;br /&gt;
* JWT bearer token in Authorization header for request specs&lt;br /&gt;
* DatabaseCleaner behavior from spec/rails_helper.rb (already configured)&lt;br /&gt;
&lt;br /&gt;
== Planned Work Breakdown ==&lt;br /&gt;
&lt;br /&gt;
=== Task 1: Baseline and Test Scaffolding ===&lt;br /&gt;
* Create both target spec files with proper requires and role setup helpers.&lt;br /&gt;
* Add shared setup helpers for authenticated requests and role seeds.&lt;br /&gt;
&lt;br /&gt;
=== Task 2: User Model Coverage ===&lt;br /&gt;
* Implement validation, association, scope, and method tests.&lt;br /&gt;
* Add edge-case and regression-focused examples.&lt;br /&gt;
&lt;br /&gt;
=== Task 3: UsersController Coverage ===&lt;br /&gt;
* Implement request specs for all REST and custom routes.&lt;br /&gt;
* Assert status codes and response body contracts.&lt;br /&gt;
&lt;br /&gt;
=== Task 4: Bug Fixes Uncovered by Tests ===&lt;br /&gt;
* Patch defects exposed by new tests (only where needed).&lt;br /&gt;
* Keep fixes minimal, targeted, and documented in commit history.&lt;br /&gt;
&lt;br /&gt;
=== Task 5: Stabilization and Pass Verification ===&lt;br /&gt;
* Run:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Ensure both files pass cleanly and do not introduce regressions in touched behavior.&lt;br /&gt;
&lt;br /&gt;
== Risks and Likely Defect Areas ==&lt;br /&gt;
&lt;br /&gt;
Based on code review before implementation, the following areas are high risk and should be validated early:&lt;br /&gt;
&lt;br /&gt;
* Role predicate behavior and delegated role checks&lt;br /&gt;
* STI conversion in User.instantiate&lt;br /&gt;
* reset_password implementation correctness&lt;br /&gt;
* managed users logic across role hierarchy&lt;br /&gt;
* Error behavior when role lookup returns nil in role-filtered endpoint&lt;br /&gt;
* Authorization and authentication interactions on request endpoints&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
&lt;br /&gt;
By project completion, we expect:&lt;br /&gt;
&lt;br /&gt;
* Two comprehensive, maintainable RSpec files covering the User model and UsersController.&lt;br /&gt;
* Meaningful coverage of normal flows, invalid input, and edge cases for core user operations.&lt;br /&gt;
* Verified behavior for JWT-authenticated requests and role-driven responses.&lt;br /&gt;
* Any discovered defects in tested paths fixed and covered by regression tests.&lt;br /&gt;
* A reproducible command that passes:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Definition of Done ==&lt;br /&gt;
&lt;br /&gt;
* spec/models/user_spec.rb exists and covers core model behavior comprehensively.&lt;br /&gt;
* spec/requests/api/v1/users_controller_spec.rb exists and covers all controller routes comprehensively.&lt;br /&gt;
* Tests follow repository conventions and use role hierarchy helpers plus FactoryBot.&lt;br /&gt;
* All tests in the project scope pass locally.&lt;br /&gt;
* Documented expected behaviors align with Expertiza domain rules for users and roles.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
E2615 establishes confidence in one of Expertiza’s most critical areas by introducing full automated coverage for user-domain logic and user API endpoints. The project focuses on correctness, authorization-aware behavior, robust error handling, and long-term maintainability through high-value RSpec tests and targeted defect fixes.&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167807</id>
		<title>CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167807"/>
		<updated>2026-04-11T00:30:52Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:390px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2615 — Testing User Model and UsersController&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Project Status''' || Planning phase (implementation not started)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
This page is a '''design document''' for E2615.  &lt;br /&gt;
No implementation work has started yet. This document defines what we will test, why it matters, and what outcomes we expect when the project is complete.&lt;br /&gt;
&lt;br /&gt;
== Why This Project Matters ==&lt;br /&gt;
&lt;br /&gt;
The User entity is the backbone of Expertiza. It controls identity, authentication, role-based behavior, and account management relationships.  &lt;br /&gt;
The User model and UsersController currently have no meaningful tests in scope (legacy placeholders only), so regressions in critical user workflows can slip through unnoticed.&lt;br /&gt;
&lt;br /&gt;
== Project Objectives ==&lt;br /&gt;
&lt;br /&gt;
* Build complete RSpec coverage for the User model.&lt;br /&gt;
* Build complete request-level RSpec coverage for UsersController.&lt;br /&gt;
* Validate role-aware behavior, authentication flow assumptions, and API error handling.&lt;br /&gt;
* Follow existing repository test patterns for setup and data creation.&lt;br /&gt;
* Deliver stable, passing tests that can serve as a baseline for future user-related changes.&lt;br /&gt;
&lt;br /&gt;
== Background: User Class ==&lt;br /&gt;
&lt;br /&gt;
=== What User Represents ===&lt;br /&gt;
The `User` model stores core account information such as:&lt;br /&gt;
&lt;br /&gt;
* username (`name`)&lt;br /&gt;
* `full_name`&lt;br /&gt;
* `email`&lt;br /&gt;
* `password_digest` (bcrypt-backed)&lt;br /&gt;
* role association (`role_id`)&lt;br /&gt;
* institution association (`institution_id`)&lt;br /&gt;
* self-referential manager relationship (`parent_id`)&lt;br /&gt;
* notification and preference flags&lt;br /&gt;
&lt;br /&gt;
It also includes business logic for:&lt;br /&gt;
&lt;br /&gt;
* role delegation helpers (student, TA, instructor, admin, super admin checks)&lt;br /&gt;
* role-scoped queries (students, tas, instructors, administrators, superadministrators)&lt;br /&gt;
* identity lookup (`login_user`)&lt;br /&gt;
* JWT creation (`generate_jwt`)&lt;br /&gt;
* impersonation and parent-chain checks&lt;br /&gt;
* JSON serialization contract for API responses&lt;br /&gt;
&lt;br /&gt;
=== User Role Structure (Conceptual) ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                   ┌───────────────────────┐&lt;br /&gt;
                   │         User          │&lt;br /&gt;
                   │   (core account)      │&lt;br /&gt;
                   └───────────┬───────────┘&lt;br /&gt;
                               │ belongs_to&lt;br /&gt;
                               ▼&lt;br /&gt;
                        ┌─────────────┐&lt;br /&gt;
                        │    Role     │&lt;br /&gt;
                        └──────┬──────┘&lt;br /&gt;
                               │ hierarchy&lt;br /&gt;
      ┌────────────────────────┼────────────────────────┐&lt;br /&gt;
      ▼                        ▼                        ▼&lt;br /&gt;
  Student              Teaching Assistant           Instructor&lt;br /&gt;
                                                    │&lt;br /&gt;
                                                    ▼&lt;br /&gt;
                                              Administrator&lt;br /&gt;
                                                    │&lt;br /&gt;
                                                    ▼&lt;br /&gt;
                                           Super Administrator&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Management Relationship (Self-Reference) ===&lt;br /&gt;
Users can manage other users through a parent-child relation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
User (parent manager) 1 ────────* User (managed account)&lt;br /&gt;
              via parent_id on child record&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This relationship is important for endpoints like managed user listing and impersonation checks.&lt;br /&gt;
&lt;br /&gt;
== Background: UsersController ==&lt;br /&gt;
&lt;br /&gt;
UsersController is a RESTful API controller with standard CRUD behavior plus three custom collection routes.&lt;br /&gt;
&lt;br /&gt;
=== Standard REST Endpoints ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Action !! Route !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| index || GET /users || List all users&lt;br /&gt;
|-&lt;br /&gt;
| show || GET /users/:id || Show one user&lt;br /&gt;
|-&lt;br /&gt;
| create || POST /users || Create a user (defaults password when omitted)&lt;br /&gt;
|-&lt;br /&gt;
| update || PATCH/PUT /users/:id || Update user attributes&lt;br /&gt;
|-&lt;br /&gt;
| destroy || DELETE /users/:id || Delete a user&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Custom Collection Endpoints ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Action !! Route !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| institution_users || GET /users/institution/:id || List users for one institution&lt;br /&gt;
|-&lt;br /&gt;
| managed_users || GET /users/:id/managed || List users managed by a given user&lt;br /&gt;
|-&lt;br /&gt;
| role_users || GET /users/role/:name || List users by role name&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Error Handling Behavior to Validate ===&lt;br /&gt;
* Not found handling for missing records.&lt;br /&gt;
* Parameter missing handling for invalid payloads.&lt;br /&gt;
* Correct HTTP codes for success and failure scenarios.&lt;br /&gt;
* Stable response payload structure for client compatibility.&lt;br /&gt;
&lt;br /&gt;
== Testing Scope (Planned) ==&lt;br /&gt;
&lt;br /&gt;
== 1) Model Tests ==&lt;br /&gt;
File to create: `spec/models/user_spec.rb`&lt;br /&gt;
&lt;br /&gt;
Planned coverage:&lt;br /&gt;
&lt;br /&gt;
* validations (presence, format, length)&lt;br /&gt;
* associations (role, institution, parent/children, related has_many links)&lt;br /&gt;
* scopes by role&lt;br /&gt;
* delegated role predicate methods&lt;br /&gt;
* default attribute initialization&lt;br /&gt;
* class methods (`instantiate`, `login_user`, `from_params`)&lt;br /&gt;
* instance methods (`instructor_id`, `generate_jwt`, `as_json`, impersonation logic)&lt;br /&gt;
* edge cases and regression guards for fragile logic&lt;br /&gt;
&lt;br /&gt;
== 2) Request Tests ==&lt;br /&gt;
File to create: `spec/requests/api/v1/users_controller_spec.rb`&lt;br /&gt;
&lt;br /&gt;
Planned coverage:&lt;br /&gt;
&lt;br /&gt;
* full CRUD endpoint behavior&lt;br /&gt;
* institution_users, managed_users, role_users&lt;br /&gt;
* response status codes and body shape&lt;br /&gt;
* invalid input and missing record behavior&lt;br /&gt;
* authentication-header-driven request setup using project conventions&lt;br /&gt;
&lt;br /&gt;
== Test Conventions We Will Follow ==&lt;br /&gt;
&lt;br /&gt;
* `require 'swagger_helper'`&lt;br /&gt;
* `require 'json_web_token'`&lt;br /&gt;
* `create_roles_hierarchy` from `spec/support/roles_helper.rb`&lt;br /&gt;
* FactoryBot for data setup&lt;br /&gt;
* Existing cleaning strategy from `spec/rails_helper.rb`&lt;br /&gt;
&lt;br /&gt;
== Deliverables ==&lt;br /&gt;
&lt;br /&gt;
* `spec/models/user_spec.rb` with comprehensive model coverage.&lt;br /&gt;
* `spec/requests/api/v1/users_controller_spec.rb` with comprehensive endpoint coverage.&lt;br /&gt;
* Updates to `spec/factories.rb` only if needed for clean setup.&lt;br /&gt;
* Bug fixes only when tests reveal real defects in User/UsersController behavior.&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
&lt;br /&gt;
At the end of the project, we expect:&lt;br /&gt;
&lt;br /&gt;
* reliable automated coverage for User model and UsersController&lt;br /&gt;
* clear pass/fail confidence for core user workflows&lt;br /&gt;
* reduced regression risk in authentication, role handling, and account management APIs&lt;br /&gt;
* successful execution of:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167806</id>
		<title>CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2615._Testing_User_model_and_Users_controller&amp;diff=167806"/>
		<updated>2026-04-11T00:29:46Z</updated>

		<summary type="html">&lt;p&gt;Awainga: Created page with &amp;quot;== About This Project ==  {| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:390px;&amp;quot; |- ! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info |- | '''Course''' || CSC/ECE 517 Spring 2026 |- | '''Project''' || E2615 — Testing User Model and UsersController |- | '''Instructor''' || Ed Gehringer |- | '''Mentor''' || Aanand Sreekumaran Nair Jayakumari |- | '''Platform''' || Expertiza (Ruby on Rails) |- | '''Framework''' || RSpec |- | '''Project Status''' |...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:390px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2615 — Testing User Model and UsersController&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Project Status''' || Planning phase (implementation not started)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
This page is a '''design document''' for E2615.  &lt;br /&gt;
No implementation work has started yet. This document defines what we will test, why it matters, and what outcomes we expect when the project is complete.&lt;br /&gt;
&lt;br /&gt;
== Why This Project Matters ==&lt;br /&gt;
&lt;br /&gt;
The User entity is the backbone of Expertiza. It controls identity, authentication, role-based behavior, and account management relationships.  &lt;br /&gt;
The User model and UsersController currently have no meaningful tests in scope (legacy placeholders only), so regressions in critical user workflows can slip through unnoticed.&lt;br /&gt;
&lt;br /&gt;
== Project Objectives ==&lt;br /&gt;
&lt;br /&gt;
* Build complete RSpec coverage for the User model.&lt;br /&gt;
* Build complete request-level RSpec coverage for UsersController.&lt;br /&gt;
* Validate role-aware behavior, authentication flow assumptions, and API error handling.&lt;br /&gt;
* Follow existing repository test patterns for setup and data creation.&lt;br /&gt;
* Deliver stable, passing tests that can serve as a baseline for future user-related changes.&lt;br /&gt;
&lt;br /&gt;
== Background: User Class ==&lt;br /&gt;
&lt;br /&gt;
=== What User Represents ===&lt;br /&gt;
The `User` model stores core account information such as:&lt;br /&gt;
&lt;br /&gt;
* username (`name`)&lt;br /&gt;
* `full_name`&lt;br /&gt;
* `email`&lt;br /&gt;
* `password_digest` (bcrypt-backed)&lt;br /&gt;
* role association (`role_id`)&lt;br /&gt;
* institution association (`institution_id`)&lt;br /&gt;
* self-referential manager relationship (`parent_id`)&lt;br /&gt;
* notification and preference flags&lt;br /&gt;
&lt;br /&gt;
It also includes business logic for:&lt;br /&gt;
&lt;br /&gt;
* role delegation helpers (student, TA, instructor, admin, super admin checks)&lt;br /&gt;
* role-scoped queries (students, tas, instructors, administrators, superadministrators)&lt;br /&gt;
* identity lookup (`login_user`)&lt;br /&gt;
* JWT creation (`generate_jwt`)&lt;br /&gt;
* impersonation and parent-chain checks&lt;br /&gt;
* JSON serialization contract for API responses&lt;br /&gt;
&lt;br /&gt;
=== User Role Structure (Conceptual) ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                   ┌───────────────────────┐&lt;br /&gt;
                   │         User          │&lt;br /&gt;
                   │   (core account)      │&lt;br /&gt;
                   └───────────┬───────────┘&lt;br /&gt;
                               │ belongs_to&lt;br /&gt;
                               ▼&lt;br /&gt;
                        ┌─────────────┐&lt;br /&gt;
                        │    Role     │&lt;br /&gt;
                        └──────┬──────┘&lt;br /&gt;
                               │ hierarchy&lt;br /&gt;
      ┌────────────────────────┼────────────────────────┐&lt;br /&gt;
      ▼                        ▼                        ▼&lt;br /&gt;
  Student              Teaching Assistant           Instructor&lt;br /&gt;
                                                    │&lt;br /&gt;
                                                    ▼&lt;br /&gt;
                                              Administrator&lt;br /&gt;
                                                    │&lt;br /&gt;
                                                    ▼&lt;br /&gt;
                                           Super Administrator&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Management Relationship (Self-Reference) ===&lt;br /&gt;
Users can manage other users through a parent-child relation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
User (parent manager) 1 ────────* User (managed account)&lt;br /&gt;
              via parent_id on child record&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This relationship is important for endpoints like managed user listing and impersonation checks.&lt;br /&gt;
&lt;br /&gt;
== Background: UsersController ==&lt;br /&gt;
&lt;br /&gt;
UsersController is a RESTful API controller with standard CRUD behavior plus three custom collection routes.&lt;br /&gt;
&lt;br /&gt;
=== Standard REST Endpoints ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Action !! Route !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| index || GET /users || List all users&lt;br /&gt;
|-&lt;br /&gt;
| show || GET /users/:id || Show one user&lt;br /&gt;
|-&lt;br /&gt;
| create || POST /users || Create a user (defaults password when omitted)&lt;br /&gt;
|-&lt;br /&gt;
| update || PATCH/PUT /users/:id || Update user attributes&lt;br /&gt;
|-&lt;br /&gt;
| destroy || DELETE /users/:id || Delete a user&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Custom Collection Endpoints ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Action !! Route !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| institution_users || GET /users/institution/:id || List users for one institution&lt;br /&gt;
|-&lt;br /&gt;
| managed_users || GET /users/:id/managed || List users managed by a given user&lt;br /&gt;
|-&lt;br /&gt;
| role_users || GET /users/role/:name || List users by role name&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Error Handling Behavior to Validate ===&lt;br /&gt;
* Not found handling for missing records.&lt;br /&gt;
* Parameter missing handling for invalid payloads.&lt;br /&gt;
* Correct HTTP codes for success and failure scenarios.&lt;br /&gt;
* Stable response payload structure for client compatibility.&lt;br /&gt;
&lt;br /&gt;
== Testing Scope (Planned) ==&lt;br /&gt;
&lt;br /&gt;
== 1) Model Tests ==&lt;br /&gt;
File to create: `spec/models/user_spec.rb`&lt;br /&gt;
&lt;br /&gt;
Planned coverage:&lt;br /&gt;
&lt;br /&gt;
* validations (presence, format, length)&lt;br /&gt;
* associations (role, institution, parent/children, related has_many links)&lt;br /&gt;
* scopes by role&lt;br /&gt;
* delegated role predicate methods&lt;br /&gt;
* default attribute initialization&lt;br /&gt;
* class methods (`instantiate`, `login_user`, `from_params`)&lt;br /&gt;
* instance methods (`instructor_id`, `generate_jwt`, `as_json`, impersonation logic)&lt;br /&gt;
* edge cases and regression guards for fragile logic&lt;br /&gt;
&lt;br /&gt;
== 2) Request Tests ==&lt;br /&gt;
File to create: `spec/requests/api/v1/users_controller_spec.rb`&lt;br /&gt;
&lt;br /&gt;
Planned coverage:&lt;br /&gt;
&lt;br /&gt;
* full CRUD endpoint behavior&lt;br /&gt;
* institution_users, managed_users, role_users&lt;br /&gt;
* response status codes and body shape&lt;br /&gt;
* invalid input and missing record behavior&lt;br /&gt;
* authentication-header-driven request setup using project conventions&lt;br /&gt;
&lt;br /&gt;
== Test Conventions We Will Follow ==&lt;br /&gt;
&lt;br /&gt;
* `require 'swagger_helper'`&lt;br /&gt;
* `require 'json_web_token'`&lt;br /&gt;
* `create_roles_hierarchy` from `spec/support/roles_helper.rb`&lt;br /&gt;
* FactoryBot for data setup&lt;br /&gt;
* Existing cleaning strategy from `spec/rails_helper.rb`&lt;br /&gt;
&lt;br /&gt;
== Deliverables ==&lt;br /&gt;
&lt;br /&gt;
* `spec/models/user_spec.rb` with comprehensive model coverage.&lt;br /&gt;
* `spec/requests/api/v1/users_controller_spec.rb` with comprehensive endpoint coverage.&lt;br /&gt;
* Updates to `spec/factories.rb` only if needed for clean setup.&lt;br /&gt;
* Bug fixes only when tests reveal real defects in User/UsersController behavior.&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
&lt;br /&gt;
At the end of the project, we expect:&lt;br /&gt;
&lt;br /&gt;
* reliable automated coverage for User model and UsersController&lt;br /&gt;
* clear pass/fail confidence for core user workflows&lt;br /&gt;
* reduced regression risk in authentication, role handling, and account management APIs&lt;br /&gt;
* successful execution of:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/user_spec.rb spec/requests/api/v1/users_controller_spec.rb&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you want, I can also provide a second version with a smaller “proposal-style” tone for your wiki main page and keep this detailed one as a subpage (for example, “E2615/Design”).&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026&amp;diff=167805</id>
		<title>CSC/ECE 517 Spring 2026</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026&amp;diff=167805"/>
		<updated>2026-04-11T00:29:22Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* [[CSC/ECE 517 Spring 2026 - E2600. Reimplement review_mapping_controller]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2601. Reimplement student quizzes]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2603. Implement ViewSubmissions frontend]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2606. Finishing Import and Export helper module]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2607. ResponseController Frontend]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2609. Review calibration]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models]]&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167624</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167624"/>
		<updated>2026-03-31T00:46:33Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
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'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behaviour || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
== Demo Video==&lt;br /&gt;
https://www.youtube.com/watch?v=KZh-Kwhduok&lt;br /&gt;
== Summary ==&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167588</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167588"/>
		<updated>2026-03-30T01:17:25Z</updated>

		<summary type="html">&lt;p&gt;Awainga: /* 2) Capacity enforcement was inconsistent */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
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'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behaviour || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167587</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167587"/>
		<updated>2026-03-30T01:17:17Z</updated>

		<summary type="html">&lt;p&gt;Awainga: /* 2) Capacity enforcement was inconsistent */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
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'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behaviour || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167586</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167586"/>
		<updated>2026-03-30T01:16:04Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
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'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behaviour || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167585</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167585"/>
		<updated>2026-03-30T00:50:28Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167584</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167584"/>
		<updated>2026-03-30T00:49:39Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167583</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167583"/>
		<updated>2026-03-30T00:49:17Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:left; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167582</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167582"/>
		<updated>2026-03-30T00:23:14Z</updated>

		<summary type="html">&lt;p&gt;Awainga: /* Notes / Known Gaps */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167581</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167581"/>
		<updated>2026-03-30T00:22:30Z</updated>

		<summary type="html">&lt;p&gt;Awainga: /* MentoredTeam (Duty-Based Mentor) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167580</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167580"/>
		<updated>2026-03-30T00:21:33Z</updated>

		<summary type="html">&lt;p&gt;Awainga: /* Duty vs. Role Distinction (Key Fix) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167579</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167579"/>
		<updated>2026-03-30T00:19:55Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction (Key Fix) ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167578</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167578"/>
		<updated>2026-03-30T00:18:57Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, &lt;br /&gt;
Krisha Darji,&lt;br /&gt;
Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction (Key Fix) ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167577</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167577"/>
		<updated>2026-03-30T00:16:04Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || &amp;lt;code&amp;gt;Atharva, Krisha, Saladin&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction (Key Fix) ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167576</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167576"/>
		<updated>2026-03-30T00:15:29Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Collaborators''' || &amp;lt;code&amp;gt;Atharva, Krisha, Saladin&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction (Key Fix) ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167575</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167575"/>
		<updated>2026-03-30T00:13:33Z</updated>

		<summary type="html">&lt;p&gt;Awainga: /* References */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction (Key Fix) ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167574</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167574"/>
		<updated>2026-03-30T00:12:44Z</updated>

		<summary type="html">&lt;p&gt;Awainga: /* References */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction (Key Fix) ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing &lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;br /&gt;
&lt;br /&gt;
[[Category:Spring 2026]]&lt;br /&gt;
[[Category:CSC517]]&lt;br /&gt;
[[Category:ECE517]]&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167571</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167571"/>
		<updated>2026-03-30T00:10:37Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;float:right; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy consists of exactly four classes: &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (superclass), &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. Teams are stored using Rails STI (single table inheritance) via the &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; column.&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of teams exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) — persist for the duration of an entire course and can be reused across multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;, a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || STI superclass, shared associations/logic || Holds membership helpers and core add/remove methods&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Members must be course participants || Supports conversion to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Members must be assignment participants || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction (Key Fix) ==&lt;br /&gt;
A critical design point is the separation between a user's '''role''' and their '''duty''' within a team.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level for a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || Team-level function of a participant on a specific team || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose '''participant record''' has been assigned the '''Mentor duty''' for a particular team. This project enforces and tests mentor identification by duty (not user role).&lt;br /&gt;
&lt;br /&gt;
== Project Goals Implemented ==&lt;br /&gt;
&lt;br /&gt;
=== Membership grounded in valid participation ===&lt;br /&gt;
Membership is mediated through &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; records:&lt;br /&gt;
* CourseTeam: must be a &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; for the same course (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
* AssignmentTeam / MentoredTeam: must be an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; for the same assignment (&amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
This is enforced in &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; by resolving the correct participant type based on the team’s subclass and the team’s &amp;lt;code&amp;gt;parent_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def add_member(participant_or_user)&lt;br /&gt;
  participant =&lt;br /&gt;
    if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)&lt;br /&gt;
      participant_or_user&lt;br /&gt;
    elsif participant_or_user.is_a?(User)&lt;br /&gt;
      participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
      participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
    else&lt;br /&gt;
      nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
  return { success: false, error: &amp;quot;#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}&amp;quot; } if participant.nil?&lt;br /&gt;
  return { success: false, error: &amp;quot;Participant already on the team&amp;quot; } if participants.exists?(id: participant.id)&lt;br /&gt;
  return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&lt;br /&gt;
  team_participant = TeamsParticipant.create(&lt;br /&gt;
    participant_id: participant.id,&lt;br /&gt;
    team_id: id,&lt;br /&gt;
    user_id: participant.user_id&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== No user should appear on multiple teams in the same scope ===&lt;br /&gt;
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A corresponding database migration adds a unique index on &amp;lt;code&amp;gt;participant_id&amp;lt;/code&amp;gt; to make this constraint durable at the DB level.&lt;br /&gt;
&lt;br /&gt;
=== Team size limits respected (capacity enforcement) ===&lt;br /&gt;
Capacity is enforced in two layers:&lt;br /&gt;
&lt;br /&gt;
1) '''Domain-level check''': &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt; rejects when &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt; is true (returns &amp;lt;code&amp;gt;{success:false, error: ...}&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
2) '''Join-model backstop''': direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; is prevented if the team is already full.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This prevents bypassing capacity by calling &amp;lt;code&amp;gt;TeamsParticipant.create!&amp;lt;/code&amp;gt; directly.&lt;br /&gt;
&lt;br /&gt;
=== Join request acceptance is race-safe ===&lt;br /&gt;
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp if team.full?&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    raise ActiveRecord::RecordInvalid, tp unless result[:success]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
  render json: { message: 'Join team request accepted successfully', ... }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== MentoredTeam (Duty-Based Mentor) ==&lt;br /&gt;
MentoredTeam mentor identification and assignment are based on participant duty:&lt;br /&gt;
&lt;br /&gt;
* A mentor duty is represented by a &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; record with name &amp;lt;code&amp;gt;'Mentor'&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The mentor is the participant on the team whose &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt; equals that duty.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb&lt;br /&gt;
def assign_mentor(user)&lt;br /&gt;
  mentor_duty = Duty.find_by(name: 'Mentor')&lt;br /&gt;
  return false unless mentor_duty&lt;br /&gt;
&lt;br /&gt;
  participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)&lt;br /&gt;
  return false unless participant&lt;br /&gt;
&lt;br /&gt;
  participant.update(duty_id: mentor_duty.id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/mentored_team.rb (mentor lookup)&lt;br /&gt;
AssignmentParticipant&lt;br /&gt;
  .joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')&lt;br /&gt;
  .where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)&lt;br /&gt;
  .first&amp;amp;.user&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage (Actual Specs in This Project) ==&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team validations + membership rules || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Presence/type validation, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;, eligibility rules&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || Assignment capacity behavior; course teams expected to be uncapped by default&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Team-parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness/presence validations; enrollment rules via &amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs (API/Controllers) ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove behavior (HTTP codes + JSON shape)&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorization rules, creating requests, accepting/declining, “team full” responses&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Update duty authorization, list/add/delete participant endpoints&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Notes / Known Gaps ==&lt;br /&gt;
* CourseTeam capacity is not configured in the current schema (courses do not have &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;), so capacity enforcement is defined for assignment-based teams via &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;.&lt;br /&gt;
* TeamsController role-based restrictions require an explicit policy in &amp;lt;code&amp;gt;TeamsController#action_allowed?&amp;lt;/code&amp;gt; to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; only when controllers define action rules.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;br /&gt;
&lt;br /&gt;
[[Category:Spring 2026]]&lt;br /&gt;
[[Category:CSC517]]&lt;br /&gt;
[[Category:ECE517]]&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167569</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167569"/>
		<updated>2026-03-29T23:58:51Z</updated>

		<summary type="html">&lt;p&gt;Awainga: Blanked the page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167568</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167568"/>
		<updated>2026-03-29T23:58:19Z</updated>

		<summary type="html">&lt;p&gt;Awainga: Created page with &amp;quot;&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt; &amp;lt;head&amp;gt; &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt; &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt; &amp;lt;title&amp;gt;Expertiza Teams Hierarchy – Wikipedia-style Article&amp;lt;/title&amp;gt; &amp;lt;style&amp;gt;   @import url('https://fonts.googleapis.com/css2?family=Linux+Libertine+Display:ital,wght@0,400;0,600;1,400&amp;amp;family=Linux+Biolinum:wght@400;600&amp;amp;display=swap');    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }    :root {     --bg: #f8f9fa...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
&amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Expertiza Teams Hierarchy – Wikipedia-style Article&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;style&amp;gt;&lt;br /&gt;
  @import url('https://fonts.googleapis.com/css2?family=Linux+Libertine+Display:ital,wght@0,400;0,600;1,400&amp;amp;family=Linux+Biolinum:wght@400;600&amp;amp;display=swap');&lt;br /&gt;
&lt;br /&gt;
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }&lt;br /&gt;
&lt;br /&gt;
  :root {&lt;br /&gt;
    --bg: #f8f9fa;&lt;br /&gt;
    --surface: #ffffff;&lt;br /&gt;
    --border: #a2a9b1;&lt;br /&gt;
    --border-light: #eaecf0;&lt;br /&gt;
    --text: #202122;&lt;br /&gt;
    --text-muted: #54595d;&lt;br /&gt;
    --text-caption: #72777d;&lt;br /&gt;
    --link: #3366cc;&lt;br /&gt;
    --link-visited: #6b4ba1;&lt;br /&gt;
    --toc-bg: #f8f9fa;&lt;br /&gt;
    --toc-border: #a2a9b1;&lt;br /&gt;
    --infobox-bg: #f8f9fa;&lt;br /&gt;
    --infobox-header: #cee0f2;&lt;br /&gt;
    --note-bg: #eaf3fb;&lt;br /&gt;
    --note-border: #3683bb;&lt;br /&gt;
    --warn-bg: #fef6e7;&lt;br /&gt;
    --warn-border: #f28500;&lt;br /&gt;
    --code-bg: #f6f6f6;&lt;br /&gt;
    --title-border: #a2a9b1;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  body {&lt;br /&gt;
    font-family: 'Linux Libertine', 'Georgia', serif;&lt;br /&gt;
    font-size: 14.4px;&lt;br /&gt;
    line-height: 1.6;&lt;br /&gt;
    color: var(--text);&lt;br /&gt;
    background: var(--bg);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .page-wrap {&lt;br /&gt;
    max-width: 960px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
    background: var(--surface);&lt;br /&gt;
    padding: 20px 24px 40px;&lt;br /&gt;
    border-left: 1px solid var(--border-light);&lt;br /&gt;
    border-right: 1px solid var(--border-light);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ── Header ── */&lt;br /&gt;
  .page-header {&lt;br /&gt;
    border-bottom: 1px solid var(--title-border);&lt;br /&gt;
    margin-bottom: 16px;&lt;br /&gt;
    padding-bottom: 4px;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  h1.page-title {&lt;br /&gt;
    font-family: 'Linux Libertine Display', 'Georgia', serif;&lt;br /&gt;
    font-size: 2em;&lt;br /&gt;
    font-weight: 400;&lt;br /&gt;
    color: var(--text);&lt;br /&gt;
    letter-spacing: -0.3px;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .hatnote {&lt;br /&gt;
    font-style: italic;&lt;br /&gt;
    color: var(--text-muted);&lt;br /&gt;
    font-size: 0.9em;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
    padding-left: 1.6em;&lt;br /&gt;
    border-left: 3px solid var(--border-light);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ── Sections ── */&lt;br /&gt;
  h2 {&lt;br /&gt;
    font-family: 'Linux Biolinum', 'Arial', sans-serif;&lt;br /&gt;
    font-size: 1.4em;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    border-bottom: 1px solid var(--border-light);&lt;br /&gt;
    margin: 28px 0 10px;&lt;br /&gt;
    padding-bottom: 4px;&lt;br /&gt;
    color: var(--text);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  h3 {&lt;br /&gt;
    font-family: 'Linux Biolinum', 'Arial', sans-serif;&lt;br /&gt;
    font-size: 1.1em;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin: 18px 0 8px;&lt;br /&gt;
    color: var(--text);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  p { margin-bottom: 10px; }&lt;br /&gt;
&lt;br /&gt;
  a { color: var(--link); text-decoration: none; }&lt;br /&gt;
  a:hover { text-decoration: underline; }&lt;br /&gt;
  a:visited { color: var(--link-visited); }&lt;br /&gt;
&lt;br /&gt;
  /* ── Table of Contents ── */&lt;br /&gt;
  .toc {&lt;br /&gt;
    float: right;&lt;br /&gt;
    clear: right;&lt;br /&gt;
    margin: 0 0 20px 24px;&lt;br /&gt;
    background: var(--toc-bg);&lt;br /&gt;
    border: 1px solid var(--toc-border);&lt;br /&gt;
    padding: 12px 16px;&lt;br /&gt;
    font-size: 0.88em;&lt;br /&gt;
    min-width: 220px;&lt;br /&gt;
    max-width: 280px;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .toc-title {&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    margin-bottom: 8px;&lt;br /&gt;
    font-size: 1em;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .toc ol {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding-left: 20px;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .toc li { margin: 3px 0; line-height: 1.4; }&lt;br /&gt;
  .toc a { color: var(--text); }&lt;br /&gt;
  .toc a:hover { text-decoration: underline; }&lt;br /&gt;
&lt;br /&gt;
  .toc ol ol { font-size: 0.95em; margin-top: 3px; }&lt;br /&gt;
&lt;br /&gt;
  /* ── Infobox ── */&lt;br /&gt;
  .infobox {&lt;br /&gt;
    float: right;&lt;br /&gt;
    clear: right;&lt;br /&gt;
    margin: 0 0 20px 24px;&lt;br /&gt;
    border: 1px solid var(--border);&lt;br /&gt;
    background: var(--infobox-bg);&lt;br /&gt;
    font-size: 0.88em;&lt;br /&gt;
    min-width: 260px;&lt;br /&gt;
    max-width: 300px;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .infobox-title {&lt;br /&gt;
    background: var(--infobox-header);&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 8px 10px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    font-family: 'Linux Biolinum', sans-serif;&lt;br /&gt;
    font-size: 1.05em;&lt;br /&gt;
    border-bottom: 1px solid var(--border);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .infobox table {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    border-collapse: collapse;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .infobox td {&lt;br /&gt;
    padding: 4px 8px;&lt;br /&gt;
    vertical-align: top;&lt;br /&gt;
    border-bottom: 1px solid var(--border-light);&lt;br /&gt;
    line-height: 1.4;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .infobox td:first-child {&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: var(--text-muted);&lt;br /&gt;
    white-space: nowrap;&lt;br /&gt;
    width: 40%;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ── Notes / Warnings ── */&lt;br /&gt;
  .note {&lt;br /&gt;
    background: var(--note-bg);&lt;br /&gt;
    border-left: 4px solid var(--note-border);&lt;br /&gt;
    padding: 10px 14px;&lt;br /&gt;
    margin: 14px 0;&lt;br /&gt;
    font-size: 0.93em;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .warn {&lt;br /&gt;
    background: var(--warn-bg);&lt;br /&gt;
    border-left: 4px solid var(--warn-border);&lt;br /&gt;
    padding: 10px 14px;&lt;br /&gt;
    margin: 14px 0;&lt;br /&gt;
    font-size: 0.93em;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ── Code ── */&lt;br /&gt;
  code {&lt;br /&gt;
    font-family: 'Courier New', monospace;&lt;br /&gt;
    background: var(--code-bg);&lt;br /&gt;
    border: 1px solid var(--border-light);&lt;br /&gt;
    padding: 1px 4px;&lt;br /&gt;
    font-size: 0.9em;&lt;br /&gt;
    border-radius: 2px;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  pre {&lt;br /&gt;
    background: var(--code-bg);&lt;br /&gt;
    border: 1px solid var(--border-light);&lt;br /&gt;
    padding: 12px 14px;&lt;br /&gt;
    overflow-x: auto;&lt;br /&gt;
    font-size: 0.88em;&lt;br /&gt;
    line-height: 1.5;&lt;br /&gt;
    margin: 12px 0;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  pre code { background: none; border: none; padding: 0; font-size: inherit; }&lt;br /&gt;
&lt;br /&gt;
  /* ── Tables ── */&lt;br /&gt;
  .wiki-table {&lt;br /&gt;
    border-collapse: collapse;&lt;br /&gt;
    margin: 16px 0;&lt;br /&gt;
    font-size: 0.9em;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .wiki-table th {&lt;br /&gt;
    background: #eaecf0;&lt;br /&gt;
    border: 1px solid var(--border);&lt;br /&gt;
    padding: 6px 10px;&lt;br /&gt;
    text-align: left;&lt;br /&gt;
    font-family: 'Linux Biolinum', sans-serif;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .wiki-table td {&lt;br /&gt;
    border: 1px solid var(--border);&lt;br /&gt;
    padding: 6px 10px;&lt;br /&gt;
    vertical-align: top;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .wiki-table tr:nth-child(even) td { background: #f8f9fa; }&lt;br /&gt;
&lt;br /&gt;
  /* ── Hierarchy diagram ── */&lt;br /&gt;
  .hierarchy-wrap {&lt;br /&gt;
    margin: 18px 0;&lt;br /&gt;
    overflow-x: auto;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ── Categories ── */&lt;br /&gt;
  .categories {&lt;br /&gt;
    border-top: 1px solid var(--border-light);&lt;br /&gt;
    margin-top: 36px;&lt;br /&gt;
    padding-top: 10px;&lt;br /&gt;
    font-size: 0.88em;&lt;br /&gt;
    color: var(--text-muted);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .categories strong { color: var(--text); }&lt;br /&gt;
&lt;br /&gt;
  /* ── References ── */&lt;br /&gt;
  .references {&lt;br /&gt;
    font-size: 0.88em;&lt;br /&gt;
    color: var(--text-muted);&lt;br /&gt;
    columns: 2;&lt;br /&gt;
    column-gap: 24px;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .references ol { padding-left: 18px; }&lt;br /&gt;
  .references li { margin-bottom: 4px; line-height: 1.4; }&lt;br /&gt;
&lt;br /&gt;
  .ref-sup {&lt;br /&gt;
    font-size: 0.75em;&lt;br /&gt;
    vertical-align: super;&lt;br /&gt;
    color: var(--link);&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ── Clearfix ── */&lt;br /&gt;
  .clearfix::after { content: ''; display: table; clear: both; }&lt;br /&gt;
&lt;br /&gt;
  /* ── Responsive ── */&lt;br /&gt;
  @media (max-width: 640px) {&lt;br /&gt;
    .infobox, .toc { float: none; max-width: 100%; margin: 0 0 16px; }&lt;br /&gt;
    .references { columns: 1; }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ── Duty vs Role comparison box ── */&lt;br /&gt;
  .compare-box {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: 1fr 1fr;&lt;br /&gt;
    gap: 0;&lt;br /&gt;
    border: 1px solid var(--border);&lt;br /&gt;
    margin: 16px 0;&lt;br /&gt;
    font-size: 0.9em;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .compare-box .col-head {&lt;br /&gt;
    background: #eaecf0;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    padding: 7px 12px;&lt;br /&gt;
    border-bottom: 1px solid var(--border);&lt;br /&gt;
    font-family: 'Linux Biolinum', sans-serif;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .compare-box .col-head:first-child { border-right: 1px solid var(--border); }&lt;br /&gt;
&lt;br /&gt;
  .compare-box .col-body {&lt;br /&gt;
    padding: 8px 12px;&lt;br /&gt;
    line-height: 1.5;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  .compare-box .col-body:first-child { border-right: 1px solid var(--border); }&lt;br /&gt;
&lt;br /&gt;
  ul, ol { padding-left: 22px; margin-bottom: 10px; }&lt;br /&gt;
  li { margin-bottom: 3px; }&lt;br /&gt;
&amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;page-wrap&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- Header --&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;page-header&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;h1 class=&amp;quot;page-title&amp;quot;&amp;gt;Expertiza Teams Hierarchy&amp;lt;/h1&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;p class=&amp;quot;hatnote&amp;quot;&amp;gt;This article covers the software design and implementation of team management in the Expertiza platform. For the broader Expertiza system, see &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;Expertiza (software)&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div class=&amp;quot;clearfix&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Infobox --&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;infobox&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;infobox-title&amp;quot;&amp;gt;Expertiza Teams Hierarchy&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;table&amp;gt;&lt;br /&gt;
        &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Platform&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Expertiza (Ruby on Rails)&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;
        &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Root class&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (abstract)&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;
        &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subclasses&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;
        &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Mentor&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Vihar Manojkumar Shah&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;
        &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Test framework&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;RSpec&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;
        &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Membership model&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Enrollment-based&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;
        &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Hierarchy depth&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;3 levels (Team → AssignmentTeam → MentoredTeam)&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;/table&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- TOC --&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;toc&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;toc-title&amp;quot;&amp;gt;Contents&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;ol&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#overview&amp;quot;&amp;gt;Overview&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#hierarchy&amp;quot;&amp;gt;Class hierarchy&amp;lt;/a&amp;gt;&lt;br /&gt;
          &amp;lt;ol&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#team&amp;quot;&amp;gt;Team (abstract root)&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#courseteam&amp;quot;&amp;gt;CourseTeam&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#assignmentteam&amp;quot;&amp;gt;AssignmentTeam&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#mentoredteam&amp;quot;&amp;gt;MentoredTeam&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
          &amp;lt;/ol&amp;gt;&lt;br /&gt;
        &amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#duty-vs-role&amp;quot;&amp;gt;Duty vs. role distinction&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#membership&amp;quot;&amp;gt;Membership rules&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#mentored-impl&amp;quot;&amp;gt;MentoredTeam implementation&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#testing&amp;quot;&amp;gt;Test coverage (RSpec)&amp;lt;/a&amp;gt;&lt;br /&gt;
          &amp;lt;ol&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#test-model&amp;quot;&amp;gt;Model validations&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#test-assoc&amp;quot;&amp;gt;Association integrity&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#test-subclass&amp;quot;&amp;gt;Subclass-specific behaviour&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#test-capacity&amp;quot;&amp;gt;Capacity and size limits&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#test-edge&amp;quot;&amp;gt;Edge cases&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#test-auth&amp;quot;&amp;gt;Controller authorization&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#test-http&amp;quot;&amp;gt;HTTP response codes&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
          &amp;lt;/ol&amp;gt;&lt;br /&gt;
        &amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;#references&amp;quot;&amp;gt;References&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
      &amp;lt;/ol&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Body text --&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;&lt;br /&gt;
      The &amp;lt;b&amp;gt;Expertiza teams hierarchy&amp;lt;/b&amp;gt; is a structured set of Ruby on Rails model classes that governs how student groups are formed, managed, and constrained within the &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt; peer-review platform. The hierarchy consists of exactly four classes — &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — and underpins every form of collaborative participation in the system, from reserving discussion topics to submitting completed assignments.&lt;br /&gt;
    &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;p&amp;gt;&lt;br /&gt;
      The design follows a classical object-oriented inheritance pattern: an intentionally abstract superclass provides the shared data model and common behaviours, while concrete subclasses carry distinct responsibilities, lifecycle rules, and membership constraints. The hierarchy also plays a central role in the platform's role and duty model, most visibly in the &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; class, which designates a team participant as a mentor through a &amp;lt;em&amp;gt;duty&amp;lt;/em&amp;gt; attribute rather than a system-level &amp;lt;em&amp;gt;role&amp;lt;/em&amp;gt;.&lt;br /&gt;
    &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;/div&amp;gt;&amp;lt;!-- end .clearfix --&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── Section 1 ── --&amp;gt;&lt;br /&gt;
  &amp;lt;h2 id=&amp;quot;overview&amp;quot;&amp;gt;Overview&amp;lt;/h2&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Expertiza organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams serve as the fundamental unit of academic participation: they allow groups of students to collectively reserve discussion topics, collaborate on coursework, and submit completed assignments for evaluation.&amp;lt;span class=&amp;quot;ref-sup&amp;quot;&amp;gt;[1]&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Two broad kinds of team exist in the system. &amp;lt;em&amp;gt;Course teams&amp;lt;/em&amp;gt; persist for the duration of an entire course and may be reused across multiple assignments, particularly in Team-Based Learning (TBL) environments. &amp;lt;em&amp;gt;Assignment teams&amp;lt;/em&amp;gt; are formed specifically for a single assignment and dissolve once that assignment closes. When an assignment is configured to automatically assign mentors, the team is instantiated as a &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — a specialised subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; that records a designated mentor guiding the group.&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── Section 2 ── --&amp;gt;&lt;br /&gt;
  &amp;lt;h2 id=&amp;quot;hierarchy&amp;quot;&amp;gt;Class hierarchy&amp;lt;/h2&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;The four classes form a three-level inheritance tree:&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div class=&amp;quot;hierarchy-wrap&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;svg width=&amp;quot;100%&amp;quot; viewBox=&amp;quot;0 0 680 260&amp;quot; xmlns=&amp;quot;http://www.w3.org/2000/svg&amp;quot; style=&amp;quot;max-width:600px;display:block;margin:0 auto&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;defs&amp;gt;&lt;br /&gt;
        &amp;lt;marker id=&amp;quot;arrow&amp;quot; viewBox=&amp;quot;0 0 10 10&amp;quot; refX=&amp;quot;8&amp;quot; refY=&amp;quot;5&amp;quot; markerWidth=&amp;quot;6&amp;quot; markerHeight=&amp;quot;6&amp;quot; orient=&amp;quot;auto-start-reverse&amp;quot;&amp;gt;&lt;br /&gt;
          &amp;lt;path d=&amp;quot;M2 1L8 5L2 9&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;#54595d&amp;quot; stroke-width=&amp;quot;1.5&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot;/&amp;gt;&lt;br /&gt;
        &amp;lt;/marker&amp;gt;&lt;br /&gt;
      &amp;lt;/defs&amp;gt;&lt;br /&gt;
      &amp;lt;!-- Team (root) --&amp;gt;&lt;br /&gt;
      &amp;lt;rect x=&amp;quot;230&amp;quot; y=&amp;quot;18&amp;quot; width=&amp;quot;220&amp;quot; height=&amp;quot;52&amp;quot; rx=&amp;quot;6&amp;quot; fill=&amp;quot;#cee0f2&amp;quot; stroke=&amp;quot;#a2a9b1&amp;quot; stroke-width=&amp;quot;1&amp;quot;/&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;340&amp;quot; y=&amp;quot;40&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;'Courier New',monospace&amp;quot; font-size=&amp;quot;14&amp;quot; font-weight=&amp;quot;600&amp;quot; fill=&amp;quot;#202122&amp;quot;&amp;gt;Team&amp;lt;/text&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;340&amp;quot; y=&amp;quot;58&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;Arial,sans-serif&amp;quot; font-size=&amp;quot;11&amp;quot; fill=&amp;quot;#54595d&amp;quot;&amp;gt;(abstract superclass)&amp;lt;/text&amp;gt;&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;!-- Arrow root → CourseTeam --&amp;gt;&lt;br /&gt;
      &amp;lt;line x1=&amp;quot;280&amp;quot; y1=&amp;quot;70&amp;quot; x2=&amp;quot;140&amp;quot; y2=&amp;quot;130&amp;quot; stroke=&amp;quot;#54595d&amp;quot; stroke-width=&amp;quot;1&amp;quot; marker-end=&amp;quot;url(#arrow)&amp;quot;/&amp;gt;&lt;br /&gt;
      &amp;lt;!-- Arrow root → AssignmentTeam --&amp;gt;&lt;br /&gt;
      &amp;lt;line x1=&amp;quot;400&amp;quot; y1=&amp;quot;70&amp;quot; x2=&amp;quot;500&amp;quot; y2=&amp;quot;130&amp;quot; stroke=&amp;quot;#54595d&amp;quot; stroke-width=&amp;quot;1&amp;quot; marker-end=&amp;quot;url(#arrow)&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;!-- CourseTeam --&amp;gt;&lt;br /&gt;
      &amp;lt;rect x=&amp;quot;40&amp;quot; y=&amp;quot;130&amp;quot; width=&amp;quot;200&amp;quot; height=&amp;quot;52&amp;quot; rx=&amp;quot;6&amp;quot; fill=&amp;quot;#eaf3fb&amp;quot; stroke=&amp;quot;#a2a9b1&amp;quot; stroke-width=&amp;quot;1&amp;quot;/&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;140&amp;quot; y=&amp;quot;152&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;'Courier New',monospace&amp;quot; font-size=&amp;quot;13&amp;quot; font-weight=&amp;quot;600&amp;quot; fill=&amp;quot;#202122&amp;quot;&amp;gt;CourseTeam&amp;lt;/text&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;140&amp;quot; y=&amp;quot;170&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;Arial,sans-serif&amp;quot; font-size=&amp;quot;11&amp;quot; fill=&amp;quot;#54595d&amp;quot;&amp;gt;Course-scoped teams&amp;lt;/text&amp;gt;&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;!-- AssignmentTeam --&amp;gt;&lt;br /&gt;
      &amp;lt;rect x=&amp;quot;390&amp;quot; y=&amp;quot;130&amp;quot; width=&amp;quot;220&amp;quot; height=&amp;quot;52&amp;quot; rx=&amp;quot;6&amp;quot; fill=&amp;quot;#eaf3fb&amp;quot; stroke=&amp;quot;#a2a9b1&amp;quot; stroke-width=&amp;quot;1&amp;quot;/&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;500&amp;quot; y=&amp;quot;152&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;'Courier New',monospace&amp;quot; font-size=&amp;quot;13&amp;quot; font-weight=&amp;quot;600&amp;quot; fill=&amp;quot;#202122&amp;quot;&amp;gt;AssignmentTeam&amp;lt;/text&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;500&amp;quot; y=&amp;quot;170&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;Arial,sans-serif&amp;quot; font-size=&amp;quot;11&amp;quot; fill=&amp;quot;#54595d&amp;quot;&amp;gt;Assignment-scoped teams&amp;lt;/text&amp;gt;&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;!-- Arrow AssignmentTeam → MentoredTeam --&amp;gt;&lt;br /&gt;
      &amp;lt;line x1=&amp;quot;500&amp;quot; y1=&amp;quot;182&amp;quot; x2=&amp;quot;500&amp;quot; y2=&amp;quot;208&amp;quot; stroke=&amp;quot;#54595d&amp;quot; stroke-width=&amp;quot;1&amp;quot; marker-end=&amp;quot;url(#arrow)&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;!-- MentoredTeam --&amp;gt;&lt;br /&gt;
      &amp;lt;rect x=&amp;quot;380&amp;quot; y=&amp;quot;208&amp;quot; width=&amp;quot;240&amp;quot; height=&amp;quot;42&amp;quot; rx=&amp;quot;6&amp;quot; fill=&amp;quot;#fef6e7&amp;quot; stroke=&amp;quot;#f28500&amp;quot; stroke-width=&amp;quot;1&amp;quot;/&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;500&amp;quot; y=&amp;quot;226&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;'Courier New',monospace&amp;quot; font-size=&amp;quot;13&amp;quot; font-weight=&amp;quot;600&amp;quot; fill=&amp;quot;#202122&amp;quot;&amp;gt;MentoredTeam&amp;lt;/text&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;500&amp;quot; y=&amp;quot;242&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;Arial,sans-serif&amp;quot; font-size=&amp;quot;11&amp;quot; fill=&amp;quot;#54595d&amp;quot;&amp;gt;Includes a duty=mentor participant&amp;lt;/text&amp;gt;&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;!-- Conversion arrow (dashed) --&amp;gt;&lt;br /&gt;
      &amp;lt;path d=&amp;quot;M240 156 Q310 195 390 156&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;#3366cc&amp;quot; stroke-width=&amp;quot;1&amp;quot; stroke-dasharray=&amp;quot;5,4&amp;quot;/&amp;gt;&lt;br /&gt;
      &amp;lt;text x=&amp;quot;315&amp;quot; y=&amp;quot;205&amp;quot; text-anchor=&amp;quot;middle&amp;quot; font-family=&amp;quot;Arial,sans-serif&amp;quot; font-size=&amp;quot;10&amp;quot; fill=&amp;quot;#3366cc&amp;quot;&amp;gt;convertible&amp;lt;/text&amp;gt;&lt;br /&gt;
    &amp;lt;/svg&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;team&amp;quot;&amp;gt;Team (abstract root)&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    The &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; class sits at the root of the hierarchy and provides the shared data model, associations, and common behaviours that all team types inherit. &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; is intentionally abstract — it is never meant to be instantiated directly in production or test code. All functional teams that appear in the application must be concrete instances of one of the three subclasses. This design enforces a clean separation between shared infrastructure and specific business logic, and it means that any attempt to create a raw &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; object should be treated as a programming error.&amp;lt;span class=&amp;quot;ref-sup&amp;quot;&amp;gt;[1]&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div class=&amp;quot;warn&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Constraint:&amp;lt;/strong&amp;gt; Instantiating &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; directly is a programming error. The test suite must explicitly verify that this constraint is honoured.&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;courseteam&amp;quot;&amp;gt;CourseTeam&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; instances remain intact throughout a course, collaborating on all assignments within it. While some instructors opt not to use them, others find &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;s beneficial, especially in Team-Based Learning environments. These teams can be converted into &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;s, and vice versa, as instructional needs change.&amp;lt;span class=&amp;quot;ref-sup&amp;quot;&amp;gt;[1]&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    A student may only be added to a &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; if they are a participant in the associated course. No user should appear on more than one course team within the same course.&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;assignmentteam&amp;quot;&amp;gt;AssignmentTeam&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; instances are formed specifically for individual assignments. Membership is restricted to users who are already participants in the associated assignment. The class is the direct superclass of &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; and participates in the bidirectional conversion relationship with &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;mentoredteam&amp;quot;&amp;gt;MentoredTeam&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    When an assignment is configured to automatically assign mentors, teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — a subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor guiding the group. The mentor is identified not by a system role but by a &amp;lt;em&amp;gt;duty&amp;lt;/em&amp;gt; attribute on the team membership record (see &amp;lt;a href=&amp;quot;#duty-vs-role&amp;quot;&amp;gt;§ Duty vs. role distinction&amp;lt;/a&amp;gt;).&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── Section 3 ── --&amp;gt;&lt;br /&gt;
  &amp;lt;h2 id=&amp;quot;duty-vs-role&amp;quot;&amp;gt;Duty vs. role distinction&amp;lt;/h2&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    A critical design point of the Expertiza platform is the separation between a user's &amp;lt;em&amp;gt;role&amp;lt;/em&amp;gt; and their &amp;lt;em&amp;gt;duty&amp;lt;/em&amp;gt; within a team. The current system's incorrect implementation of &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; conflates these two concepts; the corrected implementation must use &amp;lt;em&amp;gt;duty&amp;lt;/em&amp;gt; exclusively when identifying the mentor participant.&amp;lt;span class=&amp;quot;ref-sup&amp;quot;&amp;gt;[1]&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div class=&amp;quot;compare-box&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;col-head&amp;quot;&amp;gt;Role&amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;col-head&amp;quot;&amp;gt;Duty&amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;col-body&amp;quot;&amp;gt;&lt;br /&gt;
      A system-level permission level assigned to a user account.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
      &amp;lt;strong&amp;gt;Examples:&amp;lt;/strong&amp;gt; &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TeachingAssistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;col-body&amp;quot;&amp;gt;&lt;br /&gt;
      A function assigned to a user &amp;lt;em&amp;gt;within a specific team&amp;lt;/em&amp;gt; for a specific assignment.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
      &amp;lt;strong&amp;gt;Examples:&amp;lt;/strong&amp;gt; &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reader&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    The incorrect implementation checked whether a participant held the role of &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt; at the system level. This was incorrect because a mentor is not a privileged system account — they are an ordinary user who has been assigned the duty of &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt; on a particular &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;. The corrected implementation queries the &amp;lt;code&amp;gt;duty&amp;lt;/code&amp;gt; attribute of the team participant record rather than the &amp;lt;code&amp;gt;role&amp;lt;/code&amp;gt; attribute of the user.&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── Section 4 ── --&amp;gt;&lt;br /&gt;
  &amp;lt;h2 id=&amp;quot;membership&amp;quot;&amp;gt;Membership rules&amp;lt;/h2&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;A core correctness requirement of the entire hierarchy is that team membership must always be grounded in valid participation:&amp;lt;span class=&amp;quot;ref-sup&amp;quot;&amp;gt;[1]&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;table class=&amp;quot;wiki-table&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;thead&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;th&amp;gt;Rule&amp;lt;/th&amp;gt;&lt;br /&gt;
        &amp;lt;th&amp;gt;Applies to&amp;lt;/th&amp;gt;&lt;br /&gt;
        &amp;lt;th&amp;gt;Enforcement&amp;lt;/th&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
    &amp;lt;/thead&amp;gt;&lt;br /&gt;
    &amp;lt;tbody&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;A student may only be added if they are a participant in the associated course.&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Model validation; raises error on violation&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;A student may only be added if they are a participant in the associated assignment.&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Model validation; raises error on violation&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;No user may appear on multiple course teams within the same course.&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Uniqueness constraint at model and DB level&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;No user may appear on multiple assignment teams within the same assignment.&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Uniqueness constraint at model and DB level&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Team size must not exceed the configured maximum capacity.&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;All subclasses&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Rejected with error before save&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
    &amp;lt;/tbody&amp;gt;&lt;br /&gt;
  &amp;lt;/table&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── Section 5 ── --&amp;gt;&lt;br /&gt;
  &amp;lt;h2 id=&amp;quot;mentored-impl&amp;quot;&amp;gt;MentoredTeam implementation&amp;lt;/h2&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    The corrected &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; implementation locates the mentor participant by querying the &amp;lt;code&amp;gt;duty&amp;lt;/code&amp;gt; attribute of team member records, not the system-level &amp;lt;code&amp;gt;role&amp;lt;/code&amp;gt; of the associated user. A representative model method looks as follows:&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;# app/models/mentored_team.rb&lt;br /&gt;
class MentoredTeam &amp;amp;lt; AssignmentTeam&lt;br /&gt;
  # Returns the TeamMember record whose duty is 'Mentor'.&lt;br /&gt;
  def mentor&lt;br /&gt;
    team_members.find_by(duty: 'Mentor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Assigns a participant as the team mentor by setting their duty.&lt;br /&gt;
  def assign_mentor!(user)&lt;br /&gt;
    member = team_members.find_by(user: user)&lt;br /&gt;
    raise ArgumentError, 'User is not a member of this team' unless member&lt;br /&gt;
    member.update!(duty: 'Mentor')&lt;br /&gt;
  end&lt;br /&gt;
end&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div class=&amp;quot;note&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Note:&amp;lt;/strong&amp;gt; Earlier versions of this class called &amp;lt;code&amp;gt;user.role == 'Mentor'&amp;lt;/code&amp;gt;, which tested a system-level permission and incorrectly excluded ordinary students serving as mentors. The corrected query is &amp;lt;code&amp;gt;team_members.find_by(duty: 'Mentor')&amp;lt;/code&amp;gt;.&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── Section 6 ── --&amp;gt;&lt;br /&gt;
  &amp;lt;h2 id=&amp;quot;testing&amp;quot;&amp;gt;Test coverage (RSpec)&amp;lt;/h2&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Comprehensive RSpec tests are required to validate the hierarchy. The test suite is organised into seven areas of concern.&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;test-model&amp;quot;&amp;gt;Model validations&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Tests confirm that required fields and uniqueness constraints are enforced. This includes verifying that a &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; cannot be instantiated directly, that each subclass requires its parent association (course or assignment), and that enrollment-based membership rules reject non-participants.&amp;lt;span class=&amp;quot;ref-sup&amp;quot;&amp;gt;[1]&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;RSpec.describe CourseTeam, type: :model do&lt;br /&gt;
  it 'is invalid without an associated course' do&lt;br /&gt;
    team = build(:course_team, course: nil)&lt;br /&gt;
    expect(team).not_to be_valid&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  it 'rejects a member not enrolled in the course' do&lt;br /&gt;
    team    = create(:course_team)&lt;br /&gt;
    student = create(:user)     # no enrollment record&lt;br /&gt;
    expect { team.add_member(student) }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;test-assoc&amp;quot;&amp;gt;Association integrity&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Tests verify that teams are correctly linked to their parent course or assignment and that membership records are properly scoped. For example, querying a team's members must not leak records belonging to teams in other courses or assignments.&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;test-subclass&amp;quot;&amp;gt;Subclass-specific behaviour&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    This section covers two areas unique to the hierarchy design:&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;ul&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Bidirectional conversion:&amp;lt;/strong&amp;gt; A &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; can be converted to an &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and vice versa, with member records preserved across the conversion.&amp;lt;/li&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Mentor assignment in &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;:&amp;lt;/strong&amp;gt; Tests confirm that &amp;lt;code&amp;gt;assign_mentor!&amp;lt;/code&amp;gt; sets the correct &amp;lt;code&amp;gt;duty&amp;lt;/code&amp;gt; value, that &amp;lt;code&amp;gt;mentor&amp;lt;/code&amp;gt; returns the right participant, and that querying &amp;lt;code&amp;gt;duty&amp;lt;/code&amp;gt; (not &amp;lt;code&amp;gt;role&amp;lt;/code&amp;gt;) is the mechanism used.&amp;lt;/li&amp;gt;&lt;br /&gt;
  &amp;lt;/ul&amp;gt;&lt;br /&gt;
  &amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;RSpec.describe MentoredTeam, type: :model do&lt;br /&gt;
  it 'identifies the mentor by duty, not by role' do&lt;br /&gt;
    team    = create(:mentored_team)&lt;br /&gt;
    student = create(:user, role: 'Student')  # system role is Student&lt;br /&gt;
    create(:team_member, team: team, user: student, duty: 'Mentor')&lt;br /&gt;
&lt;br /&gt;
    expect(team.mentor.user).to eq student&lt;br /&gt;
  end&lt;br /&gt;
end&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;test-capacity&amp;quot;&amp;gt;Capacity and size limits&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Tests ensure that teams cannot exceed their configured maximum member count, and that appropriate errors are returned when this limit is breached.&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;it 'rejects a member when the team is at maximum capacity' do&lt;br /&gt;
  team = create(:assignment_team, max_members: 2)&lt;br /&gt;
  create_list(:team_member, 2, team: team)&lt;br /&gt;
  extra = create(:enrolled_user, assignment: team.assignment)&lt;br /&gt;
  expect { team.add_member(extra) }.to raise_error(TeamFullError)&lt;br /&gt;
end&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;test-edge&amp;quot;&amp;gt;Edge cases&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Edge-case tests cover the following scenarios:&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;ul&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;Performing operations on a team with zero members.&amp;lt;/li&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;Attempting to add an already-enrolled member to the same team (duplicate membership).&amp;lt;/li&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;Creating teams for assignments that have no participants yet.&amp;lt;/li&amp;gt;&lt;br /&gt;
  &amp;lt;/ul&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;test-auth&amp;quot;&amp;gt;Controller authorization&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Tests for &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; verify the correct role-based access policy:&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;ul&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Students&amp;lt;/strong&amp;gt; are blocked from viewing other teams.&amp;lt;/li&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Mentors&amp;lt;/strong&amp;gt; are restricted to their own mentored teams.&amp;lt;/li&amp;gt;&lt;br /&gt;
    &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Instructors&amp;lt;/strong&amp;gt; have unrestricted access to all teams.&amp;lt;/li&amp;gt;&lt;br /&gt;
  &amp;lt;/ul&amp;gt;&lt;br /&gt;
  &amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;describe 'GET #index' do&lt;br /&gt;
  context 'when the current user is a student' do&lt;br /&gt;
    before { sign_in create(:student) }&lt;br /&gt;
    it 'is forbidden from viewing other teams' do&lt;br /&gt;
      get :index, params: { assignment_id: assignment.id }&lt;br /&gt;
      expect(response).to have_http_status(:forbidden)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3 id=&amp;quot;test-http&amp;quot;&amp;gt;HTTP response codes&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;&lt;br /&gt;
    Tests confirm that unauthorized access attempts return appropriate HTTP status codes — specifically &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; — rather than silently succeeding or returning a &amp;lt;code&amp;gt;500 Internal Server Error&amp;lt;/code&amp;gt;.&amp;lt;span class=&amp;quot;ref-sup&amp;quot;&amp;gt;[1]&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;table class=&amp;quot;wiki-table&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;thead&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;th&amp;gt;Actor&amp;lt;/th&amp;gt;&lt;br /&gt;
        &amp;lt;th&amp;gt;Action&amp;lt;/th&amp;gt;&lt;br /&gt;
        &amp;lt;th&amp;gt;Expected HTTP status&amp;lt;/th&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
    &amp;lt;/thead&amp;gt;&lt;br /&gt;
    &amp;lt;tbody&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Student (not on team)&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;View another team's details&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Mentor (wrong team)&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Access a MentoredTeam they do not mentor&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Instructor&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Any team action&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;200 OK&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
      &amp;lt;tr&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Unauthenticated user&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;Any team action&amp;lt;/td&amp;gt;&lt;br /&gt;
        &amp;lt;td&amp;gt;&amp;lt;code&amp;gt;401 Unauthorized&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;&lt;br /&gt;
      &amp;lt;/tr&amp;gt;&lt;br /&gt;
    &amp;lt;/tbody&amp;gt;&lt;br /&gt;
  &amp;lt;/table&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── References ── --&amp;gt;&lt;br /&gt;
  &amp;lt;h2 id=&amp;quot;references&amp;quot;&amp;gt;References&amp;lt;/h2&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;references&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ol&amp;gt;&lt;br /&gt;
      &amp;lt;li&amp;gt;Expertiza project documentation — &amp;lt;em&amp;gt;Teams hierarchy testing&amp;lt;/em&amp;gt;. Mentor: Vihar Manojkumar Shah. Internal specification document.&amp;lt;/li&amp;gt;&lt;br /&gt;
      &amp;lt;li&amp;gt;Fowler, M. (2002). &amp;lt;em&amp;gt;Patterns of Enterprise Application Architecture&amp;lt;/em&amp;gt;. Addison-Wesley. (Single Table Inheritance pattern.)&amp;lt;/li&amp;gt;&lt;br /&gt;
      &amp;lt;li&amp;gt;Rails Guides — &amp;lt;em&amp;gt;Active Record Associations&amp;lt;/em&amp;gt;. &amp;lt;a href=&amp;quot;https://guides.rubyonrails.org/association_basics.html&amp;quot;&amp;gt;guides.rubyonrails.org&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
      &amp;lt;li&amp;gt;RSpec Documentation — &amp;lt;em&amp;gt;RSpec Rails&amp;lt;/em&amp;gt;. &amp;lt;a href=&amp;quot;https://rspec.info/documentation/6.0/rspec-rails/&amp;quot;&amp;gt;rspec.info&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
    &amp;lt;/ol&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- ── Categories ── --&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;categories&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Categories:&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;Software design patterns&amp;lt;/a&amp;gt; ·&lt;br /&gt;
    &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;Ruby on Rails&amp;lt;/a&amp;gt; ·&lt;br /&gt;
    &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;Peer review systems&amp;lt;/a&amp;gt; ·&lt;br /&gt;
    &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;Object-oriented programming&amp;lt;/a&amp;gt; ·&lt;br /&gt;
    &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;Team-Based Learning&amp;lt;/a&amp;gt; ·&lt;br /&gt;
    &amp;lt;a href=&amp;quot;#&amp;quot;&amp;gt;RSpec&amp;lt;/a&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&amp;lt;!-- .page-wrap --&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026&amp;diff=167567</id>
		<title>CSC/ECE 517 Spring 2026</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026&amp;diff=167567"/>
		<updated>2026-03-29T23:48:23Z</updated>

		<summary type="html">&lt;p&gt;Awainga: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* [[CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing]]&lt;/div&gt;</summary>
		<author><name>Awainga</name></author>
	</entry>
</feed>