# 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