73 KiB
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
- Zero Regression: All existing DocuSeal tests must pass
- Atomic Operations: Each migration step testable independently
- Foreign Key Validation: All relationships verified
- Performance Baseline: No degradation to existing queries
- 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
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)
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)
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
# 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
# For basic migration tests
let(:minimal_institution) { create(:institution) }
let(:minimal_cohort) { create(:cohort) }
let(:minimal_enrollment) { create(:cohort_enrollment) }
Scenario 2: Complete Workflow
# 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
# 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
# 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\" & <Chars>"),
# 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
# 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:
# 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
# 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
# Ruby coverage
bundle exec rspec --format documentation
open coverage/index.html
# JavaScript coverage (if applicable)
yarn test --coverage
Watch Mode (Development)
# 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
# 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
# .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
# 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
# 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
# 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
## 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:
- Zero Regression: Existing DocuSeal functionality remains intact
- Complete Coverage: All acceptance criteria have corresponding tests
- Brownfield Safety: Tests verify integration with existing tables
- Performance Baseline: No degradation to existing queries
- Data Integrity: All constraints and relationships verified
- Reversibility: Migration can be safely rolled back
Next Steps:
- Developer implements Story 1.1 using this test design
- Run all tests in specified order
- Verify coverage meets 80% minimum
- Submit for QA review with test results
- QA performs
*reviewcommand to validate
Document Status: ✅ Complete Ready for: Story 1.1 Implementation Test Architect Approval: Pending