diff --git a/.gitignore b/.gitignore index 9de9b70d..f0fe0cba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules lib-cov coverage.html +example diff --git a/lib/common.js b/lib/common.js index 3b99c0e5..63128121 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,9 +1,12 @@ +var path = require('path'); +var fs = require('fs'); var urllib = require('urllib'); var formstream = require('formstream'); // http://mp.weixin.qq.com/wiki/index.php?title=%E8%BF%94%E5%9B%9E%E7%A0%81%E8%AF%B4%E6%98%8E var wrapper = function (callback) { return function (err, data, res) { + if (err) { err.name = 'WeChatAPI' + err.name; return callback(err, data, res); @@ -28,6 +31,17 @@ var postJSON = function (data) { }; }; +/** + * 根据appid和appsecret创建API的构造函数 + * + * Examples: + * ``` + * var API = require('wechat').API; + * var api = new API('appid', 'secret'); + * ``` + * @param {String} appid 在公众平台上申请得到的appid + * @param {String} appsecret 在公众平台上申请得到的app secret + */ var API = function (appid, appsecret) { this.appid = appid; this.appsecret = appsecret; @@ -36,9 +50,23 @@ var API = function (appid, appsecret) { }; /** - * 根据appid和appsecret获取access token - * @param {String} appid appid - * @param {String} appsecret secret + * 根据创建API时传入的appid和appsecret获取access token + * 进行后续所有API调用时,需要先获取access token + * 详细请看: + * + * Examples: + * ``` + * api.getAccessToken(callback); + * ``` + * Callback: + * + * - `err`, 获取access token出现异常时的异常对象 + * - `result`, 成功时得到的响应结果 + * + * Result: + * ``` + * {"access_token": "ACCESS_TOKEN","expires_in": 7200} + * ``` * @param {Function} callback 回调函数 */ API.prototype.getAccessToken = function (callback) { @@ -55,7 +83,49 @@ API.prototype.getAccessToken = function (callback) { }; /** - * 创建菜单 + * 创建自定义菜单 + * 详细请看:http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口 + * + * Menu: + * ``` + * { + * "button":[ + * { + * "type":"click", + * "name":"今日歌曲", + * "key":"V1001_TODAY_MUSIC" + * }, + * { + * "name":"菜单", + * "sub_button":[ + * { + * "type":"view", + * "name":"搜索", + * "url":"http://www.soso.com/" + * }, + * { + * "type":"click", + * "name":"赞一下我们", + * "key":"V1001_GOOD" + * }] + * }] + * } + * ] + * } + * ``` + * Examples: + * ``` + * api.createMenu(menu, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode":0,"errmsg":"ok"} + * ``` * @param {Object} menu 菜单对象 * @param {Function} callback 回调函数 */ @@ -66,6 +136,34 @@ API.prototype.createMenu = function (menu, callback) { /** * 获取菜单 + * 详细请看: + * + * Examples: + * ``` + * api.getMenu(callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * // 结果示例 + * { + * "menu": { + * "button":[ + * {"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]}, + * {"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]}, + * {"name":"菜单","sub_button":[ + * {"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]}, + * {"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]}, + * {"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}] + * } + * ] + * } + * } + * ``` * @param {Function} callback 回调函数 */ API.prototype.getMenu = function (callback) { @@ -74,7 +172,21 @@ API.prototype.getMenu = function (callback) { }; /** - * 删除菜单 + * 删除自定义菜单 + * 详细请看: + * Examples: + * ``` + * api.removeMenu(callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode":0,"errmsg":"ok"} + * ``` * @param {Function} callback 回调函数 */ API.prototype.removeMenu = function (callback) { @@ -94,7 +206,24 @@ API.prototype.getQRCode = function (data, callback) { /** * 创建临时二维码 - * @param {String} sceneId 场景ID + * 详细请看: + * Examples: + * ``` + * api.createTmpQRCode(10000, 1800, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==", + * "expire_seconds":1800 + * } + * ``` + * @param {Number} sceneId 场景ID * @param {Number} expire 过期时间,单位秒。最大不超过1800 * @param {Function} callback 回调函数 */ @@ -110,7 +239,23 @@ API.prototype.createTmpQRCode = function (sceneId, expire, callback) { /** * 创建永久二维码 - * @param {String} sceneId 场景ID。ID不能大于1000 + * 详细请看: + * Examples: + * ``` + * api.createLimitQRCode(100, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==" + * } + * ``` + * @param {Number} sceneId 场景ID。ID不能大于1000 * @param {Function} callback 回调函数 */ API.prototype.createLimitQRCode = function (sceneId, callback) { @@ -123,16 +268,41 @@ API.prototype.createLimitQRCode = function (sceneId, callback) { }; /** - * 生成显示二维码的链接 + * 生成显示二维码的链接。微信扫描后,可立即进入场景 + * Examples: + * ``` + * api.showQRCodeURL(titck); + * // => https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET + * ``` * @param {String} ticket 二维码Ticket + * @return {String} 显示二维码的URL地址,通过img标签可以显示出来 */ API.prototype.showQRCodeURL = function (ticket) { return this.prefix + 'showqrcode?ticket=' + ticket; }; -// ## 分组API http://mp.weixin.qq.com/wiki/index.php?title=%E5%88%86%E7%BB%84%E7%AE%A1%E7%90%86%E6%8E%A5%E5%8F%A3 /** * 获取分组列表 + * 详情请见: + * Examples: + * ``` + * api.getGroups(callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "groups": [ + * {"id": 0, "name": "未分组", "count": 72596}, + * {"id": 1, "name": "黑名单", "count": 36} + * ] + * } + * ``` + * @param {Function} callback 回调函数 */ API.prototype.getGroups = function (callback) { // https://api.weixin.qq.com/cgi-bin/groups/get?access_token=ACCESS_TOKEN @@ -142,7 +312,22 @@ API.prototype.getGroups = function (callback) { /** * 创建分组 + * 详情请见: + * Examples: + * ``` + * api.createGroup('groupname', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"group": {"id": 107, "name": "test"}} + * ``` * @param {String} name 分组名字 + * @param {Function} callback 回调函数 */ API.prototype.createGroup = function (name, callback) { // https://api.weixin.qq.com/cgi-bin/groups/create?access_token=ACCESS_TOKEN @@ -157,8 +342,23 @@ API.prototype.createGroup = function (name, callback) { /** * 更新分组名字 + * 详情请见: + * Examples: + * ``` + * api.updateGroup(107, 'new groupname', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode": 0, "errmsg": "ok"} + * ``` * @param {Number} id 分组ID * @param {String} name 新的分组名字 + * @param {Function} callback 回调函数 */ API.prototype.updateGroup = function (id, name, callback) { // http请求方式: POST(请使用https协议) @@ -174,8 +374,23 @@ API.prototype.updateGroup = function (id, name, callback) { /** * 移动用户进分组 + * 详情请见: + * Examples: + * ``` + * api.moveUserToGroup(openid, groupId, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode": 0, "errmsg": "ok"} + * ``` * @param {String} openid 用户的openid * @param {Number} groupId 分组ID + * @param {Function} callback 回调函数 */ API.prototype.moveUserToGroup = function (openid, groupId, callback) { // http请求方式: POST(请使用https协议) @@ -191,8 +406,34 @@ API.prototype.moveUserToGroup = function (openid, groupId, callback) { }; /** - * 获取用户信息 + * 获取用户基本信息 + * 详情请见: + * Examples: + * ``` + * api.getUser(openid, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "subscribe": 1, + * "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M", + * "nickname": "Band", + * "sex": 1, + * "language": "zh_CN", + * "city": "广州", + * "province": "广东", + * "country": "中国", + * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", + * "subscribe_time": 1382694957 + * } + * ``` * @param {String} openid 用户的openid + * @param {Function} callback 回调函数 */ API.prototype.getUser = function (openid, callback) { // https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID @@ -202,8 +443,31 @@ API.prototype.getUser = function (openid, callback) { /** * 获取关注者列表 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E5%85%B3%E6%B3%A8%E8%80%85%E5%88%97%E8%A1%A8 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=获取关注者列表 + * Examples: + * ``` + * api.getFollowers(callback); + * // or + * api.getFollowers(nextOpenid, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "total":2, + * "count":2, + * "data":{ + * "openid":["","OPENID1","OPENID2"] + * }, + * "next_openid":"NEXT_OPENID" + * } + * ``` * @param {String} nextOpenid 调用一次之后,传递回来的nextOpenid。第一次获取时可不填 + * @param {Function} callback 回调函数 */ API.prototype.getFollowers = function (nextOpenid, callback) { // https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID @@ -215,21 +479,29 @@ API.prototype.getFollowers = function (nextOpenid, callback) { urllib.request(url, {dataType: 'json'}, wrapper(callback)); }; -// 客服消息 -// http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN /** - * 发送文字消息 + * 客服消息,发送文字消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendText('openid', 'Hello world', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * * @param {String} openid 用户的openid * @param {String} text 发送的消息内容 + * @param {Function} callback 回调函数 */ API.prototype.sendText = function (openid, text, callback) { // { - // "touser":"OPENID", - // "msgtype":"text", - // "text": - // { - // "content":"Hello World" - // } + // "touser":"OPENID", + // "msgtype":"text", + // "text": { + // "content":"Hello World" + // } // } var url = this.prefix + 'message/custom/send?access_token=' + this.token; var data = { @@ -244,17 +516,27 @@ API.prototype.sendText = function (openid, text, callback) { /** * 发送图片消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendImage('openid', 'media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * * @param {String} openid 用户的openid - * @param {String} mediaId 媒体文件的ID + * @param {String} mediaId 媒体文件的ID,参见uploadMedia方法 + * @param {Function} callback 回调函数 */ API.prototype.sendImage = function (openid, mediaId, callback) { // { - // "touser":"OPENID", - // "msgtype":"image", - // "image": - // { - // "media_id":"MEDIA_ID" - // } + // "touser":"OPENID", + // "msgtype":"image", + // "image": { + // "media_id":"MEDIA_ID" + // } // } var url = this.prefix + 'message/custom/send?access_token=' + this.token; var data = { @@ -269,17 +551,27 @@ API.prototype.sendImage = function (openid, mediaId, callback) { /** * 发送语音消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendVoice('openid', 'media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * * @param {String} openid 用户的openid * @param {String} mediaId 媒体文件的ID + * @param {Function} callback 回调函数 */ API.prototype.sendVoice = function (openid, mediaId, callback) { // { - // "touser":"OPENID", - // "msgtype":"image", - // "image": - // { - // "media_id":"MEDIA_ID" - // } + // "touser":"OPENID", + // "msgtype":"image", + // "image": { + // "media_id":"MEDIA_ID" + // } // } var url = this.prefix + 'message/custom/send?access_token=' + this.token; var data = { @@ -294,19 +586,29 @@ API.prototype.sendVoice = function (openid, mediaId, callback) { /** * 发送视频消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendVideo('openid', 'media_id', 'thumb_media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * * @param {String} openid 用户的openid * @param {String} mediaId 媒体文件的ID * @param {String} thumbMediaId 缩略图文件的ID + * @param {Function} callback 回调函数 */ API.prototype.sendVideo = function (openid, mediaId, thumbMediaId, callback) { // { - // "touser":"OPENID", - // "msgtype":"video", - // "image": - // { - // "media_id":"MEDIA_ID" - // "thumb_media_id":"THUMB_MEDIA_ID" - // } + // "touser":"OPENID", + // "msgtype":"video", + // "image": { + // "media_id":"MEDIA_ID" + // "thumb_media_id":"THUMB_MEDIA_ID" + // } // } var url = this.prefix + 'message/custom/send?access_token=' + this.token; var data = { @@ -322,21 +624,38 @@ API.prototype.sendVideo = function (openid, mediaId, thumbMediaId, callback) { /** * 发送音乐消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * var music = { + * title: '音乐标题', // 可选 + * description: '描述内容', // 可选 + * musicurl: 'http://url.cn/xxx', 音乐文件地址 + * hqmusicurl: "HQ_MUSIC_URL", + * thumb_media_id: "THUMB_MEDIA_ID" + * }; + * api.sendMusic('openid', music, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * * @param {String} openid 用户的openid * @param {Object} music 音乐文件 + * @param {Function} callback 回调函数 */ API.prototype.sendMusic = function (openid, music, callback) { // { - // "touser":"OPENID", - // "msgtype":"music", - // "music": - // { - // "title":"MUSIC_TITLE", // 可选 - // "description":"MUSIC_DESCRIPTION", // 可选 - // "musicurl":"MUSIC_URL", - // "hqmusicurl":"HQ_MUSIC_URL", - // "thumb_media_id":"THUMB_MEDIA_ID" - // } + // "touser":"OPENID", + // "msgtype":"music", + // "music": { + // "title":"MUSIC_TITLE", // 可选 + // "description":"MUSIC_DESCRIPTION", // 可选 + // "musicurl":"MUSIC_URL", + // "hqmusicurl":"HQ_MUSIC_URL", + // "thumb_media_id":"THUMB_MEDIA_ID" + // } // } var url = this.prefix + 'message/custom/send?access_token=' + this.token; var data = { @@ -349,29 +668,52 @@ API.prototype.sendMusic = function (openid, music, callback) { /** * 发送图文消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * var articles = [ + * { + * "title":"Happy Day", + * "description":"Is Really A Happy Day", + * "url":"URL", + * "picurl":"PIC_URL" + * }, + * { + * "title":"Happy Day", + * "description":"Is Really A Happy Day", + * "url":"URL", + * "picurl":"PIC_URL" + * }]; + * api.sendNews('openid', articles, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * * @param {String} openid 用户的openid * @param {Array} articles 图文列表 + * @param {Function} callback 回调函数 */ API.prototype.sendNews = function (openid, articles, callback) { // { - // "touser":"OPENID", - // "msgtype":"news", - // "news":{ - // "articles": [ - // { - // "title":"Happy Day", - // "description":"Is Really A Happy Day", - // "url":"URL", - // "picurl":"PIC_URL" - // }, - // { - // "title":"Happy Day", - // "description":"Is Really A Happy Day", - // "url":"URL", - // "picurl":"PIC_URL" - // } - // ] - // } + // "touser":"OPENID", + // "msgtype":"news", + // "news":{ + // "articles": [ + // { + // "title":"Happy Day", + // "description":"Is Really A Happy Day", + // "url":"URL", + // "picurl":"PIC_URL" + // }, + // { + // "title":"Happy Day", + // "description":"Is Really A Happy Day", + // "url":"URL", + // "picurl":"PIC_URL" + // }] + // } // } var url = this.prefix + 'message/custom/send?access_token=' + this.token; var data = { @@ -384,28 +726,78 @@ API.prototype.sendNews = function (openid, articles, callback) { urllib.request(url, postJSON(data), wrapper(callback)); }; -// 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) -['image', 'voice', 'video', 'thumb'].forEach(function (type) { - var method = 'upload' + type[0].toUpperCase() + type.substring(1); - API.prototype[method] = function (filepath, callback) { +/** + * 上传多媒体文件,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) + * 详情请见: + * Examples: + * ``` + * api.uploadMedia('filepath', type, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"type":"TYPE","media_id":"MEDIA_ID","created_at":123456789} + * ``` + * Shortcut: + * + * - API.prototype.uploadImage(filepath, callback); + * - API.prototype.uploadVoice(filepath, callback); + * - API.prototype.uploadVideo(filepath, callback); + * - API.prototype.uploadThumb(filepath, callback); + * + * @param {String} filepath 文件路径 + * @param {String} type 媒体类型,可用值有image、voice、video、thumb + * @param {Function} callback 回调函数 + */ +API.prototype.uploadMedia = function (filepath, type, callback) { + var that = this; + fs.stat(filepath, function (err, stat) { + if (err) { + return callback(err); + } var form = formstream(); - form.file('media', filepath); + form.file('media', filepath, path.basename(filepath), stat.size); form.headers({ 'Content-Type': 'application/json' }); - var url = this.fileServerPrefix + 'media/upload?access_token=' + this.token + '&type=' + type; - urllib.request(url, { + var url = that.fileServerPrefix + 'media/upload?access_token=' + that.token + '&type=' + type; + var opts = { dataType: 'json', type: 'POST', + timeout: 60000, // 60秒超时 headers: form.headers(), stream: form - }, wrapper(callback)); + }; + urllib.request(url, opts, wrapper(callback)); + }); +}; + +['image', 'voice', 'video', 'thumb'].forEach(function (type) { + var method = 'upload' + type[0].toUpperCase() + type.substring(1); + API.prototype[method] = function (filepath, callback) { + this.uploadMedia(filepath, type, callback); }; }); /** * 根据媒体ID获取媒体内容 + * 详情请见: + * Examples: + * ``` + * api.getMedia('media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的文件Buffer对象 + * - `res`, HTTP响应对象 + * * @param {String} mediaId 媒体文件的ID + * @param {Function} callback 回调函数 */ API.prototype.getMedia = function (mediaId, callback) { var url = this.fileServerPrefix + 'media/get?access_token=' + this.token + '&media_id=' + mediaId; diff --git a/lib/session.js b/lib/session.js index 2c2a303f..973d6ba6 100644 --- a/lib/session.js +++ b/lib/session.js @@ -1,3 +1,9 @@ +/** + * Session构造函数,用于与Connect的Session中间件集成的会话脚本 + * @param {String} id Session ID + * @param {Object} req Connect中的请求对象 + * @param {Object} data 可选的其余数据,将被合并进Session对象中 + */ var Session = function (id, req, data) { Object.defineProperty(this, 'id', { value: id }); Object.defineProperty(this, 'req', { value: req }); @@ -8,14 +14,30 @@ var Session = function (id, req, data) { } }; -Session.prototype.save = function(fn){ - this.req.sessionStore.set(this.id, this, fn || function(){}); +/** + * 保存Session对象到实际的存储中 + * + * Callback: + * + * - `err`, 错误对象,保存发生错误时传入 + * @param {Function} callback 保存Session的回调函数 + */ +Session.prototype.save = function (callback) { + this.req.sessionStore.set(this.id, this, callback || function(){}); return this; }; -Session.prototype.destroy = function(fn){ +/** + * 销毁Session对象 + * + * Callback: + * + * - `err`, 错误对象,删除发生错误时传入 + * @param {Function} callback 从存储中删除Session数据后的回调函数 + */ +Session.prototype.destroy = function (callback) { delete this.req.wxsession; - this.req.sessionStore.destroy(this.id, fn); + this.req.sessionStore.destroy(this.id, callback); return this; }; diff --git a/lib/wechat.js b/lib/wechat.js index 0739b1fd..291d7367 100644 --- a/lib/wechat.js +++ b/lib/wechat.js @@ -244,16 +244,24 @@ Handler.prototype.middlewarify = function () { }; /** + * 根据口令 + * * Examples: * ``` * wechat(token, function (req, res, next) {}); + * * wechat(token, wechat.text(function (message, req, res, next) { + * // TODO * }).location(function (message, req, res, next) { + * // TODO * })); + * * wechat(token) - * .text(function (message, req, res, next) {}) - * .location(function (message, req, res, next) {}) - * .middleware(); + * .text(function (message, req, res, next) { + * // TODO + * }).location(function (message, req, res, next) { + * // TODO + * }).middleware(); * ``` * @param {String} token 在微信平台填写的口令 * @param {Function} handle 生成的回调函数,参见示例 diff --git a/test/common.test.js b/test/common.test.js index db9e348c..d583b5ce 100644 --- a/test/common.test.js +++ b/test/common.test.js @@ -1,6 +1,7 @@ var should = require('should'); var urllib = require('urllib'); var muk = require('muk'); +var path = require('path'); var API = require('../').API; describe('common.js', function () { @@ -262,6 +263,128 @@ describe('common.js', function () { done(); }); }); + + describe('upload media', function () { + ['Image', 'Voice', 'Video', 'Thumb'].forEach(function (method) { + before(function () { + muk(urllib, 'request', function (url, args, callback) { + var resp = { + "type":"image", + "media_id":"usr5xL_gcxapoRjwH3bQZw_zKvcXL-DU4tRJtLtrtN71-3bXL52p3xX63ebp7tqA", + "created_at":1383233542 + }; + process.nextTick(function () { + callback(null, resp); + }); + }); + }); + + after(function () { + muk.restore(); + }); + it('upload' + method + ' should ok', function (done) { + api['upload' + method](path.join(__dirname, './fixture/image.jpg'), function (err, data, res) { + should.not.exist(err); + data.should.have.property('type', 'image'); + data.should.have.property('media_id'); + data.should.have.property('created_at'); + done(); + }); + }); + + it('upload' + method + ' should not ok', function (done) { + api['upload' + method](path.join(__dirname, './fixture/inexist.jpg'), function (err, data, res) { + should.exist(err); + err.should.have.property('name', 'Error'); + err.should.have.property('code', 'ENOENT'); + done(); + }); + }); + }); + }); + + describe('get media with buffer', function () { + before(function () { + muk(urllib, 'request', function (url, args, callback) { + var buffer = new Buffer('Hello world!'); + var res = { + headers: { + 'content-type': 'image/jpeg' + } + }; + process.nextTick(function () { + callback(null, buffer, res); + }); + }); + }); + + after(function () { + muk.restore(); + }); + + it('getMedia with buffer', function (done) { + api.getMedia('media_id', function (err, data, res) { + should.not.exist(err); + data.toString().should.be.equal('Hello world!'); + done(); + }); + }); + }); + + describe('get media with json', function () { + before(function () { + muk(urllib, 'request', function (url, args, callback) { + var data = JSON.stringify({"errcode":40007, "errmsg":"invalid media_id"}); + var res = { + headers: { + 'content-type': 'application/json' + } + }; + process.nextTick(function () { + callback(null, data, res); + }); + }); + }); + + after(function () { + muk.restore(); + }); + it('getMedia with json', function (done) { + api.getMedia('media_id', function (err, data, res) { + should.exist(err); + err.should.have.property('name', 'WeChatAPIError'); + err.should.have.property('message', 'invalid media_id'); + done(); + }); + }); + }); + + describe('get media with err json', function () { + before(function () { + muk(urllib, 'request', function (url, args, callback) { + var data = '{"errcode":40007, "errmsg":"invalid media_id"'; + var res = { + headers: { + 'content-type': 'application/json' + } + }; + process.nextTick(function () { + callback(null, data, res); + }); + }); + }); + + after(function () { + muk.restore(); + }); + it('getMedia with err json', function (done) { + api.getMedia('media_id', function (err, data, res) { + should.exist(err); + err.should.have.property('name', 'SyntaxError'); + done(); + }); + }); + }); }); describe('mock', function () { diff --git a/test/fixture/image.jpg b/test/fixture/image.jpg new file mode 100644 index 00000000..a0399310 Binary files /dev/null and b/test/fixture/image.jpg differ