LoopBack+Ionic

CI/CD Bitbucket+Shippable+Heroku

Hello, I am Juan

@jpizarrom

https://github.com/jpizarrom

Loyalty Rewards Club Sample app

  • auth
  • add points
  • use points
  • balance

App

  • Rest API with LoopBack
  • Front-end with Ionic
  • Testing
  • CI/CD Shippable
  • Deploy a Heroku

Project Structure

.
├── app
│   ├── index.html
│   └── js
│       ├── app.js
│       └── modules
├── bower.json
├── common
│   └── models
│       ├── shop.js
│       ├── shop.json
│       ├── transaction.js
│       └── transaction.json
├── config.xml
├── coverage
├── gulpfile.js
├── ionic.project
├── package.json
├── Procfile
├── README.md
├── scss
│   └── ionic.app.scss
├── server
│   ├── boot
│   │   ├── authentication.js
│   │   ├── explorer.js
│   │   ├── push.js
│   │   ├── rest-api.js
│   │   ├── role-resolver.js
│   │   ├── root.js
│   │   └── routes.js
│   ├── config.json
│   ├── config.production.js
│   ├── datasources.json
│   ├── datasources.production.js
│   ├── lib
│   │   └── loopback-component-passport.js
│   ├── middleware.json
│   ├── model-config.json
│   ├── models
│   │   ├── user.js
│   │   └── user.json
│   ├── providers.js
│   ├── push-demo.js
│   ├── server.js
│   └── views
│       ├── password-reset.ejs
│       ├── password-reset-request.ejs
│       ├── response.ejs
│       ├── verified.ejs
│       └── verify.ejs
├── shippable.yml
├── slack_notifier.py
├── templates
│   ├── credit.ejs
│   └── debit.ejs
└── test
    ├── api
    │   └── unit
    └── client
        ├── controllers
        ├── e2e
        ├── example-spec.js
        ├── karma.mocha.conf.js
        ├── karma.ng-scenario.conf.js
        ├── lib
        ├── protractor.conf.js
        └── services
.
├── index.html
└── js
    ├── app.js
    └── modules
        ├── common
        │   └── lb-services.js
        ├── home
        │   ├── home.controllers.js
        │   ├── home.module.js
        │   └── templates
        │       └── home.template.html
        ├── profile
        │   ├── profile.controllers.js
        │   ├── profile.directives.js
        │   ├── profile.module.js
        │   ├── profile.services.js
        │   └── templates
        │       └── login.html
        └── shop
            ├── shop.controllers.js
            ├── shop.directives.js
            ├── shop.module.js
            └── templates
                ├── balance.directive.template.html
                ├── balance.html
                ├── credit.directive.template.html
                ├── credit.html
                ├── debit.directive.template.html
                ├── debit.html
                └── mybalance.html

Data Sources

{
  "db": {
    "name": "db",
    "connector": "mongodb",
    "database": "foodpoints"
  },
  "mail": {
    "name": "mail",
    "defaultForType": "mail",
    "connector": "mail",
    "transports": [
      {
        "type": "smtp",
        "host": "smtp.mandrillapp.com",
        "port": 587,
        "auth": {
          "user": "Your-server-api-key",
          "pass": "Your-server-api-key"
        }
      }
    ]
  }
}
exports = {
  "db" : {
    "url": process.env.MONGOLAB_URI,
    "name": "db",
    "connector": "mongodb",
  }
}

mail_provider = process.env.MAIL_PROVIDER || 'mandrill';

if (mail_provider=='mandrill'){
  console.log(mail_provider);
  exports["mail"]= {
    "name": "mail",
    "defaultForType": "mail",
    "connector": "mail",
    "transports": [
      {
        "type": "smtp",
        "host": "smtp.mandrillapp.com",
        "port": 587,
        "auth": {
          "user": process.env.MANDRILL_USERNAME,
          "pass": process.env.MANDRILL_APIKEY
        }
      }
    ]
  };
} else if (mail_provider=='mailgun') {
  console.log(mail_provider);
  exports["mail"]= {
    "name": "mail",
    "defaultForType": "mail",
    "connector": "mail",
    "transports": [
      {
        "type": "smtp",
        "host": process.env.MAILGUN_SMTP_SERVER,
        "port": process.env.MAILGUN_SMTP_PORT,
        "auth": {
          "user": process.env.MAILGUN_SMTP_LOGIN,
          "pass": process.env.MAILGUN_SMTP_PASSWORD
        }
      }
    ]
  };
} else if (mail_provider=='postmark') {
  console.log(mail_provider);
  exports["mail"]= {
    "name": "mail",
    "defaultForType": "mail",
    "connector": "mail",
    "transports": [
      {
        "type": "smtp",
        "host": process.env.POSTMARK_SMTP_SERVER,
        "port": 25,
        "auth": {
          "user": process.env.POSTMARK_API_TOKEN,
          "pass": process.env.POSTMARK_API_TOKEN
        }
      }
    ]
  };
} else if (mail_provider=='sendgrid') {
  console.log(mail_provider);
  exports["mail"]= {
    "name": "mail",
    "defaultForType": "mail",
    "connector": "mail",
    "transports": [
      {
        "type": "smtp",
        "host": "smtp.sendgrid.net",
        "port": 587,
        "auth": {
          "user": process.env.SENDGRID_USERNAME,
          "pass": process.env.SENDGRID_PASSWORD
        }
      }
    ]
  };
}

module.exports = exports;

Model

{
  "name": "shop",
  "plural": "shops",
  "base": "PersistedModel",
  "strict": false,
  "idInjection": false,
  "properties": {
    "name": {
      "type": "string",
      "required": true
    },
    "description": {
      "type": "string"
    },
    "created": {
      "type": "date"
    },
    "modified": {
      "type": "date"
    }
  },
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": []
}

Model Relations

{
  "name": "user",
  "plural": "users",
  "base": "User",
  "properties": {},
  "validations": [],
  "relations": {
    "shops": {
      "type": "hasMany",
      "model": "shop",
      "foreignKey": "ownerId"
    },
    "transactions": {
      "type": "hasMany",
      "model": "transaction",
      "foreignKey": "userId"
    }
  },
  "acls": [],
  "methods": []
}
{
  "name": "shop",
  "plural": "shops",
  "base": "PersistedModel",
  "strict": false,
  "idInjection": false,
  "properties": {
    "name": {
      "type": "string",
      "required": true
    },
    "description": {
      "type": "string"
    },
    "created": {
      "type": "date"
    },
    "modified": {
      "type": "date"
    }
  },
  "validations": [],
  "relations": {
    "user": {
      "type": "belongsTo",
      "model": "user",
      "foreignKey": "ownerId"
    },
    "transactions": {
      "type": "hasMany",
      "model": "transaction",
      "foreignKey": "shopId"
    }
  },
  "acls": [],
  "methods": []
}

Users, Roles, and ACL's

{
  "name": "user",
  "plural": "users",
  "base": "User",
  "properties": {},
  "validations": [],
  "relations": {
    "shops": {
      "type": "hasMany",
      "model": "shop",
      "foreignKey": "ownerId"
    },
    "transactions": {
      "type": "hasMany",
      "model": "transaction",
      "foreignKey": "userId"
    }
  },
  "acls": [
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "admin",
      "permission": "ALLOW"
    },
    {
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY",
      "property": "create"
    },
    {
      "principalType": "ROLE",
      "principalId": "admin",
      "permission": "ALLOW",
      "property": "create"
    },
    {
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW",
      "property": "__get__shops"
    },
    {
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW",
      "property": "balance"
    }
  ],
  "methods": []
}
{
  "name": "shop",
  "plural": "shops",
  "base": "PersistedModel",
  "strict": false,
  "idInjection": false,
  "properties": {
    "name": {
      "type": "string",
      "required": true
    },
    "description": {
      "type": "string"
    },
    "created": {
      "type": "date"
    },
    "modified": {
      "type": "date"
    }
  },
  "validations": [],
  "relations": {
    "user": {
      "type": "belongsTo",
      "model": "user",
      "foreignKey": "ownerId"
    },
    "transactions": {
      "type": "hasMany",
      "model": "transaction",
      "foreignKey": "shopId"
    }
  },
  "acls": [
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    },
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "admin",
      "permission": "ALLOW"
    },
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW",
      "property": "credit"
    },
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW",
      "property": "debit"
    },
    {
      "accessType": "EXECUTE",
      "principalType": "ROLE",
      "principalId": "shopOwner",
      "permission": "ALLOW",
      "property": "balance"
    }
  ],
  "methods": []
}

Extending Models

  Shop.balance = function(email, cb) {
    var Model = this;
    var User = Model.app.models.user;
    var Transaction = Model.app.models.transaction;
    var query = {};
    query.email = email;
    User.findOne({where: query}, function(err, user) {
      if (err || !user) return cb(err,0);

      Transaction.find({where: {userId: user.id}}, function(err, items) {
        var total = items
          .map(function(item) {
            return item.amount;
          })
          .reduce(function(cur, prev) {
            return prev + cur;
          }, 0);
        cb(null, total);
      });
    });
  }

  Shop.remoteMethod('balance', {
    accepts: [
      {arg: 'email', type: 'string', required: true},
    ],
    returns: {arg: 'amount', type: 'number'},
    http: [
      {path:'/balance', verb: 'post'}
    ]
  });

API Explorer

AngularJS JavaScript SDK

lb-ng

        /**
         * @ngdoc method
         * @name lbServices.Shop#debit
         * @methodOf lbServices.Shop
         *
         * @description
         *
         * <em>
         * (The remote method definition does not provide any description.)
         * </em>
         *
         * @param {Object=} parameters Request parameters.
         *
         *   This method does not accept any parameters.
         *   Supply an empty object or omit this argument altogether.
         *
         * @param {Object} postData Request data.
         *
         *  - `id` – `{*}` - 
         *
         *  - `email` – `{string}` - 
         *
         *  - `amount` – `{number}` - 
         *
         * @param {function(Object,Object)=} successCb
         *   Success callback with two arguments: `value`, `responseHeaders`.
         *
         * @param {function(Object)=} errorCb Error callback with one argument:
         *   `httpResponse`.
         *
         * @returns {Object} An empty reference that will be
         *   populated with the actual data once the response is returned
         *   from the server.
         *
         * Data properties:
         *
         *  - `success` – `{boolean=}` - 
         */
        "debit": {
          url: urlBase + "/shops/:id/debit",
          method: "POST"
        },

Ionic

    <ion-view title="FoodPoints">
        <ion-content has-header="false" class="home-view">
            <a class="button button-full button-outline" ui-sref="mybalance" ng-show="showMyBalance()">
                Mis puntos
            </a>
            <a class="button button-full button-outline" ui-sref="credit" ng-show="showCredit()">
                Acumular
            </a>
            <a class="button button-full button-outline" ui-sref="debit" ng-show="showDebit()">
                Canjear
            </a>
            <a class="button button-full button-outline" ui-sref="balance" ng-show="showBalance()">
                Consultar puntos
            </a>
            <a class="button button-full button-outline" ui-sref="login" ng-show="showLogin()">
                Login
            </a>

            <button lb-logout>Logout</button>
        </ion-content>
    </ion-view>
Home.controller('HomeController', [
  '$scope',
  '$state',
  'ProfileService',
  function ($scope, $state, ProfileService) {

    $scope.viewTitle = 'FoodPoints';

    $scope.showMyBalance = function () {
      if (ProfileService.getCurrentUserId() && !ProfileService.getCurrentShopId()) {
        return true;
      }
      return false;
    };
<ion-view title="Acumular">
    <ion-content has-header="true" scroll="true">
        <div class="card">
            <div lb-credit-directive ng-controller="CreditController"></div>
        </div>
    </ion-content>
</ion-view>
Shop.directive('lbCreditDirective', [
  '$templateCache',
  function ($templateCache) {

    return {
      templateUrl: 'js/modules/shop/templates/credit.directive.template.html',
      controller: function ($scope) {

        //  $scope.viewTitle = 'Search';

      }
    };
  }
]);
<view>
    <content has-header="true" scroll="true">

            <div class="item item-divider assertive" ng-if="creditSuccess">
                Credit!
            </div>
            <div class="item item-divider assertive" ng-if="creditError">
                Could not Credit!
            </div>

	<form class="item item-text-wrap" ng-submit="submit()">
		<div class="list">
		  <label class="item item-input">
		    <input type="email" placeholder="Email" ng-model="credentials.email">
		  </label>
		  <label class="item item-input">
		    <input type="number" placeholder="Amount" ng-model="credentials.amount">
		  </label>
		  <button class="button button-block button-positive">
			Acumular
		  </button>
		</div>
	</form>

    </content>
</view>
Shop.controller('CreditController', [
  '$scope',
  '$state',
  'Shop',
  'ProfileService',
  '$ionicHistory',
  function ($scope, $state, Shop, ProfileService, $ionicHistory) {
    $scope.viewTitle = 'CreditController';

    $scope.credentials = {
    };
    if (ProfileService.getCurrentUserId())
      $scope.credentials.creatorId = ProfileService.getCurrentUserId();
    else {
      $ionicHistory.nextViewOptions({
        disableBack: true
      });
      $state.go('login');
    }

    if (ProfileService.getCurrentShopId())
      $scope.credentials.id = ProfileService.getCurrentShopId();
    else {
      $ionicHistory.nextViewOptions({
        disableBack: true
      });
      $state.go('login');
    }

    $scope.submit = function () {
      $scope.creditError = null;
      $scope.creditSuccess = null;
    	$scope.creditResult = Shop.credit($scope.credentials, 
        function (value, responseHeaders) {
          if(value.success){
            delete $scope.credentials.email;
            delete $scope.credentials.amount;
            $scope.creditSuccess = true;
          }else{
            $scope.creditError = true;
          }
        },
        function (res) {
          $scope.creditError = res.data.error;
        }
        );
    }

  }
]);

Auth

Profile.controller('LoginController', [
  '$scope',
  '$state',
  'User',
  function ($scope, $state, User) {
    
    $scope.credentials = {
    };
    $scope.login = function () {

      $scope.loginResult = User.login($scope.credentials,
        function () {

          window.localStorage.setItem('currentUserId', $scope.loginResult.userId);
          window.localStorage.setItem('accessToken', $scope.loginResult.id);
          User.shops(
            { id: $scope.loginResult.userId },
            function(list) {
              if (list && list.length>0)
                window.localStorage.setItem('shopId', list[0].id);
            },
            function(errorResponse) { /* error */ }

          $state.go('home');

        },
        function (res) {
          $scope.loginError = res.data.error;
        }
      );
    };
  }
]);
    if (ProfileService.getCurrentUserId())
      $scope.credentials.creatorId = ProfileService.getCurrentUserId();
    else {
      $ionicHistory.nextViewOptions({
        disableBack: true
      });
      $state.go('login');
    }

Testing

var lt = require('loopback-testing');
var assert = require('assert');
var app = require('../../../server/server.js'); //path to app.js or server.js

describe('/api/v1/shops/{id}/credit error', function() {
  lt.beforeEach.withApp(app);

  lt.beforeEach.givenLoggedInUser({id:'1a', email: 'foo2@bar.com',password:"password", "emailVerified": true});
  lt.beforeEach.givenModel('shop', {id:'1a', name:'name'});

  lt.describe.whenCalledRemotely('POST', '/api/v1/shops/1a/credit', {email: 'foo3@bar.com',password:"password"}, function() {
    lt.it.shouldBeDenied();
  });
});

describe('/api/v1/shops/{id}/credit ok', function() {
  lt.beforeEach.withApp(app);

  lt.beforeEach.givenLoggedInUser({id:'1a', email: 'foo2@bar.com',password:"password", "emailVerified": true});
  lt.beforeEach.givenModel('shop', {id:'1a', name:'name', ownerId:'1a'});

  lt.describe.whenCalledRemotely('POST', '/api/v1/shops/1a/credit', {email: 'foo3@bar.com',amount: 10}, function() {
    lt.it.shouldBeAllowed();
 	  it('should', function() {
      console.log(this.res.body);
      assert.equal('success' in this.res.body, true);
      assert.equal(this.res.body['success'], true);
    });
  });
});

describe('Controllers', function(){

    // load the controller's module
    // beforeEach(module('app'));
    beforeEach(angular.mock.module('app'));

    // tests start here
    it('should have a Home module', function() {
        expect(Home).to.not.be.undefined;
    });

  it('should have a properly working HomeController controller', 
    inject(function($rootScope, $controller, $httpBackend) {

    window.localStorage.removeItem('currentUserId');
    window.localStorage.removeItem('shopId');

    var $scope = $rootScope.$new();
    var ctrl = $controller('HomeController', {
      $scope : $scope,
    });

    expect($scope.viewTitle).to.equal('FoodPoints');
    expect($scope.showMyBalance()).to.equal(false);
    expect($scope.showCredit()).to.equal(false);
    expect($scope.showDebit()).to.equal(false);
    expect($scope.showBalance()).to.equal(false);
    expect($scope.showLogin()).to.equal(true);
    expect($scope.showRegister()).to.equal(true);

  }));
describe('Services', function(){

    beforeEach(angular.mock.module('app'));

    it('can get an instance of my factory', inject(function(ProfileService) {
        expect(ProfileService).to.not.be.undefined;
    }));

    it('not logged', inject(function(ProfileService) {
        window.localStorage.removeItem('currentUserId');
        window.localStorage.removeItem('shopId');
        expect(ProfileService.getCurrentUserId()).to.equal(null);
        expect(ProfileService.getCurrentShopId()).to.equal(null);
    }));

    it('user', inject(function(ProfileService) {
        window.localStorage.removeItem('currentUserId');
        window.localStorage.removeItem('shopId');
        window.localStorage.setItem('currentUserId', 'currentUserId');
        expect(ProfileService.getCurrentUserId()).to.equal('currentUserId');
        expect(ProfileService.getCurrentShopId()).to.equal(null);
    }));

    it('owner', inject(function(ProfileService) {
        window.localStorage.removeItem('currentUserId');
        window.localStorage.removeItem('shopId');
        window.localStorage.setItem('currentUserId', 'currentUserId');
        window.localStorage.setItem('shopId', 'shopId');
        expect(ProfileService.getCurrentUserId()).to.equal('currentUserId');
        expect(ProfileService.getCurrentShopId()).to.equal('shopId');
    }));

});
describe('login', function() {
  beforeEach(function() {
    browser.get('/');
  });

  it('login ok', function() {

    // test user
    expect(browser.getTitle()).toEqual('FoodPoints');
    expect(element(by.css('[ui-sref="login"]')).getText()).toBe('Login');
    element(by.css('[ui-sref="login"]')).click();

    expect(browser.getTitle()).toEqual('Login');
    element(by.model('credentials.email')).sendKeys('test.user@foodpoints.cl');
    element(by.model('credentials.password')).sendKeys('dddddd');
    expect(element(by.css('.button.button-block.button-positive')).getText()).toBe('Login');
    element(by.css('.button.button-block.button-positive')).click();

    expect(browser.getTitle()).toEqual('FoodPoints');
    expect(element(by.css('[ui-sref="mybalance"]')).getText()).toBe('Mis puntos');

    expect(element(by.css('[ng-click="logout()"]')).getText()).toBe('logout');
    element(by.css('[ng-click="logout()"]')).click();

    // test owner
    expect(browser.getTitle()).toEqual('FoodPoints');
    expect(element(by.css('[ui-sref="login"]')).getText()).toBe('Login');
    element(by.css('[ui-sref="login"]')).click();

    expect(browser.getTitle()).toEqual('Login');
    element(by.model('credentials.email')).sendKeys('test.restaurant@foodpoints.cl');
    element(by.model('credentials.password')).sendKeys('ddddd');
    expect(element(by.css('.button.button-block.button-positive')).getText()).toBe('Login');
    element(by.css('.button.button-block.button-positive')).click();

    expect(browser.getTitle()).toEqual('FoodPoints');
    expect(element(by.css('[ui-sref="credit"]')).getText()).toBe('Acumular');

    expect(element(by.css('[ng-click="logout()"]')).getText()).toBe('logout');
    element(by.css('[ng-click="logout()"]')).click();

  });

});

CI/CD

Shippable+Heroku


language: node_js

node_js:
  - 0.10.38

addons:
  firefox: "23.0"

services:
  - mongodb
  - selenium

env:
  global:
    - MONGOLAB_URI=mongodb://localhost/test
    - APP_UUID=aaaaaa
    - SLACK_ORG=sssssss
    - secure: QGLe0HJ0BXuAWKmjrPWLeeabGAuRGU7YuWM+nW5reb7r5JzEYGUsje5YHlSyigWd/X7cvb/9eG9yTnqS9FRN+WoQABy6X54vcaXpsl8am6ov+qpQL7KhfNduNdQKryaECcV1Jh2rlS3aTe8Dewjo65YJE5M+zhKV5OtJamMSU3DcbMg24vpCH/2dLJnZK9HY1aa9mBpG2jg/iymIjalJyZTCUBwswOLtoFazwMJ9Hj0f0a0Na5oW7ebsN+nK7m62uGD6WxMmwRVKihJcvDtZ/TE87e8weaZ0OcdFl23fvNJapEtaoWcgLrYr0oGHxfqJuODS2nVALVNZGmSKYKmPSA==
    - secure: eKxBK5oAkHOcXccAOjt07tZ5A46NjsOT7MjslmwvBFB0Q7EiW5dS+GotH97tF5xBiRku0amMhm0HFAeHDgbbN+hdksrkG8OHsyAYY38Wsp1/wx7VbstxSVFrep5osfCfc9VKitB2yGbdYGJisC1XuZEr1MXpI/+WGKyDU7FQtcy0iAbDiPI1VIk3VymJh58PHGObQyOWjVStVJV9J7cGNofiYajmmqi1SJupIOoTaSM1kkBF6IWe2YII2TEqPywCynsvUDWUOLkKx4gjYfwe1SVTnnDfT+W4c2th9dDvzXhIF8jeVk4ijOZgyxWhiZXHQheyVibHczapVKAiDyD8Aw==
    - secure: Fq/OEC43n9NM+fHYjgKmdytBxFckij4eh86ybG9h7TkkgfuI3OTAAnImLHQwBXsusbBlR3TyXM/Q5nE6anXDADNXS7Mx0hsuGd4Xk7hyt6xzajBzVjeIfEPjNbXh8R7xqpJszSmYREzrgjzkFUYfve7Bib7xrlyg2MDFrmPWh7iPwV4CfazWYl4a6PNAx80LOKLCQPmuOQGUGzKrvSbdhIPxFTigjMFvuOatzlJGRpSsdKBagz0Fvato9s0U7S0EX1Sx2TdFWsVYFer2DsLcUgLVyd8zEXDam/nCW7B0IxDVyVTh7BhVhtq0yPLb20bMcyWOa1DQ0kuSHf98EyRP7w==
    # MONGOLAB_URI
    - secure: ViN0QbUxxjWIzzM5iiB8pgEySMryOQITYjCjpzCsQP69AYguSCozxSI38+1bDlgDpNE3+5xIOindG9W3gI6h0gnNwFswfUdpDJ6Jun+vaS7+IgERiT9pvkOtKRkWHO2qmuTT5uXV57mJVuSuWCirGgTXuig0MATx90id7urapyPAVPkWAU49xFlAlj2mlFRbZkzptRAu6dgYyH6kQ2Y1SadftL9fapjKrk/+RlR86+UMcZxvyTG6ZyX7BJZv+o+rjeyard+zURxcW8dPcQg5uSqwDDmC+Ynqf4OpPtLuFDrUXLRhUkTkUugDp9YG8G+e6M+iy4KI1KBwaIMgeChC7g==

notifications:
  email:
    recipients:
      - jpizarrom@gmail.com
    on_success: change
    on_failure: always

before_install:
  - . $HOME/.rvm/scripts/rvm && rvm use ruby-2.0.0-p598 --default && ruby --version
  - which heroku || wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh
  - APP_NAME=`echo $APP_UUID-$BRANCH | cut -c 1-30`
  - npm -g install npm@2.1.1

before_script:
  - mkdir -p shippable/testresults
  - mkdir -p shippable/codecoverage
  - "export DISPLAY=:99.0"
  - "/etc/init.d/xvfb start"

script:
  - ./node_modules/loopback-testing/node_modules/.bin/mocha test/api/unit/ -R xunit
  - ./node_modules/.bin/gulp test
  # - ./node_modules/.bin/protractor --version
  # - ./node_modules/.bin/protractor --baseUrl='http://app.foodpoints.cl' test/client/protractor.conf.js
  - ./node_modules/.bin/lb-ng server/server.js ./app/js/modules/common/lb-services.js -u https://$APP_NAME.herokuapp.com/api/v1
  - ./node_modules/.bin/gulp

# after_script:
#   - "/etc/init.d/xvfb stop"
#   - ./node_modules/.bin/istanbul cover test/client/protractor.conf.js -- -u tdd
#   - ./node_modules/.bin/istanbul report cobertura --dir  shippable/codecoverage/

after_failure:
  - python slack_notifier.py --project $APP_NAME --org $SLACK_ORG --token $SLACK_TOKEN

after_success:
  - python slack_notifier.py --project $APP_NAME.herokuapp.com --org $SLACK_ORG --token $SLACK_TOKEN -s
  - test -f ~/.ssh/id_rsa.heroku || (ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.heroku && heroku keys:add ~/.ssh/id_rsa.heroku)
  - heroku apps | grep ^$APP_NAME || ( (heroku create $APP_NAME -n && heroku addons:add mandrill --app $APP_NAME) || (heroku addons:add mandrill --app $APP_NAME))
  - heroku addons --app $APP_NAME | grep deployhooks || heroku addons:add deployhooks:http --url=https://$SLACK_ORG.slack.com/services/hooks/heroku?token=$SLACK_HEROKU_TOKEN --app $APP_NAME
  - heroku config:get MAIL_HOST --app $APP_NAME | grep . || heroku config:set MAIL_HOST=$APP_NAME.herokuapp.com --app $APP_NAME
#  - >
#    [[ $BRANCH != 'master' ]] && MONGOLAB_URI=`heroku config:get MONGOLAB_URI --app $APP_UUID-master`
  - >
    [[ $BRANCH != 'master' ]] && heroku config:set MONGOLAB_URI=$MONGOLAB_URI --app $APP_NAME || echo "ok"
#  - git remote -v | grep ^heroku | grep ^$APP_NAME || heroku git:remote --ssh-git --app $APP_NAME
  - git checkout -b deploy
  - git add . -A
  - git commit -m "-"
  - git push git@heroku.com:$APP_NAME.git deploy:master -f
#  - git push -f heroku deploy:master
  - ./node_modules/.bin/protractor --baseUrl="https://$APP_NAME.herokuapp.com" test/client/protractor.conf.js
  - "/etc/init.d/xvfb stop"
  # - ./node_modules/.bin/istanbul cover test/client/protractor.conf.js -- -u tdd
  # - ./node_modules/.bin/istanbul report cobertura --dir shippable/codecoverage/

Questions

References

  • http://loopback.io/
  • http://ionicframework.com/
  • http://www.shippable.com/
  • http://www.heroku.com/
  • https://bitbucket.org/

Loopback-ionic

By Juan Pizarro

Loopback-ionic

  • 2,686