mirror of
https://github.com/kemko/nomad.git
synced 2026-01-04 17:35:43 +03:00
* Custom watchQuery equivalent on the storage index * Tests for live updates to the storage page * Deconditionalizing the pagination on storage, and fixing a bug where I was looking at filtered but not paginated DHV * Test for pagination with live-updates
503 lines
18 KiB
JavaScript
503 lines
18 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
/* eslint-disable qunit/require-expect */
|
|
import { currentURL, visit } from '@ember/test-helpers';
|
|
import { module, test } from 'qunit';
|
|
import { setupApplicationTest } from 'ember-qunit';
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
|
import StorageList from 'nomad-ui/tests/pages/storage/list';
|
|
import percySnapshot from '@percy/ember';
|
|
import faker from 'nomad-ui/mirage/faker';
|
|
|
|
const assignWriteAlloc = (volume, alloc) => {
|
|
volume.writeAllocs.add(alloc);
|
|
volume.allocations.add(alloc);
|
|
volume.save();
|
|
};
|
|
|
|
const assignReadAlloc = (volume, alloc) => {
|
|
volume.readAllocs.add(alloc);
|
|
volume.allocations.add(alloc);
|
|
volume.save();
|
|
};
|
|
|
|
module('Acceptance | storage list', function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
setupMirage(hooks);
|
|
|
|
hooks.beforeEach(function () {
|
|
faker.seed(1);
|
|
server.create('node-pool');
|
|
server.create('node');
|
|
server.create('csi-plugin', { createVolumes: false });
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
test('it passes an accessibility audit', async function (assert) {
|
|
await StorageList.visit();
|
|
await a11yAudit(assert);
|
|
});
|
|
|
|
test('visiting the now-deprecated /csi redirects to /storage', async function (assert) {
|
|
await visit('/csi');
|
|
|
|
assert.equal(currentURL(), '/storage');
|
|
});
|
|
|
|
test('visiting /storage', async function (assert) {
|
|
await StorageList.visit();
|
|
|
|
assert.equal(currentURL(), '/storage');
|
|
assert.equal(document.title, 'Storage - Nomad');
|
|
});
|
|
|
|
test('/storage/volumes should list the first page of volumes sorted by name', async function (assert) {
|
|
const volumeCount = StorageList.pageSize + 1;
|
|
server.createList('csi-volume', volumeCount);
|
|
|
|
await StorageList.visit();
|
|
|
|
await percySnapshot(assert);
|
|
|
|
const sortedVolumes = server.db.csiVolumes.sortBy('id');
|
|
|
|
assert.equal(StorageList.csiVolumes.length, StorageList.pageSize);
|
|
StorageList.csiVolumes.forEach((volume, index) => {
|
|
assert.equal(volume.name, sortedVolumes[index].id, 'Volumes are ordered');
|
|
});
|
|
});
|
|
|
|
test('each volume row should contain information about the volume', async function (assert) {
|
|
const volume = server.create('csi-volume');
|
|
const readAllocs = server.createList('allocation', 2, { shallow: true });
|
|
const writeAllocs = server.createList('allocation', 3, { shallow: true });
|
|
readAllocs.forEach((alloc) => assignReadAlloc(volume, alloc));
|
|
writeAllocs.forEach((alloc) => assignWriteAlloc(volume, alloc));
|
|
|
|
await StorageList.visit();
|
|
|
|
const volumeRow = StorageList.csiVolumes.objectAt(0);
|
|
|
|
let controllerHealthStr = 'Node Only';
|
|
if (volume.controllerRequired || volume.controllersExpected > 0) {
|
|
const healthy = volume.controllersHealthy;
|
|
const expected = volume.controllersExpected;
|
|
const isHealthy = healthy > 0;
|
|
controllerHealthStr = `${
|
|
isHealthy ? 'Healthy' : 'Unhealthy'
|
|
} ( ${healthy} / ${expected} )`;
|
|
}
|
|
|
|
const nodeHealthStr = volume.nodesHealthy > 0 ? 'Healthy' : 'Unhealthy';
|
|
|
|
assert.equal(volumeRow.name, volume.id);
|
|
assert.notOk(volumeRow.hasNamespace);
|
|
assert.equal(
|
|
volumeRow.schedulable,
|
|
volume.schedulable ? 'Schedulable' : 'Unschedulable'
|
|
);
|
|
assert.equal(volumeRow.controllerHealth, controllerHealthStr);
|
|
assert.equal(
|
|
volumeRow.nodeHealth,
|
|
`${nodeHealthStr} ( ${volume.nodesHealthy} / ${volume.nodesExpected} )`
|
|
);
|
|
assert.equal(volumeRow.plugin, volume.PluginId);
|
|
assert.equal(volumeRow.allocations, readAllocs.length + writeAllocs.length);
|
|
});
|
|
|
|
test('each volume row should link to the corresponding volume', async function (assert) {
|
|
const [, secondNamespace] = server.createList('namespace', 2);
|
|
const volume = server.create('csi-volume', {
|
|
namespaceId: secondNamespace.id,
|
|
});
|
|
|
|
await StorageList.visit({ namespace: '*' });
|
|
await StorageList.csiVolumes.objectAt(0).clickName();
|
|
assert.equal(
|
|
currentURL(),
|
|
`/storage/volumes/csi/${volume.id}@${secondNamespace.id}`
|
|
);
|
|
|
|
await StorageList.visit({ namespace: '*' });
|
|
assert.equal(currentURL(), '/storage');
|
|
});
|
|
|
|
test('when there are no csi volumes, there is an empty message', async function (assert) {
|
|
await StorageList.visit();
|
|
|
|
await percySnapshot(assert);
|
|
|
|
assert.ok(StorageList.csiIsEmpty);
|
|
assert.equal(StorageList.csiEmptyState, 'No CSI Volumes found');
|
|
});
|
|
|
|
test('when there are volumes, but no matches for a search, there is an empty message', async function (assert) {
|
|
server.create('csi-volume', { id: 'cat 1' });
|
|
server.create('csi-volume', { id: 'cat 2' });
|
|
|
|
await StorageList.visit();
|
|
await StorageList.csiSearch('dog');
|
|
assert.ok(StorageList.csiIsEmpty);
|
|
assert.ok(
|
|
StorageList.csiEmptyState.includes('No CSI volumes match your search')
|
|
);
|
|
});
|
|
|
|
test('searching resets the current page', async function (assert) {
|
|
server.createList('csi-volume', StorageList.pageSize + 1);
|
|
|
|
await StorageList.visit();
|
|
await StorageList.csiNextPage();
|
|
|
|
assert.equal(currentURL(), '/storage?csiPage=2');
|
|
|
|
await StorageList.csiSearch('foobar');
|
|
|
|
assert.equal(currentURL(), '/storage?csiFilter=foobar');
|
|
});
|
|
|
|
test('when the cluster has namespaces, each volume row includes the volume namespace', async function (assert) {
|
|
server.createList('namespace', 2);
|
|
const volume = server.create('csi-volume');
|
|
|
|
await StorageList.visit({ namespace: '*' });
|
|
|
|
const volumeRow = StorageList.csiVolumes.objectAt(0);
|
|
assert.equal(volumeRow.namespace, volume.namespaceId);
|
|
});
|
|
|
|
test('when the namespace query param is set, only matching volumes are shown and the namespace value is forwarded to app state', async function (assert) {
|
|
server.createList('namespace', 2);
|
|
const volume1 = server.create('csi-volume', {
|
|
namespaceId: server.db.namespaces[0].id,
|
|
});
|
|
const volume2 = server.create('csi-volume', {
|
|
namespaceId: server.db.namespaces[1].id,
|
|
});
|
|
|
|
await StorageList.visit();
|
|
assert.equal(StorageList.csiVolumes.length, 2);
|
|
|
|
const firstNamespace = server.db.namespaces[0];
|
|
await StorageList.visit({ namespace: firstNamespace.id });
|
|
assert.equal(StorageList.csiVolumes.length, 1);
|
|
assert.equal(StorageList.csiVolumes.objectAt(0).name, volume1.id);
|
|
|
|
const secondNamespace = server.db.namespaces[1];
|
|
await StorageList.visit({ namespace: secondNamespace.id });
|
|
|
|
assert.equal(StorageList.csiVolumes.length, 1);
|
|
assert.equal(StorageList.csiVolumes.objectAt(0).name, volume2.id);
|
|
});
|
|
|
|
test('when accessing volumes is forbidden, a message is shown with a link to the tokens page', async function (assert) {
|
|
server.pretender.get('/v1/volumes', () => [403, {}, null]);
|
|
|
|
await StorageList.visit();
|
|
assert.equal(StorageList.error.title, 'Not Authorized');
|
|
|
|
await StorageList.error.seekHelp();
|
|
assert.equal(currentURL(), '/settings/tokens');
|
|
});
|
|
|
|
testSingleSelectFacet('Namespace', {
|
|
facet: StorageList.facets.namespace,
|
|
paramName: 'namespace',
|
|
expectedOptions: ['All (*)', 'default', 'namespace-2'],
|
|
optionToSelect: 'namespace-2',
|
|
async beforeEach() {
|
|
server.create('namespace', { id: 'default' });
|
|
server.create('namespace', { id: 'namespace-2' });
|
|
server.createList('csi-volume', 2, { namespaceId: 'default' });
|
|
server.createList('csi-volume', 2, { namespaceId: 'namespace-2' });
|
|
await StorageList.visit();
|
|
},
|
|
filter(volume, selection) {
|
|
return volume.namespaceId === selection;
|
|
},
|
|
});
|
|
|
|
function testSingleSelectFacet(
|
|
label,
|
|
{ facet, paramName, beforeEach, filter, expectedOptions, optionToSelect }
|
|
) {
|
|
test(`the ${label} facet has the correct options`, async function (assert) {
|
|
await beforeEach();
|
|
await facet.toggle();
|
|
|
|
let expectation;
|
|
if (typeof expectedOptions === 'function') {
|
|
expectation = expectedOptions(server.db.jobs);
|
|
} else {
|
|
expectation = expectedOptions;
|
|
}
|
|
|
|
assert.deepEqual(
|
|
facet.options.map((option) => option.label.trim()),
|
|
expectation,
|
|
'Options for facet are as expected'
|
|
);
|
|
});
|
|
|
|
test(`the ${label} facet filters the volumes list by ${label}`, async function (assert) {
|
|
await beforeEach();
|
|
await facet.toggle();
|
|
|
|
const option = facet.options.findOneBy('label', optionToSelect);
|
|
const selection = option.label;
|
|
await option.toggle();
|
|
|
|
const expectedVolumes = server.db.csiVolumes
|
|
.filter((volume) => filter(volume, selection))
|
|
.sortBy('id');
|
|
|
|
StorageList.csiVolumes.forEach((volume, index) => {
|
|
assert.equal(
|
|
volume.name,
|
|
expectedVolumes[index].name,
|
|
`Volume at ${index} is ${expectedVolumes[index].name}`
|
|
);
|
|
});
|
|
});
|
|
|
|
test(`selecting an option in the ${label} facet updates the ${paramName} query param`, async function (assert) {
|
|
await beforeEach();
|
|
await facet.toggle();
|
|
|
|
const option = facet.options.objectAt(1);
|
|
const label = option.label;
|
|
await option.toggle();
|
|
|
|
assert.ok(
|
|
currentURL().includes(`${paramName}=${label}`),
|
|
'URL has the correct query param key and value'
|
|
);
|
|
});
|
|
|
|
module('Live updates are reflected in the list', function () {
|
|
test('When you visit the storage list page, the watch process is kicked off', async function (assert) {
|
|
await StorageList.visit();
|
|
const requests = server.pretender.handledRequests;
|
|
const dhvRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
|
);
|
|
const csiRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=csi')
|
|
);
|
|
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
|
assert.equal(csiRequests.length, 2, '2 CSI requests were made');
|
|
});
|
|
|
|
test('When a new dynamic host volume is created, the page should reflect the changes', async function (assert) {
|
|
server.create('dynamic-host-volume', {
|
|
name: 'initial-volume',
|
|
});
|
|
const controller = this.owner.lookup('controller:storage.index');
|
|
await visit('/storage');
|
|
|
|
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
|
const requests = server.pretender.handledRequests;
|
|
|
|
// Should be 2 DHV requests made: the initial one, and the watcher
|
|
let dhvRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
|
);
|
|
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
|
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
|
assert.dom('[data-test-dhv-row]').containsText('initial-volume');
|
|
|
|
server.create('dynamic-host-volume', {
|
|
name: 'new-volume',
|
|
});
|
|
|
|
await controller.watchDHV.perform({
|
|
type: 'host',
|
|
namespace: controller.qpNamespace,
|
|
});
|
|
|
|
// Now there should be a third DHV request
|
|
dhvRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
|
);
|
|
assert.equal(dhvRequests.length, 3, '3 DHV requests were made');
|
|
|
|
// and a second row
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 2 });
|
|
});
|
|
|
|
test('When a new csi volume is created, the page should reflect the changes', async function (assert) {
|
|
server.create('csi-volume', {
|
|
id: 'initial-volume',
|
|
});
|
|
const controller = this.owner.lookup('controller:storage.index');
|
|
await visit('/storage');
|
|
|
|
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
|
const requests = server.pretender.handledRequests;
|
|
|
|
// Should be 2 DHV requests made: the initial one, and the watcher
|
|
let csiRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=csi')
|
|
);
|
|
assert.equal(csiRequests.length, 2, '2 CSI requests were made');
|
|
assert.dom('[data-test-csi-volume-row]').exists({ count: 1 });
|
|
assert.dom('[data-test-csi-volume-row]').containsText('initial-volume');
|
|
|
|
server.create('csi-volume', {
|
|
id: 'new-volume',
|
|
});
|
|
|
|
await controller.watchCSI.perform({
|
|
type: 'csi',
|
|
namespace: controller.qpNamespace,
|
|
});
|
|
|
|
// Now there should be a third DHV request
|
|
csiRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=csi')
|
|
);
|
|
assert.equal(csiRequests.length, 3, '3 CSI requests were made');
|
|
|
|
// and a second row
|
|
assert.dom('[data-test-csi-volume-row]').exists({ count: 2 });
|
|
});
|
|
|
|
test('When a dynamic host volume is updated, the page should reflect the changes', async function (assert) {
|
|
const dhv = server.create('dynamic-host-volume', {
|
|
name: 'initial-volume',
|
|
});
|
|
const controller = this.owner.lookup('controller:storage.index');
|
|
await visit('/storage');
|
|
|
|
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
|
const requests = server.pretender.handledRequests;
|
|
|
|
// Should be 2 DHV requests made: the initial one, and the watcher
|
|
let dhvRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
|
);
|
|
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
|
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
|
assert.dom('[data-test-dhv-row]').containsText('initial-volume');
|
|
|
|
dhv.update('name', 'updated-volume');
|
|
|
|
await controller.watchDHV.perform({
|
|
type: 'host',
|
|
namespace: controller.qpNamespace,
|
|
});
|
|
|
|
dhvRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
|
);
|
|
assert.equal(dhvRequests.length, 3, '3 DHV requests were made');
|
|
|
|
// Still just one row
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
|
assert.dom('[data-test-dhv-row]').containsText('updated-volume');
|
|
});
|
|
|
|
test('When a dynamic host volume is deleted, the page should reflect the changes', async function (assert) {
|
|
const dhv = server.create('dynamic-host-volume', {
|
|
name: 'initial-volume',
|
|
});
|
|
const controller = this.owner.lookup('controller:storage.index');
|
|
await visit('/storage');
|
|
|
|
// Check pretender to see 2 requests related to DHV: the initial one, and another one with an index on it
|
|
const requests = server.pretender.handledRequests;
|
|
|
|
// Should be 2 DHV requests made: the initial one, and the watcher
|
|
let dhvRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
|
);
|
|
assert.equal(dhvRequests.length, 2, '2 DHV requests were made');
|
|
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
|
assert.dom('[data-test-dhv-row]').containsText('initial-volume');
|
|
|
|
dhv.destroy();
|
|
|
|
await controller.watchDHV.perform({
|
|
type: 'host',
|
|
namespace: controller.qpNamespace,
|
|
});
|
|
|
|
dhvRequests = requests.filter((request) =>
|
|
request.url.startsWith('/v1/volumes?namespace=%2A&type=host')
|
|
);
|
|
assert.equal(dhvRequests.length, 3, '3 DHV requests were made');
|
|
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 0 });
|
|
assert.ok(StorageList.dhvIsEmpty);
|
|
});
|
|
});
|
|
|
|
test('Pagination is adhered to when live updates happen', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', 10);
|
|
server.createList('dynamic-host-volume', 9);
|
|
const controller = this.owner.lookup('controller:storage.index');
|
|
|
|
await StorageList.visit();
|
|
|
|
// 9 rows should be present
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 9 });
|
|
|
|
server.create('dynamic-host-volume', { name: 'tenth-volume' });
|
|
|
|
await controller.watchDHV.perform({
|
|
type: 'host',
|
|
namespace: controller.qpNamespace,
|
|
});
|
|
|
|
// 10 rows should be present
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 10 });
|
|
|
|
// Newest (sorted by modified date by default) should show up first
|
|
assert
|
|
.dom('[data-test-dhv-row]:first-child')
|
|
.containsText('tenth-volume');
|
|
|
|
// There should still only be 1 page of pagination
|
|
assert.dom('.hds-pagination-nav__number').exists({ count: 1 });
|
|
// 10 rows should be present
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 10 });
|
|
|
|
// Create one more
|
|
server.create('dynamic-host-volume', { name: 'eleventh-volume' });
|
|
|
|
await controller.watchDHV.perform({
|
|
type: 'host',
|
|
namespace: controller.qpNamespace,
|
|
});
|
|
|
|
// 10 rows still present
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 10 });
|
|
|
|
// Newest should show up first
|
|
assert
|
|
.dom('[data-test-dhv-row]:first-child')
|
|
.containsText('eleventh-volume');
|
|
|
|
// There should now be 2 pages of pagination
|
|
assert.dom('.hds-pagination-nav__number').exists({ count: 2 });
|
|
|
|
// Clicking through to the second page changes the URL and only shows 1 row
|
|
await StorageList.dhvNextPage();
|
|
assert.equal(currentURL(), '/storage?dhvPage=2');
|
|
|
|
// 1 row should be present
|
|
assert.dom('[data-test-dhv-row]').exists({ count: 1 });
|
|
|
|
// cleanup
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
}
|
|
});
|