Skip to content

Commit

Permalink
Merge branch 'master' into feature/article-meta
Browse files Browse the repository at this point in the history
* master:
  Adding test for article service, should not make HTTP request if article is saved with no changes.
  Adding test for users paginator resolve in article controller.
  Removing unneeded code from article tests.
  Implemeted nested hydration via input function. Refactored article meta tag hydration and wrote all tests.
  • Loading branch information
Jeremy Sik committed Sep 18, 2015
2 parents 8810818 + 8bc825c commit 47a37e4
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 126 deletions.
26 changes: 16 additions & 10 deletions app/src/app/admin/articles/article/article.spec.ts
Expand Up @@ -35,22 +35,20 @@ namespace app.admin.articles.article {
saveArticleWithRelated:(article:common.models.Article) => {
return $q.when(true);
},
getArticle:(identifier:string) => {
getArticle:(identifier:string, nestedEntities:string[]) => {
return $q.when(getArticle(identifier));
},
newArticle:(author:common.models.User) => {
return getArticle('newArticle');
},
hydrateMetaCollectionFromTemplate:(articleId: string, articleMeta:common.models.ArticleMeta[], template:string[]):common.models.ArticleMeta[] => {
return [
new common.models.ArticleMeta({metaName:'foobarfoo', metaContent:'barfoo'})
];
}
},
ArticleController:ArticleController,
loggedInUser:common.models.User = common.models.UserMock.entity(),
userService = {
getAuthUser: sinon.stub().returns(loggedInUser)
getAuthUser: sinon.stub().returns(loggedInUser),
getUsersPaginator: sinon.stub().returns({
setCount: sinon.stub()
})
};

beforeEach(() => {
Expand All @@ -75,17 +73,17 @@ namespace app.admin.articles.article {

sinon.spy(notificationService, 'toast');
sinon.spy(articleService, 'saveArticleWithRelated');
sinon.spy(articleService, 'hydrateMetaCollectionFromTemplate');
sinon.spy(articleService, 'newArticle');
sinon.spy(articleService, 'getArticle');

});

afterEach(() => {

(<any>notificationService).toast.restore();
(<any>articleService).saveArticleWithRelated.restore();
(<any>articleService).hydrateMetaCollectionFromTemplate.restore();
(<any>articleService).newArticle.restore();
(<any>articleService).getArticle.restore();

});

Expand Down Expand Up @@ -129,7 +127,15 @@ namespace app.admin.articles.article {

expect(article).eventually.to.be.an.instanceOf(common.models.Article);

expect(articleService.hydrateMetaCollectionFromTemplate).to.have.been.calledWith(sinon.match.any, [testMeta], sinon.match.array);
expect(articleService.getArticle).to.have.been.called;

});

it('should be able to resolve users paginator', () => {

(<any>ArticleConfig.state.resolve).usersPaginator(userService);

expect(userService.getUsersPaginator).to.have.been.called;

});

Expand Down
12 changes: 1 addition & 11 deletions app/src/app/admin/articles/article/article.ts
Expand Up @@ -10,10 +10,6 @@ namespace app.admin.articles.article {

export class ArticleConfig {

public static articleMetaTemplate:string[] = [
'name', 'description', 'keyword', 'canonical'
];

public static state:global.IState;

static $inject = ['stateHelperServiceProvider'];
Expand Down Expand Up @@ -67,13 +63,7 @@ namespace app.admin.articles.article {
return newArticle;
}

return articleService.getArticle($stateParams.permalink, ['articlePermalinks', 'articleMetas', 'tags', 'author'])
.then((article) => {

article._articleMetas = articleService.hydrateMetaCollectionFromTemplate(article.articleId, article._articleMetas, ArticleConfig.articleMetaTemplate);

return article;
});
return articleService.getArticle($stateParams.permalink, ['articlePermalinks', 'articleMetas', 'tags', 'author']);
},
usersPaginator: (userService:common.services.user.UserService) => {
return userService.getUsersPaginator().setCount(10);
Expand Down
2 changes: 1 addition & 1 deletion app/src/common/decorators/changeAwareDecorator.spec.ts
Expand Up @@ -5,7 +5,7 @@ namespace common.decorators {
@changeAware
class TestModel extends common.models.AbstractModel {

protected _nestedEntityMap = {
protected _nestedEntityMap:common.models.INestedEntityMap = {
_nestedCollection: NestedData,
_nestedEntity: NestedData,
};
Expand Down
43 changes: 40 additions & 3 deletions app/src/common/models/abstractModel.spec.ts
Expand Up @@ -5,13 +5,23 @@ namespace common.models {

class TestModel extends AbstractModel {

protected _nestedEntityMap = {
protected _nestedEntityMap:INestedEntityMap = {
hasOne: TestChildModel,
hasMany: TestChildModel
hasMany: TestChildModel,
hydrate: this.hydrateFunction
};

public _hasOne:TestChildModel;
public _hasMany:TestChildModel[];
public _hydrate:TestChildModel[];

private hydrateFunction(data:any, exists:boolean) {
if(exists) {
return ['bar'];
}

return data._hydrate;
}

constructor(data:any, exists:boolean = false) {
super(data, exists);
Expand Down Expand Up @@ -40,7 +50,7 @@ namespace common.models {

});

it('should instantiate nested collections', () => {
it('should instantiate nested collections (class)', () => {

let model = new TestModel({
'_hasMany' : [{}]
Expand All @@ -50,6 +60,26 @@ namespace common.models {

});

it('should instantiate nested collections (function, exists)', () => {

let model = new TestModel({
'_hydrate' : ['foobar']
}, true);

expect(model._hydrate).to.deep.equal(['bar']);

});

it('should instantiate nested collections (function, non-existant)', () => {

let model = new TestModel({
'_hydrate' : ['foobar']
}, false);

expect(model._hydrate).to.deep.equal(['foobar']);

});


it('should be able to check if a model exists on the remote api', () => {

Expand All @@ -69,6 +99,13 @@ namespace common.models {

});

it('should be able to generate a UUID', () => {

let uuid:string = TestModel.generateUUID();

expect(uuid.length).to.equal(36);

});

});

Expand Down
61 changes: 48 additions & 13 deletions app/src/common/models/abstractModel.ts
@@ -1,23 +1,31 @@
//note this file MUST be loaded before any depending classes @todo resolve model load order
namespace common.models {

export interface IModel{
export interface IModel {
getAttributes(includeUnderscoredKeys?:boolean):Object;
setExists(exists:boolean):void;
exists():boolean;
}

export interface IModelClass{
export interface IModelClass {
new(data?:any, exists?:boolean):IModel;
}

export interface IModelFactory{
export interface IModelFactory {
(data:any, exists?:boolean):IModel;
}

export interface IHydrateFunction {
(data:any, exists:boolean):any;
}

export interface INestedEntityMap {
[key:string] : IModelClass | IHydrateFunction;
}

export abstract class AbstractModel implements IModel {

protected _nestedEntityMap;
protected _nestedEntityMap:INestedEntityMap;
private _exists:boolean;

constructor(data?:any, exists:boolean = false) {
Expand Down Expand Up @@ -46,32 +54,50 @@ namespace common.models {

}

/**
* Checks to see if an entity implements interface IModelClass.
*
* Note: This is valid Typescript (1.6), change when PHPStorm gets an update:
*
* private isModelClass(entity: any):entity is IModelClass { ... }
*
* @param entity
*/
private isModelClass(entity: any):boolean {
return entity.prototype && entity.prototype instanceof AbstractModel;
}

/**
* Find all the nested entities and hydrate them into model instances
* @param data
* @param exists
*/
protected hydrateNested(data:any, exists:boolean){

_.forIn(this._nestedEntityMap, (model:IModelClass, nestedKey:string) => {
_.forIn(this._nestedEntityMap, (nestedObject:IModelClass|IHydrateFunction, nestedKey:string) => {

//if the nested map is not defined with a leading _ prepend one
if (!_.startsWith(nestedKey, '_')){
nestedKey = '_' + nestedKey;
}

if (_.has(data, nestedKey) && !_.isNull(data[nestedKey])){
let nestedData = null;

if (_.isArray(data[nestedKey])){
this[nestedKey] = _.map(data[nestedKey], (entityData) => this.hydrateModel(entityData, model, exists));
}else if (_.isObject(data[nestedKey])){
this[nestedKey] = this.hydrateModel(data[nestedKey], model, exists);
if(this.isModelClass(nestedObject)) {
if(_.has(data, nestedKey) && !_.isNull(data[nestedKey])) {
if (_.isArray(data[nestedKey])){
nestedData = _.map(data[nestedKey], (entityData) => this.hydrateModel(entityData, (<IModelClass>nestedObject), exists));
} else if (_.isObject(data[nestedKey])) {
nestedData = this.hydrateModel(data[nestedKey], (<IModelClass>nestedObject), exists);
}
}

}else{
this[nestedKey] = null;
}
else {
nestedData = (<IHydrateFunction>nestedObject)(data, exists);
}

this[nestedKey] = nestedData;

});

}
Expand Down Expand Up @@ -126,6 +152,15 @@ namespace common.models {
this._exists = exists;
}

/**
* Generates a UUID using lil:
* https://github.com/lil-js/uuid
* @returns {string}
*/
public static generateUUID():string {
return lil.uuid();
}

}

}
Expand Down
2 changes: 1 addition & 1 deletion app/src/common/models/article/articleMetaModel.ts
Expand Up @@ -3,7 +3,7 @@ namespace common.models {
@common.decorators.changeAware
export class ArticleMeta extends AbstractModel {

public id:string = undefined;
public metaId:string = undefined;
public articleId:string = undefined;
public metaName:string = undefined;
public metaContent:string = undefined;
Expand Down
69 changes: 66 additions & 3 deletions app/src/common/models/article/articleModel.spec.ts
Expand Up @@ -4,12 +4,33 @@

describe('Article Model', () => {

let title = seededChance.sentence();
let articleData = {
articleId:seededChance.guid(),
let title = seededChance.sentence(),
articleId = seededChance.guid(),
articleData = {
articleId: articleId,
title: title,
permalink: title.toLowerCase().replace(' ', '-'),
content:seededChance.paragraph({sentences: 10}),
_articleMetas: [
{
metaName: 'keyword',
metaContent: 'foo',
metaId: seededChance.guid(),
articleId: articleId
},
{
metaName: 'description',
metaContent: 'bar',
metaId: seededChance.guid(),
articleId: articleId
},
{
metaName: 'foobar',
metaContent: 'foobar',
metaId: seededChance.guid(),
articleId: articleId
}
]
};

it('should instantiate a new article', () => {
Expand Down Expand Up @@ -41,6 +62,48 @@

});

it('should be able to hydrate the article metas', () => {

let article = new common.models.Article(articleData);

expect(_.size(article._articleMetas)).to.equal(5);

// The first article meta is 'name' which is added via template
expect(article._articleMetas[0].articleId).to.equal(article.articleId);

expect(_.isEmpty(article._articleMetas[0].metaId)).to.be.false;

let testableMetaTags = _.cloneDeep(article._articleMetas);
_.forEach(testableMetaTags, (tag) => {
delete(tag.metaId);
delete(tag.articleId);
});

expect(testableMetaTags).to.deep.equal([
{
metaName: 'name',
metaContent: ''
},
{
metaName: 'description',
metaContent: 'bar'
},
{
metaName: 'keyword',
metaContent: 'foo'
},
{
metaName: 'canonical',
metaContent: ''
},
{
metaName: 'foobar',
metaContent: 'foobar'
}
]);

});

});

})();

0 comments on commit 47a37e4

Please sign in to comment.