Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • exemplo/1
  • master
  • exemplo/6
  • exemplo/7
  • exemplo/5
  • exemplo/4
  • exemplo/3
  • exemplo/10
  • exemplo/9
  • exemplo/8
  • exemplo/2
11 results

Target

Select target project
  • Diego Giovane Pasqualin / gitlab-ci-by-example
  • Walmes Marques Zeviani / gitlab-ci-by-example
  • Daniel Bissani Furlin / gitlab-ci-by-example
  • Luiza Wille / gitlab-ci-by-example
  • Hamer Iboshi / gitlab-ci-by-example
  • lsa17 / gitlab-ci-by-example
  • Vytor Calixto / gitlab-ci-by-example
7 results
Select Git revision
  • master
  • exemplo/10
  • exemplo/9
  • exemplo/8
  • exemplo/7
  • exemplo/6
  • exemplo/5
  • exemplo/4
  • exemplo/2
  • exemplo/3
  • exemplo/1
11 results
Show changes
53 files
+ 3374
19
Compare changes
  • Side-by-side
  • Inline

Files

.editorconfig

0 → 100644
+17 −0
Original line number Diff line number Diff line
# http://editorconfig.org

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.json]
indent_size = 2

.gitlab-ci.yml

0 → 100644
+31 −0
Original line number Diff line number Diff line
# Esse exemplo não é muito diferente do que você já viu anteriormente. A
# diferença é que este projeto utiliza módulos para cobertura de código, que
# indicam porcentagem do código coberta pelos testes da aplicação.
# Para que o Gitlab "entenda" isso e coloque uma coluna nova na interface
# é necessário editar o campo "Test coverage parsing", em
# <project>/Settings, adicionando expressão regular de acordo com o informado
# pelo programa utilizado no seu projeto para cobertura de código.
#
# Para esse programa, a expressão regular é ^Statements\s*:\s*\d+.\d+\%

image: node:4.2

services:
  - mongo:latest

before_script:
  - npm config set cache /cache/npm
  - npm install --silent -g gulp
  - npm install --silent
  - cp config/example.yaml config/test.yaml
  - sed -i 's/database\x3a[ ]*\x27test\x27/database\x3a \x27simmc_test\x27/' config/test.yaml
  - sed -i 's/host\x3a[ ]*\x27localhost\x27/host\x3a \x27mongo\x27/' config/test.yaml

check:
  stage: test
  script:
    - gulp lint
    - gulp test
  tags:
    - node
    - mongo

.jscsrc

0 → 100644
+21 −0
Original line number Diff line number Diff line
{
	"preset": "airbnb",
	"validateIndentation": 4,
	"requireTrailingComma": false,
	"disallowTrailingComma": true,
	"requireCurlyBraces": [
	    "if",
	    "else",
	    "for",
	    "while",
	    "do",
	    "try",
	    "catch"
	],
	"requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties",
	"disallowKeywordsOnNewLine": [],
	"maximumLineLength": 80,
	"requirePaddingNewLinesAfterBlocks": {
		"allExcept": ["inCallExpressions", "inNewExpressions", "inArrayExpressions", "inProperties"]
	}
}

.jshintrc

0 → 100644
+63 −0
Original line number Diff line number Diff line
{
  /*
   * ENVIRONMENTS
   * =================
   */

  // Define globals exposed by modern browsers.
  "browser": false,

  // Define globals exposed by Node.js.
  "node": true,

  // Allow ES6.
  "esnext": true,

  /*
   * ENFORCING OPTIONS
   * =================
   */

  // Prohibit use of == and != in favor of === and !==.
  "eqeqeq": true,

  // Enforce tab width of 2 spaces.
  "indent": 4,

  // Prohibit use of a variable before it is defined.
  "latedef": false,

  // Require capitalized names for constructor functions.
  "newcap": true,

  // Enforce use of single quotation marks for strings.
  "quotmark": "single",

  // Prohibit use of explicitly undeclared variables.
  "undef": true,

  // Warn when variables are defined but never used.
  "unused": "vars",

  /*
   * RELAXING OPTIONS
   * =================
   */

  // Suppress warnings about == null comparisons.
  "eqnull": true,

  // Add global variables for Mocha and Chai
  "globals": {
       "describe": false,
       "xdescribe": false,
       "ddescribe": false,
       "it": false,
       "xit": false,
       "iit": false,
       "beforeEach": false,
       "afterEach": false,
       "before": false,
       "after": false
   }
}

LICENSE

0 → 100644
+674 −0

File added.

Preview size limit exceeded, changes collapsed.

README.md

deleted100644 → 0
+0 −19
Original line number Diff line number Diff line
Esse repositório contém exemplos de uso do sistema de integração contínua do
gitlab.

Cada exemplo encontra-se em um ramo diferente, nomeado como
`exemplo/X`, onde X é um inteiro crescente. Normalmente, quanto maior X, mais
funcionalidades são apresentadas.

Para ver o primeiro exemplo mude para o *branch* `exemplo/1` com o comando abaixo e
abra o arquivo `.gitlab-ci.yml`.

```
git checkout -t origin/exemplo/1
```

Todos os *builds* feitos aparecem no item *Builds*, aba *All*, do menu lateral esquerdo
no Gitlab.

Clique no valor sob a coluna *Status* para ver detalhes do *build*. Repare
no campo *Ref*, que indica o *branch* onde a *build* foi executada.
+185 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var FilterParser = require('filter-parser');
var createError = require('http-errors');

module.exports = {
    list: list,
    index: index,
    read: read
};

var filterParser = new FilterParser({
    identifierParser: function(ident) {
        // convert the identifier id into _id
        if (ident === 'id') {
            ident = '_id';
        }

        // only accept a few valid identifiers
        if ((ident !== '_id') &&
            (ident !== 'name') &&
            (ident !== 'adminLevel') &&
            (ident !== 'extras.city_code')) {
            throw new Error('Invalid identifier ' + ident);
        }

        return ident;
    }
});

function mapLocation(doc) {
    return {
        id: parseInt(doc.id),
        score: doc.score,
        name: doc.name,
        adminLevel: doc.adminLevel,
        border: doc.border,
        subDivisions: doc.subDivisions,
        extras: doc.extras
    };
}

function list(req, res, next) {
    var Location = req.db.model('Location');

    // retrieve query parameters
    var perPage = req.query.per_page || 10;
    var page = req.query.page || 1;
    var search = req.query.query;
    var filters = req.query.filters;

    // build the query
    var qQuery = {};
    var qProjection = { _id: 1, name: 1, adminLevel: 1, extras: 1 };
    var qSort = { _id: 1 };

    if (filters) {
        try {
            qQuery = filterParser.parse(filters);
        }
        catch (err) {
            next(createError.BadRequest(
                'Invalid filter expression. Check your syntax and try again.'));
            return;
        }
    }

    if (search) {
        qQuery.$text = { $search: search };
        qProjection.score = { $meta: 'textScore' };
        qSort = { score: { $meta: 'textScore' } };
    }

    var q = Location
        .find(qQuery, qProjection)
        .sort(qSort)
        .skip((page - 1) * perPage)
        .limit(perPage);

    // execute the query
    q.exec(function(err, docs) {
        if (err) {
            req.log.error(err);
            next(createError.BadRequest(
                'Unfortunately, the request you are making cannot be ' +
                'fulfilled by the server.'));
            return;
        }

        var locations = [];
        docs.forEach(function(doc) {
            locations.push(mapLocation(doc));
        });

        res.status(200).json(locations);
    });
}

function index(req, res, next) {
    var Location = req.db.model('Location');

    var q = Location
        .find({}, { _id: 1, adminLevel: 1, name: 1 })
        .sort({ _id: 1 });

    // execute the query
    q.exec(function(err, docs) {
        /* istanbul ignore if */
        if (err) {
            req.log.error(err);
            next(createError.InternalServerError());
            return;
        }

        var result = {
            keys: ['adminLevel', 'id', 'name'],
            index: {}
        };

        docs.forEach(function(doc) {
            if (!result.index[doc.adminLevel]) {
                result.index[doc.adminLevel] = {};
            }

            result.index[doc.adminLevel][doc.id] = doc.name;
        });

        res.status(200).json(result);
    });
}

function read(req, res, next) {
    var Location = req.db.model('Location');

    var locationId = req.params.locationId;

    var projection = {
        _id: 1,
        name: 1,
        adminLevel: 1,
        extras: 1
    };
    if (req.query.border) {
        projection.border = 1;
    }

    if (req.query.sub_divisions) {
        projection.subDivisions = 1;
    }

    Location.findOne({_id: locationId}, projection, function(err, doc) {
        /* istanbul ignore if */
        if (err) {
            req.log.error(err);
            next(createError.InternalServerError());
            return;
        }

        if (!doc) {
            next(createError.NotFound(
                'Location \'' + locationId + '\' could not be found.'));
            return;
        }

        res.status(200).json(mapLocation(doc));
    });
}
+190 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

module.exports = {
    list: list,
    create: create,
    read: read,
    update: update,
    delete: del
};

function mapUser(doc) {
    var user = {
        id: String(doc.id),
        email: doc.email,
        firstname: doc.firstname,
        lastname: doc.lastname,
        tags: doc.tags,
        created_at: doc.created_at,
        updated_at: doc.updated_at
    };

    return user;
}

function list(req, res) {
    var User = req.db.model('User');

    var perPage = req.query.per_page || 10;
    var page = req.query.page || 1;
    var offset = (page - 1) * perPage;

    var q = User
        .find()
        .skip(offset)
        .limit(perPage);

    q.exec(function(err, docs) {
        if (err) {
            console.error(err);
            res.status(500).json({ message: 'Database query failed.' });
            return;
        }

        var users = [];
        docs.forEach(function(doc) {
            users.push(mapUser(doc));
        });

        res.json(users);
    });
}

function create(req, res) {
    var User = req.db.model('User');

    var userEmail = req.body.email;
    var userPassword = req.body.password;
    var userFirstname = req.body.firstname;
    var userLastname = req.body.lastname;
    var userTags = req.body.tags;

    // add comment
    var doc = new User();
    doc.email = userEmail;
    doc.password = userPassword;
    doc.firstname = userFirstname;
    doc.lastname = userLastname;
    doc.tags = userTags;
    doc.save(function(err) {
        if (err) {
            console.error(err);
            res.status(500).json({
                message: 'Failed to save user to the database.'
            });
            return;
        }

        res.status(200).json(mapUser(doc));
    });
}

function read(req, res) {
    var User = req.db.model('User');

    var userId = req.params.userId;

    User.findOne({_id: userId}, function(err, doc) {
        if (err) {
            console.error(err);
            res.status(500).json({ message: 'Database query failed.' });
            return;
        }

        if (!doc) {
            res.status(404).json({ message: 'User could not be found.' });
            return;
        }

        res.status(200).json(mapUser(doc));
    });
}

function update(req, res) {
    var User = req.db.model('User');

    var userId = req.params.userId;

    var userEmail = req.body.email;
    var userPassword = req.body.password;
    var userFirstname = req.body.firstname;
    var userLastname = req.body.lastname;
    var userTags = req.body.tags;

    User.findOne({_id: userId}, function(err, doc) {
        if (err) {
            console.error(err);
            res.status(500).json({ message: 'Database query failed.' });
            return;
        }

        if (!doc) {
            res.status(404).json({ message: 'User could not be found.' });
            return;
        }

        doc.email = userEmail || doc.email;
        doc.password = userPassword || doc.password;
        doc.firstname = userFirstname || doc.firstname;
        doc.lastname = userLastname || doc.lastname;
        doc.tags = userTags || doc.tags;
        doc.save(function(err) {
            if (err) {
                console.error(err);
                res.status(500).json({
                    message: 'Failed to save user to the database.'
                });
                return;
            }

            res.status(200).json(mapUser(doc));
        });
    });
}

function del(req, res) {
    var User = req.db.model('User');

    var userId = req.params.userId;

    User.findOne({_id: userId}, function(err, doc) {
        if (err) {
            console.error(err);
            res.status(500).json({ message: 'Database query failed.' });
            return;
        }

        if (!doc) {
            res.status(404).json({ message: 'User could not be found.' });
            return;
        }

        doc.remove(function(err) {
            if (err) {
                console.error(err);
                res.status(500).json({ message: 'Database query failed.' });
                return;
            }

            res.status(200).json({ message: 'User sucessufully removed.' });
        });
    });
}
+44 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var mongoose = require('mongoose');
var glob = require('glob');
var path = require('path');

module.exports = function(config) {
    var connectString = 'mongodb://' + config.get('mongodb.host') + ':' +
                                       config.get('mongodb.port') + '/' +
                                       config.get('mongodb.database');

    mongoose.connect(connectString);
    mongoose.connection.on('error', function(err) {
        throw err;
    });

    glob.sync(path.resolve(__dirname, '../models/*.js'))
        .forEach(function(file) {
            require(file)();
        });

    return function(req, res, next) {
        req.db = mongoose;
        next();
    };
};

api/models/location.js

0 → 100644
+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var mongoose = require('mongoose');

module.exports = function() {
    var locationSchema = mongoose.Schema({
        _id: {type: Number, required: true},
        name: {type: String, required: true},
        adminLevel: {type: Number, required: true},
        border: {type: Object, required: true},
        subDivisions: {type: Object},
        extras: {type: mongoose.Schema.Types.Mixed}
    });

    // Workaround Alert:
    //  The following virtual is only needed as a placeholder for the
    //  calculated score of a text search on this collection. If this virtual
    //  is ommited, mongoose hides the 'score' field on the final document
    //  returned on queries.
    locationSchema.virtual('score');

    locationSchema.index({ adminLevel: 1 });

    locationSchema.index({ name: 'text', 'extras.city_code': 'text' },
        { default_language: 'portuguese' });

    return mongoose.model('Location', locationSchema);
};

api/models/user.js

0 → 100644
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var mongoose = require('mongoose');
var bcrypt = require('bcrypt');

var BCRYPT_SALT_WORK_FACTOR = 10;
var MAX_NUMBER_OF_TAGS = 256;

module.exports = function() {
    var userSchema = mongoose.Schema({
        email: {type: String, required: true, unique: true },
        password: {type: String, required: true },
        firstname: {type: String},
        lastname: {type: String},
        tags: [String],
        created_at: {type: Date },
        updated_at: {type: Date }
    });

    userSchema.pre('save', function(next) {
        var _this = this;
        var now = new Date();

        // set/update the created_at and updated_at fields
        _this.updated_at = now;
        if (!_this.created_at) {
            _this.created_at = now;
        }

        // hash the password (only if has been modified or is new)
        if (!_this.isModified('password')) {
            next();
        } else {
            // generate a salt
            bcrypt.genSalt(BCRYPT_SALT_WORK_FACTOR, function(err, salt) {
                if (err) {
                    return next(err);
                }

                // hash the password along with our new salt
                bcrypt.hash(_this.password, salt, function(err, hash) {
                    if (err) {
                        return next(err);
                    }

                    // override the cleartext password with the hashed one
                    _this.password = hash;
                    next();
                });
            });
        }
    });

    function validateEmail(value) {
        if ((value.length < 4) || (value.length > 255)) {
            return false;
        }

        if (!value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) {
            return false;
        }

        return true;
    }

    function validatePassword(value) {
        if ((value.length < 8) || (value.length > 256)) {
            return false;
        }

        if (!value.match(/^[a-zA-Z0-9$-\/:-?{-~!\"^_`\[\]]*$/)) {
            return false;
        }

        return true;
    }

    function validateTags(value) {
        if (value.length > MAX_NUMBER_OF_TAGS) {
            return false;
        }

        for (var i = 0; i < value.length; i++) {
            if (!value[i].match(/^[A-Za-z]+[A-Za-z0-9.-_]+$/)) {
                return false;
            }

            if (value[i].length > 28) {
                return false;
            }
        }

        return true;
    }

    function validateName(value) {
        if ((value.length < 3) || (value.length > 28)) {
            return false;
        }

        return true;
    }

    userSchema.path('email').validate(validateEmail);
    userSchema.path('password').validate(validatePassword);
    userSchema.path('tags').validate(validateTags);
    userSchema.path('firstname').validate(validateName);
    userSchema.path('lastname').validate(validateName);

    return mongoose.model('User', userSchema);
};

api/router-v1.js

0 → 100644
+38 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var osprey = require('osprey');

// import controllers
var users = require('./controllers/users');
var locations = require('./controllers/locations');

var router = module.exports = osprey.Router();

router.get('/users', users.list);
router.post('/users', users.create);
router.get('/users/{userId}', { userId: { type: 'string' } }, users.read);
router.put('/users/{userId}', { userId: { type: 'string' } }, users.update);
router.delete('/users/{userId}', { userId: { type: 'string' } }, users.delete);

router.get('/locations', locations.list);
router.get('/locations/_index', locations.index);
router.get('/locations/{locationId}',
    { locationId: { type: 'integer' } }, locations.read);
+6 −0
Original line number Diff line number Diff line
{
  "email": "email@something.com",
  "password": "somerandompassword",
  "firstname": "Name",
  "tags": ["SomeProject", "AnotherProject"]
}
Original line number Diff line number Diff line
[
  {
    "id": "11",
    "name": "RONDÔNIA",
    "adminLevel": 4,
    "extras": {}
  },
  {
    "id": "14",
    "name": "RORAIMA",
    "adminLevel": 4,
    "extras": {}
  },
  {
    "id": "13",
    "name": "AMAZONAS",
    "adminLevel": 4,
    "extras": {}
  },
  {
    "id": "16",
    "name": "AMAPÁ",
    "adminLevel": 4,
    "extras": {}
  }
]
Original line number Diff line number Diff line
[
  {
    "id": "5639ff8eeca3b4f8334e9917",
    "email": "email@something.com",
    "firstname": "Name",
    "tags": [
      "SomeProject",
      "AnotherProject"
    ],
    "created_at": "2015-11-04T12:52:30.781Z",
    "updated_at": "2015-11-04T12:52:30.781Z"
  },
  {
    "id": "558acc151806abf713891b6c",
    "email": "another@user.me",
    "tags": [],
    "created_at": "2015-06-24T15:26:13.281Z",
    "updated_at": "2015-06-24T15:26:13.281Z"
  },
  {
    "id": "5638a4c39517775d37a506b1",
    "email": "yet@another.biz",
    "firstname": "John",
    "lastname": "Doe",
    "tags": [
       "SomeProject"
    ],
    "created_at": "2015-11-03T12:12:51.855Z",
    "updated_at": "2015-11-03T12:12:51.855Z"
  }
]
Original line number Diff line number Diff line
{
  "id": "3528858",
  "name": "MARAPOAMA",
  "adminLevel": 8,
  "extras": {
    "city_code": "MARP"
  }
}
+11 −0
Original line number Diff line number Diff line
{
  "id": "5639ff8eeca3b4f8334e9917",
  "email": "email@something.com",
  "firstname": "Name",
  "tags": [
    "SomeProject",
    "AnotherProject"
  ],
  "created_at": "2015-11-04T12:52:30.781Z",
  "updated_at": "2015-11-04T12:52:30.781Z"
}
+25 −0
Original line number Diff line number Diff line
{
    "$schema": "http://json-schema.org/draft-03/schema",
    "id": "http://jsonschema.net",
    "type": "object",
    "properties": {
        "email": {
            "type": "string"
        },
        "password": {
            "type": "string"
        },
        "firstname": {
            "type": "string"
        },
        "lastname": {
            "type": "string"
        },
        "tags": {
            "type": "array",
            "items": {
                "type": "string"
            }
        }
    }
}
Original line number Diff line number Diff line
{
    "$schema": "http://json-schema.org/draft-03/schema",
    "id": "http://jsonschema.net",
    "type": "array",
    "items": { "$ref": "file:api/specs/schemas/items/location.json" }
}
Original line number Diff line number Diff line
{
    "$schema": "http://json-schema.org/draft-03/schema",
    "id": "http://jsonschema.net",
    "type": "array",
    "items": { "$ref": "file:api/specs/schemas/items/user.json" }
}
+29 −0
Original line number Diff line number Diff line
{
    "$schema": "http://json-schema.org/draft-03/schema",
    "id": "http://jsonschema.net",
    "type": "object",
    "properties": {
        "id": {
            "type": "integer"
        },
        "name": {
            "type": "string"
        },
        "adminLevel": {
            "type": "integer"
        },
        "border": {
            "type": "object"
        },
        "subDivisions": {
            "type": "array",
            "items": {
                "type": "object"
            }
        },
        "extras": {
            "type": "object"
        }
    },
    "required": ["id", "name", "adminLevel"]
}
+32 −0
Original line number Diff line number Diff line
{
    "$schema": "http://json-schema.org/draft-03/schema",
    "id": "http://jsonschema.net",
    "type": "object",
    "properties": {
        "id": {
            "type": "string"
        },
        "email": {
            "type": "string"
        },
        "firstname": {
            "type": "string"
        },
        "lastname": {
            "type": "string"
        },
        "tags": {
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "created_at": {
            "type": "string"
        },
        "updated_at": {
            "type": "string"
        }
    },
    "required": ["id", "email"]
}
+15 −0
Original line number Diff line number Diff line
{
    "$schema": "http://json-schema.org/draft-03/schema",
    "id": "http://jsonschema.net",
    "type": "object",
    "properties": {
        "id": {
            "type": "string",
            "required": true
        },
        "message": {
            "type": "string",
            "required": true
        }
    }
}

config/example.yaml

0 → 100644
+4 −0
Original line number Diff line number Diff line
mongodb:
    host: 'localhost'
    port: 27017
    database: 'test'

gulpfile.js

0 → 100644
+181 −0
Original line number Diff line number Diff line
'use strict';

var gulp = require('gulp');
var gutil = require('gulp-util');
var raml = require('gulp-raml');
var rename = require('gulp-rename');
var jshint = require('gulp-jshint');
var size = require('gulp-size');
var jscs = require('gulp-jscs');
var stylish = require('gulp-jscs-stylish');
var mocha = require('gulp-mocha');
var istanbul = require('gulp-istanbul');
var nodemon = require('gulp-nodemon');


var path = require('path');
var raml2html = require('raml2html');
var through = require('through2');
var yaml = require('js-yaml');

var srcFiles = [
    'api/**/*.js',
    'server.js',
    'gulpfile.js',
    'lib/**/*.js',
    'test/**/*.js'
];

function handleError(err) {
    console.error(err.toString());
    process.exit(1);
}

function generateDoc(options) {
    var simplifyMark = function(mark) {
        if (mark) {
            mark.buffer = mark.buffer
                .split('\n', mark.line + 1)[mark.line]
                .trim();
        }
    };

    if (!options) {
        options = {};
    }

    switch (options.type) {
    case 'json':
        options.config = {
            template: function(obj) {
                return JSON.stringify(obj, null, 2);
            }
        };
        break;
    case 'yaml':
        options.config = {
            template: function(obj) {
                return yaml.safeDump(obj, {
                    skipInvalid: true
                });
            }
        };
        break;
    default:
        options.type = 'html';
        if (!options.config) {
            options.config = raml2html.getDefaultConfig(
                options.https,
                options.template,
                options.resourceTemplate,
                options.itemTemplate
            );
        }
    }

    if (!options.extension) {
        options.extension = '.' + options.type;
    }

    var stream = through.obj(function(file, enc, done) {
        var fail = function(message) {
            done(new gutil.PluginError('raml2html', message));
        };

        if (file.isBuffer()) {
            var cwd = process.cwd();
            process.chdir(path.resolve(path.dirname(file.path)));
            raml2html
                .render(file.contents, options.config)
                .then(function(output) {
                        process.chdir(cwd);
                        stream.push(new gutil.File({
                            base: file.base,
                            cwd: file.cwd,
                            path: gutil.replaceExtension(
                                file.path, options.extension),
                            contents: new Buffer(output)
                        }));
                        done();
                    },
                    function(error) {
                        process.chdir(cwd);
                        simplifyMark(error.context_mark);
                        simplifyMark(error.problem_mark);
                        process.nextTick(function() {
                            fail(JSON.stringify(error, null, 2));
                        });
                    }
                );
        }
        else if (file.isStream()) {
            fail('Streams are not supported: ' + file.inspect());
        }
        else if (file.isNull()) {
            fail('Input file is null: ' + file.inspect());
        }
    });

    return stream;
}

gulp.task('raml', function() {
    gulp.src('api/specs/*.raml')
        .pipe(raml())
        .on('error', handleError)
        .pipe(raml.reporter('default'));
});

gulp.task('doc', function() {
    return gulp.src('api/specs/*.raml')
        .pipe(generateDoc())
        .on('error', handleError)
        .pipe(rename({ extname: '.html' }))
        .pipe(gulp.dest('doc/build'));
});

gulp.task('pre-test', function() {
    return gulp.src(['api/**/*.js'])
        .pipe(istanbul())
        .pipe(istanbul.hookRequire());
});

gulp.task('test', ['pre-test'], function() {
    return gulp.src('test/api/**/*.spec.js', {read: false})
        .pipe(mocha({
            require: ['./test/common.js'],
            reporter: 'spec',
            ui: 'bdd',
            recursive: true,
            colors: true,
            timeout: 60000,
            slow: 300,
            delay: true
        }))
        .pipe(istanbul.writeReports())
        .once('error', function() {
            process.exit(1);
        })
        .once('end', function() {
            process.exit();
        });
});

gulp.task('lint', function() {
    return gulp.src(srcFiles)
        .pipe(jshint())
        .pipe(jscs())
        .pipe(stylish.combineWithHintResults())
        .pipe(jshint.reporter('jshint-stylish'))
        .pipe(size());
});

gulp.task('check', ['raml', 'lint', 'test']);

gulp.task('develop', function() {
    return nodemon({
        script: 'server.js',
        ext: 'js',
        tasks: ['lint']
    });
});

lib/config-parser.js

0 → 100644
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var fs = require('fs');
var yaml = require('js-yaml');
var _ = require('lodash');

module.exports = {
    readFromFile: readFromFile
};

function readFromFile(file) {
    if (!fs.existsSync(file)) {
        throw new Error('Missing configuration file \'' + file + '\'.');
    }

    var conf = yaml.safeLoad(fs.readFileSync(file, 'utf8'));
    return new Config(conf);
}

function Config(conf) {
    this._config = _.defaultsDeep(conf, {
        mongodb: {
            host: 'localhost',
            port: 27017,
            database: ''
        }
    });

    this.get = function(path) {
        return _.get(this._config, path);
    };

    this.set = function(path, value) {
        _.set(this._config, path, value);
    };
}

lib/error-handler.js

0 → 100644
+77 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var statuses = require('statuses');

module.exports = errorHandler;

function toIdentifier(str) {
    return str.split(' ').map(function(token) {
        return token.slice(0, 1).toUpperCase() + token.slice(1);
    }).join('').replace(/[^ _0-9a-z]/gi, '');
}

function errorHandler(err, req, res, next) {
    if (res.headersSent) {
        // If headers are already sent, use the default express error
        // handler, or else the connection will be closed by express
        // and the request will be considered failed.
        return next(err);
    }

    err = formatError(err);
    res.status(err.status);
    res.json(err);
}

function formatError(err) {
    var res = {
        status: 500,
        id: 'InternalServerError',
        message: 'An internal server error has occurred.' +
                 ' Sorry for the inconvenience.'
    };

    try {
        if (typeof err !== 'object') {
            return res;
        }

        if (typeof err.status !== 'undefined') {
            res.status = err.status;
        }

        if (typeof err.id !== 'undefined') {
            res.id = err.id;
        }
        else {
            res.id = toIdentifier(statuses[res.status]);
        }

        if ((err.message) && (err.expose)) {
            res.message = err.message;
        }
    }
    catch (err) {
        // do nothing
    }

    return res;
}

lib/filter-parser.js

0 → 100644
+84 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var jsep = require('jsep');

module.exports = FilterParser;

function FilterParser(options) {
    options = options || {};

    this._parseIdentifier = options.identifierParser ||
        this.DefaultIdentifierParser;
    this._parseOperator = options.operatorParser ||
        this.DefaultOperatorParser;
}

FilterParser.prototype.parse = function(filters) {
    var parseTree = jsep(filters);
    return this._searchParseTree(parseTree);
};

FilterParser.prototype._searchParseTree = function(elem) {
    switch (elem.type) {
    case 'BinaryExpression':
        var operator = this._parseOperator(elem.operator);

        var left = this._searchParseTree(elem.left);
        var right = this._searchParseTree(elem.right);

        var ret = {};

        if ((operator === '$or') || (operator === '$and')) {
            ret[operator] = [left, right];
        }
        else {
            ret[left] = {};
            ret[left][operator] = right;
        }

        return ret;
    case 'UnaryExpression':
        return { $not: this._searchParseTree(elem.argument) };
    case 'Identifier':
        return this._parseIdentifier(elem.name);
    case 'Literal':
        return elem.value;
    }
};

FilterParser.prototype.DefaultOperatorParser = function(op) {
    switch (op) {
    case '==': return '$eq';
    case '>': return '$gt';
    case '>=': return '$gte';
    case '<': return '$lt';
    case '<=': return '$lte';
    case '!=': return '$ne';
    case '|': return '$or';
    case '&': return '$and';
    case '!': return '$not';
    default:  throw new Error('Unknown operator token: ' + op);
    }
};

FilterParser.prototype.DefaultIdentifierParser = function(ident) {
    return ident;
};

lib/fixtures.js

0 → 100644
+51 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var fs = require('fs');

module.exports = {
    load: loadFixtures
};

function loadFixtures(model, filename, callback) {
    fs.readFile(filename, 'utf8', function(err, data) {
        if (err) {
            callback(err);
            return;
        }

        try {
            var docs = JSON.parse(data);
        }
        catch (err) {
            callback(err);
            return;
        }

        model.remove({}, function(err) {
            if (err) {
                callback(err);
                return;
            }

            model.create(docs, callback);
        });
    });
}

lib/logger.js

0 → 100644
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

var bunyan = require('bunyan');
var cuid = require('cuid');
var fs = require('fs');
var PrettyStream = require('bunyan-prettystream');
var onFinished = require('on-finished');
var onHeaders = require('on-headers');

// Make sure the logs dir exist
try {
    fs.mkdirSync('logs');
}
catch (e) {
    if (e.code !== 'EEXIST') {
        throw e;
    }
}

// Setup streams
var streams = [];

// Default logger (info and above to file)
streams.push({
    level: 'info',
    type: 'file',
    path: 'logs/default.log'
});

// Log debug to file
streams.push({
    level: 'debug',
    type: 'file',
    path: 'logs/debug.log'
});

// if the process is running on a TTY (not piped), pretty print debug to stdout
if (process.stdout.isTTY) {
    // wrap stdout in this PrettyStream to print log lines in a
    // human-readable format
    var prettyStdOut = new PrettyStream();
    prettyStdOut.pipe(process.stdout);

    streams.push({
        level: 'debug',
        type: 'stream',
        stream: prettyStdOut
    });
}

// Create the default logger
var logger = module.exports = bunyan.createLogger({
    name: 'default',
    streams: streams,
    serializers: {
        req: requestSerializer,
        res: responseSerializer
    }
});

// Access logger (log express requests)
logger.accessLogger = function(env) {
    if (env === 'test') {
        return function(req, res, next) {
            // create a dummy logger that doesn't produce any output
            req.log = bunyan.createLogger({
                name: 'access',
                streams: []
            });
            next();
        };
    }

    return function(req, res, next) {
        // insert the current time into the request
        // (for later calculation of total reponse time).
        req._startAt = process.hrtime();
        req._startTime = new Date();

        // also, set response start time to undefined
        // later this will be set to the time of the start of the response
        res._startAt = undefined;
        res._startTime = undefined;

        // Insert a log object into the request.
        // The routes can use this object to log messages and all
        // of them can be traced to the request (by req_id).
        req.log = logger.child({ req_id: cuid.slug() });

        // record response start
        onHeaders(res, function() {
            this._startAt = process.hrtime();
            this._startTime = new Date();
        });

        // log when response finished
        onFinished(res, function() {
            var diff = process.hrtime(req._startAt);
            var mili = (diff[0] * 1000) + (diff[1] / 1000000);
            res.responseTime = mili.toFixed(1) + ' ms';

            var msg = req.method + ' ' + req.url + ' ' + res.statusCode;
            req.log.debug({req: req, res: res}, msg);
        });

        // Continue to the next handler
        next();
    };
};

// Serialize a request (remove unuseful information from requests,
// log only the necessary).
function requestSerializer(req) {
    if ((typeof req !== 'object') || (req === null)) {
        return req;
    }

    var headers = req.headers || {};

    return {
        method: req.method,
        url: req.url,
        headers: { // selective choice of headers (no cookies)
            Host: headers.host,
            Connection: headers.connection,
            Accept: headers.accept,
            Referer: headers.referer,
            'Cache-Control': headers['cache-control'],
            'User-Agent': headers['user-agent'],
            'Accept-Encoding': headers['accept-encoding'],
            'Accept-Language': headers['accept-language']
        },
        remoteAddress: req.ips[0] || req.ip || null
    };
}

// Serialize a reponse (remove unuseful information from responses
// and calculate the reponse time).
function responseSerializer(res) {
    if ((typeof res !== 'object') || (res === null)) {
        return res;
    }

    return {
        statusCode: res.statusCode,
        responseTime: res.responseTime
    };
}

package.json

0 → 100644
+64 −0
Original line number Diff line number Diff line
{
  "name": "simmc-api",
  "version": "0.1.0",
  "private": "true",
  "description": "SIMMC API",
  "homepage": "http://simmc.c3sl.ufpr.br",
  "author": "Centro de Computação Científica e Software Livre - C3SL <contato@c3sl.ufpr.br>",
  "license": "GPL-3.0",
  "repository": {
    "type": "git",
    "url": "https://gitlab.c3sl.ufpr.br/minicom/simmc.git"
  },
  "main": "server.js",
  "dependencies": {
    "app-module-path": "^1.0.4",
    "bcrypt": "^0.8.5",
    "body-parser": "^1.14.1",
    "bunyan": "^1.5.1",
    "bunyan-prettystream": "^0.1.3",
    "cuid": "^1.3.8",
    "express": "^4.13.3",
    "glob": "^5.0.15",
    "gulp": "^3.9.0",
    "gulp-raml": "^0.1.3",
    "gulp-rename": "^1.2.2",
    "gulp-util": "^3.0.7",
    "http-errors": "^1.3.1",
    "js-yaml": "^3.4.3",
    "jsep": "^0.3.0",
    "lodash": "^3.10.1",
    "mongoose": "^4.2.3",
    "morgan": "^1.6.1",
    "on-finished": "^2.3.0",
    "on-headers": "^1.0.1",
    "osprey": "0.2.0-beta.6",
    "osprey-method-handler": "0.8.1",
    "raml-parser": "^0.8.12",
    "raml2html": "^2.1.0",
    "statuses": "^1.2.1",
    "through2": "^2.0.0",
    "vinyl": "^1.1.0"
  },
  "devDependencies": {
    "chai": "^3.4.0",
    "chai-things": "^0.2.0",
    "gulp-coverage": "^0.3.38",
    "gulp-istanbul": "^0.10.2",
    "gulp-jscs": "^3.0.2",
    "gulp-jscs-stylish": "^1.2.1",
    "gulp-jshint": "^1.12.0",
    "gulp-mocha": "^2.1.3",
    "gulp-nodemon": "^2.0.4",
    "gulp-size": "^2.0.0",
    "gulp-util": "^3.0.7",
    "istanbul": "^0.4.0",
    "jshint-stylish": "^2.0.1",
    "mocha": "^2.3.3",
    "supertest": "^1.1.0"
  },
  "scripts": {
    "test": "gulp test",
    "start": "node server.js"
  }
}

server.js

0 → 100755
+78 −0
Original line number Diff line number Diff line
#!/usr/bin/env node
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

// Add the ./lib directory to require's search path to facilitate requiring
// libraries later on (avoiding the require('../../../../library.js') problem).
require('app-module-path').addPath(__dirname + '/lib');

// external libraries
var osprey = require('osprey');
var express = require('express');
var path = require('path');
var ramlParser = require('raml-parser');

// internal libraries (from ./lib)
var configParser = require('config-parser');
var errorHandler = require('error-handler');
var logger = require('logger');

var app = module.exports = express();

// parse and load configuration
var config = configParser.readFromFile(
        path.resolve(__dirname, './config/' + app.get('env') + '.yaml'));
app.set('port', process.env.PORT || config.get('port') || 3000);

// add an access logger middleware
app.use(logger.accessLogger(app.get('env')));

// include middlewares
app.use(require('./api/middleware/mongodb.js')(config));

// load router
var router = require('./api/router-v1.js');

// parse the RAML spec and load osprey middleware
ramlParser.loadFile(path.join(__dirname, 'api/specs/simmc-api-v1.raml'))
    .then(function(raml) {
        app.use('/api/v1',
            osprey.security(raml),
            osprey.server(raml),
            router,
            errorHandler);

        if (!module.parent) {
            var port = app.get('port');
            app.listen(port);

            if (app.get('env') === 'development') {
                logger.info('Server listening on port ' + port + '.');
            }
        }
        else {
            // signalize to the test suite that the server is ready to be tested
            app.ready = true;
        }
    },
    function(err) {
        logger.error('RAML Parsing Error: ' + err.message);
        process.exit(1);
    });
+219 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of simmc.
 *
 * simmc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * simmc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with simmc.  If not, see <http://www.gnu.org/licenses/>.
 */

/* global server:false */
/* jscs:disable maximumLineLength */
/* jshint expr:true */

var request = require('supertest');
var chai = require('chai');
var expect = chai.expect;
chai.use(require('chai-things'));

var path = require('path');
var mongoose = require('mongoose');
var fixtures = require('fixtures');

describe('locations api', function() {
    before(function(done) {
        var Location = mongoose.model('Location');

        fixtures.load(Location,
            path.resolve(__dirname, '../../fixtures/locations.json'), done);
    });

    it('should return the 10 first locations ordered by id', function(done) {
        request(server)
            .get('/api/v1/locations')
            .expect('Content-Type', /json/)
            .expect(200)
            .expect(function(res) {
                expect(res.body).to.be.an('array');
                expect(res.body).to.have.length(10);
            })
            .end(done);
    });

    it('should return 35 locations per page when requested', function(done) {
        request(server)
            .get('/api/v1/locations?per_page=35')
            .expect('Content-Type', /json/)
            .expect(200)
            .expect(function(res) {
                expect(res.body).to.be.an('array');
                expect(res.body).to.have.length(35);
            })
            .end(done);
    });

    it('should return locations filtered by administrative level', function(done) {
        request(server)
            .get('/api/v1/locations?filters=adminLevel%3D%3D8')
            .expect('Content-Type', /json/)
            .expect(200)
            .expect(function(res) {
                expect(res.body).to.be.an('array');
                expect(res.body).to.have.length(10);
                expect(res.body).all.to.have.property('adminLevel', 8);
            })
            .end(done);
    });

    it('should return locations filtered by administrative level and id number', function(done) {
        request(server)
            .get('/api/v1/locations?filters=adminLevel%3D%3D4%26id%3E%3D30%26id%3C40')
            .expect('Content-Type', /json/)
            .expect(200)
            .expect(function(res) {
                expect(res.body).to.be.an('array');
                expect(res.body).to.not.