* feat: 🎸 Add sqlite handler and parser ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17410
439 lines
13 KiB
JavaScript
439 lines
13 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import { setupTest } from 'ember-qunit';
|
|
import SqliteHandler from 'api/handlers/sqlite-handler';
|
|
import RequestManager from '@ember-data/request';
|
|
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
|
import { faker } from '@faker-js/faker';
|
|
import sinon from 'sinon';
|
|
import { assert } from '@ember/debug';
|
|
import { setupSqlite } from 'api/test-support/helpers/sqlite';
|
|
import TargetModel from 'api/models/target';
|
|
|
|
function createPaginatedResponseHandler(mirageRecords, { pageSize }) {
|
|
assert('pageSize is required', pageSize);
|
|
assert('mirage server on `this` is required', this.server);
|
|
const pagesCount = mirageRecords.length / pageSize;
|
|
|
|
const results = [];
|
|
for (let i = 0; i < pagesCount; i++) {
|
|
const items = mirageRecords.slice(i * pageSize, (i + 1) * pageSize);
|
|
const isLastPage = i === pagesCount - 1;
|
|
const list_token = `list-token-${faker.string.nanoid({ min: 5, max: 5 })}`;
|
|
|
|
results.push({
|
|
items: items.map((model) =>
|
|
this.server.serializerOrRegistry.serialize(model),
|
|
),
|
|
list_token,
|
|
response_type: !isLastPage ? 'delta' : 'complete',
|
|
});
|
|
}
|
|
|
|
function nextResult(listToken) {
|
|
const index = results.findIndex(
|
|
({ list_token }) => list_token === listToken,
|
|
);
|
|
const nextResult = index === -1 ? null : results[index + 1];
|
|
return nextResult;
|
|
}
|
|
|
|
// mirage handler compatible with paginated responses
|
|
return function (_schema, request) {
|
|
const listToken = request.queryParams.list_token;
|
|
if (!listToken) {
|
|
return results[0];
|
|
}
|
|
|
|
const result = nextResult(listToken);
|
|
|
|
if (!result) {
|
|
return new Response(
|
|
404,
|
|
{},
|
|
`No results found for list token "${listToken}"`,
|
|
);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
module('Unit | Handler | sqlite-handler', function (hooks) {
|
|
setupTest(hooks);
|
|
setupMirage(hooks);
|
|
setupSqlite(hooks);
|
|
|
|
let manager,
|
|
scope,
|
|
sqliteHandler,
|
|
store,
|
|
applicationAdapter,
|
|
sqliteHandlerSpy,
|
|
applicationAdapterSpy;
|
|
hooks.beforeEach(async function setupSqliteHandler() {
|
|
store = this.owner.lookup('service:store');
|
|
manager = new RequestManager();
|
|
sqliteHandler = new SqliteHandler(store);
|
|
applicationAdapter = store.adapterFor('application');
|
|
|
|
// spy on all handler and application adapter methods
|
|
sqliteHandlerSpy = sinon.spy(sqliteHandler);
|
|
applicationAdapterSpy = sinon.spy(applicationAdapter);
|
|
|
|
store.requestManager = manager;
|
|
manager.use([sqliteHandler]);
|
|
|
|
scope = this.server.create('scope', {
|
|
type: 'project',
|
|
name: 'Project Scope',
|
|
});
|
|
});
|
|
|
|
module('fetching and batch loading', function (hooks) {
|
|
// `testBatchLimit` and `serverResultPageLimit` are intentionally difference
|
|
// so that multiple calls to the server are required to fetch a single batch,
|
|
// and these multiples can be asserted in the tests individually
|
|
const testBatchLimit = 10;
|
|
const serverResultPageLimit = 5;
|
|
let targets, targetsResponseHandlerSpy;
|
|
|
|
hooks.beforeEach(function setupMirageData() {
|
|
sqliteHandler.batchLimit = testBatchLimit;
|
|
|
|
targets = this.server.createList('target', 50, { scope });
|
|
|
|
// This is to just add a number to each target name to guarantee uniqueness in
|
|
// case there was a duplicate rather than increasing the number of words generated by faker.
|
|
// Because we have a UNIQUE constraint on target names and we do UPSERTS, if there's a
|
|
// duplicate name we'll end up replacing it in the DB and then have fewer targets
|
|
// than expected and flaky tests.
|
|
for (let i = 0; i < targets.length; i++) {
|
|
targets[i].name = targets[i].name + i;
|
|
}
|
|
|
|
const targetResponseHandler = createPaginatedResponseHandler.call(
|
|
this,
|
|
this.server.schema.targets.all().models,
|
|
{ pageSize: serverResultPageLimit },
|
|
);
|
|
|
|
targetsResponseHandlerSpy = sinon.spy(targetResponseHandler);
|
|
this.server.get('targets', targetsResponseHandlerSpy);
|
|
});
|
|
|
|
test('it returns all data for a resource from the server', async function (assert) {
|
|
const results = await store.query('target', {});
|
|
|
|
assert.strictEqual(
|
|
targets.length,
|
|
results.length,
|
|
'all records are returned',
|
|
);
|
|
});
|
|
|
|
test('supports batching requests and writes to SQLite', async function (assert) {
|
|
await store.query('target', {});
|
|
|
|
assert.strictEqual(
|
|
sqliteHandlerSpy.writeToDb.callCount,
|
|
targets.length / testBatchLimit,
|
|
'handler #writeToDb is called once for each batch',
|
|
);
|
|
|
|
assert.strictEqual(
|
|
applicationAdapterSpy.query.callCount,
|
|
targets.length / testBatchLimit,
|
|
'application adapter #query is called once for each batch',
|
|
);
|
|
|
|
assert.strictEqual(
|
|
targetsResponseHandlerSpy.callCount,
|
|
targets.length / serverResultPageLimit,
|
|
'server handler should be called once for each page',
|
|
);
|
|
});
|
|
|
|
test('it loads from SQLite only and does not fetch from api, when peekDb option is used', async function (assert) {
|
|
const callCounts = {
|
|
get writeToDb() {
|
|
return sqliteHandlerSpy.writeToDb.callCount;
|
|
},
|
|
get targetResponseHandler() {
|
|
return targetsResponseHandlerSpy.callCount;
|
|
},
|
|
};
|
|
|
|
// before first query, no data has been fetched or written to sqlite
|
|
// the call counts should therefore be 0
|
|
assert.strictEqual(callCounts.targetResponseHandler, 0);
|
|
assert.strictEqual(callCounts.writeToDb, 0);
|
|
|
|
const results = await store.query('target', {});
|
|
assert.strictEqual(results.length, 50);
|
|
|
|
// call counts expected after first query
|
|
const expectedWriteToSqliteCallCount = 5;
|
|
const expectedResponseHandlerCallCount = 10;
|
|
|
|
// first `store.query()` calls api and writes batches to sqlite
|
|
assert.strictEqual(
|
|
callCounts.targetResponseHandler,
|
|
expectedResponseHandlerCallCount,
|
|
);
|
|
assert.strictEqual(callCounts.writeToDb, expectedWriteToSqliteCallCount);
|
|
|
|
await store.query('target', {}, { peekDb: true });
|
|
assert.strictEqual(results.length, 50);
|
|
|
|
// second run does not increase call counts to api or writes to sqlite
|
|
assert.strictEqual(
|
|
callCounts.targetResponseHandler,
|
|
expectedResponseHandlerCallCount,
|
|
);
|
|
assert.strictEqual(callCounts.writeToDb, expectedWriteToSqliteCallCount);
|
|
});
|
|
});
|
|
|
|
module('option: pushToStore', function (hooks) {
|
|
let mirageTargets;
|
|
|
|
hooks.beforeEach(function () {
|
|
mirageTargets = this.server.createList('target', 5, { scope });
|
|
});
|
|
|
|
test('it pushes fetched results to the store by default', async function (assert) {
|
|
this.server.get(
|
|
'targets',
|
|
createPaginatedResponseHandler.call(this, mirageTargets, {
|
|
pageSize: 1,
|
|
}),
|
|
);
|
|
|
|
assert.strictEqual(mirageTargets.length, 5);
|
|
assert.strictEqual(store.peekAll('target').length, 0);
|
|
const results = await store.query('target', {});
|
|
assert.strictEqual(results.length, 5);
|
|
assert.ok(
|
|
results.every((result) => result instanceof TargetModel),
|
|
'results are ember data models',
|
|
);
|
|
assert.strictEqual(store.peekAll('target').length, 5);
|
|
});
|
|
|
|
test('it pushes fetched results to the store when pushToStore is true', async function (assert) {
|
|
this.server.get(
|
|
'targets',
|
|
createPaginatedResponseHandler.call(this, mirageTargets, {
|
|
pageSize: 1,
|
|
}),
|
|
);
|
|
|
|
assert.strictEqual(mirageTargets.length, 5);
|
|
assert.strictEqual(store.peekAll('target').length, 0);
|
|
const results = await store.query('target', {}, { pushToStore: true });
|
|
assert.strictEqual(results.length, 5);
|
|
assert.ok(
|
|
results.every((result) => result instanceof TargetModel),
|
|
'results are ember data models',
|
|
);
|
|
assert.strictEqual(store.peekAll('target').length, 5);
|
|
});
|
|
|
|
test('it does not push fetched results to the store when pushToStore is false', async function (assert) {
|
|
this.server.get(
|
|
'targets',
|
|
createPaginatedResponseHandler.call(this, mirageTargets, {
|
|
pageSize: 1,
|
|
}),
|
|
);
|
|
|
|
assert.strictEqual(mirageTargets.length, 5);
|
|
assert.strictEqual(store.peekAll('target').length, 0);
|
|
const results = await store.query('target', {}, { pushToStore: false });
|
|
assert.strictEqual(results.length, 5);
|
|
assert.ok(
|
|
results.every((result) => !(result instanceof TargetModel)),
|
|
'results are not ember data models',
|
|
);
|
|
assert.strictEqual(store.peekAll('target').length, 0);
|
|
});
|
|
});
|
|
|
|
module('sorting', function () {
|
|
test('it sorts by ascending (default) `name` using string sorting', async function (assert) {
|
|
const target001 = this.server.create('target', {
|
|
name: 'Target 001',
|
|
scope,
|
|
});
|
|
const target002 = this.server.create('target', {
|
|
name: 'Target 002',
|
|
scope,
|
|
});
|
|
const target003 = this.server.create('target', {
|
|
name: 'Target 003',
|
|
scope,
|
|
});
|
|
const target004 = this.server.create('target', {
|
|
name: 'Target 004',
|
|
scope,
|
|
});
|
|
const target005 = this.server.create('target', {
|
|
name: 'Target 005',
|
|
scope,
|
|
});
|
|
|
|
const shuffledTargets = faker.helpers.shuffle([
|
|
target001,
|
|
target002,
|
|
target003,
|
|
target004,
|
|
target005,
|
|
]);
|
|
|
|
this.server.get(
|
|
'targets',
|
|
createPaginatedResponseHandler.call(this, shuffledTargets, {
|
|
pageSize: 1,
|
|
}),
|
|
);
|
|
|
|
const results = await store.query('target', {
|
|
query: { sort: { attribute: 'name' } },
|
|
});
|
|
|
|
assert.deepEqual(
|
|
results.map(({ name }) => name),
|
|
['Target 001', 'Target 002', 'Target 003', 'Target 004', 'Target 005'],
|
|
);
|
|
});
|
|
|
|
test('it sorts by descending, then ascending `name` using string sorting', async function (assert) {
|
|
const target001 = this.server.create('target', {
|
|
name: 'Target 001',
|
|
scope,
|
|
});
|
|
const target002 = this.server.create('target', {
|
|
name: 'Target 002',
|
|
scope,
|
|
});
|
|
const target003 = this.server.create('target', {
|
|
name: 'Target 003',
|
|
scope,
|
|
});
|
|
const target004 = this.server.create('target', {
|
|
name: 'Target 004',
|
|
scope,
|
|
});
|
|
|
|
const shuffledTargets = faker.helpers.shuffle([
|
|
target001,
|
|
target002,
|
|
target003,
|
|
target004,
|
|
]);
|
|
|
|
this.server.get(
|
|
'targets',
|
|
createPaginatedResponseHandler.call(this, shuffledTargets, {
|
|
pageSize: 1,
|
|
}),
|
|
);
|
|
|
|
const resultsDesc = await store.query('target', {
|
|
query: { sort: { attribute: 'name', direction: 'desc' } },
|
|
});
|
|
const resultsAsc = await store.query('target', {
|
|
query: { sort: { attribute: 'name', direction: 'asc' } },
|
|
});
|
|
assert.deepEqual(
|
|
resultsDesc.map(({ name }) => name),
|
|
['Target 004', 'Target 003', 'Target 002', 'Target 001'],
|
|
);
|
|
assert.deepEqual(
|
|
resultsAsc.map(({ name }) => name),
|
|
['Target 001', 'Target 002', 'Target 003', 'Target 004'],
|
|
);
|
|
});
|
|
});
|
|
|
|
test('it supports searching', async function (assert) {
|
|
this.server.create('target', {
|
|
name: 'Magical Target',
|
|
description: 'A target',
|
|
scope,
|
|
});
|
|
|
|
this.server.create('target', {
|
|
name: 'Target 2',
|
|
description: 'Target with magic',
|
|
scope,
|
|
});
|
|
|
|
this.server.createList('target', 4, { scope });
|
|
|
|
this.server.get(
|
|
'targets',
|
|
createPaginatedResponseHandler.call(
|
|
this,
|
|
this.server.schema.targets.all().models,
|
|
{
|
|
pageSize: 1,
|
|
},
|
|
),
|
|
);
|
|
|
|
const results = await store.query('target', {
|
|
query: { search: 'magic' },
|
|
});
|
|
|
|
assert.deepEqual(
|
|
results
|
|
// search results aren't returning in a consistent order so
|
|
// the sorting is used to be able to assert
|
|
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
|
.map(({ name, description }) => ({ name, description })),
|
|
[
|
|
{
|
|
description: 'A target',
|
|
name: 'Magical Target',
|
|
},
|
|
{
|
|
description: 'Target with magic',
|
|
name: 'Target 2',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('it supports filtering', async function (assert) {
|
|
this.server.create('target', {
|
|
name: 'specific-target',
|
|
scope,
|
|
});
|
|
|
|
this.server.createList('target', 5, { scope });
|
|
|
|
this.server.get(
|
|
'targets',
|
|
createPaginatedResponseHandler.call(
|
|
this,
|
|
this.server.schema.targets.all().models,
|
|
{
|
|
pageSize: 1,
|
|
},
|
|
),
|
|
);
|
|
|
|
const results = await store.query('target', {
|
|
query: { filters: { name: [{ equals: 'specific-target' }] } },
|
|
});
|
|
|
|
assert.strictEqual(results[0].name, 'specific-target');
|
|
});
|
|
});
|