diff --git a/src/migrations/20250708173000-amend-users-created-events.js b/src/migrations/20250708173000-amend-users-created-events.js new file mode 100644 index 0000000000..05eaa68dfe --- /dev/null +++ b/src/migrations/20250708173000-amend-users-created-events.js @@ -0,0 +1,42 @@ +exports.up = function(db, cb) { + db.runSql(` + INSERT INTO events (created_at, created_by, type, data, announced) + SELECT + u.created_at AS created_at, + 'unleash_system_user' AS created_by, + 'user-created' AS type, + json_build_object( + 'id', u.id, + 'name', u.name, + 'email', u.email + )::jsonb AS data, + true AS announced + FROM users u + WHERE + is_system = false + AND is_service = false + AND deleted_at IS NULL + AND email IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM events + WHERE type = 'user-created' + AND (data ->> 'id')::int = u.id + LIMIT 1 + ) + ON CONFLICT DO NOTHING; + `, (err, results) => { + if (err) { + console.log('Error inserting user-created events:', err); + return cb(err); + } + if (results.rowCount){ + console.log('Amended user-created event log. Number of new records:', results.rowCount); + } + cb(); + }); +}; + +exports.down = function(db, cb) { + cb(); + // No down migration needed as this is a data backfill. +}; diff --git a/src/test/e2e/api/admin/user-admin.e2e.test.ts b/src/test/e2e/api/admin/user-admin.e2e.test.ts index 2327c4323c..6a55313471 100644 --- a/src/test/e2e/api/admin/user-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/user-admin.e2e.test.ts @@ -1,3 +1,5 @@ +import postgresPkg from 'pg'; +const { Client } = postgresPkg; import { type IUnleashTest, setupAppWithCustomConfig, @@ -19,6 +21,11 @@ import { omitKeys } from '../../../../lib/util/omit-keys.js'; import type { ISessionStore } from '../../../../lib/types/stores/session-store.js'; import type { IUnleashStores } from '../../../../lib/types/index.js'; import { createHash } from 'crypto'; +import { createDb } from '../../../../lib/db/db-pool.js'; +import { migrateDb } from '../../../../migrator.js'; +import { createTestConfig } from '../../../config/test-config.js'; +import { v4 as uuidv4 } from 'uuid'; +import { getDbConfig } from '../../../../lib/server-impl.js'; let stores: IUnleashStores; let db: ITestDb; @@ -31,6 +38,67 @@ let sessionStore: ISessionStore; let editorRole: IRole; let adminRole: IRole; +describe('Users created without an event are amended', () => { + const testDbName = `migration_test_${uuidv4().replace(/-/g, '')}`; + const dbConnConfig = getDbConfig(); + beforeAll(async () => { + // create a new empty database for this test + const client = new Client(dbConnConfig); + await client.connect(); + await client.query(`CREATE DATABASE ${testDbName}`); + await client.end(); + }); + + test('should amend users created without an event', async () => { + // connect to the new database + const config = createTestConfig({ + db: { + ...dbConnConfig, + ssl: false, + database: testDbName, + }, + getLogger, + }); + const db = createDb(config); + // migrate up to the migration we want to test + await migrateDb(config, '20250707153020-unknown-flags-environment.js'); + + const eventsBefore = await db('events').select('*'); + const insertedUser = ( + await db('users') + .insert({ + created_at: new Date('2023-01-01T00:00:00Z'), + email: 'some@getunelash.io', + name: 'Some Name', + }) + .returning('*') + )[0]; + + expect(insertedUser).toBeDefined(); + expect(insertedUser.name).toBe('Some Name'); + expect(insertedUser.created_at).toBeDefined(); + const eventsAfter = await db('events').select('*'); + expect(eventsAfter.length).toBe(eventsBefore.length); + + // apply the rest of migrations + await migrateDb(config); + const eventsPostMigrations = await db('events').select('*'); + expect(eventsPostMigrations.length).toBe(eventsBefore.length + 1); + const userCreatedEvent = eventsPostMigrations.find( + (e) => e.type === USER_CREATED && e.data.id === insertedUser.id, + ); + expect(userCreatedEvent).toMatchObject({ + type: 'user-created', + created_at: new Date('2023-01-01T00:00:00Z'), + data: { + id: insertedUser.id, + email: 'some@getunelash.io', + name: 'Some Name', + }, + }); + }); +}); + describe('User Admin API with email configuration', () => { beforeAll(async () => { db = await dbInit('user_admin_api_serial', getLogger);