Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: adding more FDv2 tests #768

Merged
merged 3 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -499,5 +499,143 @@ describe.each(['caching', 'non-caching'])(
const allValues = await asyncWrapper.all(VersionedDataKinds.Features);
expect(allValues).toEqual({});
});

it('applyChanges with basis results in initialization', async () => {
await asyncWrapper.applyChanges(
true,
{
features: {
key1: {
version: 1,
},
},
},
'selector1',
);

expect(await asyncWrapper.initialized()).toBeTruthy();
expect(await asyncWrapper.all(VersionedDataKinds.Features)).toEqual({
key1: {
version: 1,
},
});
});

it('applyChanges with basis overwrites existing data', async () => {
await asyncWrapper.applyChanges(
true,
{
features: {
oldFeature: {
version: 1,
},
},
},
'selector1',
);

expect(await asyncWrapper.all(VersionedDataKinds.Features)).toEqual({
oldFeature: {
version: 1,
},
});

await asyncWrapper.applyChanges(
true,
{
features: {
newFeature: {
version: 1,
},
},
},
'selector1',
);

expect(await asyncWrapper.all(VersionedDataKinds.Features)).toEqual({
newFeature: {
version: 1,
},
});
});

it('applyChanges callback fires after all upserts complete', async () => {
let callbackCount = 0;
jest
.spyOn(mockPersistentStore, 'upsert')
.mockImplementation(async (_kind, _key, _data, cb) => {
callbackCount += 1;
// this await gives chance for execution to continue elsewhere. If there is a bug, this will lead to a failure
await new Promise((f) => {
setTimeout(f, 1);
});
cb();
});

await asyncWrapper.applyChanges(
false,
{
features: {
key1: {
version: 1,
},
key2: {
version: 1,
},
key3: {
version: 1,
},
},
},
'selector',
);
expect(callbackCount).toEqual(3);
});

it('applyChanges with basis=false merges correctly', async () => {
await asyncWrapper.applyChanges(
true,
{
features: {
key1: {
version: 1,
},
key2: {
version: 1,
},
},
},
'selector',
);

await asyncWrapper.applyChanges(
false,
{
features: {
key1: {
version: 2,
},
key3: {
version: 1,
},
},
},
'selector',
);

expect(await asyncWrapper.all(VersionedDataKinds.Features)).toEqual({
key1: {
key: 'key1',
version: 2,
},
key2: {
version: 1,
},
key3: {
key: 'key3',
version: 1,
},
});
});
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { LDFeatureStore } from '../../src/api/subsystems';
import AsyncStoreFacade from '../../src/store/AsyncStoreFacade';
import InMemoryFeatureStore from '../../src/store/InMemoryFeatureStore';
import TransactionalPersistentStore from '../../src/store/TransactionalPersistentStore';
import VersionedDataKinds from '../../src/store/VersionedDataKinds';

describe('given a non transactional store', () => {
let mockNontransactionalStore: LDFeatureStore;
let transactionalStore: TransactionalPersistentStore;

let nonTransactionalFacade: AsyncStoreFacade;
let transactionalFacade: AsyncStoreFacade;

beforeEach(() => {
mockNontransactionalStore = new InMemoryFeatureStore();
transactionalStore = new TransactionalPersistentStore(mockNontransactionalStore);

// these two facades are used to make test writing easier
nonTransactionalFacade = new AsyncStoreFacade(mockNontransactionalStore);
transactionalFacade = new AsyncStoreFacade(transactionalStore);
});

afterEach(() => {
transactionalFacade.close();
jest.restoreAllMocks();
});

it('applies changes to non transactional store', async () => {
await transactionalFacade.applyChanges(
false,
{
features: {
key1: {
version: 2,
},
},
},
'selector1',
);
expect(await nonTransactionalFacade.all(VersionedDataKinds.Features)).toEqual({
key1: {
key: 'key1',
version: 2,
},
});
expect(await transactionalFacade.all(VersionedDataKinds.Features)).toEqual({
key1: {
key: 'key1',
version: 2,
},
});
});

it('it reads through to non transactional store before basis is provided', async () => {
await nonTransactionalFacade.init({
features: {
key1: {
version: 1,
},
},
});
expect(await transactionalFacade.all(VersionedDataKinds.Features)).toEqual({
key1: {
version: 1,
},
});
});

it('it switches to memory store when basis is provided', async () => {
// situate some mock data in non transactional store
await nonTransactionalFacade.init({
features: {
nontransactionalFeature: {
version: 1,
},
},
});

await transactionalFacade.applyChanges(
true,
{
features: {
key1: {
version: 1,
},
},
},
'selector1',
);

expect(await nonTransactionalFacade.all(VersionedDataKinds.Features)).toEqual({
key1: {
version: 1,
},
});

expect(await transactionalFacade.all(VersionedDataKinds.Features)).toEqual({
key1: {
version: 1,
},
});

// corrupt non transactional store and then read from transactional store to prove it is not
// using underlying non transactional store for reads
await nonTransactionalFacade.init({
features: {
nontransactionalFeature: {
version: 1,
},
},
});

// still should read from memory
expect(await transactionalFacade.all(VersionedDataKinds.Features)).toEqual({
key1: {
version: 1,
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ export interface LDFeatureStore {

/**
* Applies the provided data onto the existing data, replacing all data or upserting depending
* on the basis parameter.
* on the basis parameter. Must call {@link applyChanges} providing basis before calling {@link applyChanges}
* that is not a basis.
*
* @param basis If true, completely overwrites the current contents of the data store
* with the provided data. If false, upserts the items in the provided data. Upserts
Expand Down
10 changes: 10 additions & 0 deletions packages/shared/sdk-server/src/store/AsyncStoreFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export default class AsyncStoreFacade {
});
}

async applyChanges(
basis: boolean,
data: LDFeatureStoreDataStorage,
selector: String | undefined,
): Promise<void> {
return promisify((cb) => {
this._store.applyChanges(basis, data, selector, cb);
});
}

close(): void {
this._store.close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ export default class InMemoryFeatureStore implements LDFeatureStore {
const old = existingItems[key];
// TODO: SDK-1046 - Determine if version check should be removed
if (!old || old.version < item.version) {
existingItems[key] = item;
existingItems[key] = { key, ...item };
}
} else {
existingItems[key] = item;
existingItems[key] = { key, ...item };
}
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default class TransactionalPersistentStore implements LDFeatureStore {
private _activeStore: LDFeatureStore;

constructor(private readonly _nonTransPersistenceStore: LDFeatureStore) {
// persistence store is inital active store
this._activeStore = this._nonTransPersistenceStore;
this._memoryStore = new InMemoryFeatureStore();
}
Expand Down
8 changes: 7 additions & 1 deletion packages/shared/sdk-server/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import AsyncStoreFacade from './AsyncStoreFacade';
import PersistentDataStoreWrapper from './PersistentDataStoreWrapper';
import { deserializePoll } from './serialization';
import TransactionalPersistentStore from './TransactionalPersistentStore';

export { AsyncStoreFacade, PersistentDataStoreWrapper, deserializePoll };
export {
AsyncStoreFacade,
PersistentDataStoreWrapper,
TransactionalPersistentStore,
deserializePoll,
};