* Add unknown state, remove maintenance state * Handle all CouchDB status codes - Set unknown status if we receive an unhandled code * Include status code in error messages * SharedWorker can send unknown status * Add test for unknown status
435 lines
15 KiB
JavaScript
435 lines
15 KiB
JavaScript
/*****************************************************************************
|
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
* as represented by the Administrator of the National Aeronautics and Space
|
|
* Administration. All rights reserved.
|
|
*
|
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*
|
|
* Open MCT includes source code licensed under additional open source
|
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
* this source code distribution or the Licensing information page available
|
|
* at runtime from the About dialog for additional information.
|
|
*****************************************************************************/
|
|
import Vue from 'vue';
|
|
import CouchPlugin from './plugin.js';
|
|
import {
|
|
createOpenMct,
|
|
resetApplicationState, spyOnBuiltins
|
|
} from 'utils/testing';
|
|
import { CONNECTED, DISCONNECTED, PENDING, UNKNOWN } from './CouchStatusIndicator';
|
|
|
|
describe('the plugin', () => {
|
|
let openmct;
|
|
let provider;
|
|
let testPath = 'http://localhost:9990/openmct';
|
|
let options;
|
|
let mockIdentifierService;
|
|
let mockDomainObject;
|
|
|
|
beforeEach((done) => {
|
|
spyOnBuiltins(['fetch'], window);
|
|
|
|
mockDomainObject = {
|
|
identifier: {
|
|
namespace: '',
|
|
key: 'some-value'
|
|
},
|
|
type: 'notebook',
|
|
modified: 0
|
|
};
|
|
options = {
|
|
url: testPath,
|
|
filter: {}
|
|
};
|
|
openmct = createOpenMct();
|
|
|
|
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
|
mockIdentifierService = jasmine.createSpyObj(
|
|
'identifierService',
|
|
['parse']
|
|
);
|
|
mockIdentifierService.parse.and.returnValue({
|
|
getSpace: () => {
|
|
return 'mct';
|
|
}
|
|
});
|
|
|
|
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
|
|
|
openmct.install(new CouchPlugin(options));
|
|
|
|
openmct.types.addType('notebook', {creatable: true});
|
|
|
|
openmct.on('start', done);
|
|
openmct.startHeadless();
|
|
|
|
provider = openmct.objects.getProvider(mockDomainObject.identifier);
|
|
spyOn(provider, 'get').and.callThrough();
|
|
spyOn(provider, 'create').and.callThrough();
|
|
spyOn(provider, 'update').and.callThrough();
|
|
spyOn(provider, 'observe').and.callThrough();
|
|
spyOn(provider, 'fetchChanges').and.callThrough();
|
|
spyOn(provider, 'onSharedWorkerMessage').and.callThrough();
|
|
spyOn(provider, 'onEventMessage').and.callThrough();
|
|
spyOn(provider, 'isObservingObjectChanges').and.callThrough();
|
|
});
|
|
|
|
afterEach(() => {
|
|
return resetApplicationState(openmct);
|
|
});
|
|
|
|
describe('the provider', () => {
|
|
let mockPromise;
|
|
beforeEach(() => {
|
|
mockPromise = Promise.resolve({
|
|
json: () => {
|
|
return {
|
|
ok: true,
|
|
_id: 'some-value',
|
|
id: 'some-value',
|
|
_rev: 1,
|
|
model: {}
|
|
};
|
|
}
|
|
});
|
|
fetch.and.returnValue(mockPromise);
|
|
});
|
|
|
|
it('gets an object', async () => {
|
|
const result = await openmct.objects.get(mockDomainObject.identifier);
|
|
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
|
|
});
|
|
|
|
it('creates an object and starts shared worker', async () => {
|
|
const result = await openmct.objects.save(mockDomainObject);
|
|
expect(provider.create).toHaveBeenCalled();
|
|
expect(provider.observe).toHaveBeenCalled();
|
|
expect(result).toBeTrue();
|
|
});
|
|
|
|
it('updates an object', (done) => {
|
|
openmct.objects.save(mockDomainObject).then((result) => {
|
|
expect(result).toBeTrue();
|
|
expect(provider.create).toHaveBeenCalled();
|
|
//Set modified timestamp it detects a change and persists the updated model.
|
|
mockDomainObject.modified = mockDomainObject.persisted + 1;
|
|
openmct.objects.save(mockDomainObject).then((updatedResult) => {
|
|
expect(updatedResult).toBeTrue();
|
|
expect(provider.update).toHaveBeenCalled();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('works without Shared Workers', async () => {
|
|
let sharedWorkerCallback;
|
|
const cachedSharedWorker = window.SharedWorker;
|
|
window.SharedWorker = undefined;
|
|
|
|
const mockEventSource = {
|
|
addEventListener: (topic, addedListener) => {
|
|
sharedWorkerCallback = addedListener;
|
|
},
|
|
removeEventListener: () => {
|
|
sharedWorkerCallback = null;
|
|
}
|
|
};
|
|
const cachedEventSource = window.EventSource;
|
|
|
|
window.EventSource = function (url) {
|
|
return mockEventSource;
|
|
};
|
|
|
|
mockDomainObject.id = mockDomainObject.identifier.key;
|
|
|
|
const fakeUpdateEvent = {
|
|
data: JSON.stringify(mockDomainObject)
|
|
};
|
|
|
|
// eslint-disable-next-line require-await
|
|
provider.request = async function (subPath, method, body, signal) {
|
|
return {
|
|
body: fakeUpdateEvent,
|
|
ok: true,
|
|
id: mockDomainObject.id,
|
|
rev: 5
|
|
};
|
|
};
|
|
|
|
const result = await openmct.objects.save(mockDomainObject);
|
|
expect(result).toBeTrue();
|
|
expect(provider.create).toHaveBeenCalled();
|
|
expect(provider.observe).toHaveBeenCalled();
|
|
expect(provider.isObservingObjectChanges).toHaveBeenCalled();
|
|
expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
|
|
|
|
//Set modified timestamp it detects a change and persists the updated model.
|
|
mockDomainObject.modified = mockDomainObject.persisted + 1;
|
|
const updatedResult = await openmct.objects.save(mockDomainObject);
|
|
openmct.objects.observe(mockDomainObject, '*', (updatedObject) => {
|
|
});
|
|
|
|
expect(updatedResult).toBeTrue();
|
|
expect(provider.update).toHaveBeenCalled();
|
|
expect(provider.fetchChanges).toHaveBeenCalled();
|
|
sharedWorkerCallback(fakeUpdateEvent);
|
|
expect(provider.onEventMessage).toHaveBeenCalled();
|
|
|
|
window.SharedWorker = cachedSharedWorker;
|
|
window.EventSource = cachedEventSource;
|
|
});
|
|
});
|
|
describe('batches requests', () => {
|
|
let mockPromise;
|
|
beforeEach(() => {
|
|
mockPromise = Promise.resolve({
|
|
json: () => {
|
|
return {
|
|
total_rows: 0,
|
|
rows: []
|
|
};
|
|
}
|
|
});
|
|
fetch.and.returnValue(mockPromise);
|
|
});
|
|
it('for multiple simultaneous gets', async () => {
|
|
const objectIds = [
|
|
{
|
|
namespace: '',
|
|
key: 'object-1'
|
|
}, {
|
|
namespace: '',
|
|
key: 'object-2'
|
|
}, {
|
|
namespace: '',
|
|
key: 'object-3'
|
|
}
|
|
];
|
|
|
|
await Promise.all(
|
|
objectIds.map((identifier) =>
|
|
openmct.objects.get(identifier)
|
|
)
|
|
);
|
|
|
|
const requestUrl = fetch.calls.mostRecent().args[0];
|
|
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
expect(requestUrl.includes('_all_docs')).toBeTrue();
|
|
expect(requestMethod).toEqual('POST');
|
|
});
|
|
|
|
it('but not for single gets', async () => {
|
|
const objectId = {
|
|
namespace: '',
|
|
key: 'object-1'
|
|
};
|
|
|
|
await openmct.objects.get(objectId);
|
|
const requestUrl = fetch.calls.mostRecent().args[0];
|
|
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();
|
|
expect(requestMethod).toEqual('GET');
|
|
});
|
|
});
|
|
describe('implements server-side search', () => {
|
|
let mockPromise;
|
|
beforeEach(() => {
|
|
mockPromise = Promise.resolve({
|
|
body: {
|
|
getReader() {
|
|
return {
|
|
read() {
|
|
return Promise.resolve({
|
|
done: true,
|
|
value: undefined
|
|
});
|
|
}
|
|
};
|
|
}
|
|
}
|
|
});
|
|
fetch.and.returnValue(mockPromise);
|
|
});
|
|
|
|
it("using Couch's 'find' endpoint", async () => {
|
|
await Promise.all(openmct.objects.search('test'));
|
|
const requestUrl = fetch.calls.mostRecent().args[0];
|
|
|
|
// we only want one call to fetch, not 2!
|
|
// see https://github.com/nasa/openmct/issues/4667
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
expect(requestUrl.endsWith('_find')).toBeTrue();
|
|
});
|
|
|
|
it("and supports search by object name", async () => {
|
|
await Promise.all(openmct.objects.search('test'));
|
|
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
|
|
|
|
expect(requestPayload).toBeDefined();
|
|
expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('the view', () => {
|
|
let openmct;
|
|
let options;
|
|
let appHolder;
|
|
let testPath = 'http://localhost:9990/openmct';
|
|
let provider;
|
|
let mockDomainObject;
|
|
beforeEach((done) => {
|
|
openmct = createOpenMct();
|
|
spyOnBuiltins(['fetch'], window);
|
|
options = {
|
|
url: testPath,
|
|
filter: {}
|
|
};
|
|
mockDomainObject = {
|
|
identifier: {
|
|
namespace: '',
|
|
key: 'some-value'
|
|
},
|
|
type: 'notebook',
|
|
modified: 0
|
|
};
|
|
openmct.install(new CouchPlugin(options));
|
|
appHolder = document.createElement('div');
|
|
document.body.appendChild(appHolder);
|
|
openmct.on('start', done);
|
|
openmct.start(appHolder);
|
|
provider = openmct.objects.getProvider(mockDomainObject.identifier);
|
|
spyOn(provider, 'onSharedWorkerMessage').and.callThrough();
|
|
});
|
|
|
|
afterEach(() => {
|
|
return resetApplicationState(openmct);
|
|
});
|
|
|
|
describe('updates CouchDB status indicator', () => {
|
|
let mockPromise;
|
|
|
|
function assertCouchIndicatorStatus(status) {
|
|
const indicator = appHolder.querySelector('.c-indicator--simple');
|
|
expect(indicator).not.toBeNull();
|
|
expect(indicator).toHaveClass(status.statusClass);
|
|
expect(indicator.textContent).toMatch(new RegExp(status.text, 'i'));
|
|
expect(indicator.title).toMatch(new RegExp(status.title, 'i'));
|
|
}
|
|
|
|
it("to 'connected' on successful request", async () => {
|
|
mockPromise = Promise.resolve({
|
|
status: 200,
|
|
json: () => {
|
|
return {
|
|
ok: true,
|
|
_id: 'some-value',
|
|
id: 'some-value',
|
|
_rev: 1,
|
|
model: {}
|
|
};
|
|
}
|
|
});
|
|
fetch.and.returnValue(mockPromise);
|
|
|
|
await openmct.objects.get({
|
|
namespace: '',
|
|
key: 'object-1'
|
|
});
|
|
await Vue.nextTick();
|
|
|
|
assertCouchIndicatorStatus(CONNECTED);
|
|
});
|
|
|
|
it("to 'disconnected' on failed request", async () => {
|
|
fetch.and.throwError(new TypeError('ERR_CONNECTION_REFUSED'));
|
|
|
|
await openmct.objects.get({
|
|
namespace: '',
|
|
key: 'object-1'
|
|
});
|
|
await Vue.nextTick();
|
|
|
|
assertCouchIndicatorStatus(DISCONNECTED);
|
|
});
|
|
|
|
it("to 'pending'", async () => {
|
|
const workerMessage = {
|
|
data: {
|
|
type: 'state',
|
|
state: 'pending'
|
|
}
|
|
};
|
|
mockPromise = Promise.resolve({
|
|
status: 200,
|
|
json: () => {
|
|
return {
|
|
ok: true,
|
|
_id: 'some-value',
|
|
id: 'some-value',
|
|
_rev: 1,
|
|
model: {}
|
|
};
|
|
}
|
|
});
|
|
fetch.and.returnValue(mockPromise);
|
|
|
|
await openmct.objects.get({
|
|
namespace: '',
|
|
key: 'object-1'
|
|
});
|
|
|
|
// Simulate 'pending' state from worker message
|
|
provider.onSharedWorkerMessage(workerMessage);
|
|
await Vue.nextTick();
|
|
|
|
assertCouchIndicatorStatus(PENDING);
|
|
});
|
|
|
|
it("to 'unknown'", async () => {
|
|
const workerMessage = {
|
|
data: {
|
|
type: 'state',
|
|
state: 'unknown'
|
|
}
|
|
};
|
|
mockPromise = Promise.resolve({
|
|
status: 200,
|
|
json: () => {
|
|
return {
|
|
ok: true,
|
|
_id: 'some-value',
|
|
id: 'some-value',
|
|
_rev: 1,
|
|
model: {}
|
|
};
|
|
}
|
|
});
|
|
fetch.and.returnValue(mockPromise);
|
|
|
|
await openmct.objects.get({
|
|
namespace: '',
|
|
key: 'object-1'
|
|
});
|
|
|
|
// Simulate 'pending' state from worker message
|
|
provider.onSharedWorkerMessage(workerMessage);
|
|
await Vue.nextTick();
|
|
|
|
assertCouchIndicatorStatus(UNKNOWN);
|
|
});
|
|
});
|
|
});
|