Ejemplo de Express.js y Mongoose:construcción de HackHall

Nota: Este texto forma parte de la Guía de Express.js:el libro completo sobre Express.js.

El proyecto HackHall se escribió utilizando Backbone.js y Underscore para la aplicación de front-end, y Express.js, MongoDB a través de Mongoose para el servidor API REST de back-end.

Ejemplo :El código fuente de HackHall está en el repositorio público de GitHub.

Se puede acceder a la demostración en vivo en hackhall.com, ya sea con AngelList o correo electrónico precargado ([email protected]) y contraseña (1).

Qué es HackHall

HackHall (ex-Accelerator.IO) es una red social de código abierto y una herramienta de colaboración solo para invitados para piratas informáticos, hipsters, empresarios y piratas (es broma). HackHall es similar a Reddit, más Hacker News, más grupos de Facebook con selección.

El proyecto HackHall se encuentra en su etapa inicial y es aproximadamente una versión beta. Planeamos ampliar la base de código en el futuro y atraer a más personas para compartir habilidades, sabiduría y pasión por la programación.

En este capítulo, cubriremos la versión 1.0 que tiene:

  • OAuth 1.0 con módulos OAuth y API AngelList
  • Autenticación de correo electrónico y contraseña
  • Modelos y esquemas de Mongoose
  • Estructura Express.js con rutas en módulos
  • API REST de JSON
  • Manejo de errores de Express.js
  • Aplicación Front-end Client Backbone.js (para obtener más información sobre Backbone.js, descargue o lea en línea nuestros tutoriales de creación rápida de prototipos con JS)
  • Variables ambientales con .env de Foreman
  • TDD con moca
  • Configuración básica de Makefile

Ejecutar HackHall

Para obtener el código fuente, puede navegar hasta hackhall carpeta o clon de GitHub:

$ git clone [email protected]:azat-co/hackhall
$ git checkout 1.0
$ npm install

Si planea probar una AngelList (opcional), HackHall está usando la configuración de Heroku y Foreman para las claves API de AngelList almacenándolas en variables ambientales, por lo que debemos agregar .evn archivo como este (abajo hay valores falsos):

ANGELLIST_CLIENT_ID=254C0335-5F9A-4607-87C0
ANGELLIST_CLIENT_SECRET=99F5C1AC-C5F7-44E6-81A1-8DF4FC42B8D9

Las claves se pueden obtener en angel.co/api después de que alguien cree y registre su aplicación AngelList.

Descargue e instale MongoDB si aún no lo tiene. Las bases de datos y las bibliotecas de terceros están fuera del alcance de este libro. Sin embargo, puede encontrar suficientes materiales en línea y en Prototipos rápidos con JS.

Para iniciar el servidor MongoDB, abra una nueva ventana de terminal y ejecute:

$ mongod

Vuelva a la carpeta del proyecto y ejecute:

$ foreman start

Después de que MongoDB se ejecuta en localhost con el puerto predeterminado 27017, es posible inicializar la base de datos hackhall con el usuario administrador predeterminado ejecutando seed.js secuencia de comandos mongo:

[Nota al margen]

Leer publicaciones de blog es bueno, pero ver cursos en video es aún mejor porque son más atractivos.

Muchos desarrolladores se quejaron de la falta de material de video de calidad asequible en Node. Es una distracción ver videos de YouTube y una locura pagar $ 500 por un curso de video de Node.

Visite Node University, que tiene cursos de video GRATUITOS en Node:node.university.

[Fin de la nota al margen]

$ mongo localhost:27017/hackhall seed.js

Siéntase libre de modificar seed.js a tu gusto (¡ojo que borra todos los datos anteriores!):

db.dropDatabase();
var seedUser ={
  firstName:'Azat',
  lastName:"Mardanov", 
  displayName:"Azat Mardanov",
  password:'1',
  email:'[email protected]',
  role:'admin',
  approved: true, 
  admin: true
};
db.users.save(seedUser);

Si abre su navegador en http://localhost:5000, debería ver la pantalla de inicio de sesión.

Introduce usuario y contraseña para entrar (los del seed.js archivo).

Después de una autenticación exitosa, los usuarios son redirigidos a la página de Publicaciones:

donde pueden crear una publicación (por ejemplo, una pregunta):

Guarda la publicación:

Me gusta publicaciones:

Visita los perfiles de otros usuarios:

Si tienen derechos de administrador, los usuarios pueden aprobar candidatos:

y administrar su cuenta en la página de perfil:

Estructura

Esto es lo que tiene cada una de las carpetas y archivos:

  • /api :rutas compartidas por la aplicación
  • /models :Modelos de mangosta
  • /public :aplicación Backbone, archivos estáticos como JavaScript front-end, CSS, HTML
  • /routes :rutas API REST
  • /tests :Pruebas de moca
  • .gitignore :lista de archivos que git debería ignorar
  • Makefile :crea un archivo para ejecutar pruebas
  • Procfile :Archivo de pila de cedro necesario para la implementación de Heroku
  • package.json :dependencias de NPM y metadatos de HackHall
  • readme.md :descripción
  • server.js :archivo principal del servidor HackHall

Aplicación Express.js

Vayamos directamente al server.js y aprenda cómo se implementa. En primer lugar, declaramos dependencias:

var express = require('express'),
  routes = require('./routes'),
  http = require('http'),
  util = require('util'),
  oauth = require('oauth'),
  querystring = require('querystring');

Luego, inicializamos la aplicación y configuramos los middlewares. El process.env.PORT se completa con Heroku y, en caso de una configuración local, recurra a 3000.

var app = express();
app.configure(function(){
  app.set('port', process.env.PORT || 3000  );
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());

Los valores pasados ​​a cookieParser y los middlewares de sesión son necesarios para la autenticación. Obviamente, se supone que los secretos de sesión son privados:

  app.use(express.cookieParser('asd;lfkajs;ldfkj'));
  app.use(express.session({
    secret: '<h1>WHEEYEEE</h1>',
    key: 'sid',
    cookie: {
      secret: true,
      expires: false
    }
  }));

Así es como servimos nuestra aplicación Backbone.js de cliente front-end y otros archivos estáticos como CSS:

  app.use(express.static(__dirname + '/public'));
  app.use(app.router);
});

El manejo de errores se divide en tres funciones con clientErrorHandler dedicado a las solicitudes AJAX/XHR de la aplicación Backbone.js (responde con JSON):

app.configure(function() {
  app.use(logErrors);
  app.use(clientErrorHandler);
  app.use(errorHandler);
});

function logErrors(err, req, res, next) {
  console.error('logErrors', err.toString());
  next(err);
}

function clientErrorHandler(err, req, res, next) {
  console.error('clientErrors ', err.toString());
  res.send(500, { error: err.toString()});
  if (req.xhr) {
    console.error(err);
    res.send(500, { error: err.toString()});
  } else {
    next(err);
  }
}

function errorHandler(err, req, res, next) {
  console.error('lastErrors ', err.toString());
  res.send(500, {error: err.toString()});
}

De la misma manera que determinamos process.env.PORT y recurrimos al valor de configuración local 3000, hacemos algo similar con la cadena de conexión MongoDB:

var dbUrl = process.env.MONGOHQ_URL
  || 'mongodb://@127.0.0.1:27017/hackhall';
var mongoose = require('mongoose');
var connection = mongoose.createConnection(dbUrl);
connection.on('error', console.error.bind(console,
  'connection error:'));

En algún momento es una buena idea iniciar sesión en el evento de apertura de conexión:

connection.once('open', function () {
  console.info('connected to database')
});

Los modelos Mongoose viven en el models carpeta:

var models = require('./models');

Este middleware proporcionará acceso a dos colecciones dentro de nuestros métodos de rutas:

function db (req, res, next) {
  req.db = {
    User: connection.model('User', models.User, 'users'),
    Post: connection.model('Post', models.Post, 'posts')
  };
  return next();
}

Solo un nuevo nombre para las funciones de autenticación importadas:

checkUser = routes.main.checkUser;
checkAdmin = routes.main.checkAdmin;
checkApplicant = routes.main.checkApplicant;

Rutas AngelList OAuth:

app.get('/auth/angellist', routes.auth.angelList);
app.get('/auth/angellist/callback',
  routes.auth.angelListCallback,
  routes.auth.angelListLogin,
  db,
  routes.users.findOrAddUser);

Principales rutas de aplicación, incluido api/profile devolver sesión de usuario si el usuario ha iniciado sesión:

//MAIN
app.get('/api/profile', checkUser, db, routes.main.profile);
app.del('/api/profile', checkUser, db, routes.main.delProfile);
app.post('/api/login', db, routes.main.login);
app.post('/api/logout', routes.main.logout);

Las solicitudes POST para crear usuarios y publicaciones:

//POSTS
app.get('/api/posts', checkUser, db, routes.posts.getPosts);
app.post('/api/posts', checkUser, db, routes.posts.add);
app.get('/api/posts/:id', checkUser, db, routes.posts.getPost);
app.put('/api/posts/:id', checkUser, db, routes.posts.updatePost);
app.del('/api/posts/:id', checkUser, db, routes.posts.del);

//USERS
app.get('/api/users', checkUser, db, routes.users.getUsers);
app.get('/api/users/:id', checkUser, db,routes.users.getUser);
app.post('/api/users', checkAdmin, db, routes.users.add);
app.put('/api/users/:id', checkAdmin, db, routes.users.update);
app.del('/api/users/:id', checkAdmin, db, routes.users.del);

Estas rutas son para nuevos miembros que aún no han sido aprobados:

//APPLICATION
app.post('/api/application',
  checkAdmin,
  db,
  routes.application.add);
app.put('/api/application',
  checkApplicant,
  db,
  routes.application.update);
app.get('/api/application',
  checkApplicant,
  db,
  routes.application.get);

La ruta de captura de todo lo demás:

app.get('*', function(req, res){
  res.send(404);
});

El require.main === module es un truco inteligente para determinar si este archivo se está ejecutando de forma independiente o como un módulo importado:

http.createServer(app);
if (require.main === module) {
  app.listen(app.get('port'), function(){
    console.info('Express server listening on port '
      + app.get('port'));
  });
}
else {
  console.info('Running app as a module')
  exports.app = app;
}

El código fuente completo para hackhall/server.js :

var express = require('express'),
  routes = require('./routes'),
  http = require('http'),
  util = require('util'),
  oauth = require('oauth'),
  querystring = require('querystring');

var app = express();
app.configure(function(){
  app.set('port', process.env.PORT || 3000  );
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser('asd;lfkajs;ldfkj'));
  app.use(express.session({
    secret: '<h1>WHEEYEEE</h1>',
    key: 'sid',
    cookie: {
      secret: true,
      expires: false      
    }
  }));
  // app.use(express.csrf());
  // app.use(function(req, res, next) {
    // res.locals.csrf = req.session._cstf;
    // return next();
  // });
  app.use(express.static(__dirname + '/public'));
  app.use(app.router);
});

app.configure(function() {
  app.use(logErrors);
  app.use(clientErrorHandler);
  app.use(errorHandler);
});

function logErrors(err, req, res, next) {
  console.error('logErrors', err.toString());
  next(err);
}

function clientErrorHandler(err, req, res, next) {
  console.error('clientErrors ', err.toString());
  res.send(500, { error: err.toString()});
  if (req.xhr) {
    console.error(err);
    res.send(500, { error: err.toString()});
  } else {
    next(err);
  }
}

function errorHandler(err, req, res, next) {
  console.error('lastErrors ', err.toString());  
  res.send(500, {error: err.toString()});
}

var dbUrl = process.env.MONGOHQ_URL || 'mongodb://@127.0.0.1:27017/hackhall';
var mongoose = require('mongoose');
var connection = mongoose.createConnection(dbUrl);
connection.on('error', console.error.bind(console, 'connection error:'));
connection.once('open', function () {
  console.info('connected to database')
});

var models = require('./models');
function db (req, res, next) {
  req.db = {
    User: connection.model('User', models.User, 'users'),
    Post: connection.model('Post', models.Post, 'posts')
  };
  return next();
}
checkUser = routes.main.checkUser;
checkAdmin = routes.main.checkAdmin;
checkApplicant = routes.main.checkApplicant;

app.get('/auth/angellist', routes.auth.angelList);
app.get('/auth/angellist/callback',
  routes.auth.angelListCallback, 
  routes.auth.angelListLogin,
  db, 
  routes.users.findOrAddUser);

//MAIN
app.get('/api/profile', checkUser, db, routes.main.profile);
app.del('/api/profile', checkUser, db, routes.main.delProfile);
app.post('/api/login', db, routes.main.login);
app.post('/api/logout', routes.main.logout);

//POSTS
app.get('/api/posts', checkUser, db, routes.posts.getPosts);
app.post('/api/posts', checkUser, db, routes.posts.add);
app.get('/api/posts/:id', checkUser, db, routes.posts.getPost);
app.put('/api/posts/:id', checkUser, db, routes.posts.updatePost);
app.del('/api/posts/:id', checkUser, db, routes.posts.del);

//USERS
app.get('/api/users', checkUser, db, routes.users.getUsers);
app.get('/api/users/:id', checkUser, db,routes.users.getUser);
app.post('/api/users', checkAdmin, db, routes.users.add);
app.put('/api/users/:id', checkAdmin, db, routes.users.update);
app.del('/api/users/:id', checkAdmin, db, routes.users.del);

//APPLICATION 
app.post('/api/application', checkAdmin, db, routes.application.add); 
app.put('/api/application', checkApplicant, db, routes.application.update);
app.get('/api/application', checkApplicant, db, routes.application.get);

app.get('*', function(req, res){
  res.send(404);
});

http.createServer(app);
if (require.main === module) {
  app.listen(app.get('port'), function(){
    console.info('Express server listening on port ' + app.get('port'));
  });
}
else {
  console.info('Running app as a module')
  exports.app = app;
}



Rutas

Las rutas HackHall residen en hackhall/routes carpeta y se agrupan en varios módulos:

  • hackhall/routes/index.js :puente entre server.js y otras rutas en la carpeta
  • hackhall/routes/auth.js :rutas que manejan OAuth dance con AngelList API
  • hackhall/routes/main.js :inicio de sesión, cierre de sesión y otras rutas
  • hackhall/routes/users.js :rutas relacionadas con la API REST de los usuarios
  • hackhall/routes/application.js :presentación de la solicitud para convertirse en usuario
  • hackhall/routes/posts.js :rutas relacionadas con publicaciones REST API

index.js

Lleguemos a hackhall/routes/index.js donde incluimos otros módulos:

exports.posts = require('./posts');
exports.main = require('./main');
exports.users = require('./users');
exports.application = require('./application');
exports.auth = require('./auth');

autorización.js

En este módulo, manejaremos OAuth dance con la API AngelList. Para hacerlo, tendremos que confiar en https biblioteca:

var https = require('https');

El ID de cliente de AngelList API y el secreto del cliente se obtienen en el sitio web angel.co/api y se almacenan en las variables de entorno:

var angelListClientId = process.env.ANGELLIST_CLIENT_ID;
var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET;

El método redirigirá a los usuarios al sitio web angel.co para la autenticación:

exports.angelList = function(req, res) {
  res.redirect('https://angel.co/api/oauth/authorize?client_id=' + angelListClientId + '&scope=email&response_type=code');
}

Después de que los usuarios permiten que nuestra aplicación acceda a su información, AngelList los envía de vuelta a esta ruta para que hagamos una nueva solicitud (HTTPS) para recuperar el token:

exports.angelListCallback = function(req, res, next) {
  var token;
  var buf = '';
  var data;
  // console.log('/api/oauth/token?client_id='
  //+ angelListClientId
  //+ '&client_secret='
  //+ angelListClientSecret
  //+ '&code='
  //+ req.query.code
  //+ '&grant_type=authorization_code');
  var angelReq = https.request({
      host: 'angel.co',
      path: '/api/oauth/token?client_id='
        + angelListClientId
        + '&client_secret='
        + angelListClientSecret
        + '&code='
        + req.query.code
        + '&grant_type=authorization_code',
      port: 443,
      method: 'POST',
      headers: {
        'content-length': 0
      }
    },
    function(angelRes) {
      angelRes.on('data', function(buffer) {
        buf += buffer;
      });
      angelRes.on('end', function() {
        try {
          data = JSON.parse(buf.toString('utf-8'));
        } catch (e) {
          if (e) return res.send(e);
        }
        if (!data || !data.access_token) return res.send(500);
        token = data.access_token;
        req.session.angelListAccessToken = token;
        if (token) next();
        else res.send(500);
      });
    });
  angelReq.end();
  angelReq.on('error', function(e) {
    console.error(e);
    next(e);
  });
}

Llame directamente a la API AngleList con el token del middleware anterior para obtener información del usuario:

exports.angelListLogin = function(req, res, next) {
  token = req.session.angelListAccessToken;
  httpsRequest = https.request({
      host: 'api.angel.co',
      path: '/1/me?access_token=' + token,
      port: 443,
      method: 'GET'
    },
    function(httpsResponse) {
      httpsResponse.on('data', function(buffer) {
        data = JSON.parse(buffer.toString('utf-8'));
        if (data) {
          req.angelProfile = data;
          next();
        }
      });
    }
  );
  httpsRequest.end();
  httpsRequest.on('error', function(e) {
    console.error(e);
  });
};

El código fuente completo de hackhall/routes/auth.js archivos:

var https = require('https');

var angelListClientId = process.env.ANGELLIST_CLIENT_ID;
var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET;

exports.angelList = function(req, res) {
  res.redirect('https://angel.co/api/oauth/authorize?client_id=' + angelListClientId + '&scope=email&response_type=code');
}
exports.angelListCallback = function(req, res, next) {
  var token;
  var buf = '';
  var data;
  // console.log('/api/oauth/token?client_id=' + angelListClientId + '&client_secret=' + angelListClientSecret + '&code=' + req.query.code + '&grant_type=authorization_code');
  var angelReq = https.request({
      host: 'angel.co',
      path: '/api/oauth/token?client_id=' + angelListClientId + '&client_secret=' + angelListClientSecret + '&code=' + req.query.code + '&grant_type=authorization_code',
      port: 443,
      method: 'POST',
      headers: {
        'content-length': 0
      }
    },
    function(angelRes) {

      angelRes.on('data', function(buffer) {
        buf += buffer;
      });
      angelRes.on('end', function() {
        try {
          data = JSON.parse(buf.toString('utf-8'));
        } catch (e) {
          if (e) return res.send(e);
        }
        if (!data || !data.access_token) return res.send(500);
        token = data.access_token;
        req.session.angelListAccessToken = token;
        if (token) next();
        else res.send(500);
      });
    });
  angelReq.end();
  angelReq.on('error', function(e) {
    console.error(e);
    next(e);
  });
}
exports.angelListLogin = function(req, res, next) {
  token = req.session.angelListAccessToken;
  httpsRequest = https.request({
      host: 'api.angel.co',
      path: '/1/me?access_token=' + token,
      port: 443,
      method: 'GET'
    },
    function(httpsResponse) {
      httpsResponse.on('data', function(buffer) {
        data = JSON.parse(buffer.toString('utf-8'));
        if (data) {
          req.angelProfile = data;
          next();
        }
      });
    }
  );
  httpsRequest.end();
  httpsRequest.on('error', function(e) {
    console.error(e);
  });
};

principal.js

El hackhall/routes/main.js El archivo también puede ser interesante.

El checkAdmin() La función realiza la autenticación para los privilegios de administrador. Si el objeto de la sesión no tiene el indicador adecuado, llamamos a Express.js next() función con un objeto de error:

exports.checkAdmin = function(request, response, next) {
  if (request.session
    && request.session.auth
    && request.session.userId
    && request.session.admin) {
    console.info('Access ADMIN: ' + request.session.userId);
    return next();
  } else {
    next('User is not an administrator.');
  }
};

De manera similar, podemos verificar solo el usuario sin verificar los derechos de administrador:

exports.checkUser = function(req, res, next) {
  if (req.session && req.session.auth && req.session.userId
    && (req.session.user.approved || req.session.admin)) {
    console.info('Access USER: ' + req.session.userId);
    return next();
  } else {
    next('User is not logged in.');
  }
};

La aplicación es solo un objeto de usuario no aprobado y también podemos comprobarlo:

exports.checkApplicant = function(req, res, next) {
  if (req.session && req.session.auth && req.session.userId
    && (!req.session.user.approved || req.session.admin)) {
    console.info('Access USER: ' + req.session.userId);
    return next();
  } else {
    next('User is not logged in.');
  }
};

En la función de inicio de sesión, buscamos coincidencias de correo electrónico y contraseña en la base de datos. En caso de éxito, almacenamos el objeto de usuario en la sesión y procedemos; de lo contrario, la solicitud falla:

exports.login = function(req, res, next) {
  req.db.User.findOne({
      email: req.body.email,
      password: req.body.password
    },
    null, {
      safe: true
    },
    function(err, user) {
      if (err) return next(err);
      if (user) {
        req.session.auth = true;
        req.session.userId = user._id.toHexString();
        req.session.user = user;
        if (user.admin) {
          req.session.admin = true;
        }
        console.info('Login USER: ' + req.session.userId);
        res.json(200, {
          msg: 'Authorized'
        });
      } else {
        next(new Error('User is not found.'));
      }
    });
};

El proceso de cierre de sesión elimina cualquier información de la sesión:

exports.logout = function(req, res) {
  console.info('Logout USER: ' + req.session.userId);
  req.session.destroy(function(error) {
    if (!error) {
      res.send({
        msg: 'Logged out'
      });
    }
  });
};

Esta ruta se utiliza tanto para la página de perfil como para Backbone.js para la autenticación de usuarios:

exports.profile = function(req, res, next) {
  req.db.User.findById(req.session.userId, 'firstName lastName'
      + 'displayName headline photoUrl admin'
      + 'approved banned role angelUrl twitterUrl'
      + 'facebookUrl linkedinUrl githubUrl', function(err, obj) {
    if (err) next(err);
    if (!obj) next(new Error('User is not found'));
    req.db.Post.find({
      author: {
        id: obj._id,
        name: obj.displayName
      }
    }, null, {
      sort: {
        'created': -1
      }
    }, function(err, list) {
      if (err) next(err);
      obj.posts.own = list || [];
      req.db.Post.find({
        likes: obj._id
      }, null, {
        sort: {
          'created': -1
        }

Esta lógica encuentra publicaciones y comentarios realizados por el usuario:

      }, function(err, list) {
        if (err) next(err);
        obj.posts.likes = list || [];
        req.db.Post.find({
          watches: obj._id
        }, null, {
          sort: {
            'created': -1
          }
        }, function(err, list) {
          if (err) next(err);
          obj.posts.watches = list || [];
          req.db.Post.find({
            'comments.author.id': obj._id
          }, null, {
            sort: {
              'created': -1
            }
          }, function(err, list) {
            if (err) next(err);
            obj.posts.comments = [];
            list.forEach(function(value, key, list) {
              obj.posts.comments.push(
                value.comments.filter(
                  function(el, i, arr) {
                    return (el.author.id.toString() == obj._id.toString());
                  }
                )
              );
            });
            res.json(200, obj);
          });
        });
      });
    });
  });
};

Es importante permitir que los usuarios eliminen sus perfiles:

exports.delProfile = function(req, res, next) {
  console.log('del profile');
  console.log(req.session.userId);
  req.db.User.findByIdAndRemove(req.session.user._id, {},
    function(err, obj) {
      if (err) next(err);
      req.session.destroy(function(error) {
        if (err) {
          next(err)
        }
      });
      res.json(200, obj);
    }
  );
};

El código fuente completo de hackhall/routes/main.js archivos:

exports.checkAdmin = function(request, response, next) {
  if (request.session && request.session.auth && request.session.userId && request.session.admin) {
    console.info('Access ADMIN: ' + request.session.userId);
    return next();
  } else {
    next('User is not an administrator.');
  }
};

exports.checkUser = function(req, res, next) {
  if (req.session && req.session.auth && req.session.userId && (req.session.user.approved || req.session.admin)) {
    console.info('Access USER: ' + req.session.userId);
    return next();
  } else {
    next('User is not logged in.');
  }
};

exports.checkApplicant = function(req, res, next) {
  if (req.session && req.session.auth && req.session.userId && (!req.session.user.approved || req.session.admin)) {
    console.info('Access USER: ' + req.session.userId);
    return next();
  } else {
    next('User is not logged in.');
  }
};

exports.login = function(req, res, next) {
  req.db.User.findOne({
      email: req.body.email,
      password: req.body.password
    },
    null, {
      safe: true
    },
    function(err, user) {
      if (err) return next(err);
      if (user) {
        req.session.auth = true;
        req.session.userId = user._id.toHexString();
        req.session.user = user;
        if (user.admin) {
          req.session.admin = true;
        }
        console.info('Login USER: ' + req.session.userId);
        res.json(200, {
          msg: 'Authorized'
        });
      } else {
        next(new Error('User is not found.'));
      }
    });
};

exports.logout = function(req, res) {
  console.info('Logout USER: ' + req.session.userId);
  req.session.destroy(function(error) {
    if (!error) {
      res.send({
        msg: 'Logged out'
      });
    }
  });
};

exports.profile = function(req, res, next) {
  req.db.User.findById(req.session.userId, 'firstName lastName displayName headline photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl', function(err, obj) {
    if (err) next(err);
    if (!obj) next(new Error('User is not found'));
    req.db.Post.find({
      author: {
        id: obj._id,
        name: obj.displayName
      }
    }, null, {
      sort: {
        'created': -1
      }
    }, function(err, list) {
      if (err) next(err);
      obj.posts.own = list || [];
      req.db.Post.find({
        likes: obj._id
      }, null, {
        sort: {
          'created': -1
        }
      }, function(err, list) {
        if (err) next(err);
        obj.posts.likes = list || [];
        req.db.Post.find({
          watches: obj._id
        }, null, {
          sort: {
            'created': -1
          }
        }, function(err, list) {
          if (err) next(err);
          obj.posts.watches = list || [];
          req.db.Post.find({
            'comments.author.id': obj._id
          }, null, {
            sort: {
              'created': -1
            }
          }, function(err, list) {
            if (err) next(err);
            obj.posts.comments = [];
            list.forEach(function(value, key, list) {
              obj.posts.comments.push(value.comments.filter(function(el, i, arr) {
                return (el.author.id.toString() == obj._id.toString());
              }));
            });
            res.json(200, obj);
          });
        });
      });
    });
  });
};

exports.delProfile = function(req, res, next) {
  console.log('del profile');
  console.log(req.session.userId);
  req.db.User.findByIdAndRemove(req.session.user._id, {}, function(err, obj) {
    if (err) next(err);
    req.session.destroy(function(error) {
      if (err) {
        next(err)
      }
    });
    res.json(200, obj);
  });
};

usuarios.js

El código fuente completo para hackhall/routes/users.js archivos:

objectId = require('mongodb').ObjectID;

exports.getUsers = function(req, res, next) {
  if (req.session.auth && req.session.userId) {
    req.db.User.find({}, 'firstName lastName displayName headline photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl', function(err, list) {
      if (err) next(err);
      res.json(200, list);
    });
  } else {
    next('User is not recognized.')
  }
}

exports.getUser = function(req, res, next) {
  req.db.User.findById(req.params.id, 'firstName lastName displayName headline photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl', function(err, obj) {
    if (err) next(err);
    if (!obj) next(new Error('User is not found'));
    req.db.Post.find({
      author: {
        id: obj._id,
        name: obj.displayName
      }
    }, null, {
      sort: {
        'created': -1
      }
    }, function(err, list) {
      if (err) next(err);
      obj.posts.own = list || [];
      req.db.Post.find({
        likes: obj._id
      }, null, {
        sort: {
          'created': -1
        }
      }, function(err, list) {
        if (err) next(err);
        obj.posts.likes = list || [];
        req.db.Post.find({
          watches: obj._id
        }, null, {
          sort: {
            'created': -1
          }
        }, function(err, list) {
          if (err) next(err);
          obj.posts.watches = list || [];
          req.db.Post.find({
            'comments.author.id': obj._id
          }, null, {
            sort: {
              'created': -1
            }
          }, function(err, list) {
            if (err) next(err);
            obj.posts.comments = [];
            list.forEach(function(value, key, list) {
              obj.posts.comments.push(value.comments.filter(function(el, i, arr) {
                return (el.author.id.toString() == obj._id.toString());
              }));
            });
            res.json(200, obj);
          });
        });
      });
    });
  });
};

exports.add = function(req, res, next) {
  var user = new req.db.User(req.body);
  user.save(function(err) {
    if (err) next(err);
    res.json(user);
  });
};

exports.update = function(req, res, next) {
  var obj = req.body;
  obj.updated = new Date();
  delete obj._id;
  req.db.User.findByIdAndUpdate(req.params.id, {
    $set: obj
  }, {
    new: true
  }, function(err, obj) {
    if (err) next(err);
    res.json(200, obj);
  });
};

exports.del = function(req, res, next) {
  req.db.User.findByIdAndRemove(req.params.id, function(err, obj) {
    if (err) next(err);
    res.json(200, obj);
  });
};

exports.findOrAddUser = function(req, res, next) {
  data = req.angelProfile;
  req.db.User.findOne({
    angelListId: data.id
  }, function(err, obj) {
    console.log('angelListLogin4');
    if (err) next(err);
    console.warn(obj);
    if (!obj) {
      req.db.User.create({
        angelListId: data.id,
        angelToken: token,
        angelListProfile: data,
        email: data.email,
        firstName: data.name.split(' ')[0],
        lastName: data.name.split(' ')[1],
        displayName: data.name,
        headline: data.bio,
        photoUrl: data.image,
        angelUrl: data.angellist_url,
        twitterUrl: data.twitter_url,
        facebookUrl: data.facebook_url,
        linkedinUrl: data.linkedin_url,
        githubUrl: data.github_url
      }, function(err, obj) { //remember the scope of variables!
        if (err) next(err);
        console.log(obj);
        req.session.auth = true;
        req.session.userId = obj._id;
        req.session.user = obj;
        req.session.admin = false; //assing regular user role by default                  
        res.redirect('/#application');
        // }
      });
    } else { //user is in the database
      req.session.auth = true;
      req.session.userId = obj._id;
      req.session.user = obj;
      req.session.admin = obj.admin; //false; //assing regular user role by default
      if (obj.approved) {
        res.redirect('/#posts');
      } else {
        res.redirect('/#application');
      }
    }
  })
}

aplicaciones.js

En la versión actual, enviar y aprobar una solicitud no generará una notificación por correo electrónico. Por lo tanto, los usuarios deben regresar al sitio web para verificar su estado.

Simplemente agregue un objeto de usuario (con aprobado=falso por defecto) a la base de datos:

exports.add = function(req, res, next) {
  req.db.User.create({
    firstName: req.body.firstName,
    lastName: req.body.lastName,
    displayName: req.body.displayName,
    headline: req.body.headline,
    photoUrl: req.body.photoUrl,
    password: req.body.password,
    email: req.body.email,
    angelList: {
      blah: 'blah'
    },
    angelUrl: req.body.angelUrl,
    twitterUrl: req.body.twitterUrl,
    facebookUrl: req.body.facebookUrl,
    linkedinUrl: req.body.linkedinUrl,
    githubUrl: req.body.githubUrl
  }, function(err, obj) {
    if (err) next(err);
    if (!obj) next('Cannot create.')
    res.json(200, obj);
  })
};

Deje que los usuarios actualicen la información en sus aplicaciones:

exports.update = function(req, res, next) {
  var data = {};
  Object.keys(req.body).forEach(function(k) {
    if (req.body[k]) {
      data[k] = req.body[k];
    }
  });
  delete data._id;
  req.db.User.findByIdAndUpdate(req.session.user._id, {
    $set: data
  }, function(err, obj) {
    if (err) next(err);
    if (!obj) next('Cannot save.')
    res.json(200, obj);
  });
};

Seleccione un objeto en particular con el get() función:

exports.get = function(req, res, next) {
  req.db.User.findById(req.session.user._id,
    'firstName lastName photoUrl headline displayName'
      + 'angelUrl facebookUrl twitterUrl linkedinUrl'
      + 'githubUrl', {}, function(err, obj) {
      if (err) next(err);
      if (!obj) next('cannot find');
      res.json(200, obj);
    })
};

El código fuente completo de hackhall/routes/applications.js archivos:

***Error:** File "applications.js" does not exist at this path*

publicaciones.js

El último módulo de rutas que dividimos es hackhall/routes/posts.js . Se encarga de agregar, editar y eliminar publicaciones, así como de comentar, ver y dar me gusta.

Usamos ID de objeto para la conversión de cadenas HEX a objetos adecuados:

objectId = require('mongodb').ObjectID;

La coloración es agradable para iniciar sesión pero, por supuesto, es opcional. Lo logramos con secuencias de escape:

var red, blue, reset;
red   = '\u001b[31m';
blue  = '\u001b[34m';
reset = '\u001b[0m';
console.log(red + 'This is red' + reset + ' while ' + blue + ' this is blue' + reset);

Los valores predeterminados para la paginación de las publicaciones:

var LIMIT = 10;
var SKIP = 0;

El add() función maneja la creación de nuevas publicaciones:

exports.add = function(req, res, next) {
  if (req.body) {
    req.db.Post.create({
      title: req.body.title,
      text: req.body.text || null,
      url: req.body.url || null,
      author: {
        id: req.session.user._id,
        name: req.session.user.displayName
      }
    }, function(err, docs) {
      if (err) {
        console.error(err);
        next(err);
      } else {
        res.json(200, docs);
      }

    });
  } else {
    next(new Error('No data'));
  }
};

Para recuperar la lista de publicaciones:


exports.getPosts = function(req, res, next) {
  var limit = req.query.limit || LIMIT;
  var skip = req.query.skip || SKIP;
  req.db.Post.find({}, null, {
    limit: limit,
    skip: skip,
    sort: {
      '_id': -1
    }
  }, function(err, obj) {
    if (!obj) next('There are not posts.');
    obj.forEach(function(item, i, list) {
      if (req.session.user.admin) {
        item.admin = true;
      } else {
        item.admin = false;
      }
      if (item.author.id == req.session.userId) {
        item.own = true;
      } else {
        item.own = false;
      }
      if (item.likes
        && item.likes.indexOf(req.session.userId) > -1) {
        item.like = true;
      } else {
        item.like = false;
      }
      if (item.watches
        && item.watches.indexOf(req.session.userId) > -1) {
        item.watch = true;
      } else {
        item.watch = false;
      }
    });
    var body = {};
    body.limit = limit;
    body.skip = skip;
    body.posts = obj;
    req.db.Post.count({}, function(err, total) {
      if (err) next(err);
      body.total = total;
      res.json(200, body);
    });
  });
};

Para la página de publicación individual, necesitamos el getPost() método:

exports.getPost = function(req, res, next) {
  if (req.params.id) {
    req.db.Post.findById(req.params.id, {
      title: true,
      text: true,
      url: true,
      author: true,
      comments: true,
      watches: true,
      likes: true
    }, function(err, obj) {
      if (err) next(err);
      if (!obj) {
        next('Nothing is found.');
      } else {
        res.json(200, obj);
      }
    });
  } else {
    next('No post id');
  }
};

El del() La función elimina publicaciones específicas de la base de datos. El findById() y remove() En este fragmento se utilizan métodos de Mongoose. Sin embargo, se puede lograr lo mismo con solo remove() .

exports.del = function(req, res, next) {
  req.db.Post.findById(req.params.id, function(err, obj) {
    if (err) next(err);
    if (req.session.admin || req.session.userId === obj.author.id) {
      obj.remove();
      res.json(200, obj);
    } else {
      next('User is not authorized to delete post.');
    }
  })
};

Para dar me gusta a la publicación, actualizamos el elemento de la publicación anteponiendo post.likes array con el ID del usuario:

function likePost(req, res, next) {
  req.db.Post.findByIdAndUpdate(req.body._id, {
    $push: {
      likes: req.session.userId
    }
  }, {}, function(err, obj) {
    if (err) {
      next(err);
    } else {
      res.json(200, obj);
    }
  });
};

Asimismo, cuando un usuario realiza la acción de vigilancia, el sistema agrega una nueva ID al post.watches matriz:

function watchPost(req, res, next) {
  req.db.Post.findByIdAndUpdate(req.body._id, {
    $push: {
      watches: req.session.userId
    }
  }, {}, function(err, obj) {
    if (err) next(err);
    else {
      res.json(200, obj);
    }
  });
};

El updatePost() es lo que llama a las funciones "me gusta" o "mirar" según el indicador de acción enviado con la solicitud. Además, el updatePost() procesa los cambios en la publicación y los comentarios:


exports.updatePost = function(req, res, next) {
  var anyAction = false;
  if (req.body._id && req.params.id) {
    if (req.body && req.body.action == 'like') {
      anyAction = true;
      likePost(req, res);
    }
    if (req.body && req.body.action == 'watch') {
      anyAction = true;
      watchPost(req, res);
    }
    if (req.body && req.body.action == 'comment'
      && req.body.comment && req.params.id) {
      anyAction = true;
      req.db.Post.findByIdAndUpdate(req.params.id, {
        $push: {
          comments: {
            author: {
              id: req.session.userId,
              name: req.session.user.displayName
            },
            text: req.body.comment
          }
        }
      }, {
        safe: true,
        new: true
      }, function(err, obj) {
        if (err) throw err;
        res.json(200, obj);
      });
    }
    if (req.session.auth && req.session.userId && req.body
      && req.body.action != 'comment' &&
      req.body.action != 'watch' && req.body != 'like' &&
      req.params.id && (req.body.author.id == req.session.user._id
      || req.session.user.admin)) {
      req.db.Post.findById(req.params.id, function(err, doc) {
        if (err) next(err);
        doc.title = req.body.title;
        doc.text = req.body.text || null;
        doc.url = req.body.url || null;
        doc.save(function(e, d) {
          if (e) next(e);
          res.json(200, d);
        });
      })
    } else {
      if (!anyAction) next('Something went wrong.');
    }

  } else {
    next('No post ID.');
  }
};

El código fuente completo del hackhall/routes/posts.js archivo:

objectId = require('mongodb').ObjectID;
var red, blue, reset;
red = '\u001b[31m';
blue = '\u001b[34m';
reset = '\u001b[0m';
console.log(red + 'This is red' + reset + ' while ' + blue + ' this is blue' + reset);

var LIMIT = 10;
var SKIP = 0;

exports.add = function(req, res, next) {
  if (req.body) {
    req.db.Post.create({
      title: req.body.title,
      text: req.body.text || null,
      url: req.body.url || null,
      author: {
        id: req.session.user._id,
        name: req.session.user.displayName
      }
    }, function(err, docs) {
      if (err) {
        console.error(err);
        next(err);
      } else {
        res.json(200, docs);
      }

    });
  } else {
    next(new Error('No data'));
  }
};

exports.getPosts = function(req, res, next) {
  var limit = req.query.limit || LIMIT;
  var skip = req.query.skip || SKIP;
  req.db.Post.find({}, null, {
    limit: limit,
    skip: skip,
    sort: {
      '_id': -1
    }
  }, function(err, obj) {
    if (!obj) next('There are not posts.');
    obj.forEach(function(item, i, list) {
      if (req.session.user.admin) {
        item.admin = true;
      } else {
        item.admin = false;
      }
      if (item.author.id == req.session.userId) {
        item.own = true;
      } else {
        item.own = false;
      }
      if (item.likes && item.likes.indexOf(req.session.userId) > -1) {
        item.like = true;
      } else {
        item.like = false;
      }
      if (item.watches && item.watches.indexOf(req.session.userId) > -1) {
        item.watch = true;
      } else {
        item.watch = false;
      }
    });
    var body = {};
    body.limit = limit;
    body.skip = skip;
    body.posts = obj;
    req.db.Post.count({}, function(err, total) {
      if (err) next(err);
      body.total = total;
      res.json(200, body);
    });
  });
};


exports.getPost = function(req, res, next) {
  if (req.params.id) {
    req.db.Post.findById(req.params.id, {
      title: true,
      text: true,
      url: true,
      author: true,
      comments: true,
      watches: true,
      likes: true
    }, function(err, obj) {
      if (err) next(err);
      if (!obj) {
        next('Nothing is found.');
      } else {
        res.json(200, obj);
      }
    });
  } else {
    next('No post id');
  }
};

exports.del = function(req, res, next) {
  req.db.Post.findById(req.params.id, function(err, obj) {
    if (err) next(err);
    if (req.session.admin || req.session.userId === obj.author.id) {
      obj.remove();
      res.json(200, obj);
    } else {
      next('User is not authorized to delete post.');
    }
  })
};

function likePost(req, res, next) {
  req.db.Post.findByIdAndUpdate(req.body._id, {
    $push: {
      likes: req.session.userId
    }
  }, {}, function(err, obj) {
    if (err) {
      next(err);
    } else {
      res.json(200, obj);
    }
  });
};

function watchPost(req, res, next) {
  req.db.Post.findByIdAndUpdate(req.body._id, {
    $push: {
      watches: req.session.userId
    }
  }, {}, function(err, obj) {
    if (err) next(err);
    else {
      res.json(200, obj);
    }
  });
};

exports.updatePost = function(req, res, next) {
  var anyAction = false;
  if (req.body._id && req.params.id) {
    if (req.body && req.body.action == 'like') {
      anyAction = true;
      likePost(req, res);
    }
    if (req.body && req.body.action == 'watch') {
      anyAction = true;
      watchPost(req, res);
    }
    if (req.body && req.body.action == 'comment' && req.body.comment && req.params.id) {
      anyAction = true;
      req.db.Post.findByIdAndUpdate(req.params.id, {
        $push: {
          comments: {
            author: {
              id: req.session.userId,
              name: req.session.user.displayName
            },
            text: req.body.comment
          }
        }
      }, {
        safe: true,
        new: true
      }, function(err, obj) {
        if (err) throw err;
        res.json(200, obj);
      });
    }
    if (req.session.auth && req.session.userId && req.body && req.body.action != 'comment' &&
      req.body.action != 'watch' && req.body != 'like' &&
      req.params.id && (req.body.author.id == req.session.user._id || req.session.user.admin)) {
      req.db.Post.findById(req.params.id, function(err, doc) {
        if (err) next(err);
        doc.title = req.body.title;
        doc.text = req.body.text || null;
        doc.url = req.body.url || null;
        doc.save(function(e, d) {
          if (e) next(e);
          res.json(200, d);
        });
      })
    } else {
      if (!anyAction) next('Something went wrong.');
    }

  } else {
    next('No post ID.');
  }
};

Modelos de mangosta

Idealmente, en una aplicación grande, dividiríamos cada modelo en un archivo separado. Ahora mismo en la aplicación HackHall, los tenemos todos en hackhall/models/index.js .

Como siempre, nuestras dependencias se ven mejor en la parte superior:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var roles = 'user staff mentor investor founder'.split(' ');

El modelo de publicación representa la publicación con sus Me gusta, comentarios y relojes.

exports.Post = new Schema ({
  title: {
    required: true,
    type: String,
    trim: true,
    // match: /^([[:alpha:][:space:][:punct:]]{1,100})$/
    match: /^([\w ,.!?]{1,100})$/
  },
  url: {
    type: String,
    trim: true,
    max: 1000
  },
  text: {
    type: String,
    trim: true,
    max: 2000
  },
  comments: [{
    text: {
      type: String,
      trim: true,
      max:2000
    },
    author: {
      id: {
        type: Schema.Types.ObjectId,
        ref: 'User'
      },
      name: String
    }
  }],
  watches: [{
    type: Schema.Types.ObjectId,
    ref: 'User'
  }],
  likes: [{
    type: Schema.Types.ObjectId,
    ref: 'User'
  }],
  author: {
    id: {
      type: Schema.Types.ObjectId,
      ref: 'User',
      required: true
    },
    name: {
      type: String,
      required: true
    }
  },
  created: {
    type: Date,
    default: Date.now,
    required: true
  },
  updated:  {
    type: Date,
    default: Date.now,
    required: true
  },
  own: Boolean,
  like: Boolean,
  watch: Boolean,
  admin: Boolean,
  action: String
});

El modelo de Usuario también puede servir como un objeto de aplicación (cuando approved=false ):

exports.User = new Schema({
  angelListId: String,
  angelListProfile: Schema.Types.Mixed,
  angelToken: String,
    firstName: {
    type: String,
    required: true,
    trim: true
  },
  lastName: {
    type: String,
    required: true,
    trim: true
  },
  displayName: {
    type: String,
    required: true,
    trim: true
  },
    password: String,
    email: {
    type: String,
    required: true,
    trim: true
  },
  role: {
    type:String,
    enum: roles,
    required: true,
    default: roles[0]
  },
  approved: {
    type: Boolean,
    default: false
  },
  banned: {
    type: Boolean,
    default: false
  },
  admin: {
    type: Boolean,
    default: false
  },
  headline: String,
  photoUrl: String,
  angelList: Schema.Types.Mixed,
  created: {
    type: Date,
    default: Date.now
  },
  updated:  {
    type: Date, default: Date.now
  },
  angelUrl: String,
  twitterUrl: String,
  facebookUrl: String,
  linkedinUrl: String,
  githubUrl: String,
  own: Boolean,
  posts: {
    own: [Schema.Types.Mixed],
    likes: [Schema.Types.Mixed],
    watches: [Schema.Types.Mixed],
    comments: [Schema.Types.Mixed]
  }
});

El código fuente completo para hackhall/models/index.js :

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var roles = 'user staff mentor investor founder'.split(' ');

exports.Post = new Schema ({
  title: {
    required: true,
    type: String,
    trim: true,
    // match: /^([[:alpha:][:space:][:punct:]]{1,100})$/
    match: /^([\w ,.!?]{1,100})$/
  },
  url: {
    type: String,
    trim: true,
    max: 1000
  },
  text: {
    type: String,
    trim: true,
    max: 2000
  },
  comments: [{
    text: {
      type: String,
      trim: true,
      max:2000
    },
    author: {
      id: { 
        type: Schema.Types.ObjectId, 
        ref: 'User' 
      },
      name: String
    }
  }],
  watches: [{ 
    type: Schema.Types.ObjectId, 
    ref: 'User' 
  }],
  likes: [{
    type: Schema.Types.ObjectId, 
    ref: 'User'
  }],
  author: {
    id: { 
      type: Schema.Types.ObjectId, 
      ref: 'User',
      required: true
    },
    name: {
      type: String,
      required: true
    }
  },
  created: { 
    type: Date, 
    default: Date.now,
    required: true
  },
  updated:  { 
    type: Date, 
    default: Date.now, 
    required: true
  },  
  own: Boolean,
  like: Boolean,
  watch: Boolean,
  admin: Boolean,
  action: String  
});

exports.User = new Schema({
  angelListId: String,
  angelListProfile: Schema.Types.Mixed,
  angelToken: String, 
    firstName: {
    type: String,
    required: true,
    trim: true
  },
  lastName: {
    type: String,
    required: true,
    trim: true
  },
  displayName: {
    type: String,
    required: true,
    trim: true
  },
    password: String,
    email: {
    type: String,
    required: true,
    trim: true
  },
  role: {
    type:String,
    enum: roles,
    required: true,
    default: roles[0]
  },
  approved: {
    type: Boolean,
    default: false
  },
  banned: {
    type: Boolean,
    default: false
  },  
  admin: {
    type: Boolean,
    default: false
  },  
  headline: String,
  photoUrl: String,
  angelList: Schema.Types.Mixed,
  created: { 
    type: Date, 
    default: Date.now 
  },
  updated:  { 
    type: Date, default: Date.now 
  },
  angelUrl: String,
  twitterUrl: String,
  facebookUrl: String,
  linkedinUrl: String,
  githubUrl: String,
  own: Boolean,
  posts: {
    own: [Schema.Types.Mixed],
    likes: [Schema.Types.Mixed],
    watches: [Schema.Types.Mixed],
    comments: [Schema.Types.Mixed]
  }
});

Pruebas de moca

Uno de los beneficios de usar la arquitectura del servidor REST API es que cada ruta y la aplicación en su conjunto se vuelven muy comprobables. La seguridad de las pruebas aprobadas es un maravilloso complemento durante el desarrollo:el llamado enfoque de desarrollo basado en pruebas.

Para ejecutar pruebas, utilizamos Makefile:

REPORTER = list
MOCHA_OPTS = --ui tdd --ignore-leaks

test: 
  clear
  echo Starting test *********************************************************
  ./node_modules/mocha/bin/mocha \
  --reporter $(REPORTER) \
  $(MOCHA_OPTS) \
  tests/*.js
  echo Ending test

test-w: 
  ./node_modules/mocha/bin/mocha \
  --reporter $(REPORTER) \
  --growl \
  --watch \
  $(MOCHA_OPTS) \
  tests/*.js 

users: 
  mocha tests/users.js --ui tdd --reporter list --ignore-leaks
  
posts: 
  clear
  echo Starting test *********************************************************
  ./node_modules/mocha/bin/mocha \
  --reporter $(REPORTER) \
  $(MOCHA_OPTS) \
  tests/posts.js 
  echo Ending test

application: 
  mocha tests/application.js --ui tdd --reporter list --ignore-leaks
  
.PHONY: test test-w posts application

Por lo tanto, podemos comenzar las pruebas con:$ make comando.

Todas las 20 pruebas deben pasar:

Las pruebas de HackHall en vivo en tests carpeta y consta de:

  • hackhall/tests/application.js :pruebas funcionales para información de usuarios no aprobados
  • hackhall/tests/posts.js :pruebas funcionales para publicaciones
  • hackhall/tests/users.js :pruebas funcionales para usuarios

Pruebe usar una biblioteca llamada superagent (GitHub).

El contenido completo para hackhall/tests/application.js :

var app = require ('../server').app,
  assert = require('assert'),
  request = require('superagent');

app.listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

var user1 = request.agent();
var port = 'http://localhost:'+app.get('port');
var userId;

suite('APPLICATION API', function (){
  suiteSetup(function(done){
    done();
  });
  test('log in as admin', function(done){
    user1.post(port+'/api/login').send({email:'[email protected]',password:'1'}).end(function(res){
        assert.equal(res.status,200);
      done();
    });       
  });
  test('get profile for admin',function(done){
    user1.get(port+'/api/profile').end(function(res){
        assert.equal(res.status,200);
      done();
    });
  }); 
  test('submit applicaton for user [email protected]', function(done){
    user1.post(port+'/api/application').send({
        firstName: 'Dummy',
      lastName: 'Application',
      displayName: 'Dummy Application',
        password: '3',
        email: '[email protected]',
      headline: 'Dummy Appliation',
      photoUrl: '/img/user.png',
      angelList: {blah:'blah'},
      angelUrl: 'http://angel.co.com/someuser',
      twitterUrl: 'http://twitter.com/someuser',
      facebookUrl: 'http://facebook.com/someuser',
      linkedinUrl: 'http://linkedin.com/someuser',
      githubUrl: 'http://github.com/someuser'     
    }).end(function(res){
      assert.equal(res.status,200);
      userId = res.body._id;        
      done();
    });
    
  });
  test('logout admin',function(done){
    user1.post(port+'/api/logout').end(function(res){
        assert.equal(res.status,200);
      done();
    });
  });
  test('get profile again after logging out',function(done){
    user1.get(port+'/api/profile').end(function(res){
        assert.equal(res.status,500);
      done();
    });
  }); 
  test('log in as user3 - unapproved', function(done){
    user1.post(port+'/api/login').send({email:'[email protected]',password:'3'}).end(function(res){
        assert.equal(res.status,200);
      done();
    });       
  }); 
  test('get user application', function(done){
    user1.get(port+'/api/application/').end(function(res){
      // console.log(res.body)
      assert.equal(res.status, 200);  
      done();
    });
  }); 
  test('update user application', function(done){
    user1.put(port+'/api/application/').send({      
      firstName: 'boo'}).end(function(res){
      // console.log(res.body)
      assert.equal(res.status, 200);  
      done();
    });
  }); 
  test('get user application', function(done){
    user1.get(port+'/api/application/').end(function(res){
      // console.log(res.body)
      assert.equal(res.status, 200);  
      done();
    });
  }); 
  test('check for posts - fail (unapproved?)', function(done){
    user1.get(port+'/api/posts/').end(function(res){
      // console.log(res.body)
      assert.equal(res.status, 500);
  
      done();
    });
  });
  test('logout user',function(done){
    user1.post(port+'/api/logout').end(function(res){
        assert.equal(res.status,200);
      done();
    });
  }); 
  test('log in as admin', function(done){
    user1.post(port+'/api/login').send({email:'[email protected]',password:'1'}).end(function(res){
        assert.equal(res.status,200);
      done();
    });       
  }); 
  test('delete user3', function(done){
    user1.del(port+'/api/users/'+userId).end(function(res){
      assert.equal(res.status, 200);
      done();
    });   
  }); 
  test('logout admin',function(done){
    user1.post(port+'/api/logout').end(function(res){
        assert.equal(res.status,200);
      done();
    });
  }); 
  test('log in as user - should fail', function(done){
    user1.post(port+'/api/login').send({email:'[email protected]',password:'3'}).end(function(res){
      // console.log(res.body)
        assert.equal(res.status,500);
      
      done();
    });       
  }); 
  test('check for posts - must fail', function(done){
    user1.get(port+'/api/posts/').end(function(res){
      // console.log(res.body)
      assert.equal(res.status, 500);
  
      done();
    });
  }); 
  suiteTeardown(function(done){
    done();
  });

});

El contenido completo para hackhall/tests/posts.js :

var app = require('../server').app,
  assert = require('assert'),
  request = require('superagent');

app.listen(app.get('port'), function() {
  console.log('Express server listening on port ' + app.get('port'));
});

var user1 = request.agent();
var port = 'http://localhost:' + app.get('port');
var postId;

suite('POSTS API', function() {
  suiteSetup(function(done) {
    done();
  });
  test('log in', function(done) {
    user1.post(port + '/api/login').send({
      email: '[email protected]',
      password: '1'
    }).end(function(res) {
      assert.equal(res.status, 200);
      done();
    });
  });
  test('add post', function(done) {
    user1.post(port + '/api/posts').send({
      title: 'Yo Test Title',
      text: 'Yo Test text',
      url: ''
    }).end(function(res) {
      assert.equal(res.status, 200);
      postId = res.body._id;
      done();
    });

  });
  test('get profile', function(done) {
    user1.get(port + '/api/profile').end(function(res) {
      assert.equal(res.status, 200);
      done();
    });
  });
  test('post get', function(done) {
    // console.log('000'+postId);
    user1.get(port + '/api/posts/' + postId).end(function(res) {
      assert.equal(res.status, 200);
      assert.equal(res.body._id, postId);
      done();
    });
  });
  test('delete post', function(done) {
    user1.del(port + '/api/posts/' + postId).end(function(res) {
      assert.equal(res.status, 200);
      done();
    });
  });
  test('check for deleted post', function(done) {
    user1.get(port + '/api/posts/' + postId).end(function(res) {
      // console.log(res.body)
      assert.equal(res.status, 500);

      done();
    });
  });
  suiteTeardown(function(done) {
    done();
  });

});

El contenido completo para hackhall/tests/users.js :

var app = require('../server').app,
  assert = require('assert'),
  request = require('superagent');
// http = require('support/http');

var user1 = request.agent();
var port = 'http://localhost:' + app.get('port');


app.listen(app.get('port'), function() {
  console.log('Express server listening on port ' + app.get('port'));
});

suite('Test root', function() {
  setup(function(done) {
    console.log('setup');

    done();
  });

  test('check /', function(done) {
    request.get('http://localhost:3000').end(function(res) {
      assert.equal(res.status, 200);
      done();
    });
  });
  test('check /api/profile', function(done) {
    request.get('http://localhost:' + app.get('port') + '/api/profile').end(function(res) {
      assert.equal(res.status, 500);
      done();
    });
  });
  test('check /api/users', function(done) {
    user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(res) {
      assert.equal(res.status, 500);
      // console.log(res.text.length);
      done();
    });
    // done();
  });
  test('check /api/posts', function(done) {
    user1.get('http://localhost:' + app.get('port') + '/api/posts').end(function(res) {
      assert.equal(res.status, 500);
      // console.log(res.text.length);
      done();
    });
    // done();
  });
  teardown(function(done) {
    console.log('teardown');
    done();
  });

});

suite('Test log in', function() {
  setup(function(done) {
    console.log('setup');

    done();
  });
  test('login', function(done) {
    user1.post('http://localhost:3000/api/login').send({
      email: '[email protected]',
      password: '1'
    }).end(function(res) {
      assert.equal(res.status, 200);
      done();
    });

  });
  test('check /api/profile', function(done) {
    user1.get('http://localhost:' + app.get('port') + '/api/profile').end(function(res) {
      assert.equal(res.status, 200);
      // console.log(res.text.length);
      done();
    });
    // done();
  });
  test('check /api/users', function(done) {
    user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(res) {
      assert.equal(res.status, 200);
      // console.log(res.text);
      done();
    });
    // done();
  });
  test('check /api/posts', function(done) {
    user1.get('http://localhost:' + app.get('port') + '/api/posts').end(function(res) {
      assert.equal(res.status, 200);
      // console.log(res.text.length);
      done();
    });
    // done();
  });

  teardown(function(done) {
    console.log('teardown');
    done();
  });

});
suite('User control', function() {
  var user2 = {
    firstName: 'Bob',
    lastName: 'Dilan',
    displayName: 'Bob Dilan',
    email: '[email protected]'
  };
  suiteSetup(function(done) {
    user1.post('http://localhost:3000/api/login').send({
      email: '[email protected]',
      password: '1'
    }).end(function(res) {
      assert.equal(res.status, 200);
      // done();
    });
    user1.get('http://localhost:' + app.get('port') + '/api/profile').end(function(res) {
      assert.equal(res.status, 200);
      // console.log(res.text.length);
      // done();
    });

    done();
  })

  test('new user POST /api/users', function(done) {
    user1.post(port + '/api/users')
      .send(user2)
      .end(function(res) {
        assert.equal(res.status, 200);
        // console.log(res.text.length);
        user2 = res.body;
        // console.log(user2)
        done();
      })
  });
  test('get user list and check for new user GET /api/users', function(done) {
    user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(res) {
      assert.equal(res.status, 200);
      // console.log(res.body)
      var user3 = res.body.filter(function(el, i, list) {
        return (el._id == user2._id);
      });
      assert(user3.length === 1);
      // assert(res.body.indexOf(user2)>-1);
      // console.log(res.body.length)
      done();
    })
  });
  test('Approve User: PUT /api/users/' + user2._id, function(done) {
    assert(user2._id != '');
    user1.put(port + '/api/users/' + user2._id)
      .send({
        approved: true
      })
      .end(function(res) {
        assert.equal(res.status, 200);
        // console.log(res.text.length);
        assert(res.body.approved);
        user1.get(port + '/api/users/' + user2._id).end(function(res) {
          assert(res.status, 200);
          assert(res.body.approved);
          done();
        })

      })
  });
  test('Banned User: PUT /api/users/' + user2._id, function(done) {
    assert(user2._id != '');
    user1.put(port + '/api/users/' + user2._id)
      .send({
        banned: true
      })
      .end(function(res) {
        assert.equal(res.status, 200);
        // console.log(res.text.length);
        assert(res.body.banned);
        user1.get(port + '/api/users/' + user2._id).end(function(res) {
          assert(res.status, 200);
          assert(res.body.banned);
          done();
        })

      })
  });
  test('Promote User: PUT /api/users/' + user2._id, function(done) {
    assert(user2._id != '');
    user1.put(port + '/api/users/' + user2._id)
      .send({
        admin: true
      })
      .end(function(res) {
        assert.equal(res.status, 200);
        // console.log(res.text.length);
        assert(res.body.admin);
        user1.get(port + '/api/users/' + user2._id).end(function(res) {
          assert(res.status, 200);
          assert(res.body.admin);
          done();
        })

      })
  });
  test('Delete User: DELETE /api/users/:id', function(done) {
    assert(user2._id != '');
    user1.del(port + '/api/users/' + user2._id)
      .end(function(res) {
        assert.equal(res.status, 200);
        // console.log('id:' + user2._id)
        user1.get(port + '/api/users').end(function(res) {
          assert.equal(res.status, 200);
          var user3 = res.body.filter(function(el, i, list) {
            return (el._id === user2._id);
          });
          // console.log('***');
          // console.warn(user3);
          assert(user3.length === 0);
          done();
        });
      });


  });
});
// app.close();   
// console.log(app)

Advertencia: No almacene contraseñas/claves simples en la base de datos. Cualquier aplicación de producción seria debería al menos saltear las contraseñas antes de almacenarlas.

Conclusión

HackHall aún está en desarrollo, pero hay importantes componentes de aplicaciones de producción real, como la arquitectura REST API, OAuth, Mongoose y sus modelos, la estructura MVC de las aplicaciones Express.js, el acceso a las variables de entorno, etc.