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.
290 lines
11 KiB
290 lines
11 KiB
# frozen_string_literal: true
|
|
|
|
# Migration Spec: Create FloDoc Tables
|
|
# Purpose: Verify migration correctness, reversibility, and data integrity
|
|
# Coverage: Core migration functionality
|
|
|
|
require 'rails_helper'
|
|
require_relative '../../db/migrate/20260114000001_create_flo_doc_tables'
|
|
|
|
RSpec.describe CreateFloDocTables, type: :migration do
|
|
let(:migration) { described_class.new }
|
|
let(:conn) { ActiveRecord::Base.connection }
|
|
|
|
# Helper to drop tables for testing
|
|
def drop_tables_if_exist
|
|
%i[cohort_enrollments cohorts institutions].each do |table|
|
|
conn.drop_table(table, if_exists: true)
|
|
end
|
|
end
|
|
|
|
# Helper to drop FKs
|
|
def drop_fks_if_exist
|
|
%i[cohorts cohort_enrollments].each do |table|
|
|
conn.foreign_keys(table).each do |fk|
|
|
conn.remove_foreign_key(table, name: fk.name)
|
|
end
|
|
end
|
|
rescue StandardError
|
|
# Ignore errors if FKs don't exist
|
|
end
|
|
|
|
# Ensure clean state before each test
|
|
before do
|
|
drop_fks_if_exist
|
|
drop_tables_if_exist
|
|
end
|
|
|
|
after do
|
|
drop_fks_if_exist
|
|
drop_tables_if_exist
|
|
end
|
|
|
|
describe 'tables creation' do
|
|
it 'creates institutions table' do
|
|
expect { migration.change }.to change { conn.table_exists?(:institutions) }.from(false).to(true)
|
|
end
|
|
|
|
it 'creates cohorts table' do
|
|
expect { migration.change }.to change { conn.table_exists?(:cohorts) }.from(false).to(true)
|
|
end
|
|
|
|
it 'creates cohort_enrollments table' do
|
|
expect { migration.change }.to change { conn.table_exists?(:cohort_enrollments) }.from(false).to(true)
|
|
end
|
|
end
|
|
|
|
describe 'schema validation' do
|
|
before { migration.change }
|
|
|
|
it 'has correct columns for institutions' do
|
|
columns = conn.columns(:institutions).map(&:name)
|
|
expect(columns).to include('name', 'email', 'contact_person', 'phone',
|
|
'settings', 'created_at', 'updated_at', 'deleted_at')
|
|
end
|
|
|
|
it 'has correct columns for cohorts' do
|
|
columns = conn.columns(:cohorts).map(&:name)
|
|
expect(columns).to include('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 'has correct columns for cohort_enrollments' do
|
|
columns = conn.columns(:cohort_enrollments).map(&:name)
|
|
expect(columns).to include('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
|
|
end
|
|
|
|
describe 'column types and constraints' do
|
|
before { migration.change }
|
|
|
|
it 'has JSONB columns for flexible data' do
|
|
# Institutions settings
|
|
settings_column = conn.columns(:institutions).find { |c| c.name == 'settings' }
|
|
expect(settings_column.type).to eq(:jsonb)
|
|
|
|
# Cohorts required_student_uploads and metadata
|
|
uploads_column = conn.columns(:cohorts).find { |c| c.name == 'required_student_uploads' }
|
|
expect(uploads_column.type).to eq(:jsonb)
|
|
metadata_column = conn.columns(:cohorts).find { |c| c.name == 'cohort_metadata' }
|
|
expect(metadata_column.type).to eq(:jsonb)
|
|
|
|
# CohortEnrollments uploaded_documents and values
|
|
docs_column = conn.columns(:cohort_enrollments).find { |c| c.name == 'uploaded_documents' }
|
|
expect(docs_column.type).to eq(:jsonb)
|
|
values_column = conn.columns(:cohort_enrollments).find { |c| c.name == 'values' }
|
|
expect(values_column.type).to eq(:jsonb)
|
|
end
|
|
|
|
it 'has NOT NULL constraints on required fields' do
|
|
# Institutions
|
|
name_column = conn.columns(:institutions).find { |c| c.name == 'name' }
|
|
expect(name_column.null).to be false
|
|
email_column = conn.columns(:institutions).find { |c| c.name == 'email' }
|
|
expect(email_column.null).to be false
|
|
|
|
# Cohorts
|
|
institution_id_column = conn.columns(:cohorts).find { |c| c.name == 'institution_id' }
|
|
expect(institution_id_column.null).to be false
|
|
template_id_column = conn.columns(:cohorts).find { |c| c.name == 'template_id' }
|
|
expect(template_id_column.null).to be false
|
|
name_column = conn.columns(:cohorts).find { |c| c.name == 'name' }
|
|
expect(name_column.null).to be false
|
|
program_type_column = conn.columns(:cohorts).find { |c| c.name == 'program_type' }
|
|
expect(program_type_column.null).to be false
|
|
sponsor_email_column = conn.columns(:cohorts).find { |c| c.name == 'sponsor_email' }
|
|
expect(sponsor_email_column.null).to be false
|
|
|
|
# CohortEnrollments
|
|
cohort_id_column = conn.columns(:cohort_enrollments).find { |c| c.name == 'cohort_id' }
|
|
expect(cohort_id_column.null).to be false
|
|
submission_id_column = conn.columns(:cohort_enrollments).find { |c| c.name == 'submission_id' }
|
|
expect(submission_id_column.null).to be false
|
|
student_email_column = conn.columns(:cohort_enrollments).find { |c| c.name == 'student_email' }
|
|
expect(student_email_column.null).to be false
|
|
end
|
|
|
|
it 'has default values for status fields' do
|
|
# Cohorts status
|
|
cohort_status_column = conn.columns(:cohorts).find { |c| c.name == 'status' }
|
|
expect(cohort_status_column.default).to eq('draft')
|
|
|
|
# CohortEnrollments status and role
|
|
enrollment_status_column = conn.columns(:cohort_enrollments).find { |c| c.name == 'status' }
|
|
expect(enrollment_status_column.default).to eq('waiting')
|
|
role_column = conn.columns(:cohort_enrollments).find { |c| c.name == 'role' }
|
|
expect(role_column.default).to eq('student')
|
|
end
|
|
end
|
|
|
|
describe 'indexes' do
|
|
before { migration.change }
|
|
|
|
it 'creates correct indexes on cohorts' do
|
|
expect(conn.index_exists?(:cohorts, %i[institution_id status])).to be true
|
|
expect(conn.index_exists?(:cohorts, :template_id)).to be true
|
|
expect(conn.index_exists?(:cohorts, :sponsor_email)).to be true
|
|
end
|
|
|
|
it 'creates correct indexes on cohort_enrollments' do
|
|
expect(conn.index_exists?(:cohort_enrollments, %i[cohort_id status])).to be true
|
|
expect(conn.index_exists?(:cohort_enrollments, %i[cohort_id student_email], unique: true)).to be true
|
|
expect(conn.index_exists?(:cohort_enrollments, [:submission_id], unique: true)).to be true
|
|
end
|
|
end
|
|
|
|
describe 'foreign keys' do
|
|
before { migration.change }
|
|
|
|
it 'creates foreign keys for cohorts' do
|
|
expect(conn.foreign_key_exists?(:cohorts, :institutions)).to be true
|
|
expect(conn.foreign_key_exists?(:cohorts, :templates)).to be true
|
|
end
|
|
|
|
it 'creates foreign keys for cohort_enrollments' do
|
|
expect(conn.foreign_key_exists?(:cohort_enrollments, :cohorts)).to be true
|
|
expect(conn.foreign_key_exists?(:cohort_enrollments, :submissions)).to be true
|
|
end
|
|
end
|
|
|
|
describe 'reversibility' do
|
|
# Reversibility tests need clean state - no before hook
|
|
it 'is reversible' do
|
|
# Ensure clean state
|
|
drop_fks_if_exist
|
|
drop_tables_if_exist
|
|
|
|
# Tables should not exist before running migration
|
|
expect(conn.table_exists?(:institutions)).to be false
|
|
|
|
expect { migration.change }.not_to raise_error
|
|
migration.down
|
|
|
|
expect(conn.table_exists?(:institutions)).to be false
|
|
expect(conn.table_exists?(:cohorts)).to be false
|
|
expect(conn.table_exists?(:cohort_enrollments)).to be false
|
|
end
|
|
|
|
it 'removes indexes on rollback' do
|
|
# Ensure clean state
|
|
drop_fks_if_exist
|
|
drop_tables_if_exist
|
|
|
|
migration.change
|
|
migration.down
|
|
|
|
expect(conn.index_exists?(:cohorts, %i[institution_id status])).to be false
|
|
expect(conn.index_exists?(:cohort_enrollments, %i[cohort_id student_email], unique: true)).to be false
|
|
end
|
|
|
|
it 'removes foreign keys on rollback' do
|
|
# Ensure clean state
|
|
drop_fks_if_exist
|
|
drop_tables_if_exist
|
|
|
|
migration.change
|
|
migration.down
|
|
|
|
expect(conn.foreign_key_exists?(:cohorts, :institutions)).to be false
|
|
expect(conn.foreign_key_exists?(:cohort_enrollments, :submissions)).to be false
|
|
end
|
|
end
|
|
|
|
describe 'data integrity constraints' do
|
|
before { migration.change }
|
|
|
|
it 'enforces NOT NULL via database constraints' do
|
|
# Institutions - name
|
|
expect do
|
|
conn.execute(
|
|
'INSERT INTO institutions (email, created_at, updated_at) ' \
|
|
'VALUES (' + 'test@example.com' + ', NOW(), NOW())'
|
|
)
|
|
end.to raise_error(ActiveRecord::StatementInvalid)
|
|
|
|
# Institutions - email
|
|
expect do
|
|
conn.execute(
|
|
'INSERT INTO institutions (name, created_at, updated_at) ' \
|
|
'VALUES (' + 'Test' + ', NOW(), NOW())'
|
|
)
|
|
end.to raise_error(ActiveRecord::StatementInvalid)
|
|
|
|
# Cohorts - name (without required fields)
|
|
expect do
|
|
conn.execute(
|
|
'INSERT INTO cohorts (institution_id, template_id, program_type, sponsor_email, created_at, updated_at) ' \
|
|
'VALUES (1, 1, ' + 'learnership' + ', ' + 'test@example.com' + ', NOW(), NOW())'
|
|
)
|
|
end.to raise_error(ActiveRecord::StatementInvalid)
|
|
|
|
# CohortEnrollments - student_email
|
|
expect do
|
|
conn.execute(
|
|
'INSERT INTO cohort_enrollments (cohort_id, submission_id, created_at, updated_at) ' \
|
|
'VALUES (1, 1, NOW(), NOW())'
|
|
)
|
|
end.to raise_error(ActiveRecord::StatementInvalid)
|
|
end
|
|
|
|
it 'prevents orphaned records via foreign keys' do
|
|
# Try to create cohort with non-existent institution
|
|
expect do
|
|
conn.execute(
|
|
'INSERT INTO cohorts (institution_id, template_id, name, program_type, sponsor_email, created_at, updated_at) ' \
|
|
'VALUES (999999, 1, ' + 'Test' + ', ' + 'learnership' + ', ' + 'test@example.com' + ', NOW(), NOW())'
|
|
)
|
|
end.to raise_error(ActiveRecord::StatementInvalid)
|
|
|
|
# Try to create enrollment with non-existent cohort
|
|
expect do
|
|
conn.execute(
|
|
'INSERT INTO cohort_enrollments (cohort_id, submission_id, student_email, created_at, updated_at) ' \
|
|
'VALUES (999999, 1, ' + 'test@example.com' + ', NOW(), NOW())'
|
|
)
|
|
end.to raise_error(ActiveRecord::StatementInvalid)
|
|
end
|
|
end
|
|
|
|
describe 'default values and JSONB structure' do
|
|
before { migration.change }
|
|
|
|
it 'creates institutions with correct defaults' do
|
|
conn.execute(
|
|
'INSERT INTO institutions (name, email, created_at, updated_at) ' \
|
|
'VALUES (' + 'Test' + ', ' + 'test@example.com' + ', NOW(), NOW())'
|
|
)
|
|
result = conn.select_one('SELECT settings, deleted_at FROM institutions WHERE name = Test')
|
|
# JSONB returns string in raw SQL, but empty object
|
|
expect(result['settings']).to be_in([{}, '{}'])
|
|
expect(result['deleted_at']).to be_nil
|
|
end
|
|
end
|
|
end
|