# Test Design - Story 1.1: Database Schema Extension **Document**: Comprehensive Test Design for FloDoc Database Schema **Story**: 1.1 - Database Schema Extension **Date**: 2026-01-15 **Status**: Draft **Test Architect**: QA Agent --- ## ๐Ÿ“Š Test Strategy Overview ### Brownfield Context This is a **brownfield enhancement** to the existing DocuSeal codebase. The primary risk is **regression** - ensuring existing DocuSeal functionality remains intact while adding FloDoc's 3-portal cohort management system. ### Test Pyramid Distribution ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ E2E/System Tests: 10% โ”‚ โ”‚ - Full cohort workflow โ”‚ โ”‚ - Integration with DocuSeal templates/submissions โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ฒ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Integration Tests: 25% โ”‚ โ”‚ - Referential integrity โ”‚ โ”‚ - Cross-table queries โ”‚ โ”‚ - Existing table compatibility โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ฒ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Unit Tests: 65% โ”‚ โ”‚ - Migration tests (table creation, indexes, FKs) โ”‚ โ”‚ - Schema validation โ”‚ โ”‚ - Reversibility โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ### Key Test Principles 1. **Zero Regression**: All existing DocuSeal tests must pass 2. **Atomic Operations**: Each migration step testable independently 3. **Foreign Key Validation**: All relationships verified 4. **Performance Baseline**: No degradation to existing queries 5. **Data Integrity**: Constraints and unique indexes enforced --- ## ๐Ÿงช Unit Test Scenarios (Migration Tests) ### 1.1 Migration File Creation **File**: `spec/migrations/20260114000001_create_flo_doc_tables_spec.rb` #### Test Suite 1: Table Creation ```ruby RSpec.describe CreateFloDocTables, type: :migration do describe '1.1.1: Table existence' do it 'creates institutions table' do expect { migration.change }.to change { table_exists?(:institutions) }.from(false).to(true) end it 'creates cohorts table' do expect { migration.change }.to change { table_exists?(:cohorts) }.from(false).to(true) end it 'creates cohort_enrollments table' do expect { migration.change }.to change { table_exists?(:cohort_enrollments) }.from(false).to(true) end end end ``` #### Test Suite 2: Schema Validation (Institutions) ```ruby describe '1.1.2: Institutions schema' do before { migration.change } it 'has all required columns' do columns = ActiveRecord::Base.connection.columns(:institutions).map(&:name) expect(columns).to include( 'id', 'name', 'email', 'contact_person', 'phone', 'settings', 'created_at', 'updated_at', 'deleted_at' ) end it 'has correct column types' do columns_hash = ActiveRecord::Base.connection.columns(:institutions).each_with_object({}) do |col, hash| hash[col.name] = col.type end expect(columns_hash['name']).to eq(:string) expect(columns_hash['email']).to eq(:string) expect(columns_hash['settings']).to eq(:jsonb) expect(columns_hash['deleted_at']).to eq(:datetime) end it 'enforces NOT NULL constraints' do expect { Institution.create!(name: nil, email: 'test@example.com') } .to raise_error(ActiveRecord::NotNullViolation) expect { Institution.create!(name: 'Test', email: nil) } .to raise_error(ActiveRecord::NotNullViolation) end it 'enforces unique constraints on name' do Institution.create!(name: 'Unique', email: 'test1@example.com') expect { Institution.create!(name: 'Unique', email: 'test2@example.com') } .to raise_error(ActiveRecord::RecordNotUnique) end it 'enforces unique constraints on email' do Institution.create!(name: 'Test1', email: 'unique@example.com') expect { Institution.create!(name: 'Test2', email: 'unique@example.com') } .to raise_error(ActiveRecord::RecordNotUnique) end it 'allows JSONB settings to be empty' do inst = Institution.create!(name: 'Test', email: 'test@example.com') expect(inst.settings).to eq({}) end it 'stores JSONB settings correctly' do inst = Institution.create!( name: 'Test', email: 'test@example.com', settings: { logo_url: '/logo.png', color: '#123456' } ) inst.reload expect(inst.settings['logo_url']).to eq('/logo.png') expect(inst.settings['color']).to eq('#123456') end end ``` #### Test Suite 3: Schema Validation (Cohorts) ```ruby describe '1.1.3: Cohorts schema' do before { migration.change } it 'has all required columns' do columns = ActiveRecord::Base.connection.columns(:cohorts).map(&:name) expect(columns).to include( 'id', 'institution_id', 'template_id', 'name', 'program_type', 'sponsor_email', 'required_student_uploads', 'cohort_metadata', 'status', 'tp_signed_at', 'students_completed_at', 'sponsor_completed_at', 'finalized_at', 'created_at', 'updated_at', 'deleted_at' ) end it 'enforces NOT NULL on required fields' do institution = create_institution template = create_template expect { Cohort.create!( institution_id: nil, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com' ) }.to raise_error(ActiveRecord::NotNullViolation) expect { Cohort.create!( institution_id: institution.id, template_id: nil, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com' ) }.to raise_error(ActiveRecord::NotNullViolation) expect { Cohort.create!( institution_id: institution.id, template_id: template.id, name: nil, program_type: 'learnership', sponsor_email: 'test@example.com' ) }.to raise_error(ActiveRecord::NotNullViolation) expect { Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: nil, sponsor_email: 'test@example.com' ) }.to raise_error(ActiveRecord::NotNullViolation) expect { Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: nil ) }.to raise_error(ActiveRecord::NotNullViolation) end it 'validates program_type inclusion' do institution = create_institution template = create_template valid_types = %w[learnership internship candidacy] valid_types.each do |type| cohort = Cohort.new( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: type, sponsor_email: 'test@example.com' ) expect(cohort).to be_valid end expect { Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'invalid_type', sponsor_email: 'test@example.com' ) }.to raise_error(ActiveRecord::RecordInvalid) end it 'validates status inclusion' do institution = create_institution template = create_template valid_statuses = %w[draft active completed] valid_statuses.each do |status| cohort = Cohort.new( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com', status: status ) expect(cohort).to be_valid end expect { Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com', status: 'invalid_status' ) }.to raise_error(ActiveRecord::RecordInvalid) end it 'defaults status to draft' do institution = create_institution template = create_template cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com' ) expect(cohort.status).to eq('draft') end it 'stores JSONB fields correctly' do institution = create_institution template = create_template cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com', required_student_uploads: ['id_copy', 'matric_certificate'], cohort_metadata: { start_date: '2026-02-01', duration_months: 12 } ) cohort.reload expect(cohort.required_student_uploads).to eq(['id_copy', 'matric_certificate']) expect(cohort.cohort_metadata['start_date']).to eq('2026-02-01') expect(cohort.cohort_metadata['duration_months']).to eq(12) end it 'allows datetime fields to be nil initially' do institution = create_institution template = create_template cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com' ) expect(cohort.tp_signed_at).to be_nil expect(cohort.students_completed_at).to be_nil expect(cohort.sponsor_completed_at).to be_nil expect(cohort.finalized_at).to be_nil end it 'stores datetime fields correctly' do institution = create_institution template = create_template time = Time.current cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com', tp_signed_at: time, students_completed_at: time, sponsor_completed_at: time, finalized_at: time ) cohort.reload expect(cohort.tp_signed_at).to be_within(1.second).of(time) expect(cohort.students_completed_at).to be_within(1.second).of(time) expect(cohort.sponsor_completed_at).to be_within(1.second).of(time) expect(cohort.finalized_at).to be_within(1.second).of(time) end end ``` #### Test Suite 4: Schema Validation (CohortEnrollments) ```ruby describe '1.1.4: CohortEnrollments schema' do before { migration.change } it 'has all required columns' do columns = ActiveRecord::Base.connection.columns(:cohort_enrollments).map(&:name) expect(columns).to include( 'id', 'cohort_id', 'submission_id', 'student_email', 'student_name', 'student_surname', 'student_id', 'status', 'role', 'uploaded_documents', 'values', 'completed_at', 'created_at', 'updated_at', 'deleted_at' ) end it 'enforces NOT NULL on required fields' do cohort = create_cohort submission = create_submission expect { CohortEnrollment.create!( cohort_id: nil, submission_id: submission.id, student_email: 'test@example.com' ) }.to raise_error(ActiveRecord::NotNullViolation) expect { CohortEnrollment.create!( cohort_id: cohort.id, submission_id: nil, student_email: 'test@example.com' ) }.to raise_error(ActiveRecord::NotNullViolation) expect { CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: nil ) }.to raise_error(ActiveRecord::NotNullViolation) end it 'defaults status to waiting' do cohort = create_cohort submission = create_submission enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com' ) expect(enrollment.status).to eq('waiting') end it 'defaults role to student' do cohort = create_cohort submission = create_submission enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com' ) expect(enrollment.role).to eq('student') end it 'validates status inclusion' do cohort = create_cohort submission = create_submission valid_statuses = %w[waiting in_progress complete] valid_statuses.each do |status| enrollment = CohortEnrollment.new( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com', status: status ) expect(enrollment).to be_valid end expect { CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com', status: 'invalid_status' ) }.to raise_error(ActiveRecord::RecordInvalid) end it 'validates role inclusion' do cohort = create_cohort submission = create_submission valid_roles = %w[student sponsor] valid_roles.each do |role| enrollment = CohortEnrollment.new( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com', role: role ) expect(enrollment).to be_valid end expect { CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com', role: 'invalid_role' ) }.to raise_error(ActiveRecord::RecordInvalid) end it 'stores JSONB fields correctly' do cohort = create_cohort submission = create_submission enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com', uploaded_documents: { 'id_copy' => true, 'matric' => false }, values: { full_name: 'John Doe', student_number: 'STU001' } ) enrollment.reload expect(enrollment.uploaded_documents['id_copy']).to be true expect(enrollment.uploaded_documents['matric']).to be false expect(enrollment.values['full_name']).to eq('John Doe') expect(enrollment.values['student_number']).to eq('STU001') end it 'allows completed_at to be nil initially' do cohort = create_cohort submission = create_submission enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com' ) expect(enrollment.completed_at).to be_nil end it 'stores completed_at correctly' do cohort = create_cohort submission = create_submission time = Time.current enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com', completed_at: time ) enrollment.reload expect(enrollment.completed_at).to be_within(1.second).of(time) end end ``` #### Test Suite 5: Indexes ```ruby describe '1.1.5: Indexes' do before { migration.change } describe 'Institutions indexes' do it 'creates unique index on name' do expect(index_exists?(:institutions, :name, unique: true)).to be true end it 'creates unique index on email' do expect(index_exists?(:institutions, :email, unique: true)).to be true end end describe 'Cohorts indexes' do it 'creates composite index on institution_id and status' do expect(index_exists?(:cohorts, [:institution_id, :status])).to be true end it 'creates index on template_id' do expect(index_exists?(:cohorts, :template_id)).to be true end it 'creates index on sponsor_email' do expect(index_exists?(:cohorts, :sponsor_email)).to be true end end describe 'CohortEnrollments indexes' do it 'creates composite index on cohort_id and status' do expect(index_exists?(:cohort_enrollments, [:cohort_id, :status])).to be true end it 'creates unique composite index on cohort_id and student_email' do expect(index_exists?(:cohort_enrollments, [:cohort_id, :student_email], unique: true)).to be true end it 'creates unique index on submission_id' do expect(index_exists?(:cohort_enrollments, [:submission_id], unique: true)).to be true end end end ``` #### Test Suite 6: Foreign Keys ```ruby describe '1.1.6: Foreign keys' do before { migration.change } describe 'Cohorts foreign keys' do it 'references institutions' do expect(foreign_key_exists?(:cohorts, :institutions)).to be true end it 'references templates' do expect(foreign_key_exists?(:cohorts, :templates)).to be true end it 'enforces referential integrity on institutions' do institution = create_institution template = create_template cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com' ) # Delete institution should fail or cascade expect { institution.destroy }.to raise_error(ActiveRecord::InvalidForeignKey) end it 'enforces referential integrity on templates' do institution = create_institution template = create_template cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'test@example.com' ) # Delete template should fail or cascade expect { template.destroy }.to raise_error(ActiveRecord::InvalidForeignKey) end end describe 'CohortEnrollments foreign keys' do it 'references cohorts' do expect(foreign_key_exists?(:cohort_enrollments, :cohorts)).to be true end it 'references submissions' do expect(foreign_key_exists?(:cohort_enrollments, :submissions)).to be true end it 'enforces referential integrity on cohorts' do cohort = create_cohort submission = create_submission enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com' ) expect { cohort.destroy }.to raise_error(ActiveRecord::InvalidForeignKey) end it 'enforces referential integrity on submissions' do cohort = create_cohort submission = create_submission enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'test@example.com' ) expect { submission.destroy }.to raise_error(ActiveRecord::InvalidForeignKey) end end end ``` #### Test Suite 7: Reversibility ```ruby describe '1.1.7: Migration reversibility' do it 'is reversible' do expect { migration.change }.not_to raise_error expect { migration.reverse }.not_to raise_error end it 'drops all tables on rollback' do migration.change expect(table_exists?(:institutions)).to be true expect(table_exists?(:cohorts)).to be true expect(table_exists?(:cohort_enrollments)).to be true migration.reverse expect(table_exists?(:institutions)).to be false expect(table_exists?(:cohorts)).to be false expect(table_exists?(:cohort_enrollments)).to be false end it 'drops all indexes on rollback' do migration.change migration.reverse # Verify indexes are removed expect(index_exists?(:institutions, :name, unique: true)).to be false expect(index_exists?(:cohorts, [:institution_id, :status])).to be false expect(index_exists?(:cohort_enrollments, [:submission_id], unique: true)).to be false end it 'drops all foreign keys on rollback' do migration.change migration.reverse expect(foreign_key_exists?(:cohorts, :institutions)).to be false expect(foreign_key_exists?(:cohort_enrollments, :cohorts)).to be false end end ``` #### Test Suite 8: Data Integrity ```ruby describe '1.1.8: Data integrity constraints' do before { migration.change } describe 'Unique constraints' do it 'prevents duplicate institution names' do Institution.create!(name: 'Test', email: 'test1@example.com') expect { Institution.create!(name: 'Test', email: 'test2@example.com') }.to raise_error(ActiveRecord::RecordNotUnique) end it 'prevents duplicate institution emails' do Institution.create!(name: 'Test1', email: 'test@example.com') expect { Institution.create!(name: 'Test2', email: 'test@example.com') }.to raise_error(ActiveRecord::RecordNotUnique) end it 'prevents duplicate student enrollments per cohort' do cohort = create_cohort submission1 = create_submission submission2 = create_submission CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission1.id, student_email: 'student@example.com' ) expect { CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission2.id, student_email: 'student@example.com' ) }.to raise_error(ActiveRecord::RecordNotUnique) end it 'prevents duplicate submission references' do cohort = create_cohort submission = create_submission CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'student1@example.com' ) cohort2 = create_cohort expect { CohortEnrollment.create!( cohort_id: cohort2.id, submission_id: submission.id, student_email: 'student2@example.com' ) }.to raise_error(ActiveRecord::RecordNotUnique) end end describe 'Cascading deletes' do it 'soft deletes institutions' do institution = create_institution institution.soft_delete expect(institution.deleted_at).not_to be_nil expect(Institution.where(deleted_at: nil).count).to eq(0) expect(Institution.with_deleted.count).to eq(1) end it 'soft deletes cohorts' do cohort = create_cohort cohort.soft_delete expect(cohort.deleted_at).not_to be_nil expect(Cohort.where(deleted_at: nil).count).to eq(0) end it 'soft deletes cohort_enrollments' do enrollment = create_cohort_enrollment enrollment.soft_delete expect(enrollment.deleted_at).not_to be_nil expect(CohortEnrollment.where(deleted_at: nil).count).to eq(0) end end end ``` #### Helper Methods for Tests ```ruby private def create_institution Institution.create!( name: "Test Institution #{SecureRandom.hex(4)}", email: "test_#{SecureRandom.hex(4)}@example.com" ) end def create_template account = Account.create!(name: 'Test Account') Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) end def create_submission account = Account.create!(name: 'Test Account') template = create_template Submission.create!( account_id: account.id, template_id: template.id, slug: "test-#{SecureRandom.hex(4)}", values: '{}' ) end def create_cohort institution = create_institution template = create_template Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test Cohort', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) end def create_cohort_enrollment cohort = create_cohort submission = create_submission CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'student@example.com' ) end ``` --- ## ๐Ÿ”— Integration Test Scenarios ### 2.1 Cross-Table Referential Integrity **File**: `spec/integration/cohort_workflow_spec.rb` ```ruby RSpec.describe 'Cohort Workflow Integration', type: :integration do describe '2.1.1: Existing DocuSeal tables remain unchanged' do it 'templates table still works' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Original Template', schema: '[]', fields: '[]', submitters: '[]' ) expect(template.name).to eq('Original Template') expect(Template.count).to eq(1) end it 'submissions table still works' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test', schema: '[]', fields: '[]', submitters: '[]' ) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'test-slug', values: '{}' ) expect(submission.slug).to eq('test-slug') expect(Submission.count).to eq(1) end it 'submitters table still works' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test', schema: '[]', fields: '[]', submitters: '[]' ) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'test-slug', values: '{}' ) submitter = Submitter.create!( submission_id: submission.id, email: 'submitter@example.com', name: 'Submitter' ) expect(submitter.email).to eq('submitter@example.com') expect(Submitter.count).to eq(1) end end describe '2.1.2: New tables reference existing tables' do it 'cohorts reference templates correctly' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) institution = Institution.create!( name: 'Test Institution', email: 'test@example.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test Cohort', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) expect(cohort.template).to eq(template) expect(cohort.template.name).to eq('Test Template') end it 'cohort_enrollments reference submissions correctly' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'test-slug', values: '{}' ) institution = Institution.create!( name: 'Test Institution', email: 'test@example.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test Cohort', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'student@example.com' ) expect(enrollment.submission).to eq(submission) expect(enrollment.submission.slug).to eq('test-slug') end it 'maintains bidirectional relationships' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'test-slug', values: '{}' ) institution = Institution.create!( name: 'Test Institution', email: 'test@example.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test Cohort', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'student@example.com' ) # Cohort -> Enrollments expect(cohort.cohort_enrollments).to include(enrollment) # Enrollment -> Cohort expect(enrollment.cohort).to eq(cohort) # Cohort -> Template expect(cohort.template).to eq(template) # Enrollment -> Submission expect(enrollment.submission).to eq(submission) # Template -> Cohorts (reverse association) expect(template.cohorts).to include(cohort) # Submission -> CohortEnrollments (reverse association) expect(submission.cohort_enrollments).to include(enrollment) end end describe '2.1.3: Complex queries across tables' do it 'joins new and existing tables correctly' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) institution = Institution.create!( name: 'Test Institution', email: 'test@example.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test Cohort', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) # Create multiple enrollments 3.times do |i| submission = Submission.create!( account_id: account.id, template_id: template.id, slug: "test-slug-#{i}", values: '{}' ) CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: "student#{i}@example.com", status: i.even? ? 'complete' : 'waiting' ) end # Query: Get all students for a cohort with their submission details result = CohortEnrollment .joins(:submission, :cohort) .where(cohort_id: cohort.id) .select('cohort_enrollments.*, submissions.slug, cohorts.name as cohort_name') .to_a expect(result.length).to eq(3) expect(result.map(&:cohort_name).uniq).to eq(['Test Cohort']) expect(result.map { |r| r.slug }).to include('test-slug-0', 'test-slug-1', 'test-slug-2') end it 'filters by status across tables' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) institution = Institution.create!( name: 'Test Institution', email: 'test@example.com' ) cohort1 = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Cohort 1', program_type: 'learnership', sponsor_email: 'sponsor@example.com', status: 'active' ) cohort2 = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Cohort 2', program_type: 'internship', sponsor_email: 'sponsor@example.com', status: 'draft' ) expect(Cohort.where(status: 'active').count).to eq(1) expect(Cohort.where(status: 'active').first.name).to eq('Cohort 1') end it 'counts related records correctly' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) institution = Institution.create!( name: 'Test Institution', email: 'test@example.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test Cohort', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) # Create 5 enrollments 5.times do |i| submission = Submission.create!( account_id: account.id, template_id: template.id, slug: "test-slug-#{i}", values: '{}' ) CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: "student#{i}@example.com", status: i < 3 ? 'complete' : 'waiting' ) end expect(cohort.cohort_enrollments.count).to eq(5) expect(cohort.cohort_enrollments.completed.count).to eq(3) expect(cohort.cohort_enrollments.where(status: 'waiting').count).to eq(2) end end describe '2.1.4: Performance with existing data' do it 'handles large numbers of existing DocuSeal records' do # Create 100 existing templates and submissions account = Account.create!(name: 'Test') templates = 100.times.map do |i| Template.create!( account_id: account.id, author_id: 1, name: "Template #{i}", schema: '[]', fields: '[]', submitters: '[]' ) end submissions = 100.times.map do |i| Submission.create!( account_id: account.id, template_id: templates[i].id, slug: "submission-#{i}", values: '{}' ) end # Now add FloDoc data institution = Institution.create!( name: 'Test Institution', email: 'test@example.com' ) # Create 10 cohorts referencing existing templates cohorts = 10.times.map do |i| Cohort.create!( institution_id: institution.id, template_id: templates[i].id, name: "Cohort #{i}", program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) end # Create 50 enrollments referencing existing submissions 50.times do |i| CohortEnrollment.create!( cohort_id: cohorts[i % 10].id, submission_id: submissions[i].id, student_email: "student#{i}@example.com" ) end # Verify no degradation expect(Template.count).to eq(100) expect(Submission.count).to eq(100) expect(Cohort.count).to eq(10) expect(CohortEnrollment.count).to eq(50) # Query performance should be acceptable start_time = Time.current result = CohortEnrollment .joins(:submission, :cohort) .where(cohorts: { institution_id: institution.id }) .limit(10) .to_a end_time = Time.current expect(result.length).to eq(10) expect(end_time - start_time).to be < 0.1 # Should be fast end end end ``` ### 2.2 Integration with Existing DocuSeal Workflow ```ruby describe '2.2: Integration with DocuSeal workflows' do it 'allows cohort to reference a template used in existing submissions' do account = Account.create!(name: 'Test') # Existing DocuSeal workflow template = Template.create!( account_id: account.id, author_id: 1, name: 'Standard Contract', schema: '[]', fields: '[]', submitters: '[]' ) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'existing-contract', values: '{}' ) submitter = Submitter.create!( submission_id: submission.id, email: 'client@example.com', name: 'Client' ) # New FloDoc workflow institution = Institution.create!( name: 'Training Institute', email: 'admin@institute.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, # Same template! name: '2026 Learnership', program_type: 'learnership', sponsor_email: 'sponsor@company.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, # Same submission! student_email: 'client@example.com', student_name: 'Client' ) # Verify both workflows coexist expect(template.submissions.count).to eq(1) expect(template.cohorts.count).to eq(1) expect(submission.cohort_enrollments.count).to eq(1) # Verify data integrity expect(cohort.template).to eq(template) expect(enrollment.submission).to eq(submission) expect(submitter.submission).to eq(submission) end it 'does not interfere with existing submission completion' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test Template', schema: '[]', fields: '[]', submitters: '[]' ) # Existing submission workflow submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'test-slug', values: '{}' ) submitter = Submitter.create!( submission_id: submission.id, email: 'user@example.com', name: 'User' ) # Complete the submission (existing workflow) submitter.update!(completed_at: Time.current) # Now add FloDoc data institution = Institution.create!( name: 'Institution', email: 'inst@example.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test Cohort', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'user@example.com' ) # Verify existing workflow still works submission.reload expect(submission.submitters.first.completed_at).not_to be_nil # Verify FloDoc data is separate expect(enrollment.completed_at).to be_nil end end ``` --- ## ๐Ÿ–ฅ๏ธ System/End-to-End Test Scenarios ### 3.1 Full Cohort Lifecycle **File**: `spec/system/cohort_lifecycle_spec.rb` ```ruby RSpec.describe 'Cohort Lifecycle', type: :system do describe '3.1.1: Complete 5-step workflow' do it 'executes full cohort creation to completion' do # Setup account = Account.create!(name: 'Test Institution') template = Template.create!( account_id: account.id, author_id: 1, name: 'Learnership Contract', schema: '[]', fields: '[]', submitters: '[]' ) institution = Institution.create!( name: 'TechPro Academy', email: 'admin@techpro.co.za', contact_person: 'Jane Smith', phone: '+27 11 123 4567' ) # Step 1: Create cohort (draft) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: '2026 Q1 Learnership', program_type: 'learnership', sponsor_email: 'sponsor@company.co.za', required_student_uploads: ['id_copy', 'matric_certificate'], cohort_metadata: { start_date: '2026-02-01', duration_months: 12, stipend_amount: 3500 }, status: 'draft' ) expect(cohort.status).to eq('draft') expect(cohort.tp_signed_at).to be_nil # Step 2: TP signs (activate) cohort.update!( status: 'active', tp_signed_at: Time.current ) expect(cohort.status).to eq('active') expect(cohort.tp_signed_at).not_to be_nil # Step 3: Students enroll 5.times do |i| submission = Submission.create!( account_id: account.id, template_id: template.id, slug: "student-#{i}-submission", values: '{}' ) CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: "student#{i}@example.com", student_name: "Student#{i}", student_surname: "Lastname#{i}", student_id: "STU#{i.to_s.rjust(3, '0')}", status: 'waiting' ) end expect(cohort.cohort_enrollments.count).to eq(5) # Step 4: Students complete cohort.cohort_enrollments.each do |enrollment| enrollment.update!( status: 'complete', completed_at: Time.current, values: { full_name: "#{enrollment.student_name} #{enrollment.student_surname}" } ) end cohort.update!(students_completed_at: Time.current) expect(cohort.cohort_enrollments.completed.count).to eq(5) expect(cohort.students_completed_at).not_to be_nil # Step 5: Sponsor signs cohort.update!( sponsor_completed_at: Time.current ) # Step 6: TP finalizes cohort.update!( status: 'completed', finalized_at: Time.current ) expect(cohort.status).to eq('completed') expect(cohort.finalized_at).not_to be_nil expect(cohort.sponsor_completed_at).not_to be_nil end end describe '3.1.2: State transitions' do it 'follows correct state flow' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test', schema: '[]', fields: '[]', submitters: '[]' ) institution = Institution.create!( name: 'Test', email: 'test@example.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com', status: 'draft' ) # Draft -> Active expect(cohort.status).to eq('draft') cohort.update!(status: 'active', tp_signed_at: Time.current) expect(cohort.status).to eq('active') # Active -> Completed cohort.update!(status: 'completed', finalized_at: Time.current) expect(cohort.status).to eq('completed') end end end ``` ### 3.2 Database Performance Verification ```ruby describe '3.2: Performance tests' do it 'migration runs in acceptable time' do start_time = Time.current migration.change end_time = Time.current expect(end_time - start_time).to be < 30 # seconds end it 'queries use indexes' do migration.change # Create test data institution = Institution.create!(name: 'Test', email: 'test@example.com') template = Template.create!( account_id: 1, author_id: 1, name: 'Test', schema: '[]', fields: '[]', submitters: '[]' ) 100.times do |i| Cohort.create!( institution_id: institution.id, template_id: template.id, name: "Cohort #{i}", program_type: 'learnership', sponsor_email: 'sponsor@example.com', status: i.even? ? 'active' : 'draft' ) end # Verify index usage explain = Cohort.where(institution_id: institution.id, status: 'active').explain expect(explain).to include('Index Scan') || expect(explain).to include('index') end it 'no performance degradation on existing tables' do # Create many existing records account = Account.create!(name: 'Test') 100.times do |i| template = Template.create!( account_id: account.id, author_id: 1, name: "Template #{i}", schema: '[]', fields: '[]', submitters: '[]' ) Submission.create!( account_id: account.id, template_id: template.id, slug: "submission-#{i}", values: '{}' ) end # Measure query time before FloDoc tables start_time = Time.current Template.active.limit(10).to_a time_before = Time.current - start_time # Add FloDoc tables migration.change # Measure query time after FloDoc tables start_time = Time.current Template.active.limit(10).to_a time_after = Time.current - start_time # Should not degrade significantly (allow 50% increase) expect(time_after).to be < (time_before * 1.5) end end ``` --- ## โšก Non-Functional Test Scenarios ### 4.1 Security Tests ```ruby describe '4.1: Security requirements' do before { migration.change } describe '4.1.1: Soft delete implementation' do it 'includes deleted_at on all tables' do %i[institutions cohorts cohort_enrollments].each do |table| columns = ActiveRecord::Base.connection.columns(table).map(&:name) expect(columns).to include('deleted_at') end end it 'does not physically delete records' do institution = Institution.create!(name: 'Test', email: 'test@example.com') cohort = Cohort.create!( institution_id: institution.id, template_id: 1, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: 1, student_email: 'student@example.com' ) # Soft delete institution.soft_delete cohort.soft_delete enrollment.soft_delete # Verify records still exist expect(Institution.with_deleted.count).to eq(1) expect(Cohort.with_deleted.count).to eq(1) expect(CohortEnrollment.with_deleted.count).to eq(1) # But not in active scope expect(Institution.where(deleted_at: nil).count).to eq(0) end end describe '4.1.2: Foreign key constraints prevent orphaned records' do it 'prevents orphaned cohorts' do institution = Institution.create!(name: 'Test', email: 'test@example.com') template = Template.create!( account_id: 1, author_id: 1, name: 'Test', schema: '[]', fields: '[]', submitters: '[]' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) # Try to delete institution with cohort expect { institution.destroy }.to raise_error(ActiveRecord::InvalidForeignKey) # Cohort still exists expect(Cohort.exists?(cohort.id)).to be true end it 'prevents orphaned enrollments' do cohort = Cohort.create!( institution_id: 1, template_id: 1, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: 1, student_email: 'student@example.com' ) # Try to delete cohort with enrollments expect { cohort.destroy }.to raise_error(ActiveRecord::InvalidForeignKey) # Enrollment still exists expect(CohortEnrollment.exists?(enrollment.id)).to be true end end describe '4.1.3: Sensitive data handling' do it 'stores emails in plain text (unless encryption policy enabled)' do # Note: Per PRD, encryption is optional institution = Institution.create!(name: 'Test', email: 'sensitive@example.com') cohort = Cohort.create!( institution_id: institution.id, template_id: 1, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: 1, student_email: 'student@example.com' ) # Verify emails are stored expect(institution.email).to eq('sensitive@example.com') expect(cohort.sponsor_email).to eq('sponsor@example.com') expect(enrollment.student_email).to eq('student@example.com') # Verify they can be queried expect(Institution.find_by(email: 'sensitive@example.com')).to eq(institution) expect(Cohort.find_by(sponsor_email: 'sponsor@example.com')).to eq(cohort) expect(CohortEnrollment.find_by(student_email: 'student@example.com')).to eq(enrollment) end end end ``` ### 4.2 Data Integrity Tests ```ruby describe '4.2: Data integrity' do before { migration.change } describe '4.2.1: Referential integrity' do it 'maintains consistency across all tables' do account = Account.create!(name: 'Test') template = Template.create!( account_id: account.id, author_id: 1, name: 'Test', schema: '[]', fields: '[]', submitters: '[]' ) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'test', values: '{}' ) institution = Institution.create!(name: 'Test', email: 'test@example.com') cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'student@example.com' ) # Verify all relationships expect(cohort.institution).to eq(institution) expect(cohort.template).to eq(template) expect(enrollment.cohort).to eq(cohort) expect(enrollment.submission).to eq(submission) # Verify reverse relationships expect(institution.cohorts).to include(cohort) expect(template.cohorts).to include(cohort) expect(cohort.cohort_enrollments).to include(enrollment) expect(submission.cohort_enrollments).to include(enrollment) end end describe '4.2.2: Unique constraints' do it 'enforces institution uniqueness' do Institution.create!(name: 'Unique', email: 'test1@example.com') expect { Institution.create!(name: 'Unique', email: 'test2@example.com') }.to raise_error(ActiveRecord::RecordNotUnique) end it 'enforces student per cohort uniqueness' do cohort = Cohort.create!( institution_id: 1, template_id: 1, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) CohortEnrollment.create!( cohort_id: cohort.id, submission_id: 1, student_email: 'student@example.com' ) expect { CohortEnrollment.create!( cohort_id: cohort.id, submission_id: 2, student_email: 'student@example.com' ) }.to raise_error(ActiveRecord::RecordNotUnique) end it 'enforces submission uniqueness' do cohort1 = Cohort.create!( institution_id: 1, template_id: 1, name: 'Test1', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) cohort2 = Cohort.create!( institution_id: 1, template_id: 1, name: 'Test2', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) CohortEnrollment.create!( cohort_id: cohort1.id, submission_id: 1, student_email: 'student1@example.com' ) expect { CohortEnrollment.create!( cohort_id: cohort2.id, submission_id: 1, student_email: 'student2@example.com' ) }.to raise_error(ActiveRecord::RecordNotUnique) end end describe '4.2.3: Default values' do it 'sets correct defaults' do institution = Institution.create!(name: 'Test', email: 'test@example.com') cohort = Cohort.create!( institution_id: institution.id, template_id: 1, name: 'Test', program_type: 'learnership', sponsor_email: 'sponsor@example.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: 1, student_email: 'student@example.com' ) expect(cohort.status).to eq('draft') expect(cohort.required_student_uploads).to eq([]) expect(cohort.cohort_metadata).to eq({}) expect(enrollment.status).to eq('waiting') expect(enrollment.role).to eq('student') expect(enrollment.uploaded_documents).to eq({}) expect(enrollment.values).to eq({}) end end end ``` ### 4.3 Compatibility Tests ```ruby describe '4.3: Backward compatibility' do it 'does not modify existing DocuSeal schema' do # Get schema before migration migration.change # Verify existing tables unchanged templates_columns = ActiveRecord::Base.connection.columns(:templates).map(&:name) expect(templates_columns).to include('name', 'account_id', 'author_id') expect(templates_columns).not_to include('flo_doc_specific') submissions_columns = ActiveRecord::Base.connection.columns(:submissions).map(&:name) expect(submissions_columns).to include('template_id', 'slug', 'values') expect(submissions_columns).not_to include('flo_doc_specific') submitters_columns = ActiveRecord::Base.connection.columns(:submitters).map(&:name) expect(submitters_columns).to include('submission_id', 'email', 'name') expect(submitters_columns).not_to include('flo_doc_specific') end it 'existing DocuSeal operations still work' do account = Account.create!(name: 'Test') # Create template (existing operation) template = Template.create!( account_id: account.id, author_id: 1, name: 'Old Template', schema: '[]', fields: '[]', submitters: '[]' ) expect(template.name).to eq('Old Template') # Create submission (existing operation) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'old-slug', values: '{}' ) expect(submission.slug).to eq('old-slug') # Create submitter (existing operation) submitter = Submitter.create!( submission_id: submission.id, email: 'old@example.com', name: 'Old User' ) expect(submitter.email).to eq('old@example.com') # Complete submission (existing operation) submitter.update!(completed_at: Time.current) expect(submitter.completed_at).not_to be_nil # Verify all still work after FloDoc tables exist migration.change template2 = Template.create!( account_id: account.id, author_id: 1, name: 'New Template', schema: '[]', fields: '[]', submitters: '[]' ) expect(template2.name).to eq('New Template') end it 'allows mixed usage of old and new workflows' do account = Account.create!(name: 'Test') # Old workflow: Template -> Submission -> Submitter template = Template.create!( account_id: account.id, author_id: 1, name: 'Contract', schema: '[]', fields: '[]', submitters: '[]' ) submission = Submission.create!( account_id: account.id, template_id: template.id, slug: 'contract-1', values: '{}' ) submitter = Submitter.create!( submission_id: submission.id, email: 'client@example.com', name: 'Client' ) # New workflow: Institution -> Cohort -> CohortEnrollment institution = Institution.create!( name: 'Training Co', email: 'admin@training.com' ) cohort = Cohort.create!( institution_id: institution.id, template_id: template.id, name: 'Training Program', program_type: 'learnership', sponsor_email: 'sponsor@company.com' ) enrollment = CohortEnrollment.create!( cohort_id: cohort.id, submission_id: submission.id, student_email: 'client@example.com', student_name: 'Client' ) # Both workflows coexist expect(Template.count).to eq(1) expect(Submission.count).to eq(1) expect(Submitter.count).to eq(1) expect(Institution.count).to eq(1) expect(Cohort.count).to eq(1) expect(CohortEnrollment.count).to eq(1) # Cross-references work expect(template.submissions).to include(submission) expect(template.cohorts).to include(cohort) expect(submission.cohort_enrollments).to include(enrollment) end end ``` --- ## ๐Ÿ“‹ Test Data Requirements ### 5.1 Factory Definitions **File**: `spec/factories/flo_doc_factories.rb` ```ruby # Institution Factory FactoryBot.define do factory :institution do sequence(:name) { |n| "Institution #{n}" } sequence(:email) { |n| "institution#{n}@example.com" } contact_person { 'John Doe' } phone { '+27 11 123 4567' } settings { {} } trait :with_logo do settings { { logo_url: '/logo.png', primary_color: '#123456' } } end trait :deleted do deleted_at { Time.current } end end end # Cohort Factory FactoryBot.define do factory :cohort do association :institution association :template sequence(:name) { |n| "Cohort #{n}" } program_type { 'learnership' } sequence(:sponsor_email) { |n| "sponsor#{n}@example.com" } required_student_uploads { ['id_copy', 'matric_certificate'] } cohort_metadata { { start_date: '2026-02-01', duration_months: 12 } } status { 'draft' } trait :draft do status { 'draft' } end trait :active do status { 'active' } tp_signed_at { Time.current } end trait :completed do status { 'completed' } tp_signed_at { Time.current } students_completed_at { Time.current } sponsor_completed_at { Time.current } finalized_at { Time.current } end trait :with_students do after(:create) do |cohort| create_list(:cohort_enrollment, 3, cohort: cohort) end end trait :deleted do deleted_at { Time.current } end end end # CohortEnrollment Factory FactoryBot.define do factory :cohort_enrollment do association :cohort association :submission sequence(:student_email) { |n| "student#{n}@example.com" } student_name { 'John' } student_surname { 'Doe' } sequence(:student_id) { |n| "STU#{n.to_s.rjust(3, '0')}" } status { 'waiting' } role { 'student' } uploaded_documents { {} } values { {} } trait :waiting do status { 'waiting' } end trait :in_progress do status { 'in_progress' } end trait :completed do status { 'complete' } completed_at { Time.current } values { { full_name: 'John Doe' } } end trait :sponsor do role { 'sponsor' } end trait :deleted do deleted_at { Time.current } end end end ``` ### 5.2 Test Data Scenarios #### Scenario 1: Minimal Data ```ruby # For basic migration tests let(:minimal_institution) { create(:institution) } let(:minimal_cohort) { create(:cohort) } let(:minimal_enrollment) { create(:cohort_enrollment) } ``` #### Scenario 2: Complete Workflow ```ruby # For integration tests let(:complete_workflow) do institution = create(:institution) template = create(:template) cohort = create(:cohort, :active, institution: institution, template: template) 5.times do |i| submission = create(:submission, template: template) create(:cohort_enrollment, :completed, cohort: cohort, submission: submission) end cohort.update!(students_completed_at: Time.current) cohort.update!(sponsor_completed_at: Time.current) cohort.update!(status: 'completed', finalized_at: Time.current) cohort end ``` #### Scenario 3: Large Dataset ```ruby # For performance tests let(:large_dataset) do institution = create(:institution) template = create(:template) 100.times do |i| cohort = create(:cohort, institution: institution, template: template) 50.times do submission = create(:submission, template: template) create(:cohort_enrollment, cohort: cohort, submission: submission) end end end ``` #### Scenario 4: Edge Cases ```ruby # For boundary testing let(:edge_cases) do { # Empty JSONB fields empty_metadata: create(:cohort, cohort_metadata: {}, required_student_uploads: []), # Long strings long_name: create(:cohort, name: 'A' * 500), # Special characters special_chars: create(:cohort, name: "Test's \"Special\" & "), # Multiple emails multiple_enrollments: create_list(:cohort_enrollment, 10, cohort: create(:cohort)), # Deleted records deleted_institution: create(:institution, :deleted), deleted_cohort: create(:cohort, :deleted), deleted_enrollment: create(:cohort_enrollment, :deleted) } end ``` ### 5.3 Database Cleaner Configuration ```ruby # spec/support/database_cleaner.rb RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.clean_with(:truncation) end config.before(:each) do DatabaseCleaner.strategy = :transaction end config.before(:each, type: :system) do DatabaseCleaner.strategy = :truncation end config.before(:each, type: :migration) do DatabaseCleaner.strategy = :truncation end config.before(:each) do DatabaseCleaner.start end config.after(:each) do DatabaseCleaner.clean end end ``` --- ## ๐Ÿš€ Test Execution Plan ### 6.1 Execution Order Tests must be run in this specific order to ensure proper dependency resolution: ```bash # Phase 1: Unit Tests (Migration) bundle exec rspec spec/migrations/20260114000001_create_flo_doc_tables_spec.rb # Phase 2: Integration Tests bundle exec rspec spec/integration/cohort_workflow_spec.rb # Phase 3: System Tests bundle exec rspec spec/system/cohort_lifecycle_spec.rb # Phase 4: Existing DocuSeal Tests (Regression) bundle exec rspec spec/models/template_spec.rb bundle exec rspec spec/models/submission_spec.rb bundle exec rspec spec/models/submitter_spec.rb bundle exec rspec spec/requests/api/v1/templates_spec.rb bundle exec rspec spec/requests/api/v1/submissions_spec.rb # Phase 5: Full Suite bundle exec rspec ``` ### 6.2 Test Execution Commands #### Individual Test Files ```bash # Migration tests only bundle exec rspec spec/migrations/20260114000001_create_flo_doc_tables_spec.rb --format documentation # Integration tests only bundle exec rspec spec/integration/cohort_workflow_spec.rb --format documentation # System tests only bundle exec rspec spec/system/cohort_lifecycle_spec.rb --format documentation # All FloDoc tests bundle exec rspec spec/migrations/ spec/integration/ spec/system/ --format documentation # All existing DocuSeal tests (regression) bundle exec rspec spec/models/ spec/controllers/ spec/requests/ --tag ~flo_doc --format documentation ``` #### With Coverage ```bash # Ruby coverage bundle exec rspec --format documentation open coverage/index.html # JavaScript coverage (if applicable) yarn test --coverage ``` #### Watch Mode (Development) ```bash # For continuous testing during development bundle exec rspec spec/migrations/20260114000001_create_flo_doc_tables_spec.rb --format documentation --fail-fast ``` ### 6.3 Test Data Setup ```bash # Before running tests bin/rails db:test:prepare # Or manually RAILS_ENV=test bundle exec rails db:create RAILS_ENV=test bundle exec rails db:schema:load # Verify test database RAILS_ENV=test bundle exec rails db:version ``` ### 6.4 CI/CD Integration ```yaml # .github/workflows/story-1-1-tests.yml name: Story 1.1 - Database Schema Tests on: push: branches: [ story/1.1-database-schema ] pull_request: branches: [ master ] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:14 env: POSTGRES_PASSWORD: password options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 steps: - uses: actions/checkout@v3 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.4.2 bundler-cache: true - name: Setup Node uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Install dependencies run: | bundle install yarn install - name: Setup test database env: DATABASE_URL: postgresql://postgres:password@localhost:5432/flo_doc_test RAILS_ENV: test run: | bundle exec rails db:create bundle exec rails db:schema:load - name: Run Story 1.1 migration tests env: DATABASE_URL: postgresql://postgres:password@localhost:5432/flo_doc_test RAILS_ENV: test run: bundle exec rspec spec/migrations/20260114000001_create_flo_doc_tables_spec.rb --format documentation - name: Run integration tests env: DATABASE_URL: postgresql://postgres:password@localhost:5432/flo_doc_test RAILS_ENV: test run: bundle exec rspec spec/integration/cohort_workflow_spec.rb --format documentation - name: Run system tests env: DATABASE_URL: postgresql://postgres:password@localhost:5432/flo_doc_test RAILS_ENV: test run: bundle exec rspec spec/system/cohort_lifecycle_spec.rb --format documentation - name: Run regression tests env: DATABASE_URL: postgresql://postgres:password@localhost:5432/flo_doc_test RAILS_ENV: test run: bundle exec rspec spec/models/template_spec.rb spec/models/submission_spec.rb --format documentation - name: Upload test results uses: actions/upload-artifact@v3 if: always() with: name: test-results path: | coverage/ tmp/screenshots/ - name: Check coverage env: DATABASE_URL: postgresql://postgres:password@localhost:5432/flo_doc_test RAILS_ENV: test run: | bundle exec rspec --format documentation if [ $(cat coverage/coverage.last_run.json | jq '.result.line') -lt 80 ]; then echo "Coverage below 80%" exit 1 fi ``` ### 6.5 Manual Verification Checklist #### Before Developer Commits - [ ] All migration tests pass (100%) - [ ] All integration tests pass (100%) - [ ] All system tests pass (100%) - [ ] All existing DocuSeal tests pass (100%) - [ ] Coverage โ‰ฅ 80% for new code - [ ] No new RuboCop warnings - [ ] Migration is reversible - [ ] Performance tests pass (< 30s migration, < 100ms queries) #### QA Verification - [ ] Run full test suite on clean database - [ ] Run tests with existing DocuSeal data - [ ] Verify rollback works correctly - [ ] Check database schema.rb is updated - [ ] Confirm no regression in existing features #### Database Verification ```bash # After migration bin/rails db:migrate bin/rails db:rollback bin/rails db:migrate # Check schema cat db/schema.rb | grep -A 50 "create_table \"institutions\"" cat db/schema.rb | grep -A 50 "create_table \"cohorts\"" cat db/schema.rb | grep -A 50 "create_table \"cohort_enrollments\"" # Verify indexes bin/rails runner "puts ActiveRecord::Base.connection.indexes(:cohorts).map(&:name)" bin/rails runner "puts ActiveRecord::Base.connection.indexes(:cohort_enrollments).map(&:name)" # Verify foreign keys bin/rails runner "puts ActiveRecord::Base.connection.foreign_keys(:cohorts).map(&:to_sql)" bin/rails runner "puts ActiveRecord::Base.connection.foreign_keys(:cohort_enrollments).map(&:to_sql)" ``` ### 6.6 Troubleshooting #### Common Issues ```bash # Issue: Migration fails due to existing data # Solution: Clean test database RAILS_ENV=test bundle exec rails db:drop db:create db:schema:load # Issue: Foreign key constraint violations # Solution: Check test data setup order # Ensure parent records exist before child records # Issue: Indexes not being used # Solution: Run ANALYZE on test database RAILS_ENV=test bundle exec rails runner "ActiveRecord::Base.connection.execute('ANALYZE')" # Issue: Tests pass locally but fail in CI # Solution: Check database version differences # Ensure CI uses same PostgreSQL version as local ``` #### Debug Commands ```bash # Check table structure RAILS_ENV=test bundle exec rails runner "p ActiveRecord::Base.connection.columns(:cohorts).map { |c| [c.name, c.type, c.null] }" # Check indexes RAILS_ENV=test bundle exec rails runner "p ActiveRecord::Base.connection.indexes(:cohorts).map(&:name)" # Check foreign keys RAILS_ENV=test bundle exec rails runner "p ActiveRecord::Base.connection.foreign_keys(:cohorts).map { |fk| [fk.from_table, fk.to_table, fk.column] }" # Test data count RAILS_ENV=test bundle exec rails runner "p Institution.count; p Cohort.count; p CohortEnrollment.count" # Query performance RAILS_ENV=test bundle exec rails runner "puts Cohort.where(status: 'active').explain" ``` --- ## ๐Ÿ“Š Success Criteria ### 7.1 Functional Success - โœ… All 3 tables created with correct schema - โœ… All indexes created and functional - โœ… All foreign keys enforced - โœ… Migrations are 100% reversible - โœ… No modifications to existing DocuSeal tables - โœ… All acceptance criteria met ### 7.2 Quality Success - โœ… 100% of migration tests pass - โœ… 100% of integration tests pass - โœ… 100% of system tests pass - โœ… 100% of existing DocuSeal tests pass (zero regression) - โœ… Code coverage โ‰ฅ 80% for new code - โœ… Migration time < 30 seconds - โœ… Query performance < 100ms for common operations ### 7.3 Integration Success - โœ… New tables reference existing tables correctly - โœ… Existing workflows remain unaffected - โœ… Mixed usage (old + new) works seamlessly - โœ… Referential integrity maintained across all tables ### 7.4 Security Success - โœ… All tables have soft delete (deleted_at) - โœ… Foreign keys prevent orphaned records - โœ… Unique constraints enforced - โœ… NOT NULL constraints on required fields --- ## ๐Ÿ“ Test Results Template ```markdown ## Test Results - Story 1.1: Database Schema Extension **Date**: [DATE] **Tester**: [NAME] **Environment**: [LOCAL/CI] ### Unit Tests (Migration) | Test Suite | Status | Pass | Fail | Duration | |------------|--------|------|------|----------| | Table Creation | โœ… | 3/3 | 0/3 | [TIME] | | Schema Validation | โœ… | 30/30 | 0/30 | [TIME] | | Indexes | โœ… | 7/7 | 0/7 | [TIME] | | Foreign Keys | โœ… | 6/6 | 0/6 | [TIME] | | Reversibility | โœ… | 4/4 | 0/4 | [TIME] | | Data Integrity | โœ… | 12/12 | 0/12 | [TIME] | | **Total** | **โœ…** | **62/62** | **0/62** | **[TIME]** | ### Integration Tests | Test Suite | Status | Pass | Fail | Duration | |------------|--------|------|------|----------| | Existing Tables Unchanged | โœ… | 3/3 | 0/3 | [TIME] | | New Table References | โœ… | 4/4 | 0/4 | [TIME] | | Complex Queries | โœ… | 4/4 | 0/4 | [TIME] | | Performance | โœ… | 2/2 | 0/2 | [TIME] | | DocuSeal Integration | โœ… | 2/2 | 0/2 | [TIME] | | **Total** | **โœ…** | **15/15** | **0/15** | **[TIME]** | ### System Tests | Test Suite | Status | Pass | Fail | Duration | |------------|--------|------|------|----------| | Full Lifecycle | โœ… | 2/2 | 0/2 | [TIME] | | State Transitions | โœ… | 1/1 | 0/1 | [TIME] | | Performance | โœ… | 3/3 | 0/3 | [TIME] | | **Total** | **โœ…** | **6/6** | **0/6** | **[TIME]** | ### Regression Tests | Test Suite | Status | Pass | Fail | Duration | |------------|--------|------|------|----------| | Template Model | โœ… | [X]/[Y] | 0/[Y] | [TIME] | | Submission Model | โœ… | [X]/[Y] | 0/[Y] | [TIME] | | Submitter Model | โœ… | [X]/[Y] | 0/[Y] | [TIME] | | Template API | โœ… | [X]/[Y] | 0/[Y] | [TIME] | | Submission API | โœ… | [X]/[Y] | 0/[Y] | [TIME] | | **Total** | **โœ…** | **[X]/[Y]** | **0/[Y]** | **[TIME]** | ### Coverage - **Line Coverage**: [XX]% - **Branch Coverage**: [XX]% - **Function Coverage**: [XX]% ### Performance - **Migration Time**: [X]s (Target: <30s) - **Query Time (avg)**: [X]ms (Target: <100ms) - **No Degradation**: โœ… ### Database Verification - **Tables Created**: โœ… 3/3 - **Indexes Created**: โœ… 7/7 - **Foreign Keys Created**: โœ… 4/4 - **Schema.rb Updated**: โœ… - **Rollback Works**: โœ… ### Overall Status **[PASS/FAIL]** **Summary**: [Brief summary of results] **Issues Found**: [List any issues] **Recommendation**: [Proceed/Block/Needs Fix] ``` --- ## ๐ŸŽฏ Conclusion This test design provides **comprehensive coverage** for Story 1.1: Database Schema Extension, ensuring: 1. **Zero Regression**: Existing DocuSeal functionality remains intact 2. **Complete Coverage**: All acceptance criteria have corresponding tests 3. **Brownfield Safety**: Tests verify integration with existing tables 4. **Performance Baseline**: No degradation to existing queries 5. **Data Integrity**: All constraints and relationships verified 6. **Reversibility**: Migration can be safely rolled back **Next Steps**: 1. Developer implements Story 1.1 using this test design 2. Run all tests in specified order 3. Verify coverage meets 80% minimum 4. Submit for QA review with test results 5. QA performs `*review` command to validate --- **Document Status**: โœ… Complete **Ready for**: Story 1.1 Implementation **Test Architect Approval**: Pending