UI: Repo layer integration tests (#4454) (#4563)

ui: Repo layer integration tests for methods that touch the API

Includes a `repo` test helper to make repetitive tasks easier, plus a
injectable reporter for sending performance metrics to a centralized metrics
system

Also noticed somewhere in the ember models that I'd like to improve, but left
for the moment to make sure I concentrate on one task at a time, more or less:

The tests currently asserts against the existing JSON tree, which doesn't
seem to be a very nice tree.

The work at hand here is to refactor what is there, so test for the not
nice tree to ensure we don't get any regression, and add a skipped test
so I can come back here later
This commit is contained in:
John Cowen 2018-08-29 10:00:15 +01:00 committed by GitHub
parent accdefd18e
commit 4232561a38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 678 additions and 11 deletions

View File

@ -9,4 +9,5 @@ export default Model.extend({
[SLUG_KEY]: attr('string'),
Coord: attr(),
Segment: attr('string'),
Datacenter: attr('string'),
});

View File

@ -9,6 +9,7 @@ export const SLUG_KEY = 'Name';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
// TODO: Are these required?
Services: hasMany('service'),
Nodes: hasMany('node'),
});

View File

@ -11,6 +11,7 @@ const model = Model.extend({
SourceNS: attr('string'),
SourceName: attr('string'),
DestinationName: attr('string'),
DestinationNS: attr('string'),
Precedence: attr('number'),
SourceType: attr('string', { defaultValue: 'consul' }),
Action: attr('string', { defaultValue: 'deny' }),

View File

@ -18,8 +18,8 @@ export default Model.extend({
// preferably removeNull would be done in this layer also as if a property is `null`
// default Values don't kick in, which also explains `Tags` elsewhere
Value: attr('string'), //, {defaultValue: function() {return '';}}
CreateIndex: attr('string'),
ModifyIndex: attr('string'),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
Session: attr('string'),
Datacenter: attr('string'),

View File

@ -9,6 +9,7 @@ export default Service.extend({
dc: dc,
});
},
// TODO: Why Key? Probably should be findBySlug like the others
findByKey: function(slug, dc) {
return get(this, 'store').queryRecord('session', {
id: slug,

View File

@ -27,6 +27,7 @@ export const get = function(_url, options = { headers: { cookie: {} } }) {
}, {}),
},
{
set: function() {},
send: function(content) {
resolve(JSON.parse(content));
},

View File

@ -0,0 +1,5 @@
/* eslint no-console: "off" */
import getMeasure from 'consul-ui/tests/lib/measure/getMeasure';
let report;
let len = 1;
export default getMeasure(len, report);

100
ui-v2/tests/helpers/repo.js Normal file
View File

@ -0,0 +1,100 @@
import { get as httpGet } from 'consul-ui/tests/helpers/api';
import { get } from '@ember/object';
import measure from 'consul-ui/tests/helpers/measure';
/** Stub an ember-data adapter response using the private method
*
* Allows you to easily specify a HTTP response for the Adapter. The stub only works
* during the 'lifetime' of `cb` and is reset to normal unstubbed functionality afterwards.
*
* Please Note: This overwrites a private ember-data Adapter method, please understand
* the consequences of doing this if you are using it
*
* @param {function} cb - The callback, or test case, to run using the stubbed response
* @param {object} payload - The payload to use as the response
* @param {DS.Adapter} adapter - An instance of an ember-data Adapter
*/
const stubAdapterResponse = function(cb, payload, adapter) {
const payloadClone = JSON.parse(JSON.stringify(payload));
const ajax = adapter._ajaxRequest;
adapter._ajaxRequest = function(options) {
options.success(payloadClone, '200', {
status: 200,
textStatus: '200',
getAllResponseHeaders: function() {
return '';
},
});
};
return cb(payload).then(function(result) {
adapter._ajaxRequest = ajax;
return result;
});
};
/** `repo` a helper function to faciliate easy integration testing of ember-data Service 'repo' layers
*
* Test performance is also measured using `consul-ui/tests/helpers/measure` and therefore results
* can optionally be sent to a centralized metrics collection stack
*
* @param {string} name - The name of your repo Service (only used for meta purposes)
* @param {string} payload - The method you are testing (only used for meta purposes)
* @param {Service} service - An instance of an ember-data based repo Service
* @param {function} stub - A function that receives a `stub` function allowing you to stub
* an API endpoint with a set of cookies/env vars used by the double
* @param {function} test - Your test case. This function receives an instance of the Service provided
* above as a first and only argument, it should return the result of your test
* @param {function} assert - Your assertion. This receives the result of the previous function as the first
* argument and a function to that receives the stubbed payload giving you an
* opportunity to mutate it before returning for use in your assertion
*/
export default function(name, method, service, stub, test, assert) {
const adapter = get(service, 'store').adapterFor(name.toLowerCase());
let tags = {};
const requestHeaders = function(url, cookies = {}) {
const key = Object.keys(cookies).find(function(item) {
return item.indexOf('COUNT') !== -1;
});
tags = {
count: typeof key !== 'undefined' ? parseInt(cookies[key]) : 1,
};
return httpGet(url, {
headers: {
cookie: cookies,
},
});
};
const parseResponse = function(response) {
let actual;
if (typeof response.toArray === 'function') {
actual = response.toArray().map(function(item) {
return get(item, 'data');
});
} else {
if (typeof response.get === 'function') {
actual = get(response, 'data');
} else {
actual = response;
}
}
return actual;
};
return stub(requestHeaders).then(function(payload) {
return stubAdapterResponse(
function(payload) {
return measure(
function() {
return test(service);
},
`${name}Service.${method}`,
tags
).then(function(response) {
assert(parseResponse(response), function(cb) {
return cb(payload);
});
});
},
payload,
adapter
);
});
}

View File

@ -0,0 +1,61 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
moduleFor('service:acls', 'Integration | Service | acls', {
// Specify the other units that are required for this test.
needs: ['service:store', 'model:acl', 'adapter:acl', 'serializer:acl', 'service:settings'],
});
const dc = 'dc-1';
const id = 'token-name';
test('findByDatacenter returns the correct data for list endpoint', function(assert) {
return repo(
'Acl',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/acl/list?dc=${dc}`, {
CONSUL_ACL_COUNT: '100',
});
},
function performTest(service) {
return service.findAllByDatacenter(dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
})
);
})
);
}
);
});
test('findBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Acl',
'findBySlug',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/acl/info/${id}?dc=${dc}`);
},
function performTest(service) {
return service.findBySlug(id, dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
const item = payload[0];
return Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
});
})
);
}
);
});

View File

@ -0,0 +1,43 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
const NAME = 'coordinate';
moduleFor(`service:${NAME}s`, `Integration | Service | ${NAME}s`, {
// Specify the other units that are required for this test.
needs: [
'service:settings',
'service:store',
`adapter:${NAME}`,
`serializer:${NAME}`,
`model:${NAME}`,
],
});
const dc = 'dc-1';
test('findAllByDatacenter returns the correct data for list endpoint', function(assert) {
return repo(
'Coordinate',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/coordinate/nodes?dc=${dc}`, {
CONSUL_NODE_COUNT: '100',
});
},
function performTest(service) {
return service.findAllByDatacenter(dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.Node}"]`,
})
);
})
);
}
);
});

View File

@ -0,0 +1,45 @@
import { moduleFor, test } from 'ember-qunit';
import { skip } from 'qunit';
import repo from 'consul-ui/tests/helpers/repo';
const NAME = 'dc';
moduleFor(`service:${NAME}`, `Integration | Service | ${NAME}s`, {
// Specify the other units that are required for this test.
needs: [
'service:settings',
'service:store',
`adapter:${NAME}`,
`serializer:${NAME}`,
`model:${NAME}`,
// relationships
'model:service',
'model:node',
],
});
skip("findBySlug (doesn't interact with the API) but still needs an int test");
test('findAll returns the correct data for list endpoint', function(assert) {
return repo(
'Dc',
'findAll',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/catalog/datacenters`, {
CONSUL_DATACENTER_COUNT: '100',
});
},
function performTest(service) {
return service.findAll();
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item => ({ Name: item })).sort(function(a, b) {
if (a.Name < b.Name) return -1;
if (a.Name > b.Name) return 1;
return 0;
});
})
);
}
);
});

View File

@ -0,0 +1,73 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
const NAME = 'intention';
moduleFor(`service:${NAME}s`, `Integration | Service | ${NAME}s`, {
// Specify the other units that are required for this test.
needs: [
'service:settings',
'service:store',
`adapter:${NAME}`,
`serializer:${NAME}`,
`model:${NAME}`,
],
});
const dc = 'dc-1';
const id = 'token-name';
test('findAllByDatacenter returns the correct data for list endpoint', function(assert) {
return repo(
'Intention',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/connect/intentions?dc=${dc}`, {
CONSUL_INTENTION_COUNT: '100',
});
},
function performTest(service) {
return service.findAllByDatacenter(dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
CreatedAt: new Date(item.CreatedAt),
UpdatedAt: new Date(item.UpdatedAt),
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
})
);
})
);
}
);
});
test('findBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Intention',
'findBySlug',
this.subject(),
function(stub) {
return stub(`/v1/connect/intentions/${id}?dc=${dc}`);
},
function(service) {
return service.findBySlug(id, dc);
},
function(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
const item = payload;
return Object.assign({}, item, {
CreatedAt: new Date(item.CreatedAt),
UpdatedAt: new Date(item.UpdatedAt),
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
});
})
);
}
);
});

View File

@ -0,0 +1,70 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
const NAME = 'kv';
moduleFor(`service:${NAME}`, `Integration | Service | ${NAME}s`, {
// Specify the other units that are required for this test.
needs: [
'service:settings',
'service:store',
`adapter:${NAME}`,
`serializer:${NAME}`,
`model:${NAME}`,
'service:atob',
],
});
const dc = 'dc-1';
const id = 'key-name';
test('findAllBySlug returns the correct data for list endpoint', function(assert) {
return repo(
'Kv',
'findAllBySlug',
this.subject(),
function retrieveTest(stub) {
return stub(`/v1/kv/${id}?keys&dc=${dc}`, {
CONSUL_KV_COUNT: '1',
});
},
function performTest(service) {
return service.findAllBySlug(id, dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item => {
return {
Datacenter: dc,
uid: `["${dc}","${item}"]`,
Key: item,
};
});
})
);
}
);
});
test('findAllBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Kv',
'findAllBySlug',
this.subject(),
function(stub) {
return stub(`/v1/kv/${id}?dc=${dc}`);
},
function(service) {
return service.findBySlug(id, dc);
},
function(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
const item = payload[0];
return Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.Key}"]`,
});
})
);
}
);
});

View File

@ -0,0 +1,73 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
const NAME = 'node';
moduleFor(`service:${NAME}s`, `Integration | Service | ${NAME}s`, {
// Specify the other units that are required for this test.
needs: [
'service:settings',
'service:store',
`adapter:${NAME}`,
`serializer:${NAME}`,
`model:${NAME}`,
'service:coordinates',
'adapter:coordinate',
'serializer:coordinate',
'model:coordinate',
],
});
const dc = 'dc-1';
const id = 'token-name';
test('findByDatacenter returns the correct data for list endpoint', function(assert) {
return repo(
'Node',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/internal/ui/nodes?dc=${dc}`, {
CONSUL_NODE_COUNT: '100',
});
},
function performTest(service) {
return service.findAllByDatacenter(dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
})
);
})
);
}
);
});
test('findBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Node',
'findBySlug',
this.subject(),
function(stub) {
return stub(`/v1/internal/ui/node/${id}?dc=${dc}`);
},
function(service) {
return service.findBySlug(id, dc);
},
function(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
const item = payload;
return Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
});
})
);
}
);
});

View File

@ -0,0 +1,82 @@
import { moduleFor, test } from 'ember-qunit';
import { skip } from 'qunit';
import repo from 'consul-ui/tests/helpers/repo';
moduleFor('service:services', 'Integration | Service | services', {
// Specify the other units that are required for this test.
needs: [
'service:store',
'model:service',
'adapter:service',
'serializer:service',
'service:settings',
],
});
const dc = 'dc-1';
const id = 'token-name';
test('findByDatacenter returns the correct data for list endpoint', function(assert) {
return repo(
'Service',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/internal/ui/services?dc=${dc}`, {
CONSUL_SERVICE_COUNT: '100',
});
},
function performTest(service) {
return service.findAllByDatacenter(dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.Name}"]`,
})
);
})
);
}
);
});
skip('findBySlug returns a sane tree');
test('findBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Service',
'findBySlug',
this.subject(),
function(stub) {
return stub(`/v1/health/service/${id}?dc=${dc}`, {
CONSUL_NODE_COUNT: 1,
});
},
function(service) {
return service.findBySlug(id, dc);
},
function(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
// TODO: So this tree is all 'wrong', it's not having any major impact
// this this tree needs revisting to something that makes more sense
payload = Object.assign(
{},
{ Nodes: payload },
{
Datacenter: dc,
uid: `["${dc}","${id}"]`,
}
);
const nodes = payload.Nodes;
const service = payload.Nodes[0];
service.Nodes = nodes;
service.Tags = payload.Nodes[0].Service.Tags;
return service;
})
);
}
);
});

View File

@ -0,0 +1,69 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
const NAME = 'session';
moduleFor(`service:${NAME}`, `Integration | Service | ${NAME}s`, {
// Specify the other units that are required for this test.
needs: [
'service:settings',
'service:store',
`adapter:${NAME}`,
`serializer:${NAME}`,
`model:${NAME}`,
],
});
const dc = 'dc-1';
const id = 'node-name';
test('findByNode returns the correct data for list endpoint', function(assert) {
return repo(
'Session',
'findByNode',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/session/node/${id}?dc=${dc}`, {
CONSUL_SESSION_COUNT: '100',
});
},
function performTest(service) {
return service.findByNode(id, dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
})
);
})
);
}
);
});
test('findByKey returns the correct data for item endpoint', function(assert) {
return repo(
'Session',
'findByKey',
this.subject(),
function(stub) {
return stub(`/v1/session/info/${id}?dc=${dc}`);
},
function(service) {
return service.findByKey(id, dc);
},
function(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
const item = payload[0];
return Object.assign({}, item, {
Datacenter: dc,
uid: `["${dc}","${item.ID}"]`,
});
})
);
}
);
});

View File

@ -0,0 +1,26 @@
/* eslint no-console: "off" */
const log = function(results, measurement, tags) {
console.log(measurement, results, tags);
};
export default function(len = 10000, report = log, performance = window.performance) {
return function(cb, measurement, tags) {
let actual;
return new Array(len)
.fill(true)
.reduce(function(prev, item, i) {
return prev.then(function(ms) {
return new Promise(function(resolve) {
const start = performance.now();
cb().then(function(res) {
actual = res;
resolve(ms + (performance.now() - start));
});
});
});
}, Promise.resolve(0))
.then(function(total) {
report({ avg: total / len, total: total, count: len }, measurement, tags);
return actual;
});
};
}

View File

@ -70,8 +70,8 @@
"@glimmer/di" "^0.2.0"
"@hashicorp/api-double@^1.3.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.4.0.tgz#17ddad8e55370de0d24151a38c5f029bc207cafe"
version "1.4.1"
resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.4.1.tgz#4f4be42f0e2fec07450415cfe19294654bc7ad00"
dependencies:
"@gardenhq/o" "^8.0.1"
"@gardenhq/tick-control" "^2.0.0"
@ -82,12 +82,12 @@
js-yaml "^3.10.0"
"@hashicorp/consul-api-double@^1.4.0":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-1.4.1.tgz#547643b98c3a26a1fe1584189bd05e4d9f383966"
version "1.4.3"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-1.4.3.tgz#0d08e167b1163200885636e6d368585004db1c98"
"@hashicorp/ember-cli-api-double@^1.3.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-1.4.0.tgz#4190b30f8b6a51ec33a707c45effede6e93e6b38"
version "1.5.1"
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-1.5.1.tgz#92789eaf2073b5871d859700bc696e9552bb835b"
dependencies:
"@hashicorp/api-double" "^1.3.0"
array-range "^1.0.1"
@ -111,6 +111,10 @@
version "10.0.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.0.8.tgz#37b4d91d4e958e4c2ba0be2b86e7ed4ff19b0858"
"@xg-wang/whatwg-fetch@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@xg-wang/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#f7b222c012a238e7d6e89ed3d72a1e0edb58453d"
JSONStream@^1.0.3:
version "1.3.2"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
@ -6689,12 +6693,22 @@ miller-rabin@^4.0.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
mime-db@~1.35.0:
version "1.35.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47"
mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.17, mime-types@~2.1.7:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
dependencies:
mime-db "~1.33.0"
mime-types@~2.1.18:
version "2.1.19"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
dependencies:
mime-db "~1.35.0"
mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
@ -7468,9 +7482,10 @@ preserve@^0.2.0:
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
pretender@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pretender/-/pretender-2.0.0.tgz#5adae189f1d5b25f86113f9225df25bed54f4072"
version "2.1.1"
resolved "https://registry.yarnpkg.com/pretender/-/pretender-2.1.1.tgz#5085f0a1272c31d5b57c488386f69e6ca207cb35"
dependencies:
"@xg-wang/whatwg-fetch" "^3.0.0"
fake-xml-http-request "^2.0.0"
route-recognizer "^0.3.3"