Skip to content

Commit 20f2bf6

Browse files
committed
chore(databases-collections): re-introduce old tests; add plugin service mock helper; add new plugin tests
1 parent 80545c9 commit 20f2bf6

File tree

5 files changed

+266
-14
lines changed

5 files changed

+266
-14
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { expect } from 'chai';
2+
import sinon from 'sinon';
3+
import { handleFLE2Options } from './create-namespace';
4+
import type { Binary } from 'bson';
5+
import { UUID } from 'bson';
6+
7+
describe('create collection module', function () {
8+
describe('#handleFLE2Options', function () {
9+
let ds: { createDataKey: sinon.SinonStub<any[], Promise<Binary>> };
10+
let uuid: Binary;
11+
12+
beforeEach(function () {
13+
uuid = new UUID().toBinary();
14+
ds = {
15+
createDataKey: sinon.stub().resolves(uuid),
16+
};
17+
});
18+
19+
it('parses an encryptedFields config', async function () {
20+
expect(
21+
await handleFLE2Options(ds, {
22+
encryptedFields: '',
23+
})
24+
).to.deep.equal({});
25+
expect(
26+
await handleFLE2Options(ds, {
27+
encryptedFields: '{}',
28+
})
29+
).to.deep.equal({});
30+
expect(
31+
await handleFLE2Options(ds, {
32+
encryptedFields: '{ foo: "bar" }',
33+
})
34+
).to.deep.equal({ encryptedFields: { foo: 'bar' } });
35+
});
36+
37+
it('rejects unparseable encryptedFields config', async function () {
38+
try {
39+
await handleFLE2Options(ds, {
40+
encryptedFields: '{',
41+
});
42+
expect.fail('missed exception');
43+
} catch (err) {
44+
expect((err as Error).message).to.include(
45+
'Could not parse encryptedFields config'
46+
);
47+
}
48+
});
49+
50+
it('creates data keys for missing fields if kms and key encryption key are provided', async function () {
51+
expect(
52+
await handleFLE2Options(ds, {
53+
encryptedFields: '{ fields: [{ path: "foo", bsonType: "string" }] }',
54+
kmsProvider: 'local',
55+
keyEncryptionKey: '',
56+
})
57+
).to.deep.equal({
58+
encryptedFields: {
59+
fields: [{ path: 'foo', bsonType: 'string', keyId: uuid }],
60+
},
61+
});
62+
});
63+
64+
it('does not create data keys if encryptedFields.fields is not an array', async function () {
65+
expect(
66+
await handleFLE2Options(ds, {
67+
encryptedFields: '{ fields: {x: "y"} }',
68+
kmsProvider: 'local',
69+
keyEncryptionKey: '',
70+
})
71+
).to.deep.equal({
72+
encryptedFields: {
73+
fields: { x: 'y' },
74+
},
75+
});
76+
});
77+
78+
it('fails when creating data keys fails', async function () {
79+
ds.createDataKey.rejects(new Error('createDataKey failed'));
80+
try {
81+
await handleFLE2Options(ds, {
82+
encryptedFields: '{ fields: [{ path: "foo", bsonType: "string" }] }',
83+
kmsProvider: 'local',
84+
keyEncryptionKey: '',
85+
});
86+
expect.fail('missed exception');
87+
} catch (err) {
88+
expect((err as Error).message).to.equal('createDataKey failed');
89+
}
90+
});
91+
});
92+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import Sinon from 'sinon';
3+
import { CreateNamespacePlugin } from '../index';
4+
import AppRegistry from 'hadron-app-registry';
5+
import {
6+
render,
7+
cleanup,
8+
screen,
9+
waitForElementToBeRemoved,
10+
} from '@testing-library/react';
11+
import userEvent from '@testing-library/user-event';
12+
import { expect } from 'chai';
13+
14+
describe('DropNamespacePlugin', function () {
15+
const sandbox = Sinon.createSandbox();
16+
const appRegistry = sandbox.spy(new AppRegistry());
17+
const dataService = {
18+
createCollection: sandbox.stub().resolves({}),
19+
createDataKey: sandbox.stub().resolves({}),
20+
configuredKMSProviders: sandbox.stub().returns([]),
21+
};
22+
const instance = {
23+
on: sandbox.stub(),
24+
off: sandbox.stub(),
25+
build: { version: '999.999.999' },
26+
topologyDescription: { type: 'Unknown' },
27+
};
28+
29+
beforeEach(function () {
30+
const Plugin = CreateNamespacePlugin.withMockServices({
31+
globalAppRegistry: appRegistry,
32+
dataService,
33+
instance: instance as any,
34+
});
35+
render(<Plugin></Plugin>);
36+
});
37+
38+
afterEach(function () {
39+
sandbox.resetHistory();
40+
cleanup();
41+
});
42+
43+
it('should handle create database flow on `open-create-database` event', async function () {
44+
appRegistry.emit('open-create-database');
45+
46+
expect(screen.getByRole('heading', { name: 'Create Database' })).to.exist;
47+
48+
userEvent.type(
49+
screen.getByRole('textbox', { name: 'Database Name' }),
50+
'db'
51+
);
52+
53+
userEvent.type(
54+
screen.getByRole('textbox', { name: 'Collection Name' }),
55+
'coll1'
56+
);
57+
58+
userEvent.click(screen.getByRole('button', { name: 'Create Database' }));
59+
60+
await waitForElementToBeRemoved(
61+
screen.queryByRole('heading', { name: 'Create Database' })
62+
);
63+
64+
expect(dataService.createCollection).to.have.been.calledOnceWith(
65+
'db.coll1',
66+
{}
67+
);
68+
});
69+
70+
it('should handle create collection flow on `open-create-collection` event', async function () {
71+
appRegistry.emit('open-create-collection', { database: 'db' });
72+
73+
expect(screen.getByRole('heading', { name: 'Create Collection' })).to.exist;
74+
75+
expect(screen.queryByRole('textbox', { name: 'Database Name' })).to.not
76+
.exist;
77+
78+
userEvent.type(
79+
screen.getByRole('textbox', { name: 'Collection Name' }),
80+
'coll2'
81+
);
82+
83+
userEvent.click(screen.getByRole('button', { name: 'Create Collection' }));
84+
85+
await waitForElementToBeRemoved(
86+
screen.queryByRole('heading', { name: 'Create Collection' })
87+
);
88+
89+
expect(dataService.createCollection).to.have.been.calledOnceWith(
90+
'db.coll2',
91+
{}
92+
);
93+
});
94+
});

packages/databases-collections/src/stores/drop-namespace.spec.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import Sinon from 'sinon';
3-
import { DropNamespaceComponent, activatePlugin } from './drop-namespace';
3+
import { DropNamespacePlugin } from '../index';
44
import AppRegistry from 'hadron-app-registry';
55
import toNS from 'mongodb-ns';
66
import { render, cleanup, screen, waitFor } from '@testing-library/react';
@@ -14,19 +14,17 @@ describe('DropNamespacePlugin', function () {
1414
dropDatabase: sandbox.stub().resolves(true),
1515
dropCollection: sandbox.stub().resolves(true),
1616
};
17-
const logger = { track: sandbox.stub() };
1817

1918
beforeEach(function () {
20-
render(<DropNamespaceComponent></DropNamespaceComponent>);
21-
activatePlugin(
22-
{},
23-
{ globalAppRegistry: appRegistry, dataService, logger: logger as any }
24-
);
19+
const Plugin = DropNamespacePlugin.withMockServices({
20+
globalAppRegistry: appRegistry,
21+
dataService,
22+
});
23+
render(<Plugin></Plugin>);
2524
});
2625

2726
afterEach(function () {
2827
sandbox.resetHistory();
29-
appRegistry.deactivate();
3028
cleanup();
3129
});
3230

packages/hadron-app-registry/src/react-context.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ import { globalAppRegistry, AppRegistry } from './app-registry';
99
import createDebug from 'debug';
1010
const debug = createDebug('hadron-app-registry:react');
1111

12-
const GlobalAppRegistryContext = createContext(globalAppRegistry);
13-
const LocalAppRegistryContext = createContext<AppRegistry | null>(null);
12+
/**
13+
* @internal exported for the mock plugin helper implementation
14+
*/
15+
export const GlobalAppRegistryContext = createContext(globalAppRegistry);
16+
17+
/**
18+
* @internal exported for the mock plugin helper implementation
19+
*/
20+
export const LocalAppRegistryContext = createContext<AppRegistry | null>(null);
1421

1522
type AppRegistryProviderProps =
1623
| {

packages/hadron-app-registry/src/register-plugin.tsx

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ import React, { useRef, useState } from 'react';
22
import type { Store as RefluxStore } from 'reflux';
33
import { Provider as ReduxStoreProvider } from 'react-redux';
44
import type { Actions } from './actions';
5-
import { type Store, type AppRegistry, isReduxStore } from './app-registry';
6-
import { useGlobalAppRegistry, useLocalAppRegistry } from './react-context';
5+
import {
6+
type Store,
7+
AppRegistry,
8+
isReduxStore,
9+
globalAppRegistry,
10+
} from './app-registry';
11+
import {
12+
GlobalAppRegistryContext,
13+
LocalAppRegistryContext,
14+
useGlobalAppRegistry,
15+
useLocalAppRegistry,
16+
} from './react-context';
717

818
function LegacyRefluxProvider({
919
store,
@@ -78,8 +88,29 @@ export type HadronPluginConfig<T, S extends Record<string, () => unknown>> = {
7888
};
7989
};
8090

81-
export type HadronPluginComponent<T> = React.FunctionComponent<T> & {
91+
export type HadronPluginComponent<
92+
T,
93+
S extends Record<string, () => unknown>
94+
> = React.FunctionComponent<T> & {
8295
displayName: string;
96+
/**
97+
* Convenience method for testing: allows to override services and app
98+
* registries available in the plugin context
99+
*
100+
* @example
101+
* const PluginWithLogger = registerHadronPlugin({ ... }, { logger: loggerLocator });
102+
*
103+
* const MockPlugin = PluginWithLogger.withMockServices({ logger: Sinon.stub() });
104+
*
105+
* @param mocks Overrides for the services locator values and registries
106+
* passed to the plugin in runtime. When `globalAppRegistry`
107+
* override is passed, it will be also used for the
108+
* localAppRegistry override unless `localAppRegistry` is also
109+
* explicitly passed as a mock service.
110+
*/
111+
withMockServices(
112+
mocks: Partial<Registries & Services<S>>
113+
): React.FunctionComponent<T>;
83114
};
84115

85116
/**
@@ -138,7 +169,7 @@ export type HadronPluginComponent<T> = React.FunctionComponent<T> & {
138169
export function registerHadronPlugin<
139170
T,
140171
S extends Record<string, () => unknown>
141-
>(config: HadronPluginConfig<T, S>, services?: S): HadronPluginComponent<T> {
172+
>(config: HadronPluginConfig<T, S>, services?: S): HadronPluginComponent<T, S> {
142173
const Component = config.component;
143174
const registryName = `${config.name}.Plugin`;
144175

@@ -194,6 +225,36 @@ export function registerHadronPlugin<
194225
},
195226
{
196227
displayName: config.name,
228+
withMockServices(
229+
mocks: Partial<Registries & Services<S>>
230+
): React.FunctionComponent<T> {
231+
const {
232+
globalAppRegistry: _globalAppRegistry,
233+
localAppRegistry: _localAppRegistry,
234+
...mockServices
235+
} = mocks;
236+
const mockServiceLocators = Object.fromEntries(
237+
Object.keys(services ?? {}).map((s) => {
238+
return [s, mockServices[s] ? () => mockServices[s] : services?.[s]];
239+
})
240+
) as unknown as S;
241+
const MockPlugin = registerHadronPlugin(config, mockServiceLocators);
242+
return function MockPluginWithContext(props: T) {
243+
return (
244+
<GlobalAppRegistryContext.Provider
245+
value={_globalAppRegistry ?? globalAppRegistry}
246+
>
247+
<LocalAppRegistryContext.Provider
248+
value={
249+
_localAppRegistry ?? _globalAppRegistry ?? new AppRegistry()
250+
}
251+
>
252+
<MockPlugin {...(props as any)}></MockPlugin>
253+
</LocalAppRegistryContext.Provider>
254+
</GlobalAppRegistryContext.Provider>
255+
);
256+
};
257+
},
197258
}
198259
);
199260
}

0 commit comments

Comments
 (0)