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:
parent
d764bac6af
commit
4dd7e34c96
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: Ensure dc selector correctly shows the currently selected dc
|
||||
```
|
|
@ -1,4 +1,4 @@
|
|||
{{#if (can "choose partitions")}}
|
||||
{{#if (can "choose partitions" dc=@dc)}}
|
||||
<li
|
||||
class="partitions"
|
||||
data-test-partition-menu
|
||||
|
@ -49,5 +49,12 @@
|
|||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
</li>
|
||||
{{else}}
|
||||
<li
|
||||
class="partition"
|
||||
aria-label="Admin Partition"
|
||||
>
|
||||
{{@partition}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -20,7 +20,10 @@ export default class PartitionAbility extends BaseAbility {
|
|||
}
|
||||
|
||||
get canChoose() {
|
||||
return this.canUse;
|
||||
if(typeof this.dc === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return this.canUse && this.dc.Primary;
|
||||
}
|
||||
|
||||
get canUse() {
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
{{/if}}
|
||||
</label>
|
||||
{{/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'}}">
|
||||
<span>Source Partition</span>
|
||||
<PowerSelectWithCreate
|
||||
|
@ -123,7 +123,7 @@
|
|||
{{/if}}
|
||||
</label>
|
||||
{{/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'}}">
|
||||
<span>Destination Partition</span>
|
||||
<PowerSelectWithCreate
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
>
|
||||
<DataForm
|
||||
@type="intention"
|
||||
@dc={{@dc}}
|
||||
@dc={{@dc.Name}}
|
||||
@nspace={{@nspace}}
|
||||
@partition={{@partition}}
|
||||
@autofill={{@autofill}}
|
||||
|
@ -77,7 +77,7 @@ as |api|>
|
|||
@src={{uri '/${partition}/*/${dc}/services'
|
||||
(hash
|
||||
partition=@partition
|
||||
dc=@dc
|
||||
dc=@dc.Name
|
||||
)
|
||||
}}
|
||||
@onchange={{action this.createServices item}}
|
||||
|
@ -88,7 +88,7 @@ as |api|>
|
|||
@src={{uri '/${partition}/*/${dc}/namespaces'
|
||||
(hash
|
||||
partition=@partition
|
||||
dc=@dc
|
||||
dc=@dc.Name
|
||||
)
|
||||
}}
|
||||
@onchange={{action this.createNspaces item}}
|
||||
|
@ -99,7 +99,7 @@ as |api|>
|
|||
<DataSource
|
||||
@src={{uri '/*/*/${dc}/partitions'
|
||||
(hash
|
||||
dc=@dc
|
||||
dc=@dc.Name
|
||||
)
|
||||
}}
|
||||
@onchange={{action this.createPartitions item}}
|
||||
|
@ -114,6 +114,7 @@ as |api|>
|
|||
>
|
||||
<Consul::Intention::Form::Fieldsets
|
||||
@nspaces={{this.nspaces}}
|
||||
@dc={{@dc}}
|
||||
@partitions={{this.partitions}}
|
||||
@services={{this.services}}
|
||||
@SourceName={{this.SourceName}}
|
||||
|
|
|
@ -33,11 +33,18 @@
|
|||
{{#each (sort-by 'Name' @dcs) as |item|}}
|
||||
<MenuItem
|
||||
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)}}
|
||||
>
|
||||
<BlockSlot @name="label">
|
||||
{{item.Name}}
|
||||
{{#if item.Primary}}
|
||||
<span>Primary</span>
|
||||
{{/if}}
|
||||
{{#if item.Local}}
|
||||
<span>Local</span>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.hashicorp-consul {
|
||||
%hashicorp-consul {
|
||||
[role='banner'] nav .dcs {
|
||||
@extend %main-nav-vertical-hoisted;
|
||||
left: 100px;
|
||||
|
@ -27,3 +27,6 @@
|
|||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
.hashicorp-consul {
|
||||
@extend %hashicorp-consul;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
/* 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 {
|
||||
@extend %main-nav-vertical-action;
|
||||
}
|
||||
|
@ -21,6 +30,8 @@
|
|||
%main-nav-vertical > ul > li > label {
|
||||
@extend %main-nav-vertical-action;
|
||||
}
|
||||
/**/
|
||||
|
||||
%main-nav-vertical .popover-menu {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
|
|
@ -12,11 +12,15 @@
|
|||
visibility: hidden;
|
||||
}
|
||||
%main-nav-vertical li.partitions,
|
||||
%main-nav-vertical li.nspaces,
|
||||
%main-nav-vertical li.dcs {
|
||||
%main-nav-vertical li.partition,
|
||||
%main-nav-vertical li.nspaces {
|
||||
margin-bottom: 25px;
|
||||
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
|
||||
// items, left and right aligned. We should remove this and look to use
|
||||
// align-self for anything that needs right aligning instead.
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
%main-nav-vertical-action {
|
||||
color: rgb(var(--tone-gray-800));
|
||||
}
|
||||
%main-nav-vertical-item,
|
||||
%main-nav-vertical-action-intent,
|
||||
%main-nav-vertical-action-active {
|
||||
color: rgb(var(--tone-gray-999));
|
||||
|
@ -40,13 +41,16 @@
|
|||
background-color: rgb(var(--tone-gray-150));
|
||||
border-color: rgb(var(--tone-gray-999));
|
||||
}
|
||||
%main-nav-vertical li[aria-label]::before,
|
||||
%main-nav-vertical .popover-menu[aria-label]::before {
|
||||
color: rgb(var(--tone-gray-700));
|
||||
content: attr(aria-label);
|
||||
display: block;
|
||||
margin-top: -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;
|
||||
color: rgb(var(--tone-gray-000));
|
||||
background-color: rgb(var(--tone-gray-500));
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
{{will-destroy this.disconnect}}
|
||||
<section
|
||||
{{did-insert (fn this.attributeChanged 'element')}}
|
||||
{{did-update (fn this.attributeChanged 'model' @model)}}
|
||||
class="outlet"
|
||||
data-outlet={{@name}}
|
||||
data-route={{this.routeName}}
|
||||
|
|
|
@ -22,6 +22,8 @@ export default class Outlet extends Component {
|
|||
@tracked previousState;
|
||||
@tracked endTransition;
|
||||
|
||||
@tracked route;
|
||||
|
||||
get model() {
|
||||
return this.args.model || {};
|
||||
}
|
||||
|
@ -52,6 +54,9 @@ export default class Outlet extends Component {
|
|||
this.setAppRoute(this.router.currentRouteName);
|
||||
}
|
||||
break;
|
||||
case 'model':
|
||||
this.route._model = this.args.model;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,8 @@ import Helper from '@ember/component/helper';
|
|||
import { getOwner } from '@ember/application';
|
||||
|
||||
export default class CachedHelper extends Helper {
|
||||
compute([model], hash) {
|
||||
return () => {
|
||||
compute([model, params], hash) {
|
||||
const container = getOwner(this);
|
||||
return container.lookup(`service:repository/${model}`).cached(hash);
|
||||
};
|
||||
return container.lookup(`service:repository/${model}`).cached(params);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export default class Datacenter extends Model {
|
|||
@attr('string') uid;
|
||||
@attr('string') Name;
|
||||
@attr('boolean') Local;
|
||||
@attr('boolean') Primary;
|
||||
@attr('string') DefaultACLPolicy;
|
||||
|
||||
@attr('boolean', { defaultValue: () => true }) MeshEnabled;
|
||||
|
|
|
@ -1,31 +1,42 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
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 {
|
||||
@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) {
|
||||
return respond(function(headers, body) {
|
||||
return {
|
||||
body,
|
||||
headers,
|
||||
};
|
||||
});
|
||||
}
|
||||
return super.respondForQuery(
|
||||
cb => respond((headers, body) => {
|
||||
body = body.map(item => ({
|
||||
Datacenter: '',
|
||||
[this.slugKey]: item,
|
||||
}));
|
||||
body = cb(headers, body);
|
||||
headers = body[HEADERS_SYMBOL];
|
||||
|
||||
normalizePayload(payload, id, requestType) {
|
||||
switch (requestType) {
|
||||
case 'query':
|
||||
return payload.body.map(item => {
|
||||
return {
|
||||
Local: this.env.var('CONSUL_DATACENTER_LOCAL') === item,
|
||||
[this.primaryKey]: item,
|
||||
DefaultACLPolicy: payload.headers['x-consul-default-acl-policy'],
|
||||
};
|
||||
});
|
||||
}
|
||||
return payload;
|
||||
const Local = this.env.var('CONSUL_DATACENTER_LOCAL');
|
||||
const Primary = this.env.var('CONSUL_DATACENTER_PRIMARY');
|
||||
const DefaultACLPolicy = headers[DEFAULT_ACL_POLICY.toLowerCase()];
|
||||
|
||||
return body.map(item => ({
|
||||
...item,
|
||||
Local: item.Name === Local,
|
||||
Primary: item.Name === Primary,
|
||||
DefaultACLPolicy: DefaultACLPolicy,
|
||||
}));
|
||||
}),
|
||||
query
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,6 +124,10 @@ export default class RepositoryService extends Service {
|
|||
return this.store.peekRecord(this.getModelName(), id);
|
||||
}
|
||||
|
||||
peekAll() {
|
||||
return this.store.peekAll(this.getModelName());
|
||||
}
|
||||
|
||||
cached(params) {
|
||||
const entries = Object.entries(params);
|
||||
return this.store.peekAll(this.getModelName()).filter(item => {
|
||||
|
|
|
@ -15,8 +15,10 @@ export default class DiscoveryChainService extends RepositoryService {
|
|||
|
||||
@dataSource('/:partition/:ns/:dc/discovery-chain/:id')
|
||||
findBySlug(params, configuration = {}) {
|
||||
const datacenter = this.dcs.peekOne(params.dc);
|
||||
if (datacenter !== null && !get(datacenter, 'MeshEnabled')) {
|
||||
// peekAll and find is fine here as datacenter count should be relatively
|
||||
// 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 super.findBySlug(...arguments).catch(e => {
|
||||
|
@ -24,7 +26,7 @@ export default class DiscoveryChainService extends RepositoryService {
|
|||
const body = (get(e, 'errors.firstObject.detail') || '').trim();
|
||||
switch (code) {
|
||||
case '500':
|
||||
if (datacenter !== null && body.endsWith(ERROR_MESH_DISABLED)) {
|
||||
if (typeof datacenter !== 'undefined' && body.endsWith(ERROR_MESH_DISABLED)) {
|
||||
set(datacenter, 'MeshEnabled', false);
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -135,6 +135,7 @@ export default class RoutletService extends Service {
|
|||
const outlet = outlets.get(keys[key]);
|
||||
if (typeof outlet !== 'undefined') {
|
||||
route._model = outlet.model;
|
||||
outlet.route = route;
|
||||
// TODO: Try to avoid the double computation bug
|
||||
schedule('afterRender', () => {
|
||||
outlet.routeName = route.args.name;
|
||||
|
|
|
@ -73,39 +73,45 @@ as |dcs|>
|
|||
{{! 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 }}
|
||||
{{#let
|
||||
|
||||
(or
|
||||
|
||||
(get (object-at 0 (cached-model
|
||||
(if nofound.dc
|
||||
(object-at 0 (cached-model
|
||||
'dc'
|
||||
(hash
|
||||
Name=notfound.dc
|
||||
)
|
||||
)) 'Name')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(get (object-at 0 (cached-model
|
||||
(object-at 0 (cached-model
|
||||
'dc'
|
||||
(hash
|
||||
Name=route.params.dc
|
||||
)
|
||||
)) 'Name')
|
||||
)
|
||||
)
|
||||
|
||||
(env "CONSUL_DATACENTER_LOCAL")
|
||||
(hash
|
||||
Name=(env "CONSUL_DATACENTER_LOCAL")
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
dcs.data
|
||||
|
||||
as |dc dcs|}}
|
||||
|
||||
{{#if (and (gt dc.length 0) dcs nspace partition)}}
|
||||
{{#if (and (gt dc.Name.length 0) dcs nspace partition)}}
|
||||
|
||||
{{! figure out our current DC and convert it to a model }}
|
||||
<DataSource
|
||||
@src={{uri '/${partition}/*/${dc}/datacenter/${name}'
|
||||
(hash
|
||||
dc=dc
|
||||
dc=dc.Name
|
||||
partition=partition
|
||||
name=dc
|
||||
name=dc.Name
|
||||
)
|
||||
}}
|
||||
as |dc|>
|
||||
|
|
|
@ -45,7 +45,7 @@ as |item|}}
|
|||
<BlockSlot @name="content">
|
||||
<Consul::Intention::Form
|
||||
@item={{item}}
|
||||
@dc={{route.params.dc}}
|
||||
@dc={{route.model.dc}}
|
||||
@nspace={{route.params.nspace}}
|
||||
@partition={{route.params.partition}}
|
||||
@onsubmit={{route-action 'transitionTo' 'dc.intentions.index'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
as |route|>
|
||||
|
||||
<DataLoader
|
||||
@src={{uri '/${partition}/${nspace}/${dc}/service-instances/for-service/${name}'
|
||||
(hash
|
||||
|
@ -83,6 +82,7 @@ as |items item dc|}}
|
|||
@onchange={{action (mut chain) value="data"}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{did-insert (set this 'chain' undefined) route.params.dc}}
|
||||
{{/if}}
|
||||
{{#let
|
||||
(hash
|
||||
|
|
|
@ -2,10 +2,11 @@ import { get } from '@ember/object';
|
|||
|
||||
export default function(foreignKey, nspaceKey, partitionKey, hash = JSON.stringify) {
|
||||
return function(primaryKey, slugKey, foreignKeyValue, nspaceValue, partitionValue) {
|
||||
if (foreignKeyValue == null || foreignKeyValue.length < 1) {
|
||||
return function(item) {
|
||||
foreignKeyValue = foreignKeyValue == null ? item[foreignKey] : foreignKeyValue;
|
||||
if (foreignKeyValue == null) {
|
||||
throw new Error('Unable to create fingerprint, missing foreignKey value');
|
||||
}
|
||||
return function(item) {
|
||||
const slugKeys = slugKey.split(',');
|
||||
const slugValues = slugKeys.map(function(slugKey) {
|
||||
if (get(item, slugKey) == null || get(item, slugKey).length < 1) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
export const HEADERS_PARTITION = 'X-Consul-Partition';
|
||||
export const HEADERS_NAMESPACE = 'X-Consul-Namespace';
|
||||
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_TOKEN = 'X-Consul-Token';
|
||||
export const HEADERS_DIGEST = 'X-Consul-ContentHash';
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
"*":
|
||||
headers:
|
||||
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']))}
|
||||
|
|
|
@ -36,7 +36,7 @@ Feature: dc / services / instances / gateway: Show Gateway Service Instance
|
|||
When I click addresses on the tabs
|
||||
And I see addressesIsSelected on the tabs
|
||||
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
|
||||
- 92.68.0.0:8081
|
||||
|
|
|
@ -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 the "[data-test-error]" element
|
||||
# FIXME: Does one dc not having connect and another having connect ever actually happen
|
||||
# And I visit the service page for yaml
|
||||
# ---
|
||||
# dc: dc2
|
||||
# service: service-1
|
||||
# ---
|
||||
# And I see routing on the tabs
|
||||
# And I visit the service page for yaml
|
||||
# ---
|
||||
# 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
|
||||
# And I don't see the "[data-test-error]" element
|
||||
# Not entirely sure if having one dc not having connect
|
||||
# and another having connect ever actually happen
|
||||
And I visit the service page for yaml
|
||||
---
|
||||
dc: dc2
|
||||
service: service-1
|
||||
---
|
||||
And I see routing on the tabs
|
||||
And I visit the service page for yaml
|
||||
---
|
||||
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
|
||||
And I don't see the "[data-test-error]" element
|
||||
|
|
|
@ -1,24 +1,43 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
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) {
|
||||
setupTest(hooks);
|
||||
test('respondForQuery returns the correct data for list endpoint', function(assert) {
|
||||
const serializer = this.owner.lookup('serializer:dc');
|
||||
let env = this.owner.lookup('service:env');
|
||||
env = env.var.bind(env);
|
||||
const request = {
|
||||
url: `/v1/catalog/datacenters`,
|
||||
};
|
||||
return get(request.url).then(function(payload) {
|
||||
const expected = {
|
||||
body: payload,
|
||||
headers: {},
|
||||
};
|
||||
const ALLOW = 'allow';
|
||||
const expected = payload.map(item => (
|
||||
{
|
||||
Name: item,
|
||||
Datacenter: '',
|
||||
Local: item === env('CONSUL_DATACENTER_LOCAL'),
|
||||
Primary: item === env('CONSUL_DATACENTER_PRIMARY'),
|
||||
DefaultACLPolicy: ALLOW
|
||||
}
|
||||
))
|
||||
const actual = serializer.respondForQuery(function(cb) {
|
||||
const headers = {};
|
||||
const body = payload;
|
||||
return cb(headers, body);
|
||||
const headers = {
|
||||
[DEFAULT_ACL_POLICY]: ALLOW
|
||||
};
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,12 +21,10 @@ test('findAll returns the correct data for list endpoint', function(assert) {
|
|||
return service.findAll();
|
||||
},
|
||||
function performAssertion(actual, expected) {
|
||||
assert.deepEqual(
|
||||
actual,
|
||||
expected(function(payload) {
|
||||
return payload.map((item, i) => ({ Name: item, Local: i === 0 ? true : false }));
|
||||
})
|
||||
);
|
||||
actual.forEach((item, i) => {
|
||||
assert.equal(actual[i].Name, item.Name);
|
||||
assert.equal(item.Local, i === 0);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ module('Unit | Utility | create fingerprinter', function() {
|
|||
const fingerprint = createFingerprinter('Datacenter', 'Namespace', 'Partition');
|
||||
[undefined, null].forEach(function(item) {
|
||||
assert.throws(function() {
|
||||
fingerprint('uid', 'ID', item);
|
||||
fingerprint('uid', 'ID', item)({Datacenter: item});
|
||||
}, /missing foreignKey/);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue