摘要:下一步,關閉數據庫。出于多個原因,這個文件不應該被置入源代碼控制。主中間件被稱為路由器,它會監聽,并將動作傳遞到個指定的處理函數。當索引不存在于數據庫中時,將會負責索引的建立。唯一性約束保障將去除重復出現的可能。
在本系列文章的第一部分旨在介紹一些應用程序的基礎技術細節和如何進行數據建模,而這個部分文章將著手建立驗證應用程序行為的測試,并會指出如何啟動和運行應用程序。
首先,編寫測試首先定義一些小型配置庫。文件名:test/config/test_config.js
module.exports = { url : "http://localhost:8000/api/v1.0" }
服務器運行端口是 localhost 8000 ,對于初始的測試來說非常適合。之后,如果改變產品系統的位置或者端口號,只需要簡單地修改這個文件就可以。為了良好地測試,首先應該建立 1 個好的測試環境,這點可以通過下面的代碼保證。首先,連接到數據庫。
文件名: est/setup_tests.js 。
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,這么做可以了解數據庫狀態。
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,隨后刪點所有測試應用程序中的用戶。
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); }); }
下一步,關閉數據庫。
function closeDB(callback) { reader_test_db.close(); }
最終,調用 async.series 來保證所有函數都按次序運行。
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":”", "sp_api_key_secret":” ” }, { "_id":"54ad6c3be764de42070b27b2”, "email":"testuser2@example.com", "firstName":"Test", "lastName":"User2”, "sp_api_key_id":” ", "sp_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 SECRETExpress.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 則可以被使用,當然向下兼容。
要啟動服務器和運行測試,這里需要遵循幾個步驟。
保證 MongoDB 實例運行,mongod。
安裝 Node 庫,npm install。
開啟 REST API 服務器,node server.js。
運行測試用例: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
參考文獻:HTTP status code definitions
Chad Tindel’s Github Repository
M101JS: MongoDB for Node.js Developers
Data Models
Data Modeling Considerations for MongoDB Applications
本文系 OneAPM 工程師編譯整理。想閱讀更多技術文章,請訪問 OneAPM 官方博客。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/18764.html
摘要:近日,在中,介紹了一個基于新時代架構的實踐,和。是的一個現代替代者,在九十年代末,曾是應用程序的主流構建方式。指定了對象的作用范圍,而服務器則會通過結果代碼和有效的進行響應。代表創建讀取更新和刪除。下圖顯示了使用后的請求和響應流。 摘要:90 年代,LAMP 曾風靡一時,然而隨著需求的變遷和數據流量的激增,LAMP 已不可避免的走下神壇。近日,在 MongoDB Blog 中,Dana...
閱讀 2016·2021-11-12 10:36
閱讀 1865·2021-11-09 09:49
閱讀 2591·2021-11-04 16:12
閱讀 1144·2021-10-09 09:57
閱讀 3235·2019-08-29 17:24
閱讀 1909·2019-08-29 15:12
閱讀 1272·2019-08-29 14:07
閱讀 1285·2019-08-29 12:53