/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ 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'; 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); }); });