Files
threshold-ui/addons/api/tests/unit/utils/indexed-db-query-test.js
T
Cameron Perera d3c789c66c Icu 15181 add pagination to session recordings (#2502)
* refactor: 💡 move model hook to session-recordings/index route

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* test: 💍 add missing tests for session recordings controller

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* feat: 🎸 paginate session-recordings and search

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* refactor: 💡 fix failing redirect and tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* test: 💍 add search tests for session recordings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* feat: 🎸 add user filtering for session recordings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* feat: 🎸 add more attributes to indexed-db session recordings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* feat: 🎸 add target filter for session recordings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* refactor: 💡 update filterOptions method

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15130

* feat: 🎸 add scopes filtering to session recording

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* feat: 🎸 add time filter to session recordings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 add no filter results text

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 update Exist variables to doExist

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 updates from PR feedback

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 remove scope_id filter

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 change session recording placeholder text

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* chore: 🤖 increment indexedDB version

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* test: 💍 add Date object tests for indexed-db

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 small changes from PR feedback

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 change times filter array to time date object

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* test: 💍 add more date specific indexed-db test

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* refactor: 💡 change Today time shortcut to "Last 24 hours"

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181

* test: 💍 fix date mock in session-recording controller tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181
2024-10-01 13:55:15 -05:00

610 lines
18 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { queryIndexedDb } from 'api/utils/indexed-db-query';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { pluralize } from 'ember-inflector';
import { camelize } from '@ember/string';
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
import { faker } from '@faker-js/faker';
const seedIndexDb = async (resource, store, indexedDb, server) => {
const resourceData =
server.schema[pluralize(camelize(resource))].all().models;
const serializedData = resourceData.map((resource) =>
JSON.parse(JSON.stringify(server.serializerOrRegistry.serialize(resource))),
);
const serializer = store.serializerFor(resource);
const schema = store.modelFor(resource);
const normalizedPayload = serializer.normalizeResponse(
store,
schema,
{ items: serializedData },
null,
'query',
);
await indexedDb.db[resource].bulkPut(
normalizedPayload.data.map((datum) =>
indexedDb.normalizeData({
data: datum,
cleanData: true,
schema,
serializer,
}),
),
);
};
module('Unit | Utility | indexed-db-query', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let indexedDb;
let store;
let scope1;
let scope2;
const resource = 'target';
hooks.beforeEach(function () {
indexedDb = this.owner.lookup('service:indexed-db');
store = this.owner.lookup('service:store');
scope1 = this.server.create('scope', {
id: 'p_123',
type: 'project',
});
scope2 = this.server.create('scope', {
id: 'p_456',
type: 'project',
});
this.server.createList(resource, 5, {
type: TYPE_TARGET_SSH,
scopeId: scope1.id,
});
this.server.createList(resource, 5, {
type: TYPE_TARGET_TCP,
scopeId: scope2.id,
});
});
test('it filters by single field', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "and" logicalOperator', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'and',
values: [{ equals: TYPE_TARGET_SSH }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "or" logicalOperator', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'or',
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by multiple fields', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by multiple fields with "or" logicalOperator', async function (assert) {
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'or',
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 6);
});
test('it filters by multiple fields with "and" logicalOperator', async function (assert) {
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'and',
values: [{ equals: TYPE_TARGET_SSH }, { notEquals: TYPE_TARGET_TCP }],
},
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by multiple fields and uses two "or" logicalOperators', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'or',
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
scope_id: {
logicalOperator: 'or',
values: [{ equals: scope1.id }, { equals: scope2.id }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by multiple fields and uses two "and" logicalOperators', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'and',
values: [{ notEquals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it cannot filter with "contains" as initial operation', async function (assert) {
assert.expect(1);
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ contains: scope1.id }],
},
};
try {
await queryIndexedDb(indexedDb.db, resource, query);
} catch (e) {
assert.strictEqual(
e.message,
'Contains is not supported as the first filter.',
);
}
});
test('it cannot filter with "contains" as subsequent operation', async function (assert) {
assert.expect(2);
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope1.id }, { contains: scope2.id.slice(0, 2) }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
try {
await queryIndexedDb(indexedDb.db, resource, query);
} catch (e) {
assert.strictEqual(
e.message,
'Contains is not supported as a filter option with "or" operator.',
);
}
});
// TODO: Modify test cases that use comparison opertators to compare fields that
// are of type number. Additionally, add test case for searching on boolean fields.
test('it filters by single field with "gt" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ gt: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "gt" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { gt: scope1.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "gte" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ gte: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "gte" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { gte: scope1.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "lt" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ lt: scope2.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "lt" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope1.id }, { lt: scope2.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "lte" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ lte: scope2.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "lte" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope1.id }, { lte: scope2.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "notEquals" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ notEquals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "notEquals" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters and returns zero results', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: [{ equals: 'unknown' }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 0);
});
test('it searches with a query that matches a field exactly', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = { search: scope1.id };
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it searches with a query that matches a field partially', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = { search: scope2.id.slice(0, 2) };
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it searches and returns no results with a query that matches no fields', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = { search: `${scope1.id}456789` };
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 0);
});
test('it filters by a single field and searches', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
search: scope1.id,
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by multiple fields and searches', async function (assert) {
const target = this.server.create(resource, {
name: 'Generated_ssh_target',
type: TYPE_TARGET_SSH,
scopeId: scope1.id,
});
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
search: target.name,
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 1);
});
test('it filters on boolean type fields with truthy value', async function (assert) {
const authMethod = this.server.create('auth-method', {
name: 'Generated_auth_method',
scopeId: scope1.id,
});
scope1.update({ primaryAuthMethodId: authMethod.id });
await seedIndexDb('auth-method', store, indexedDb, this.server);
const query = { filters: { is_primary: [{ equals: 'true' }] } };
const result = await queryIndexedDb(indexedDb.db, 'auth-method', query);
assert.strictEqual(result.length, 1);
});
test('it filters on boolean type fields with falsy value', async function (assert) {
const authMethod = this.server.create('auth-method', {
name: 'Generated_auth_method',
scopeId: scope1.id,
});
scope1.update({ primaryAuthMethodId: authMethod.id });
await seedIndexDb('auth-method', store, indexedDb, this.server);
const query = { filters: { is_primary: [{ equals: 'false' }] } };
const result = await queryIndexedDb(indexedDb.db, 'auth-method', query);
assert.strictEqual(result.length, 0);
});
test('it filters on date type fields and returns results', async function (assert) {
const pastDate = new Date('2024-09-19T10:00:00.000Z');
const createdTime = new Date('2024-09-23T10:00:00.000Z');
this.server.create('session-recording', {
created_time: createdTime,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = { filters: { created_time: [{ gte: pastDate }] } };
const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);
assert.strictEqual(result.length, 1);
});
test('it filters on date type fields and returns results - from and to', async function (assert) {
const from = new Date('2024-09-19T10:00:00.000Z');
const to = new Date('2024-09-25T10:00:00.000Z');
const createdTime = new Date('2024-09-23T10:00:00.000Z');
const createdTime2 = new Date('2024-09-28T10:00:00.000Z');
this.server.create('session-recording', {
created_time: createdTime,
});
this.server.create('session-recording', {
created_time: createdTime2,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = {
filters: {
created_time: {
logicalOperator: 'and',
values: [{ gte: from }, { lte: to }],
},
},
};
const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);
assert.strictEqual(result.length, 1);
assert.strictEqual(
result[0].attributes.created_time.toISOString(),
createdTime.toISOString(),
);
});
test('it filters on date type fields and returns no results', async function (assert) {
const createdTime = faker.date.between({
from: '2024-09-19T00:00:00.000Z',
to: '2024-09-20T00:00:00.000Z',
});
const pastDate = faker.date.between({
from: '2024-09-23T00:00:00.000Z',
to: '2024-09-24T00:00:00.000Z',
});
this.server.create('session-recording', {
created_time: createdTime,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = { filters: { created_time: [{ gte: pastDate }] } };
const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);
assert.strictEqual(result.length, 0);
});
});