unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
504 lines • 17.5 kB
JavaScript
import { APPLICATION_CREATED, FEATURE_CREATED, FEATURE_DELETED, FEATURE_TAGGED, FEATURE_UPDATED, SEGMENT_UPDATED, } from '../../../lib/events/index.js';
import { FeatureCreatedEvent, FeatureDeletedEvent, FeatureTaggedEvent, FeatureUpdatedEvent, } from '../../../lib/types/index.js';
import dbInit from '../helpers/database-init.js';
import getLogger from '../../fixtures/no-logger.js';
import { withTransactional, } from '../../../lib/db/transaction.js';
import { EventStore } from '../../../lib/features/events/event-store.js';
import { vi } from 'vitest';
let db;
let stores;
let eventStore;
const TEST_USER_ID = -9999;
const testAudit = {
id: TEST_USER_ID,
username: 'test@example.com',
ip: '127.0.0.1',
};
beforeAll(async () => {
db = await dbInit('event_store_serial', getLogger);
stores = db.stores;
eventStore = stores.eventStore;
});
beforeEach(async () => {
await eventStore.deleteAll();
});
afterAll(async () => {
if (db) {
await db.destroy();
}
});
test('Should include id and createdAt when saving', async () => {
vi.useFakeTimers();
const event1 = {
type: APPLICATION_CREATED,
createdBy: '127.0.0.1',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: {
clientIp: '127.0.0.1',
appName: 'test1',
},
};
const seen = [];
eventStore.on(APPLICATION_CREATED, (e) => seen.push(e));
await eventStore.store(event1);
await eventStore.publishUnannouncedEvents();
expect(seen).toHaveLength(1);
expect(seen[0].id).toBeTruthy();
expect(seen[0].createdAt).toBeTruthy();
expect(seen[0].data.clientIp).toBe(event1.data.clientIp);
expect(seen[0].data.appName).toBe(event1.data.appName);
vi.useRealTimers();
});
test('Should include empty tags array for new event', async () => {
expect.assertions(2);
const event = {
type: FEATURE_CREATED,
createdBy: 'me@mail.com',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: {
name: 'someName',
enabled: true,
strategies: [{ name: 'default' }],
},
};
const promise = new Promise((resolve) => {
eventStore.on(FEATURE_CREATED, (storedEvent) => {
expect(storedEvent.data.name).toBe(event.data.name);
expect(Array.isArray(storedEvent.tags)).toBe(true);
resolve();
});
});
// Trigger
await eventStore.store(event);
await eventStore.publishUnannouncedEvents();
return promise;
});
test('Should be able to store multiple events at once', async () => {
vi.useFakeTimers();
const event1 = {
type: APPLICATION_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: '127.0.0.1',
data: {
clientIp: '127.0.0.1',
appName: 'test1',
},
ip: '127.0.0.1',
};
const event2 = {
type: APPLICATION_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: '127.0.0.1',
data: {
clientIp: '127.0.0.1',
appName: 'test2',
},
ip: '127.0.0.1',
};
const event3 = {
type: APPLICATION_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: '127.0.0.1',
data: {
clientIp: '127.0.0.1',
appName: 'test3',
},
tags: [{ type: 'simple', value: 'mytest' }],
ip: '127.0.0.1',
};
const seen = [];
eventStore.on(APPLICATION_CREATED, (e) => seen.push(e));
await eventStore.batchStore([event1, event2, event3]);
await eventStore.publishUnannouncedEvents();
expect(seen.length).toBe(3);
seen.forEach((e) => {
expect(e.id).toBeTruthy();
expect(e.createdAt).toBeTruthy();
});
vi.useRealTimers();
});
test('Should get all stored events', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'me@mail.com',
createdByUserId: TEST_USER_ID,
data: {
name: 'someName',
enabled: true,
strategies: [{ name: 'default' }],
},
ip: '127.0.0.1',
};
await eventStore.store(event);
const events = await eventStore.getAll();
const lastEvent = events[0];
expect(lastEvent.type).toBe(event.type);
expect(lastEvent.createdBy).toBe(event.createdBy);
});
test('Should delete stored event', async () => {
const event = {
type: FEATURE_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: 'me@mail.com',
data: {
name: 'someName',
enabled: true,
strategies: [{ name: 'default' }],
},
ip: '127.0.0.1',
};
await eventStore.store(event);
await eventStore.store(event);
const events = await eventStore.getAll();
const lastEvent = events[0];
await eventStore.delete(lastEvent.id);
const eventsAfterDelete = await eventStore.getAll();
const lastEventAfterDelete = eventsAfterDelete[0];
expect(events.length - eventsAfterDelete.length).toBe(1);
expect(lastEventAfterDelete.id).not.toBe(lastEvent.id);
});
test('Should get stored event by id', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'me@mail.com',
createdByUserId: TEST_USER_ID,
data: {
name: 'someName',
enabled: true,
strategies: [{ name: 'default' }],
},
ip: '127.0.0.1',
};
await eventStore.store(event);
const events = await eventStore.getAll();
const lastEvent = events[0];
const exists = await eventStore.exists(lastEvent.id);
const byId = await eventStore.get(lastEvent.id);
expect(lastEvent).toStrictEqual(byId);
expect(exists).toBe(true);
});
test('Should delete all stored events', async () => {
await eventStore.deleteAll();
const events = await eventStore.getAll();
expect(events).toHaveLength(0);
});
test('Should get all events of type', async () => {
const data = { name: 'someName', project: 'test-project' };
await Promise.all([0, 1, 2, 3, 4, 5].map(async (id) => {
const event = id % 2 === 0
? new FeatureCreatedEvent({
project: data.project,
featureName: data.name,
auditUser: testAudit,
data,
})
: new FeatureDeletedEvent({
project: data.project,
preData: data,
featureName: data.name,
auditUser: testAudit,
tags: [],
});
return eventStore.store(event);
}));
const featureCreatedEvents = await eventStore.searchEvents({
offset: 0,
limit: 10,
}, [
{
field: 'type',
operator: 'IS',
values: [FEATURE_CREATED],
},
]);
expect(featureCreatedEvents).toHaveLength(3);
const featureDeletedEvents = await eventStore.searchEvents({
offset: 0,
limit: 10,
}, [
{
field: 'type',
operator: 'IS',
values: [FEATURE_DELETED],
},
]);
expect(featureDeletedEvents).toHaveLength(3);
});
test('getMaxRevisionId should exclude FEATURE_CREATED and FEATURE_TAGGED events', async () => {
const featureName = 'test-feature';
const project = 'test-project';
const featureCreatedEvent = new FeatureCreatedEvent({
project,
featureName,
auditUser: testAudit,
data: { name: featureName, project },
});
const featureTaggedEvent = new FeatureTaggedEvent({
project,
featureName,
auditUser: testAudit,
data: { type: 'simple', value: 'test-tag' },
});
const featureUpdatedEvent = new FeatureUpdatedEvent({
project,
featureName,
auditUser: testAudit,
data: { name: featureName, enabled: false },
});
const segmentUpdatedEvent = {
type: SEGMENT_UPDATED,
createdBy: testAudit.username,
createdByUserId: testAudit.id,
ip: testAudit.ip,
data: { id: 1, name: 'test-segment' },
};
await eventStore.store(featureCreatedEvent);
const maxRevisionAfterCreated = await eventStore.getMaxRevisionId();
await eventStore.store(featureTaggedEvent);
const maxRevisionAfterTagged = await eventStore.getMaxRevisionId();
await eventStore.store(featureUpdatedEvent);
const maxRevisionAfterUpdated = await eventStore.getMaxRevisionId();
await eventStore.store(segmentUpdatedEvent);
const maxRevisionAfterSegment = await eventStore.getMaxRevisionId();
const allEvents = await eventStore.getAll();
const createdEvent = allEvents.find((e) => e.type === FEATURE_CREATED);
const taggedEvent = allEvents.find((e) => e.type === FEATURE_TAGGED);
const updatedEvent = allEvents.find((e) => e.type === FEATURE_UPDATED);
const segmentEvent = allEvents.find((e) => e.type === SEGMENT_UPDATED);
expect(maxRevisionAfterCreated).toBe(0);
expect(maxRevisionAfterTagged).toBe(0);
expect(maxRevisionAfterUpdated).toBe(updatedEvent.id);
expect(maxRevisionAfterSegment).toBe(segmentEvent.id);
expect(createdEvent).toBeDefined();
expect(taggedEvent).toBeDefined();
expect(updatedEvent).toBeDefined();
expect(segmentEvent).toBeDefined();
expect(updatedEvent.id).toBeGreaterThan(createdEvent.id);
expect(updatedEvent.id).toBeGreaterThan(taggedEvent.id);
expect(segmentEvent.id).toBeGreaterThan(updatedEvent.id);
});
test('Should filter events by ID using IS operator', async () => {
const event1 = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: { name: 'feature1' },
};
const event2 = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: { name: 'feature2' },
};
const event3 = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: { name: 'feature3' },
};
await eventStore.store(event1);
await eventStore.store(event2);
await eventStore.store(event3);
const allEvents = await eventStore.getAll();
const targetEvent = allEvents.find((e) => e.data.name === 'feature2');
const filteredEvents = await eventStore.searchEvents({
offset: 0,
limit: 10,
}, [
{
field: 'id',
operator: 'IS',
values: [targetEvent.id.toString()],
},
]);
expect(filteredEvents).toHaveLength(1);
expect(filteredEvents[0].id).toBe(targetEvent.id);
expect(filteredEvents[0].data.name).toBe('feature2');
});
test('Should filter events by ID using IS_ANY_OF operator', async () => {
const event1 = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: { name: 'feature1' },
};
const event2 = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: { name: 'feature2' },
};
const event3 = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: { name: 'feature3' },
};
await eventStore.store(event1);
await eventStore.store(event2);
await eventStore.store(event3);
const allEvents = await eventStore.getAll();
const targetEvent1 = allEvents.find((e) => e.data.name === 'feature1');
const targetEvent3 = allEvents.find((e) => e.data.name === 'feature3');
const filteredEvents = await eventStore.searchEvents({
offset: 0,
limit: 10,
}, [
{
field: 'id',
operator: 'IS_ANY_OF',
values: [
targetEvent1.id.toString(),
targetEvent3.id.toString(),
],
},
]);
expect(filteredEvents).toHaveLength(2);
const eventIds = filteredEvents.map((e) => e.id);
expect(eventIds).toContain(targetEvent1.id);
expect(eventIds).toContain(targetEvent3.id);
expect(eventIds).not.toContain(allEvents.find((e) => e.data.name === 'feature2').id);
});
test('Should return empty result when filtering by non-existent ID', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
data: { name: 'feature1' },
};
await eventStore.store(event);
const filteredEvents = await eventStore.searchEvents({
offset: 0,
limit: 10,
}, [
{
field: 'id',
operator: 'IS',
values: ['999999'],
},
]);
expect(filteredEvents).toHaveLength(0);
});
test('Should store and retrieve transaction context fields', async () => {
const mockTransactionContext = {
type: 'change-request',
id: '01HQVX5K8P9EXAMPLE123456',
};
const eventStoreService = withTransactional((db) => new EventStore(db, getLogger), db.rawDatabase);
const event = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'test-feature-with-context',
project: 'test-project',
ip: '127.0.0.1',
data: {
name: 'test-feature-with-context',
enabled: true,
strategies: [{ name: 'default' }],
},
};
await eventStoreService.transactional(async (transactionalEventStore) => {
await transactionalEventStore.store(event);
}, mockTransactionContext);
const events = await eventStore.getAll();
const storedEvent = events.find((e) => e.featureName === 'test-feature-with-context');
expect(storedEvent).toBeTruthy();
expect(storedEvent.groupType).toBe('change-request');
expect(storedEvent.groupId).toBe('01HQVX5K8P9EXAMPLE123456');
});
test('Should handle missing transaction context gracefully', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'test-feature-no-context',
project: 'test-project',
ip: '127.0.0.1',
data: {
name: 'test-feature-no-context',
enabled: true,
strategies: [{ name: 'default' }],
},
};
await eventStore.store(event);
const events = await eventStore.getAll();
const storedEvent = events.find((e) => e.featureName === 'test-feature-no-context');
expect(storedEvent).toBeTruthy();
expect(storedEvent.groupType).toBeUndefined();
expect(storedEvent.groupId).toBeUndefined();
});
test('Should store transaction context in batch operations', async () => {
const mockTransactionContext = {
type: 'transaction',
id: '01HQVX5K8P9BATCH123456',
};
const eventStoreService = withTransactional((db) => new EventStore(db, getLogger), db.rawDatabase);
const events = [
{
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'batch-feature-1',
project: 'test-project',
ip: '127.0.0.1',
data: { name: 'batch-feature-1' },
},
{
type: FEATURE_UPDATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'batch-feature-2',
project: 'test-project',
ip: '127.0.0.1',
data: { name: 'batch-feature-2' },
},
];
await eventStoreService.transactional(async (transactionalEventStore) => {
await transactionalEventStore.batchStore(events);
}, mockTransactionContext);
const allEvents = await eventStore.getAll();
const batchEvents = allEvents.filter((e) => e.featureName === 'batch-feature-1' ||
e.featureName === 'batch-feature-2');
expect(batchEvents).toHaveLength(2);
batchEvents.forEach((event) => {
expect(event.groupType).toBe('transaction');
expect(event.groupId).toBe('01HQVX5K8P9BATCH123456');
});
});
test('Should auto-generate transaction context when none provided', async () => {
const eventStoreService = withTransactional((db) => new EventStore(db, getLogger), db.rawDatabase);
const event = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'test-feature-auto-context',
project: 'test-project',
ip: '127.0.0.1',
data: {
name: 'test-feature-auto-context',
enabled: true,
strategies: [{ name: 'default' }],
},
};
await eventStoreService.transactional(async (transactionalEventStore) => {
await transactionalEventStore.store(event);
});
const events = await eventStore.getAll();
const storedEvent = events.find((e) => e.featureName === 'test-feature-auto-context');
expect(storedEvent).toBeTruthy();
expect(storedEvent.groupType).toBe('transaction');
expect(storedEvent.groupId).toBeTruthy();
expect(typeof storedEvent.groupId).toBe('string');
expect(storedEvent.groupId).toMatch(/^[0-9A-HJKMNP-TV-Z]{26}$/);
});
//# sourceMappingURL=event-store.e2e.test.js.map