ui: Ensure dc selector correctly shows the currently selected dc (#11380)

* ui: Ensure dc selector correctly shows the currently selected dc

* ui: Restrict access to non-default partitions in non-primaries (#11420)

This PR restricts access via the UI to only the default partition when in a non-primary datacenter i.e. you can only have multiple (non-default) partitions in the primary datacenter.
This commit is contained in:
John Cowen 2021-10-26 19:26:04 +01:00 committed by GitHub
parent b96794401f
commit 76f5de1455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 190 additions and 98 deletions

3
.changelog/11380.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Ensure dc selector correctly shows the currently selected dc
```

View File

@ -1,4 +1,4 @@
{{#if (can "choose partitions")}} {{#if (can "choose partitions" dc=@dc)}}
<li <li
class="partitions" class="partitions"
data-test-partition-menu data-test-partition-menu
@ -49,5 +49,12 @@
</BlockSlot> </BlockSlot>
</PopoverMenu> </PopoverMenu>
</li> </li>
{{else}}
<li
class="partition"
aria-label="Admin Partition"
>
{{@partition}}
</li>
{{/if}} {{/if}}

View File

@ -20,7 +20,10 @@ export default class PartitionAbility extends BaseAbility {
} }
get canChoose() { get canChoose() {
return this.canUse; if(typeof this.dc === 'undefined') {
return false;
}
return this.canUse && this.dc.Primary;
} }
get canUse() { get canUse() {

View File

@ -51,7 +51,7 @@
{{/if}} {{/if}}
</label> </label>
{{/if}} {{/if}}
{{#if (can 'choose partitions')}} {{#if (can 'choose partitions' dc=@dc)}}
<label data-test-source-partition class="type-select{{if item.error.SourcePartition ' has-error'}}"> <label data-test-source-partition class="type-select{{if item.error.SourcePartition ' has-error'}}">
<span>Source Partition</span> <span>Source Partition</span>
<PowerSelectWithCreate <PowerSelectWithCreate
@ -123,7 +123,7 @@
{{/if}} {{/if}}
</label> </label>
{{/if}} {{/if}}
{{#if (can 'choose partitions')}} {{#if (can 'choose partitions' dc=@dc)}}
<label data-test-destination-partition class="type-select{{if item.error.DestinationPartition ' has-error'}}"> <label data-test-destination-partition class="type-select{{if item.error.DestinationPartition ' has-error'}}">
<span>Destination Partition</span> <span>Destination Partition</span>
<PowerSelectWithCreate <PowerSelectWithCreate

View File

@ -4,7 +4,7 @@
> >
<DataForm <DataForm
@type="intention" @type="intention"
@dc={{@dc}} @dc={{@dc.Name}}
@nspace={{@nspace}} @nspace={{@nspace}}
@partition={{@partition}} @partition={{@partition}}
@autofill={{@autofill}} @autofill={{@autofill}}
@ -77,7 +77,7 @@ as |api|>
@src={{uri '/${partition}/*/${dc}/services' @src={{uri '/${partition}/*/${dc}/services'
(hash (hash
partition=@partition partition=@partition
dc=@dc dc=@dc.Name
) )
}} }}
@onchange={{action this.createServices item}} @onchange={{action this.createServices item}}
@ -88,7 +88,7 @@ as |api|>
@src={{uri '/${partition}/*/${dc}/namespaces' @src={{uri '/${partition}/*/${dc}/namespaces'
(hash (hash
partition=@partition partition=@partition
dc=@dc dc=@dc.Name
) )
}} }}
@onchange={{action this.createNspaces item}} @onchange={{action this.createNspaces item}}
@ -99,7 +99,7 @@ as |api|>
<DataSource <DataSource
@src={{uri '/*/*/${dc}/partitions' @src={{uri '/*/*/${dc}/partitions'
(hash (hash
dc=@dc dc=@dc.Name
) )
}} }}
@onchange={{action this.createPartitions item}} @onchange={{action this.createPartitions item}}
@ -114,6 +114,7 @@ as |api|>
> >
<Consul::Intention::Form::Fieldsets <Consul::Intention::Form::Fieldsets
@nspaces={{this.nspaces}} @nspaces={{this.nspaces}}
@dc={{@dc}}
@partitions={{this.partitions}} @partitions={{this.partitions}}
@services={{this.services}} @services={{this.services}}
@SourceName={{this.SourceName}} @SourceName={{this.SourceName}}

View File

@ -33,11 +33,18 @@
{{#each (sort-by 'Name' @dcs) as |item|}} {{#each (sort-by 'Name' @dcs) as |item|}}
<MenuItem <MenuItem
data-test-datacenter-picker data-test-datacenter-picker
class={{concat (if (eq @dc.Name item.Name) 'is-active') (if item.Local ' is-local') }} class={{concat
(if (eq @dc.Name item.Name) 'is-active')
(if item.Local ' is-local')
(if item.Primary ' is-primary')
}}
@href={{href-to '.' params=(hash dc=item.Name)}} @href={{href-to '.' params=(hash dc=item.Name)}}
> >
<BlockSlot @name="label"> <BlockSlot @name="label">
{{item.Name}} {{item.Name}}
{{#if item.Primary}}
<span>Primary</span>
{{/if}}
{{#if item.Local}} {{#if item.Local}}
<span>Local</span> <span>Local</span>
{{/if}} {{/if}}

View File

@ -1,4 +1,4 @@
.hashicorp-consul { %hashicorp-consul {
[role='banner'] nav .dcs { [role='banner'] nav .dcs {
@extend %main-nav-vertical-hoisted; @extend %main-nav-vertical-hoisted;
left: 100px; left: 100px;
@ -27,3 +27,6 @@
margin-left: 2px; margin-left: 2px;
} }
} }
.hashicorp-consul {
@extend %hashicorp-consul;
}

View File

@ -1,6 +1,15 @@
@import './skin'; @import './skin';
@import './layout'; @import './layout';
/* things that should look like nav buttons */ /* things that should look like nav buttons */
/* items are single things that look like button */
/* but aren't clickable */
%main-nav-vertical > ul > li[aria-label] {
@extend %main-nav-vertical-item;
}
/**/
/* actual clickable button-y things plus states */
%main-nav-vertical > ul > li > a { %main-nav-vertical > ul > li > a {
@extend %main-nav-vertical-action; @extend %main-nav-vertical-action;
} }
@ -21,6 +30,8 @@
%main-nav-vertical > ul > li > label { %main-nav-vertical > ul > li > label {
@extend %main-nav-vertical-action; @extend %main-nav-vertical-action;
} }
/**/
%main-nav-vertical .popover-menu { %main-nav-vertical .popover-menu {
margin-top: 0.5rem; margin-top: 0.5rem;
} }

View File

@ -12,11 +12,15 @@
visibility: hidden; visibility: hidden;
} }
%main-nav-vertical li.partitions, %main-nav-vertical li.partitions,
%main-nav-vertical li.nspaces, %main-nav-vertical li.partition,
%main-nav-vertical li.dcs { %main-nav-vertical li.nspaces {
margin-bottom: 25px; margin-bottom: 25px;
padding: 0 26px; padding: 0 26px;
} }
%main-nav-vertical li.dcs {
margin-bottom: 18px;
padding: 0 18px;
}
// TODO: We no longer have the rule that menu-panel buttons only contain two // TODO: We no longer have the rule that menu-panel buttons only contain two
// items, left and right aligned. We should remove this and look to use // items, left and right aligned. We should remove this and look to use
// align-self for anything that needs right aligning instead. // align-self for anything that needs right aligning instead.

View File

@ -32,6 +32,7 @@
%main-nav-vertical-action { %main-nav-vertical-action {
color: rgb(var(--tone-gray-800)); color: rgb(var(--tone-gray-800));
} }
%main-nav-vertical-item,
%main-nav-vertical-action-intent, %main-nav-vertical-action-intent,
%main-nav-vertical-action-active { %main-nav-vertical-action-active {
color: rgb(var(--tone-gray-999)); color: rgb(var(--tone-gray-999));
@ -40,13 +41,16 @@
background-color: rgb(var(--tone-gray-150)); background-color: rgb(var(--tone-gray-150));
border-color: rgb(var(--tone-gray-999)); border-color: rgb(var(--tone-gray-999));
} }
%main-nav-vertical li[aria-label]::before,
%main-nav-vertical .popover-menu[aria-label]::before { %main-nav-vertical .popover-menu[aria-label]::before {
color: rgb(var(--tone-gray-700));
content: attr(aria-label); content: attr(aria-label);
display: block; display: block;
margin-top: -0.5rem; margin-top: -0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
%main-nav-vertical .is-local span:last-of-type { %main-nav-vertical .is-primary span,
%main-nav-vertical .is-local span {
@extend %pill-200; @extend %pill-200;
color: rgb(var(--tone-gray-000)); color: rgb(var(--tone-gray-000));
background-color: rgb(var(--tone-gray-500)); background-color: rgb(var(--tone-gray-500));

View File

@ -2,6 +2,7 @@
{{will-destroy this.disconnect}} {{will-destroy this.disconnect}}
<section <section
{{did-insert (fn this.attributeChanged 'element')}} {{did-insert (fn this.attributeChanged 'element')}}
{{did-update (fn this.attributeChanged 'model' @model)}}
class="outlet" class="outlet"
data-outlet={{@name}} data-outlet={{@name}}
data-route={{this.routeName}} data-route={{this.routeName}}

View File

@ -22,6 +22,8 @@ export default class Outlet extends Component {
@tracked previousState; @tracked previousState;
@tracked endTransition; @tracked endTransition;
@tracked route;
get model() { get model() {
return this.args.model || {}; return this.args.model || {};
} }
@ -52,6 +54,9 @@ export default class Outlet extends Component {
this.setAppRoute(this.router.currentRouteName); this.setAppRoute(this.router.currentRouteName);
} }
break; break;
case 'model':
this.route._model = this.args.model;
break;
} }
} }

View File

@ -2,10 +2,8 @@ import Helper from '@ember/component/helper';
import { getOwner } from '@ember/application'; import { getOwner } from '@ember/application';
export default class CachedHelper extends Helper { export default class CachedHelper extends Helper {
compute([model], hash) { compute([model, params], hash) {
return () => { const container = getOwner(this);
const container = getOwner(this); return container.lookup(`service:repository/${model}`).cached(params);
return container.lookup(`service:repository/${model}`).cached(hash);
};
} }
} }

View File

@ -8,6 +8,7 @@ export default class Datacenter extends Model {
@attr('string') uid; @attr('string') uid;
@attr('string') Name; @attr('string') Name;
@attr('boolean') Local; @attr('boolean') Local;
@attr('boolean') Primary;
@attr('string') DefaultACLPolicy; @attr('string') DefaultACLPolicy;
@attr('boolean', { defaultValue: () => true }) MeshEnabled; @attr('boolean', { defaultValue: () => true }) MeshEnabled;

View File

@ -1,31 +1,42 @@
import { inject as service } from '@ember/service';
import Serializer from './application'; import Serializer from './application';
import { inject as service } from '@ember/service';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/dc';
import {
HEADERS_SYMBOL,
HEADERS_DEFAULT_ACL_POLICY as DEFAULT_ACL_POLICY,
} from 'consul-ui/utils/http/consul';
export default class DcSerializer extends Serializer { export default class DcSerializer extends Serializer {
@service('env') env; @service('env') env;
primaryKey = 'Name'; primaryKey = PRIMARY_KEY;
slugKey = SLUG_KEY;
// datacenters come in as an array of plain strings. Convert to objects
// instead and collect all the other datacenter info from other places and
// add it to each datacenter object
respondForQuery(respond, query) { respondForQuery(respond, query) {
return respond(function(headers, body) { return super.respondForQuery(
return { cb => respond((headers, body) => {
body, body = body.map(item => ({
headers, Datacenter: '',
}; [this.slugKey]: item,
}); }));
} body = cb(headers, body);
headers = body[HEADERS_SYMBOL];
normalizePayload(payload, id, requestType) { const Local = this.env.var('CONSUL_DATACENTER_LOCAL');
switch (requestType) { const Primary = this.env.var('CONSUL_DATACENTER_PRIMARY');
case 'query': const DefaultACLPolicy = headers[DEFAULT_ACL_POLICY.toLowerCase()];
return payload.body.map(item => {
return { return body.map(item => ({
Local: this.env.var('CONSUL_DATACENTER_LOCAL') === item, ...item,
[this.primaryKey]: item, Local: item.Name === Local,
DefaultACLPolicy: payload.headers['x-consul-default-acl-policy'], Primary: item.Name === Primary,
}; DefaultACLPolicy: DefaultACLPolicy,
}); }));
} }),
return payload; query
);
} }
} }

View File

@ -124,6 +124,10 @@ export default class RepositoryService extends Service {
return this.store.peekRecord(this.getModelName(), id); return this.store.peekRecord(this.getModelName(), id);
} }
peekAll() {
return this.store.peekAll(this.getModelName());
}
cached(params) { cached(params) {
const entries = Object.entries(params); const entries = Object.entries(params);
return this.store.peekAll(this.getModelName()).filter(item => { return this.store.peekAll(this.getModelName()).filter(item => {

View File

@ -15,8 +15,10 @@ export default class DiscoveryChainService extends RepositoryService {
@dataSource('/:partition/:ns/:dc/discovery-chain/:id') @dataSource('/:partition/:ns/:dc/discovery-chain/:id')
findBySlug(params, configuration = {}) { findBySlug(params, configuration = {}) {
const datacenter = this.dcs.peekOne(params.dc); // peekAll and find is fine here as datacenter count should be relatively
if (datacenter !== null && !get(datacenter, 'MeshEnabled')) { // low, and DCs are the top bucket (when talking dc's partitions, nspaces)
const datacenter = this.dcs.peekAll().findBy('Name', params.dc);
if (typeof datacenter !== 'undefined' && !get(datacenter, 'MeshEnabled')) {
return Promise.resolve(); return Promise.resolve();
} }
return super.findBySlug(...arguments).catch(e => { return super.findBySlug(...arguments).catch(e => {
@ -24,7 +26,7 @@ export default class DiscoveryChainService extends RepositoryService {
const body = (get(e, 'errors.firstObject.detail') || '').trim(); const body = (get(e, 'errors.firstObject.detail') || '').trim();
switch (code) { switch (code) {
case '500': case '500':
if (datacenter !== null && body.endsWith(ERROR_MESH_DISABLED)) { if (typeof datacenter !== 'undefined' && body.endsWith(ERROR_MESH_DISABLED)) {
set(datacenter, 'MeshEnabled', false); set(datacenter, 'MeshEnabled', false);
} }
return; return;

View File

@ -135,6 +135,7 @@ export default class RoutletService extends Service {
const outlet = outlets.get(keys[key]); const outlet = outlets.get(keys[key]);
if (typeof outlet !== 'undefined') { if (typeof outlet !== 'undefined') {
route._model = outlet.model; route._model = outlet.model;
outlet.route = route;
// TODO: Try to avoid the double computation bug // TODO: Try to avoid the double computation bug
schedule('afterRender', () => { schedule('afterRender', () => {
outlet.routeName = route.args.name; outlet.routeName = route.args.name;

View File

@ -73,39 +73,45 @@ as |dcs|>
{{! Once we have a list of DCs make sure the DC we are asking for exists }} {{! Once we have a list of DCs make sure the DC we are asking for exists }}
{{! If not use the DC that the UI is running in }} {{! If not use the DC that the UI is running in }}
{{#let {{#let
(or (or
(get (object-at 0 (cached-model (if nofound.dc
'dc' (object-at 0 (cached-model
(hash 'dc'
Name=notfound.dc (hash
Name=notfound.dc
)
) )
)) 'Name') )
)
(get (object-at 0 (cached-model (object-at 0 (cached-model
'dc' 'dc'
(hash (hash
Name=route.params.dc Name=route.params.dc
) )
)) 'Name') )
)
(env "CONSUL_DATACENTER_LOCAL") (hash
Name=(env "CONSUL_DATACENTER_LOCAL")
)
) )
dcs.data dcs.data
as |dc dcs|}} as |dc dcs|}}
{{#if (and (gt dc.Name.length 0) dcs nspace partition)}}
{{#if (and (gt dc.length 0) dcs nspace partition)}}
{{! figure out our current DC and convert it to a model }} {{! figure out our current DC and convert it to a model }}
<DataSource <DataSource
@src={{uri '/${partition}/*/${dc}/datacenter/${name}' @src={{uri '/${partition}/*/${dc}/datacenter/${name}'
(hash (hash
dc=dc dc=dc.Name
partition=partition partition=partition
name=dc name=dc.Name
) )
}} }}
as |dc|> as |dc|>

View File

@ -45,7 +45,7 @@ as |item|}}
<BlockSlot @name="content"> <BlockSlot @name="content">
<Consul::Intention::Form <Consul::Intention::Form
@item={{item}} @item={{item}}
@dc={{route.params.dc}} @dc={{route.model.dc}}
@nspace={{route.params.nspace}} @nspace={{route.params.nspace}}
@partition={{route.params.partition}} @partition={{route.params.partition}}
@onsubmit={{route-action 'transitionTo' 'dc.intentions.index' @onsubmit={{route-action 'transitionTo' 'dc.intentions.index'

View File

@ -1,7 +1,6 @@
<Route <Route
@name={{routeName}} @name={{routeName}}
as |route|> as |route|>
<DataLoader <DataLoader
@src={{uri '/${partition}/${nspace}/${dc}/service-instances/for-service/${name}' @src={{uri '/${partition}/${nspace}/${dc}/service-instances/for-service/${name}'
(hash (hash
@ -69,7 +68,7 @@ as |items item dc|}}
{{! and use this to set MeshEnabled on the Datacenter }} {{! and use this to set MeshEnabled on the Datacenter }}
{{! if once chain is set, i.e. we've checked this dc we remove the DataSource }} {{! if once chain is set, i.e. we've checked this dc we remove the DataSource }}
{{! which will mark it for closure, which possibly could be reopened if }} {{! which will mark it for closure, which possibly could be reopened if }}
{{! the user clicks the routing/disco-chain tab}} {{! the user clicks the routing/disco-chain tab}}
{{#if (not chain)}} {{#if (not chain)}}
<DataSource <DataSource
@src={{uri '/${partition}/${nspace}/${dc}/discovery-chain/${name}' @src={{uri '/${partition}/${nspace}/${dc}/discovery-chain/${name}'
@ -83,6 +82,7 @@ as |items item dc|}}
@onchange={{action (mut chain) value="data"}} @onchange={{action (mut chain) value="data"}}
/> />
{{/if}} {{/if}}
{{did-insert (set this 'chain' undefined) route.params.dc}}
{{/if}} {{/if}}
{{#let {{#let
(hash (hash

View File

@ -2,10 +2,11 @@ import { get } from '@ember/object';
export default function(foreignKey, nspaceKey, partitionKey, hash = JSON.stringify) { export default function(foreignKey, nspaceKey, partitionKey, hash = JSON.stringify) {
return function(primaryKey, slugKey, foreignKeyValue, nspaceValue, partitionValue) { return function(primaryKey, slugKey, foreignKeyValue, nspaceValue, partitionValue) {
if (foreignKeyValue == null || foreignKeyValue.length < 1) {
throw new Error('Unable to create fingerprint, missing foreignKey value');
}
return function(item) { return function(item) {
foreignKeyValue = foreignKeyValue == null ? item[foreignKey] : foreignKeyValue;
if (foreignKeyValue == null) {
throw new Error('Unable to create fingerprint, missing foreignKey value');
}
const slugKeys = slugKey.split(','); const slugKeys = slugKey.split(',');
const slugValues = slugKeys.map(function(slugKey) { const slugValues = slugKeys.map(function(slugKey) {
if (get(item, slugKey) == null || get(item, slugKey).length < 1) { if (get(item, slugKey) == null || get(item, slugKey).length < 1) {

View File

@ -2,6 +2,7 @@
export const HEADERS_PARTITION = 'X-Consul-Partition'; export const HEADERS_PARTITION = 'X-Consul-Partition';
export const HEADERS_NAMESPACE = 'X-Consul-Namespace'; export const HEADERS_NAMESPACE = 'X-Consul-Namespace';
export const HEADERS_DATACENTER = 'X-Consul-Datacenter'; export const HEADERS_DATACENTER = 'X-Consul-Datacenter';
export const HEADERS_DEFAULT_ACL_POLICY = 'X-Consul-Default-Acl-Policy';
export const HEADERS_INDEX = 'X-Consul-Index'; export const HEADERS_INDEX = 'X-Consul-Index';
export const HEADERS_TOKEN = 'X-Consul-Token'; export const HEADERS_TOKEN = 'X-Consul-Token';
export const HEADERS_DIGEST = 'X-Consul-ContentHash'; export const HEADERS_DIGEST = 'X-Consul-ContentHash';

View File

@ -4,4 +4,4 @@
"*": "*":
headers: headers:
response: response:
x-consul-default-acl-policy: ${env('CONSUL_ACL_POLICY', fake.helpers.randomize(['allow', 'deny']))} X-Consul-Default-Acl-Policy: ${env('CONSUL_ACL_POLICY', fake.helpers.randomize(['allow', 'deny']))}

View File

@ -36,7 +36,7 @@ Feature: dc / services / instances / gateway: Show Gateway Service Instance
When I click addresses on the tabs When I click addresses on the tabs
And I see addressesIsSelected on the tabs And I see addressesIsSelected on the tabs
And I see 2 of the addresses object And I see 2 of the addresses object
And I see address on the addresses like yaml And I see address on the addresses vertically like yaml
--- ---
- 127.0.0.1:8080 - 127.0.0.1:8080
- 92.68.0.0:8081 - 92.68.0.0:8081

View File

@ -44,18 +44,19 @@ Feature: dc / services / show-routing: Show Routing for Service
--- ---
And I don't see routing on the tabs And I don't see routing on the tabs
And I don't see the "[data-test-error]" element And I don't see the "[data-test-error]" element
# FIXME: Does one dc not having connect and another having connect ever actually happen # Not entirely sure if having one dc not having connect
# And I visit the service page for yaml # and another having connect ever actually happen
# --- And I visit the service page for yaml
# dc: dc2 ---
# service: service-1 dc: dc2
# --- service: service-1
# And I see routing on the tabs ---
# And I visit the service page for yaml And I see routing on the tabs
# --- And I visit the service page for yaml
# dc: dc1 ---
# service: service-0 dc: dc1
# --- service: service-0
# Then a GET request wasn't made to "/v1/discovery-chain/service-0?dc=dc1&ns=@namespace" ---
# And I don't see routing on the tabs Then a GET request wasn't made to "/v1/discovery-chain/service-0?dc=dc1&ns=@namespace"
# And I don't see the "[data-test-error]" element And I don't see routing on the tabs
And I don't see the "[data-test-error]" element

View File

@ -1,24 +1,43 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit'; import { setupTest } from 'ember-qunit';
import { get } from 'consul-ui/tests/helpers/api'; import { get } from 'consul-ui/tests/helpers/api';
import {
HEADERS_DEFAULT_ACL_POLICY as DEFAULT_ACL_POLICY,
} from 'consul-ui/utils/http/consul';
module('Integration | Serializer | dc', function(hooks) { module('Integration | Serializer | dc', function(hooks) {
setupTest(hooks); setupTest(hooks);
test('respondForQuery returns the correct data for list endpoint', function(assert) { test('respondForQuery returns the correct data for list endpoint', function(assert) {
const serializer = this.owner.lookup('serializer:dc'); const serializer = this.owner.lookup('serializer:dc');
let env = this.owner.lookup('service:env');
env = env.var.bind(env);
const request = { const request = {
url: `/v1/catalog/datacenters`, url: `/v1/catalog/datacenters`,
}; };
return get(request.url).then(function(payload) { return get(request.url).then(function(payload) {
const expected = { const ALLOW = 'allow';
body: payload, const expected = payload.map(item => (
headers: {}, {
}; Name: item,
Datacenter: '',
Local: item === env('CONSUL_DATACENTER_LOCAL'),
Primary: item === env('CONSUL_DATACENTER_PRIMARY'),
DefaultACLPolicy: ALLOW
}
))
const actual = serializer.respondForQuery(function(cb) { const actual = serializer.respondForQuery(function(cb) {
const headers = {}; const headers = {
const body = payload; [DEFAULT_ACL_POLICY]: ALLOW
return cb(headers, body); };
return cb(headers, payload);
}, {
dc: '*',
});
actual.forEach((item, i) => {
assert.equal(actual[i].Name, expected[i].Name);
assert.equal(actual[i].Local, expected[i].Local);
assert.equal(actual[i].Primary, expected[i].Primary);
assert.equal(actual[i].DefaultACLPolicy, expected[i].DefaultACLPolicy);
}); });
assert.deepEqual(actual, expected);
}); });
}); });
}); });

View File

@ -21,12 +21,10 @@ test('findAll returns the correct data for list endpoint', function(assert) {
return service.findAll(); return service.findAll();
}, },
function performAssertion(actual, expected) { function performAssertion(actual, expected) {
assert.deepEqual( actual.forEach((item, i) => {
actual, assert.equal(actual[i].Name, item.Name);
expected(function(payload) { assert.equal(item.Local, i === 0);
return payload.map((item, i) => ({ Name: item, Local: i === 0 ? true : false })); });
})
);
} }
); );
}); });

View File

@ -37,7 +37,7 @@ module('Unit | Utility | create fingerprinter', function() {
const fingerprint = createFingerprinter('Datacenter', 'Namespace', 'Partition'); const fingerprint = createFingerprinter('Datacenter', 'Namespace', 'Partition');
[undefined, null].forEach(function(item) { [undefined, null].forEach(function(item) {
assert.throws(function() { assert.throws(function() {
fingerprint('uid', 'ID', item); fingerprint('uid', 'ID', item)({Datacenter: item});
}, /missing foreignKey/); }, /missing foreignKey/);
}); });
}); });