mirror of https://github.com/docusealco/docuseal
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.
464 lines
14 KiB
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
|
|
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).not_to include('flo_doc_specific')
|
|
expect(submission_columns).not_to 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.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
|