finish v2 model layer and add some unit tests for adapters

This commit is contained in:
Matthew Irish 2018-10-05 22:02:23 -05:00
parent b2e24ce2b1
commit b74ed60497
9 changed files with 227 additions and 44 deletions

View File

@ -1,10 +1,11 @@
/* eslint-disable */
import { isEmpty } from '@ember/utils';
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
namespace: 'v1',
_url(backend, id) {
let url = `${this.buildURL()}/${backend}/data/`;
_url(backend, id, infix = 'data') {
let url = `${this.buildURL()}/${backend}/${infix}/`;
if (!isEmpty(id)) {
url = url + id;
}
@ -16,8 +17,37 @@ export default ApplicationAdapter.extend({
return this._url(backend, path) + `?version=${version}`;
},
urlForCreateRecord(modelName, snapshot) {
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
let path = snapshot.attr('path');
return this._url(backend, path);
},
createRecord(store, modelName, snapshot) {
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
let path = snapshot.attr('path');
return this._super(...arguments).then(resp => {
resp.id = JSON.stringify([backend, path, resp.version]);
return resp;
});
},
urlForUpdateRecord(id) {
let [backend, path] = JSON.parse(id);
return this._url(backend, path);
},
deleteRecord(store, type, snapshot) {
// use adapterOptions to determine if it's delete or destroy for the version
// deleteType should be 'delete', 'destroy', 'undelete'
let infix = snapshot.adapterOptions.deleteType;
let [backend, path, version] = JSON.parse(snapshot.id);
return this.ajax(this._url(backend, path, infix), 'POST', { data: { versions: [version] } });
},
handleResponse(/*status, headers, payload, requestData*/) {
// the body of the 404 will have some relevant information
return this._super(...arguments);
},
});

View File

@ -16,26 +16,35 @@ export default ApplicationAdapter.extend({
// concerns and we only want to send "list" to the server
query(store, type, query) {
let { backend, id } = query;
return this.ajax(this._url(backend, id), 'GET', { data: { list: true } });
return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }).then(resp => {
resp.id = id;
return resp;
});
},
urlForQueryRecord(query) {
let { id, backend } = query;
return this._url(backend) + id;
return this._url(backend, id);
},
queryRecord(store, type, query) {
let { backend, id } = query;
return this._super(...arguments).then(resp => {
return this.ajax(this._url(backend, id), 'GET').then(resp => {
resp.id = id;
resp.backend = backend;
return resp;
});
},
urlForDeleteRecord(store, type, snapshot) {
let backend = snapshot.belongsTo('secret-engine', { id: true });
urlForUpdateRecord(store, type, snapshot) {
let backend = snapshot.belongsTo('engine', { id: true });
let { id } = snapshot;
return this.urlForQueryRecord({ id, backend });
return this._url(backend, id);
},
urlForDeleteRecord(store, type, snapshot) {
let backend = snapshot.belongsTo('engine', { id: true });
let { id } = snapshot;
return this._url(backend, id);
},
});

View File

@ -3,6 +3,9 @@ import Mixin from '@ember/object/mixin';
import utils from '../lib/key-utils';
export default Mixin.create({
// what attribute has the path for the key
// will.be 'path' for v2 or 'id' v1
pathAttr: 'id',
flags: null,
initialParentKey: null,
@ -11,33 +14,39 @@ export default Mixin.create({
return this.get('initialParentKey') != null;
}),
isFolder: computed('id', function() {
return utils.keyIsFolder(this.get('id'));
pathVal() {
return this.get(this.pathAttr);
},
// rather than using defineProperty for all of these,
// we're just going to hardcode the known keys for the path ('id' and 'path')
isFolder: computed('id', 'path', function() {
return utils.keyIsFolder(this.pathVal());
}),
keyParts: computed('id', function() {
return utils.keyPartsForKey(this.get('id'));
keyParts: computed('id', 'path', function() {
return utils.keyPartsForKey(this.pathVal());
}),
parentKey: computed('id', 'isCreating', {
parentKey: computed('id', 'path', 'isCreating', {
get: function() {
return this.get('isCreating') ? this.get('initialParentKey') : utils.parentKeyForKey(this.get('id'));
return this.isCreating ? this.initialParentKey : utils.parentKeyForKey(this.pathVal());
},
set: function(_, value) {
return value;
},
}),
keyWithoutParent: computed('id', 'parentKey', {
keyWithoutParent: computed('id', 'path', 'parentKey', {
get: function() {
var key = this.get('id');
return key ? key.replace(this.get('parentKey'), '') : null;
var key = this.pathVal();
return key ? key.replace(this.parentKey, '') : null;
},
set: function(_, value) {
if (value && value.trim()) {
this.set('id', this.get('parentKey') + value);
this.set(this.pathAttr, this.parentKey + value);
} else {
this.set('id', null);
this.set(this.pathAttr, null);
}
return value;
},

View File

@ -1,8 +1,12 @@
import Secret from './secret';
import DS from 'ember-data';
const { attr } = DS;
const { attr, belongsTo } = DS;
export default Secret.extend({
pathAttr: 'path',
version: attr('number'),
secret: belongsTo('secret-v2'),
path: attr('string'),
currentVersion: attr('number'),
});

View File

@ -1,15 +1,17 @@
import Secret from './secret';
import DS from 'ember-data';
import { match } from '@ember/object/computed';
const { attr, hasMany, belongsTo, Model } = DS;
export default Model.extend({
engine: belongsTo('secret-engine'),
versions: hasMany('secret-v2-version', { async: false }),
versions: hasMany('secret-v2-version', { async: false, inverse: null }),
selectedVersion: belongsTo('secret-v2-version', { inverse: 'secret' }),
createdTime: attr(),
updatedTime: attr(),
currentVersion: attr('number'),
oldestVersion: attr('number'),
maxVersions: attr('number'),
casRequired: attr('boolean'),
isFolder: match('id', /\/$/),
});

View File

@ -3,23 +3,28 @@ import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
secretDataPath: 'data.data',
normalizeItems(payload, requestType) {
normalizeItems(payload) {
let path = this.secretDataPath;
// move response that is the contents of the secret from the dataPath
// to `secret_data` so it will be `secretData` in the model
payload.secret_data = get(payload, path);
payload = Object.assign({}, payload, payload.data.metadata);
delete payload.data;
payload.path = payload.id;
// return the payload if it's expecting a single object or wrap
// it as an array if not
return payload;
},
serialize(snapshot) {
return {
let data = {
data: snapshot.attr('secretData'),
options: {
cas: snapshot.attr('currentVerion'),
},
};
if (snapshot.attr('currentVersion')) {
data.options = {
cas: snapshot.attr('currentVerion'),
};
}
return data;
},
});

View File

@ -25,14 +25,15 @@ export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
return { id: fullSecretPath };
});
}
// transform versions to an array with composite IDs
if (payload.data.versions) {
payload.data.versions = Object.keys(payload.data.versions).map(version => {
let body = payload.data.versions[version];
body.version = version;
body.path = payload.id;
body.id = JSON.stringify([payload.backend, payload.id, version]);
return body;
});
console.log(payload);
}
payload.data.id = payload.id;
return requestType === 'queryRecord' ? payload.data : [payload.data];

View File

@ -1,26 +1,70 @@
import { resolve } from 'rsvp';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
module('Unit | Adapter | secret-v2', function(hooks) {
setupTest(hooks);
test('secret api urls', function(assert) {
let url, method, options;
let adapter = this.owner.factoryFor('adapter:secret-v2').create({
ajax: (...args) => {
[url, method, options] = args;
return resolve({});
hooks.beforeEach(function() {
this.server = apiStub();
});
hooks.afterEach(function() {
this.server.shutdown();
});
[
['query', null, {}, { id: '', backend: 'secret' }, 'GET', '/v1/secret/metadata/?list=true'],
['queryRecord', null, {}, { id: 'foo', backend: 'secret' }, 'GET', '/v1/secret/metadata/foo'],
[
'updateRecord',
{
serializerFor() {
return {
serializeIntoHash() {},
};
},
},
{},
{
id: 'foo',
belongsTo() {
return 'secret';
},
},
'PUT',
'/v1/secret/metadata/foo',
],
[
'deleteRecord',
{
serializerFor() {
return {
serializeIntoHash() {},
};
},
},
{},
{
id: 'foo',
belongsTo() {
return 'secret';
},
},
'DELETE',
'/v1/secret/metadata/foo',
],
].forEach(([adapterMethod, store, type, queryOrSnapshot, expectedHttpVerb, expectedURL]) => {
test(`secret-v2: ${adapterMethod}`, function(assert) {
let adapter = this.owner.lookup('adapter:secret-v2');
adapter[adapterMethod](store, type, queryOrSnapshot);
let { url, method } = this.server.handledRequests[0];
assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`);
assert.equal(
method,
expectedHttpVerb,
`${adapterMethod} uses the correct http verb: ${expectedHttpVerb}`
);
});
adapter.query({}, 'secret', { id: '', backend: 'secret' });
assert.equal(url, '/v1/secret/metadata/', 'query generic url OK');
assert.equal('GET', method, 'query generic method OK');
assert.deepEqual(options, { data: { list: true } }, 'query generic url OK');
adapter.queryRecord({}, 'secret', { id: 'foo', backend: 'secret' });
assert.equal(url, '/v1/secret/data/foo', 'queryRecord generic url OK');
assert.equal('GET', method, 'queryRecord generic method OK');
});
});

View File

@ -0,0 +1,79 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
module('Unit | Adapter | secret-v2-version', function(hooks) {
setupTest(hooks);
hooks.beforeEach(function() {
this.server = apiStub();
});
hooks.afterEach(function() {
this.server.shutdown();
});
[
[
'findRecord with version',
'findRecord',
[null, {}, JSON.stringify(['secret', 'foo', '2']), {}],
'GET',
'/v1/secret/data/foo?version=2',
],
[
'deleteRecord with delete',
'deleteRecord',
[null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'delete' } }],
'POST',
'/v1/secret/delete/foo',
{ versions: ['2'] },
],
[
'deleteRecord with destroy',
'deleteRecord',
[null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'destroy' } }],
'POST',
'/v1/secret/destroy/foo',
{ versions: ['2'] },
],
[
'deleteRecord with destroy',
'deleteRecord',
[null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'undelete' } }],
'POST',
'/v1/secret/undelete/foo',
{ versions: ['2'] },
],
[
'updateRecord makes calls to correct url',
'updateRecord',
[
{
serializerFor() {
return { serializeIntoHash() {} };
},
},
{},
{ id: JSON.stringify(['secret', 'foo', '2']) },
],
'PUT',
'/v1/secret/data/foo',
],
].forEach(([testName, adapterMethod, args, expectedHttpVerb, expectedURL, exptectedRequestBody]) => {
test(`secret-v2: ${testName}`, function(assert) {
let adapter = this.owner.lookup('adapter:secret-v2-version');
adapter[adapterMethod](...args);
let { url, method, requestBody } = this.server.handledRequests[0];
assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`);
assert.equal(
method,
expectedHttpVerb,
`${adapterMethod} uses the correct http verb: ${expectedHttpVerb}`
);
if (exptectedRequestBody) {
assert.deepEqual(JSON.parse(requestBody), exptectedRequestBody);
}
});
});
});