MEAN實踐——LAMP的新時代替代方案(下)

在本系列文章的第一部分旨在介紹一些應用程序的基礎技術細節和如何進行數據建模,而這個部分文章將着手創建驗證應用程序行爲的測試,並會指出如何啓動和運行應用程序。html

首先,編寫測試

首先定義一些小型配置庫。文件名:test/config/test_config.jsnode

module.exports = {
    url : 'http://localhost:8000/api/v1.0'
}

服務器運行端口是 localhost 8000 ,對於初始的測試來講很是適合。以後,若是改變產品系統的位置或者端口號,只須要簡單地修改這個文件就能夠。爲了良好地測試,首先應該創建 1 個好的測試環境,這點能夠經過下面的代碼保證。首先,鏈接到數據庫。git

文件名: est/setup_tests.js 。github

function connectDB(callback) {
    mongoClient.connect(dbConfig.testDBURL, function(err, db) {
        assert.equal(null, err);
        reader_test_db = db;
        console.log("Connected correctly to server");
        callback(0);
    });
}

下一步,drop user collection,這麼作能夠了解數據庫狀態。mongodb

function dropUserCollection(callback) {
        console.log("dropUserCollection");
        user = reader_test_db.collection('user');
        if (undefined != user) {
            user.drop(function(err, reply) {
                console.log('user collection dropped');
                callback(0);
            });
        } else {
            callback(0);
        }
    },

下一步,drop user feed entry collection。數據庫

function dropUserFeedEntryCollection(callback) {
    console.log("dropUserFeedEntryCollection");
    user_feed_entry = reader_test_db.collection('user_feed_entry');
    if (undefined != user_feed_entry) {
        user_feed_entry.drop(function(err, reply) {
            console.log('user_feed_entry collection dropped');
            callback(0);
        });
    } else {
        callback(0);
    }
}

下一步,鏈接到Stormpath,隨後刪點全部測試應用程序中的用戶。npm

function getApplication(callback) {
    console.log("getApplication");
    client.getApplications({
        name: SP_APP_NAME
    }, function(err, applications) {
        console.log(applications);
        if (err) {
            log("Error in getApplications");
            throw err;
        }
        app = applications.items[0];
        callback(0);
    });
},
function deleteTestAccounts(callback) {
    app.getAccounts({
        email: TU_EMAIL_REGEX
    }, function(err, accounts) {
        if (err) throw err;
        accounts.items.forEach(function deleteAccount(account) {
            account.delete(function deleteError(err) {
                if (err) throw err;
            });
        });
        callback(0);
    });
}

下一步,關閉數據庫。json

function closeDB(callback) {
    reader_test_db.close();
}

最終,調用 async.series 來保證全部函數都按次序運行。api

async.series([connectDB, dropUserCollection,    dropUserFeedEntryCollection, dropUserFeedEntryCollection, getApplication, deleteTestAccounts, closeDB]);

Frisby 在初期就被創建,這裏將使用它定義測試用例,以下:數組

文件名:test/createaccountserror_spec.js

TU1_FN = "Test";
TU1_LN = "User1";
TU1_EMAIL = "testuser1@example.com";
TU1_PW = "testUser123";
TU_EMAIL_REGEX = 'testuser*';
SP_APP_NAME = 'Reader Test';

var frisby = require('frisby');
var tc = require('./config/test_config');

下面代碼將從 enroll route 開始。這個用例故意丟掉了 first name 字段,所以得到 1 個 400 與 1 個 JSON error(顯示 first name 未定義)返回,下面就 toss that frisby:

frisby.create('POST missing firstName')
.post(tc.url + '/user/enroll',
      { 'lastName' : TU1_LN,
        'email' : TU1_EMAIL,
        'password' : TU1_PW })
.expectStatus(400)
.expectHeader('Content-Type', 'application/json; charset=utf-8')
.expectJSON({'error' : 'Undefined First Name'})
.toss()

下面用例將測試不包含小寫字母,這一樣會致使 Stormpath 返回錯誤,以及返回400 狀態。


下面將測試一個無效郵箱地址。所以,指望返回的是未發現 @ 標誌,以及 emali地址缺乏域名,同時也會得到 1 個 400 狀態。

文件名:test/createaccountsspec.js

frisby.create('POST invalid email address')
.post(tc.url + '/user/enroll',
      { 'firstName' : TU1_FN,
        'lastName' : TU1_LN,
        'email' : "invalid.email",
        'password' : 'testUser' })
.expectStatus(400)
.expectHeader('Content-Type', 'application/json; charset=utf-8')
.expectJSONTypes({'error' : String})
.toss()

下面着眼一些能夠運行的例子,首先須要定義 3 個用戶。

文件名:test/createaccountsspec.js

TEST_USERS = [{'fn' : 'Test', 'ln' : 'User1',
         'email' : 'testuser1@example.com', 'pwd' : 'testUser123'},
          {'fn' : 'Test', 'ln' : 'User2',
           'email' : 'testuser2@example.com', 'pwd' : 'testUser123'},
          {'fn' : 'Test', 'ln' : 'User3',
           'email' : 'testuser3@example.com', 'pwd' : 'testUser123'}]

SP_APP_NAME = 'Reader Test';

var frisby = require('frisby');
var tc = require('./config/test_config');

下面用例將發送 1 個包含上文已定義 3 個用戶的數組,固然指望得到表明成功的 201 狀態。返回的 JSON document 將展現已創建的用戶對象,所以這裏能夠檢查測試數據匹配與否。

TEST_USERS.forEach(function createUser(user, index, array) {
    frisby.create('POST enroll user ' + user.email)
    .post(tc.url + '/user/enroll',
          { 'firstName' : user.fn,
            'lastName' : user.ln,
            'email' : user.email,
            'password' : user.pwd })
    .expectStatus(201)
    .expectHeader('Content-Type', 'application/json; charset=utf-8')
    .expectJSON({ 'firstName' : user.fn,
                  'lastName' : user.ln,
                  'email' : user.email })
    .toss()
});

下一步將測試重複用戶。下例將驗證這個用戶註冊的 email 地址已經被使用。

frisby.create('POST enroll duplicate user ')
    .post(tc.url + '/user/enroll',
      { 'firstName' : TEST_USERS[0].fn,
        'lastName' : TEST_USERS[0].ln,
        'email' : TEST_USERS[0].email,
        'password' : TEST_USERS[0].pwd })
.expectStatus(400)
.expectHeader('Content-Type', 'application/json; charset=utf-8')
.expectJSON({'error' : 'Account with that email already exists.  Please choose another email.'})
.toss()

這裏存在一個重要問題,沒法知道 Stormpath 會優先返回哪一個 API key。所以,這裏須要創建一個動態文件。隨後可使用這個對文件來驗證測試用例——用戶身份驗證組件。

文件名稱: /tmp/readerTestCreds.js

TEST_USERS = 
[{    "_id":"54ad6c3ae764de42070b27b1",
"email":"testuser1@example.com",
"firstName":"Test",
"lastName":"User1",
"sp_api_key_id":」<API KEY ID>",
"sp_api_key_secret":」<API KEY SECRET>」
},
{    "_id":"54ad6c3be764de42070b27b2」,
    "email":"testuser2@example.com",
    "firstName":"Test",
    "lastName":"User2」,
    "sp_api_key_id":」<API KEY ID>",
    "sp_api_key_secret":」<API KEY SECRET>」
}];
module.exports = TEST_USERS;

爲了創建上面這個臨時文件,這裏須要鏈接 MongoDB 從而檢索用戶信息。代碼以下:

文件名:tests/writeCreds.js

TU_EMAIL_REGEX = new RegExp('^testuser*');
SP_APP_NAME = 'Reader Test';
TEST_CREDS_TMP_FILE = '/tmp/readerTestCreds.js';

var async = require('async');
var dbConfig = require('./config/db.js');
var mongodb = require('mongodb');
assert = require('assert');

var mongoClient = mongodb.MongoClient
var reader_test_db = null;
var users_array = null;

function connectDB(callback) {
     mongoClient.connect(dbConfig.testDBURL, function(err, db) {
     assert.equal(null, err);
     reader_test_db = db;
     callback(null);
     });
 }

 function lookupUserKeys(callback) {
     console.log("lookupUserKeys");
     user_coll = reader_test_db.collection('user');
     user_coll.find({email :    TU_EMAIL_REGEX}).toArray(function(err, users) {
         users_array = users;
         callback(null);
     });
 }

function writeCreds(callback) {
     var fs = require('fs');
     fs.writeFileSync(TEST_CREDS_TMP_FILE, 'TEST_USERS = ');
     fs.appendFileSync(TEST_CREDS_TMP_FILE,   JSON.stringify(users_array));
     fs.appendFileSync(TEST_CREDS_TMP_FILE, '; module.exports =  TEST_USERS;');
     callback(0);
 }

 function closeDB(callback) {
     reader_test_db.close();
 }

 async.series([connectDB, lookupUserKeys, writeCreds, closeDB]);

着眼下面代碼,上文創建的臨時文件在第一行就會被使用。同時,有多個 feeds 被創建,好比 Dilbert 和 the Eater Blog 。

文件名:tests/feed_spec.js

TEST_USERS = require('/tmp/readerTestCreds.js');

var frisby = require('frisby');
var tc = require('./config/test_config');
var async = require('async');
var dbConfig = require('./config/db.js');

var dilbertFeedURL = 'http://feeds.feedburner.com/DilbertDailyStrip';
var nycEaterFeedURL = 'http://feeds.feedburner.com/eater/nyc';

首先,一些用戶會被創建,固然他們並無訂閱任何 feeds。下面代碼將測試 feeds 的訂閱。請注意,這裏一樣須要進行身份驗證,經過使用 .auth 和 Stormpath API keys 完成。

function addEmptyFeedListTest(callback) {
     var user = TEST_USERS[0];
     frisby.create('GET empty feed list for user ' + user.email)
             .get(tc.url + '/feeds')
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(200)
         .expectHeader('Content-Type', 'application/json; charset=utf-8')
         .expectJSON({feeds : []})
         .toss()
         callback(null);
}

下面用例將爲第一個測試用戶訂閱 Dilbert feed 。


這個用例將嘗試爲用戶 feed 重複訂閱。

function subDuplicateFeed(callback) {
 var user = TEST_USERS[0];
 frisby.create('PUT Add duplicate feed sub for user ' + user.email)
         .put(tc.url + '/feeds/subscribe',
              {'feedURL' : dilbertFeedURL})
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(201)
         .expectHeader('Content-Type', 'application/json; charset=utf-8')
         .expectJSONLength('user.subs', 1)
         .toss()
 callback(null);
}

下一步,將爲測試用戶添加一個新的 feed,返回的結果應該是用戶當下已經訂閱了 2 個 feed。

function subSecondFeed(callback) {
 var user = TEST_USERS[0];
 frisby.create('PUT Add second feed sub for user ' + user.email)
         .put(tc.url + '/feeds/subscribe',
              {'feedURL' : nycEaterFeedURL})
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(201)
         .expectHeader('Content-Type', 'application/json; charset=utf-8')
         .expectJSONLength('user.subs', 2)
         .toss()
 callback(null);
 }

下一步,將使用第 2 個測試用戶來訂閱 1 個 feed 。

function subOneFeedSecondUser(callback) {
     var user = TEST_USERS[1];
 frisby.create('PUT Add one feed sub for second user ' + user.email)
         .put(tc.url + '/feeds/subscribe',
              {'feedURL' : nycEaterFeedURL})
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(201)
         .expectHeader('Content-Type', 'application/json; charset=utf-8')
         .expectJSONLength('user.subs', 1)
         .toss()
 callback(null);
}

async.series([addEmptyFeedListTest, subOneFeed, subDuplicateFeed, subSecondFeed, subOneFeedSecondUser]);

REST API

在開始編寫 REST API 代碼以前,首先須要定義一些實用工具庫。首先,需求定義應用程序如何鏈接到數據庫。將這個信息寫入一個獨立的文件容許應用程序靈活地添加新數據庫 URL,以應對開發或者生產系統。

文件名:config/db.js


若是指望打開數據庫驗證,這裏須要將信息存入 1 個文件,以下文代碼所示。出於多個緣由,這個文件不該該被置入源代碼控制。

文件名稱:config/security.js

module.exports = {
 stormpath_secret_key : ‘YOUR STORMPATH APPLICATION KEY’;
}

Stormpath API 和 Secret keys 應該被保存到屬性文件,以下文代碼所示,同事還須要嚴加註意。

文件名:config/stormpath_apikey.properties

apiKey.id = YOUR STORMPATH API KEY ID
apiKey.secret = YOUR STORMPATH API KEY SECRET

Express.js 簡述

在 Express.js 中會創建應用程序(APP)。這個應用程序會監聽制定的端口來響應 HTTP 請求。當請求涌入,它們會被傳輸到 1 箇中間件鏈。中間件鏈中的每一個 link 都會被給予 1 個請求和 1 個響應對象用以存儲結果。link 分爲兩種類型,工做或者傳遞到下一個 link 。這裏會經過 app.use() 來添加新的中間件。主中間件被稱爲「router(路由器)」,它會監聽 URL,並將 URL/ 動做傳遞到 1 個指定的處理函數。

創建應用程序

如今開始聚焦應用程序代碼,鑑於能夠在獨立文件中爲不一樣的 routes 嵌入處理器,因此應用程序的體積很是小。

文件名:server.js

在 chain 中末尾定義中間件來處理壞 URLs。


如今,應用程序就會監聽 8000 端口。


在控制檯將消息打印給用戶。

console.log('Magic happens on port ' + port);

exports = module.exports = app;

定義 Mongoose 數據模型

這裏會使用 Mongoose 將 Node.js 上的對象映射成 MongoDB 文檔。如上文所述,這裏將創建 4 個 collections:

  • Feed collection。

  • Feed entry collection。

  • User collection。

  • User feed-entry-mapping collection。

下一步,將爲 4 個 collections 定義 schema。首先,從 user schema 開始。注意,這裏一樣能夠格式化數據,好比講字母都轉換成小寫,使用 trim 消除首/末空格。

文件名:app/routes.js

var userSchema = new mongoose.Schema({
         active: Boolean,
     email: { type: String, trim: true, lowercase: true },
     firstName: { type: String, trim: true },
     lastName: { type: String, trim: true },
     sp_api_key_id: { type: String, trim: true },
     sp_api_key_secret: { type: String, trim: true },
     subs: { type: [mongoose.Schema.Types.ObjectId], default: [] },
     created: { type: Date, default: Date.now },
     lastLogin: { type: Date, default: Date.now },
 },
 { collection: 'user' }
);

下面代碼將告訴 Mongoose 須要哪些索引。當索引不存在於 MongoDB 數據庫中時,Mongoose 將會負責索引的創建。惟一性約束保障將去除重複出現的可能。「email : 1」 將以升序的方式維護地址,而「email : -1」則是降序。

在其餘 3 個 collections 上重複這個步驟。

var UserModel = mongoose.model( 'User', userSchema );

var feedSchema = new mongoose.Schema({
     feedURL: { type: String, trim:true },
     link: { type: String, trim:true },
     description: { type: String, trim:true },
     state: { type: String, trim:true, lowercase:true, default: 'new' },
     createdDate: { type: Date, default: Date.now },
     modifiedDate: { type: Date, default: Date.now },
 },
 { collection: 'feed' }
);

feedSchema.index({feedURL : 1}, {unique:true});
feedSchema.index({link : 1}, {unique:true, sparse:true});

var FeedModel = mongoose.model( 'Feed', feedSchema );

var feedEntrySchema = new mongoose.Schema({
     description: { type: String, trim:true },
     title: { type: String, trim:true },
     summary: { type: String, trim:true },
     entryID: { type: String, trim:true },
     publishedDate: { type: Date },
     link: { type: String, trim:true  },
     feedID: { type: mongoose.Schema.Types.ObjectId },
     state: { type: String, trim:true, lowercase:true, default: 'new' },
     created: { type: Date, default: Date.now },
 },
 { collection: 'feedEntry' }
);

feedEntrySchema.index({entryID : 1});
feedEntrySchema.index({feedID : 1});

var FeedEntryModel = mongoose.model( 'FeedEntry', feedEntrySchema     );

var userFeedEntrySchema = new mongoose.Schema({
     userID: { type: mongoose.Schema.Types.ObjectId },
     feedEntryID: { type: mongoose.Schema.Types.ObjectId },
     feedID: { type: mongoose.Schema.Types.ObjectId },
     read : { type: Boolean, default: false },
 },
 { collection: 'userFeedEntry' }
 );

下面是複合索引實例,每一個索引都以升序維護。

userFeedEntrySchema.index({userID : 1, feedID : 1, feedEntryID : 1, read : 1});

var UserFeedEntryModel = mongoose.model('UserFeedEntry', userFeedEntrySchema );

每一個用於 GET、POST、PUT 和 DELETE 的請求須要擁有 1 個正確的內容類型,也就是 application/json。而後下一個 link 會被調用。


下一步須要爲每一個 URL/verb 定義處理器。參考資料部分附上了全部代碼,下面只是代碼片斷。在這些代碼中,Stormpath 帶來的便捷盡收眼底。此外,這裏定義的是 /api/v1.0 ,舉個例子,這裏客戶端能夠調用的是 /api/v1.0/user/enroll。若是使用 /api/v2.0,/api/v2.0 則能夠被使用,固然向下兼容。

啓動服務器並運行測試

要啓動服務器和運行測試,這裏須要遵循幾個步驟。

  1. 保證 MongoDB 實例運行,mongod。

  2. 安裝 Node 庫,npm install。

  3. 開啓 REST API 服務器,node server.js。

  4. 運行測試用例:node setup_tests.js;jasmine-node create_accounts_error_spec.js;jasmine-node create_accounts_spec.js;node write_creds.js;jasmine-node feed_spec.js

原文連接Building your first application with MongoDB: Creating a REST API using the MEAN Stack - Part 2

參考文獻:

本文系 OneAPM 工程師編譯整理。想閱讀更多技術文章,請訪問 OneAPM 官方博客

相關文章
相關標籤/搜索