You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/spec/integration/cohort_workflow_spec.rb

464 lines
14 KiB

# frozen_string_literal: true
# Integration Spec: Cohort Workflow
# Purpose: Verify referential integrity between new and existing DocuSeal tables
# Coverage: 25% of test strategy (cross-table relationships)
require 'rails_helper'
RSpec.describe 'Cohort Workflow Integration', type: :integration do
# Create test data for each test (transactional fixtures isolate the database)
let(:account) do
Account.create!(
name: 'Test Training Institution',
timezone: 'UTC',
locale: 'en',
uuid: SecureRandom.uuid
)
end
let(:user) do
User.create!(
first_name: 'Test',
last_name: 'User',
email: "test-#{SecureRandom.hex(4)}@example.com",
role: 'admin',
account_id: account.id,
password: 'password123',
password_confirmation: 'password123'
)
end
describe 'referential integrity with existing DocuSeal tables' do
it 'maintains integrity between cohorts and templates' do
# Create a real template (existing DocuSeal table)
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Learnership Agreement',
schema: '[]',
fields: '[]',
submitters: '[]'
)
# Create institution (FloDoc table)
institution = Institution.create!(
name: 'Test Training Institution',
email: 'admin@example.com'
)
# Create cohort referencing the template
cohort = Cohort.create!(
institution_id: institution.id,
template_id: template.id,
name: 'Q1 2026 Learnership Cohort',
program_type: 'learnership',
sponsor_email: 'sponsor@example.com'
)
# Verify relationship
expect(cohort.template).to eq(template)
expect(cohort.institution).to eq(institution)
expect(cohort.template.account).to eq(account)
end
it 'maintains integrity between cohort_enrollments and submissions' do
# Create existing DocuSeal entities
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Test Template',
schema: '[]',
fields: '[]',
submitters: '[]'
)
submission = Submission.create!(
account_id: account.id,
template_id: template.id,
slug: "test-slug-#{SecureRandom.hex(4)}",
variables: '{}'
)
# Create FloDoc entities
institution = Institution.create!(
name: 'Test Institution',
email: 'admin@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 enrollment linking to submission
enrollment = CohortEnrollment.create!(
cohort_id: cohort.id,
submission_id: submission.id,
student_email: 'student@example.com',
student_name: 'John',
student_surname: 'Doe'
)
# Verify relationships
expect(enrollment.submission).to eq(submission)
expect(enrollment.cohort).to eq(cohort)
expect(enrollment.cohort.template).to eq(template)
end
it 'handles cascading queries across new and existing tables' do
# Setup
template1 = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Template 1',
schema: '[]',
fields: '[]',
submitters: '[]'
)
template2 = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Template 2',
schema: '[]',
fields: '[]',
submitters: '[]'
)
institution = Institution.create!(
name: 'Multi-Cohort Institution',
email: 'admin@example.com'
)
# Create cohorts
cohort1 = Cohort.create!(
institution_id: institution.id,
template_id: template1.id,
name: 'Cohort 1',
program_type: 'learnership',
sponsor_email: 'sponsor1@example.com',
status: 'active'
)
cohort2 = Cohort.create!(
institution_id: institution.id,
template_id: template2.id,
name: 'Cohort 2',
program_type: 'internship',
sponsor_email: 'sponsor2@example.com',
status: 'draft'
)
# Create submissions
submission1 = Submission.create!(
account_id: account.id,
template_id: template1.id,
slug: "slug-1-#{SecureRandom.hex(4)}",
variables: '{}'
)
submission2 = Submission.create!(
account_id: account.id,
template_id: template2.id,
slug: "slug-2-#{SecureRandom.hex(4)}",
variables: '{}'
)
# Create enrollments
CohortEnrollment.create!(
cohort_id: cohort1.id,
submission_id: submission1.id,
student_email: 'student1@example.com',
status: 'complete'
)
CohortEnrollment.create!(
cohort_id: cohort2.id,
submission_id: submission2.id,
student_email: 'student2@example.com',
status: 'waiting'
)
# Complex query: Get all active cohorts with their templates and enrollments
results = Cohort
.joins(:template, :institution)
.where(status: 'active')
.includes(:cohort_enrollments)
.map do |c|
{
cohort_name: c.name,
template_name: c.template.name,
institution_name: c.institution.name,
enrollment_count: c.cohort_enrollments.count,
active_enrollments: c.cohort_enrollments.where(status: 'complete').count
}
end
expect(results.length).to eq(1)
expect(results.first[:cohort_name]).to eq('Cohort 1')
expect(results.first[:template_name]).to eq('Template 1')
expect(results.first[:enrollment_count]).to eq(1)
expect(results.first[:active_enrollments]).to eq(1)
end
it 'prevents deletion of referenced records' do
# Setup
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Test Template',
schema: '[]',
fields: '[]',
submitters: '[]'
)
submission = Submission.create!(
account_id: account.id,
template_id: template.id,
slug: "test-slug-#{SecureRandom.hex(4)}",
variables: '{}'
)
institution = Institution.create!(
name: 'Test Institution',
email: 'admin@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'
)
# Try to delete template (should fail due to FK constraint from cohorts)
expect { template.destroy }.to raise_error(ActiveRecord::InvalidForeignKey)
# Try to delete submission (should fail due to FK constraint from cohort_enrollments)
expect { submission.destroy }.to raise_error(ActiveRecord::InvalidForeignKey)
# Cohort deletion cascades (dependent: :destroy) - verify enrollment is also deleted
expect { cohort.destroy }.to change(CohortEnrollment, :count).by(-1)
expect(CohortEnrollment.find_by(id: enrollment.id)).to be_nil
end
end
describe 'soft delete behavior' do
it 'marks records as deleted instead of removing them' do
institution = Institution.create!(
name: 'Test Institution',
email: 'admin@example.com'
)
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Test Template',
schema: '[]',
fields: '[]',
submitters: '[]'
)
cohort = Cohort.create!(
institution_id: institution.id,
template_id: template.id,
name: 'Test Cohort',
program_type: 'learnership',
sponsor_email: 'sponsor@example.com'
)
# Soft delete
cohort.update!(deleted_at: Time.current)
# Record still exists in database (using unscoped to bypass default scope)
expect(Cohort.unscoped.find(cohort.id)).to be_present
expect(Cohort.unscoped.find(cohort.id).deleted_at).to be_present
# But not visible in default scope
expect(Cohort.find_by(id: cohort.id)).to be_nil
end
end
describe 'query performance' do
it 'uses indexes for cohort queries' do
# Setup
institution = Institution.create!(name: 'Perf Test', email: 'perf@example.com')
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Perf Template',
schema: '[]',
fields: '[]',
submitters: '[]'
)
# Create test data
10.times do |i|
cohort = Cohort.create!(
institution_id: institution.id,
template_id: template.id,
name: "Cohort #{i}",
program_type: 'learnership',
sponsor_email: "sponsor#{i}@example.com",
status: i.even? ? 'active' : 'draft'
)
5.times do |j|
submission = Submission.create!(
account_id: account.id,
template_id: template.id,
slug: "slug-#{i}-#{j}-#{SecureRandom.hex(2)}",
variables: '{}'
)
CohortEnrollment.create!(
cohort_id: cohort.id,
submission_id: submission.id,
student_email: "student#{i}-#{j}@example.com",
status: i.even? ? 'complete' : 'waiting'
)
end
end
# Query with EXPLAIN to verify index usage
# Note: With small datasets, query planner may choose Seq Scan
# The important thing is that indexes exist and are valid
explain = Cohort.where(institution_id: institution.id, status: 'active').explain.inspect
expect(explain).to match(/Index Scan|Seq Scan|index/)
# Query with joins - verify the query executes without error
# Index usage depends on data size and query planner decisions
results = Cohort
.joins(:cohort_enrollments)
.where(cohort_enrollments: { status: 'complete' })
.to_a
expect(results.length).to be > 0
end
it 'performs well with large datasets' do
# Measure query time
start_time = Time.current
results = Cohort
.joins(:institution, :template)
.where(status: 'active')
.includes(:cohort_enrollments)
.limit(100)
.to_a
end_time = Time.current
query_time = (end_time - start_time) * 1000 # in ms
expect(query_time).to be < 120 # NFR1: DB query < 120ms
end
end
describe 'backward compatibility' do
it 'does not modify existing DocuSeal tables' do
# Check that existing tables still have their original structure
template_columns = ActiveRecord::Base.connection.columns(:templates).map(&:name)
expect(template_columns).to include('account_id', 'author_id', 'name', 'schema', 'fields', 'submitters')
submission_columns = ActiveRecord::Base.connection.columns(:submissions).map(&:name)
expect(submission_columns).to include('account_id', 'template_id', 'slug')
# Verify no new columns were added to existing tables
expect(template_columns).to_not include('flo_doc_specific')
expect(submission_columns).to_not include('flo_doc_specific')
end
it 'allows existing DocuSeal workflows to continue working' do
# Create a standard DocuSeal workflow
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Standard Template',
schema: '[]',
fields: '[]',
submitters: '[]'
)
submission = Submission.create!(
account_id: account.id,
template_id: template.id,
slug: "standard-slug-#{SecureRandom.hex(4)}",
variables: '{}'
)
submitter = Submitter.create!(
account_id: account.id,
submission_id: submission.id,
email: 'submitter@example.com',
name: 'Submitter',
uuid: SecureRandom.uuid
)
# Verify standard workflow still works
expect(template.submissions.count).to eq(1)
expect(submission.submitters.count).to eq(1)
expect(account.templates.count).to eq(1)
end
end
describe 'state machine readiness' do
it 'supports cohort status transitions' do
institution = Institution.create!(name: 'Test', email: 'test@example.com')
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Test',
schema: '[]',
fields: '[]',
submitters: '[]'
)
cohort = Cohort.create!(
institution_id: institution.id,
template_id: template.id,
name: 'Test',
program_type: 'learnership',
sponsor_email: 'test@example.com',
status: 'draft'
)
# Status transitions
expect(cohort.status).to eq('draft')
cohort.update!(status: 'active')
expect(cohort.status).to eq('active')
cohort.update!(status: 'completed')
expect(cohort.status).to eq('completed')
end
it 'tracks workflow timestamps' do
institution = Institution.create!(name: 'Test', email: 'test@example.com')
template = Template.create!(
account_id: account.id,
author_id: user.id,
name: 'Test',
schema: '[]',
fields: '[]',
submitters: '[]'
)
cohort = Cohort.create!(
institution_id: institution.id,
template_id: template.id,
name: 'Test',
program_type: 'learnership',
sponsor_email: 'test@example.com'
)
# Initially nil
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
# Set timestamps
time = Time.current
cohort.update!(
tp_signed_at: time,
students_completed_at: time + 1.hour,
sponsor_completed_at: time + 2.hours,
finalized_at: time + 3.hours
)
expect(cohort.tp_signed_at).to be_within(1.second).of(time)
expect(cohort.students_completed_at).to be_within(1.second).of(time + 1.hour)
end
end
end