├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── lib └── feed.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.DS_Store 3 | 4 | *.log 5 | 6 | *.sublime-project 7 | *.sublime-workspace -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 5 5 | - node 6 | notifications: 7 | email: false 8 | slack: 9 | secure: T9PQYr9cu9MV5UTJzNfQSToXSApj+LVw/jaDqtOosFQ9dBtVO/3U3YUu+8VMopuNLanfbkVZJLXk74g0ojkiVbodDMWE+XhWkYL4Gzi3/VKxW1g9xNbz5vdn3+mb1BF5RVmeRQ7V3Qoyl5CjjgywCK2MM/kpLEfajqtumHWJf6U= 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 53seven, LLC 2 | Originally Created by Kiernan McGowan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-blindparser 2 | ---------------- 3 | [![build status](https://secure.travis-ci.org/53seven/node-blindparser.svg)](http://travis-ci.org/53seven/node-blindparser) 4 | 5 | blindparser is a RSS/ATOM feed parser that returns the requested feed urls in a json object that is formatted so that you will not have to worry (much) about the format of the requested feed. 6 | 7 | Motivation 8 | ---------- 9 | 10 | RSS and ATOM feeds are both trying to deliver similar content, but are different enough with their structure to be aggravating. The purpose of blindparser is to allow for the important parts of the feeds (article titles, links, etc) to be returned in a standard format, but to also return the rest of the feed in a reasonable way. 11 | 12 | Installing 13 | ---------- 14 | 15 | ``` 16 | npm install blindparser 17 | ``` 18 | 19 | Usage 20 | ----- 21 | 22 | Using blind parser is easy, just call: 23 | 24 | ```js 25 | var parser = require('blindparser'); 26 | 27 | // with no options 28 | parser.parseURL('http://rss.cnn.com/rss/cnn_topstories.rss', function(err, out){ 29 | console.log(out); 30 | }); 31 | 32 | var options = { 33 | followRedirect: false, 34 | timeout: 1000 35 | }; 36 | //rss feeds 37 | parser.parseURL('http://rss.cnn.com/rss/cnn_topstories.rss', options, function(err, out){ 38 | console.log(out); 39 | }); 40 | //atom feeds 41 | parser.parseURL('http://www.blogger.com/feeds/10861780/posts/default', options, function(err, out){ 42 | console.log(out); 43 | }); 44 | ``` 45 | 46 | Options 47 | ------- 48 | 49 | The options hash is passed through to [request](https://github.com/mikeal/request) for fetching a given url. 50 | 51 | Output 52 | ------ 53 | 54 | The point of `blindparser` is to try and hide the format of the originally requested feed. Thus RSS and ATOM feeds are returned in a common format. Similar fields (pubDate vs update) will be mapped to the same field in the output. 55 | 56 | The 'minimal' output format is: 57 | 58 | ```js 59 | { 60 | type:"rss" or "atom" 61 | metadata:{ 62 | title: Title of the feed 63 | desc: description or subtitle 64 | url: url of the feed 65 | update: pubDate or update time of the feed 66 | }, 67 | items:[ 68 | { 69 | title: Title of article 70 | desc: Description or content of article 71 | link: Link to article 72 | date: Time article was published 73 | }... 74 | ] 75 | ``` 76 | 77 | Tests 78 | ----- 79 | 80 | Tests for blindparser can be run using the command: 81 | 82 | ``` 83 | npm test 84 | ``` 85 | 86 | Make sure that you machine has an internet connection before running the 87 | tests. 88 | 89 | License 90 | ------- 91 | Copyright (c) 2016 53seven, LLC 92 | 93 | Originally Created by Kiernan McGowan 94 | 95 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 96 | 97 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 98 | 99 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 100 | -------------------------------------------------------------------------------- /lib/feed.js: -------------------------------------------------------------------------------- 1 | //feed.js 2 | var xml2js = require('xml2js'); 3 | var _ = require('lodash'); 4 | var request = require('request'); 5 | var URL = require('url'); 6 | 7 | /** 8 | All you need to do is send a feed URL that can be opened via fs 9 | Options are optional, see xml2js for extensive list 10 | And a callback of course 11 | 12 | The returned formats will be structually the same, but you should still check the 'format' property 13 | **/ 14 | function parseURL(feedURL, options, callback) { 15 | if (typeof options == 'function' && !callback) { 16 | callback = options; 17 | options = {}; 18 | } 19 | var defaults = {uri: feedURL, jar: false, proxy: false, followRedirect: true, timeout: 1000 * 30}; 20 | options = _.extend(defaults, options); 21 | //check that the protocall is either http or https 22 | var u = URL.parse(feedURL); 23 | if (u.protocol == 'http:' || u.protocol == 'https:') { 24 | //make sure to have a 30 second timeout 25 | request(options, function(err, response, xml) { 26 | if (err || xml === null) { 27 | if (err) { 28 | callback(err); 29 | } else { 30 | callback(new Error('Failed to retrive source')); 31 | } 32 | } else if (response.statusCode && response.statusCode >= 400) { 33 | callback(new Error('status code ' + response.statusCode)); 34 | } else { 35 | parseString(xml, callback); 36 | } 37 | }); 38 | } else { 39 | callback(new Error('Only http or https protocalls are accepted')); 40 | } 41 | } 42 | module.exports.parseURL = parseURL; 43 | 44 | function parseString(xml, callback) { 45 | var parser = new xml2js.Parser(); 46 | parser.parseString(xml, function(err, jsonDOM) { 47 | if (jsonDOM) { 48 | jsonDOM = normalize(jsonDOM); 49 | var output; 50 | if (isRSS(jsonDOM)) { 51 | output = formatRSS(jsonDOM); 52 | } else { 53 | output = formatATOM(jsonDOM); 54 | } 55 | callback(null, output); 56 | } else if (err instanceof Error) { 57 | callback(err); 58 | } else { 59 | callback(new Error('Failed to parse xml string')); 60 | } 61 | }); 62 | } 63 | module.exports.parseString = parseString; 64 | 65 | //detects if RSS, otherwise assume atom 66 | function isRSS(json) { 67 | return (json.channel != null); 68 | } 69 | 70 | // normalizes input to make feed burner work 71 | function normalize(json) { 72 | if (json.rss || json['rdf:RDF']) { 73 | return json.rss || json['rdf:RDF']; 74 | } 75 | return json; 76 | } 77 | 78 | //xml2js will return commented material in a # tag which can be a pain 79 | //this will remove the # tag and set its child text in it's place 80 | //ment to work on a feed item, so will iterate over json's and check 81 | function flattenComments(json) { 82 | for (var key in json) { 83 | if (json[key]['#']) { 84 | json[key] = json[key]['#']; 85 | } 86 | } 87 | return json; 88 | } 89 | 90 | //parse a title from the channel 91 | function getTitle(title) { 92 | if (_.isArray(title)) { 93 | title = _.first(title); 94 | } 95 | if (_.isObject(title) && _.isString(title._)) { 96 | title = title._ 97 | } 98 | return title; 99 | } 100 | 101 | //parse a desc from the channel 102 | function getDesc(desc) { 103 | if (_.isArray(desc)) { 104 | desc = _.first(desc); 105 | } 106 | if (_.isObject(desc) && _.isString(desc._)) { 107 | desc = desc._; 108 | } 109 | return desc; 110 | } 111 | 112 | //parse a link from the channel 113 | function getURL(link) { 114 | var url = ''; 115 | if (link) { 116 | if (_.isArray(link)) { 117 | // You'll get the alt link or the last link defined 118 | _.each(link, function(val, i) { 119 | if (val.$ && val.$.rel === 'alternate') { 120 | url = val.$.href; 121 | return false; 122 | } 123 | else if (val.rel === 'alternate') { 124 | url = val.href; 125 | return false; 126 | } 127 | else { 128 | url = val; 129 | } 130 | }); 131 | if (_.isObject(url)) { 132 | url = url.$ && url.$.href || url.href; 133 | } 134 | } else if (_.isObject(link)) { 135 | url = link.$ && link.$.href || link.href; 136 | } else { 137 | url = link; 138 | } 139 | } 140 | return url; 141 | } 142 | 143 | //parse categories from the channel 144 | function getCategories(category) { 145 | var categories = []; 146 | if (category) { 147 | if (_.isArray(category)) { 148 | _.each(category, function(val, i) { 149 | if (_.isObject(val) && _.isString(val._)) { 150 | categories.push(val._); 151 | } else { 152 | categories.push(val); 153 | } 154 | }); 155 | } else if (_.isObject(category) && _.isString(category._)) { 156 | categories.push(category._); 157 | } else { 158 | categories.push(category); 159 | } 160 | } 161 | return categories; 162 | } 163 | 164 | //formats the RSS feed to the needed outpu 165 | //also parses FeedBurner 166 | function formatRSS(json) { 167 | var output = {'type': 'rss', metadata: {}, items: []}; 168 | 169 | //Start with the metadata for the feed 170 | var metadata = {}; 171 | var channel = json.channel; 172 | 173 | if (_.isArray(json.channel)) { 174 | channel = json.channel[0]; 175 | } 176 | 177 | //Channel title 178 | if (channel.title) { 179 | metadata.title = getTitle(channel.title); 180 | } 181 | 182 | //Channel description 183 | if (channel.description) { 184 | metadata.desc = getDesc(channel.description); 185 | } 186 | 187 | //Channel URL 188 | if (channel.link) { 189 | metadata.url = getURL(channel.link); 190 | } 191 | 192 | //Channel update date/time 193 | if (channel.lastBuildDate) { 194 | metadata.lastBuildDate = channel.lastBuildDate; 195 | if (_.isArray(metadata.lastBuildDate)) { 196 | metadata.lastBuildDate = _.first(metadata.lastBuildDate); 197 | } 198 | } 199 | if (channel.pubDate) { 200 | metadata.update = channel.pubDate; 201 | if (_.isArray(metadata.update)) { 202 | metadata.update = _.first(metadata.update); 203 | } 204 | } 205 | if (channel.ttl) { 206 | metadata.ttl = channel.ttl; 207 | if (_.isArray(metadata.ttl)) { 208 | metadata.ttl = _.first(metadata.ttl); 209 | } 210 | } 211 | 212 | //Channel image info 213 | if (channel.image) { 214 | metadata.image = []; 215 | channel.image.forEach(function(image, index) { 216 | metadata.image[index] = {}; 217 | Object.keys(image).forEach(function(attr) { 218 | metadata.image[index][attr] = _.isArray(image[attr]) ? _.first(image[attr]) : image[attr]; 219 | }); 220 | }); 221 | } 222 | 223 | output.metadata = metadata; 224 | 225 | //ok, now lets get into the meat of the feed 226 | //just double check that it exists 227 | var items = json.item || channel.item; 228 | if (items) { 229 | if (!_.isArray(items)) { 230 | items = [items]; 231 | } 232 | _.each(items, function(val, index) { 233 | val = flattenComments(val); 234 | var obj = {}; 235 | //item title/desc/etc. 236 | obj.title = getTitle(val.title); 237 | obj.desc = getDesc(val.description); 238 | obj.link = getURL(val.link); 239 | obj.category = getCategories(val.category); 240 | 241 | //since we are going to format the date, we want to make sure it exists 242 | var pubDate = val.pubDate || val['dc:date']; 243 | if (pubDate) { 244 | if (_.isArray(pubDate)) { 245 | pubDate = _.first(pubDate); 246 | } 247 | //lets try basic js date parsing for now 248 | obj.date = Date.parse(pubDate); 249 | } 250 | 251 | //now lets handel the GUID 252 | if (val.guid) { 253 | var link = val.guid; 254 | var isPermaLink = true; 255 | if (_.isArray(link)) { 256 | link = _.first(link); 257 | } 258 | if (_.isObject(link) && _.isString(link._)) { 259 | link = link._; 260 | } 261 | obj.guid = {'link': link, isPermaLink: isPermaLink}; 262 | } 263 | 264 | //grab media content if exists 265 | if (val['media:content']) { 266 | var content = val['media:content']; 267 | if (_.isArray(content)) { 268 | content = _.first(content); 269 | } 270 | if (_.isObject(content) && content.$) { 271 | content = content.$; 272 | } 273 | obj.media = val.media || {}; 274 | obj.media.content = content; 275 | } 276 | //grab thumbnail if exists 277 | if (val['media:thumbnail']) { 278 | var thumbnail = val['media:thumbnail']; 279 | if (_.isArray(thumbnail)) { 280 | thumbnail = _.first(thumbnail); 281 | } 282 | if (_.isObject(thumbnail) && thumbnail.$) { 283 | thumbnail = thumbnail.$; 284 | } 285 | obj.media = val.media || {}; 286 | obj.media.thumbnail = thumbnail; 287 | } 288 | //now push the obj onto the stack 289 | output.items.push(obj); 290 | }); 291 | } 292 | return output; 293 | } 294 | 295 | //formats the ATOM feed to the needed output 296 | function formatATOM(json) { 297 | var output = {'type': 'atom', metadata: {}, items: []}; 298 | 299 | //Start with the metadata for the feed 300 | var metadata = {}; 301 | var channel = json.feed || json; 302 | 303 | //Channel title 304 | if (channel.title) { 305 | metadata.title = getTitle(channel.title); 306 | } 307 | 308 | //Channel description 309 | if (channel.subtitle) { 310 | metadata.desc = getDesc(channel.subtitle); 311 | } 312 | 313 | //Channel URL 314 | if (channel.link) { 315 | metadata.url = getURL(channel.link); 316 | } 317 | 318 | //Channel id 319 | if (channel.id) { 320 | metadata.id = channel.id; 321 | if (_.isArray(metadata.id)) { 322 | metadata.id = _.first(metadata.id); 323 | } 324 | } 325 | 326 | //Channel update date/time 327 | if (channel.updated) { 328 | metadata.update = channel.updated; 329 | if (_.isArray(metadata.update)) { 330 | metadata.update = _.first(metadata.update); 331 | } 332 | } 333 | 334 | //Channel author info 335 | if (channel.author) { 336 | metadata.author = channel.author; 337 | if (_.isArray(metadata.author)) { 338 | metadata.author = _.first(metadata.author); 339 | } 340 | if (_.isObject(metadata.author)) { 341 | _.each(metadata.author, function(val, prop) { 342 | if (_.isArray(val)) { 343 | val = _.first(val); 344 | } 345 | if (_.isObject(val) && val.$) { 346 | val = val.$; 347 | } 348 | metadata.author[prop] = val; 349 | }); 350 | } 351 | } 352 | 353 | output.metadata = metadata; 354 | //just double check that it exists and that it is an array 355 | if (channel.entry) { 356 | if (!_.isArray(channel.entry)) { 357 | channel.entry = [channel.entry]; 358 | } 359 | _.each(channel.entry, function(val, index) { 360 | val = flattenComments(val); 361 | var obj = {}; 362 | //item id 363 | obj.id = val.id; 364 | if (_.isArray(obj.id)) { 365 | obj.id = _.first(obj.id); 366 | } 367 | 368 | //item title/desc/etc. 369 | if (!val.title) { 370 | console.log(json); 371 | } 372 | obj.title = getTitle(val.title); 373 | obj.desc = getDesc(val.content || val.summary); 374 | obj.link = getURL(val.link); 375 | obj.category = getCategories(val.category); 376 | 377 | //since we are going to format the date, we want to make sure it exists 378 | var pubDate = val.published || val.pubDate; 379 | if (pubDate) { 380 | if (_.isArray(pubDate)) { 381 | pubDate = _.first(pubDate); 382 | } 383 | //lets try basic js date parsing for now 384 | obj.date = Date.parse(pubDate); 385 | } 386 | if (val.updated) { 387 | if (_.isArray(val.updated)) { 388 | val.updated = _.first(val.updated); 389 | } 390 | //lets try basic js date parsing for now 391 | obj.updated = Date.parse(val.updated); 392 | } 393 | 394 | //grab thumbnail if exists 395 | if (val['media:thumbnail']) { 396 | var thumbnail = val['media:thumbnail']; 397 | if (_.isArray(thumbnail)) { 398 | thumbnail = _.first(thumbnail); 399 | } 400 | if (_.isObject(thumbnail) && thumbnail.$) { 401 | thumbnail = thumbnail.$; 402 | } 403 | obj.media = val.media || {}; 404 | obj.media.thumbnail = thumbnail; 405 | } 406 | //grab media content if exists 407 | if (val['media:content']) { 408 | var content = val['media:content']; 409 | if (_.isArray(content)) { 410 | content = _.first(content); 411 | } 412 | if (_.isObject(content) && content.$) { 413 | content = content.$; 414 | } 415 | obj.media = val.media || {}; 416 | obj.media.content = content; 417 | } 418 | //now push the obj onto the stack 419 | output.items.push(obj); 420 | }); 421 | } 422 | return output; 423 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blindparser", 3 | "version": "1.0.3", 4 | "description": "blindparser is an all purpose RSS/ATOM feed parser that parses feeds into a common format so that you do not have to care if they are RSS or ATOM feeds.", 5 | "keywords": [ 6 | "rss", 7 | "atom", 8 | "feed", 9 | "parser" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "http://github.com/53seven/node-blindparser.git" 15 | }, 16 | "author": { 17 | "name": "kiernan", 18 | "email": "kiernan@537.io", 19 | "url": "537.io" 20 | }, 21 | "dependencies": { 22 | "lodash": "^4.13.1", 23 | "request": "^2.72.0", 24 | "xml2js": "^0.4.16" 25 | }, 26 | "devDependencies": { 27 | "vows": "*" 28 | }, 29 | "scripts": { 30 | "test": "vows --spec" 31 | }, 32 | "main": "./lib/feed.js" 33 | } 34 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // test.js 2 | var vows = require('vows'); 3 | var assert = require('assert'); 4 | 5 | var parser = require('../lib/feed.js'); 6 | 7 | 8 | vows.describe('bindparser').addBatch({ 9 | 'rss tests': { 10 | topic: function() { 11 | parser.parseURL('http://rss.cnn.com/rss/cnn_topstories.rss', {}, this.callback); 12 | }, 13 | 'response is not null': function(err, docs) { 14 | assert.isNull(err); 15 | assert.isNotNull(docs); 16 | }, 17 | 'response is properly formatted': function(err, docs) { 18 | assert.equal(docs.type, 'rss'); 19 | assert.isObject(docs.metadata); 20 | assert.isString(docs.metadata.title); 21 | assert.isString(docs.metadata.desc); 22 | assert.isString(docs.metadata.url); 23 | assert.isString(docs.metadata.lastBuildDate); 24 | assert.isString(docs.metadata.update); 25 | assert.isString(docs.metadata.ttl); 26 | assert.isArray(docs.metadata.image); 27 | }, 28 | 'response contains items': function(err, docs) { 29 | assert.isArray(docs.items); 30 | assert.ok(docs.items.length > 0); 31 | var item = docs.items[0]; 32 | assert.isString(item.title); 33 | assert.isString(item.desc); 34 | assert.isArray(item.category); 35 | assert.isString(item.link); 36 | assert.isNumber(item.date); 37 | assert.isObject(item.guid); 38 | assert.isString(item.guid.link); 39 | } 40 | }, 41 | 'atom tests': { 42 | topic: function() { 43 | parser.parseURL('http://www.blogger.com/feeds/10861780/posts/default', {}, this.callback); 44 | }, 45 | 'response is not null': function(err, docs) { 46 | assert.isNull(err); 47 | assert.isNotNull(docs); 48 | }, 49 | 'response is properly formatted': function(err, docs) { 50 | assert.equal(docs.type, 'atom'); 51 | assert.isObject(docs.metadata); 52 | assert.isString(docs.metadata.title); 53 | assert.isString(docs.metadata.desc); 54 | assert.isString(docs.metadata.url); 55 | assert.isString(docs.metadata.id); 56 | assert.isString(docs.metadata.update); 57 | assert.isObject(docs.metadata.author); 58 | }, 59 | 'response contains items': function(err, docs) { 60 | assert.isArray(docs.items); 61 | assert.ok(docs.items.length > 0); 62 | var item = docs.items[0]; 63 | assert.isString(item.id); 64 | assert.isString(item.title); 65 | assert.isString(item.desc); 66 | assert.isArray(item.category); 67 | assert.isString(item.link); 68 | assert.isNumber(item.date); 69 | assert.isNumber(item.updated); 70 | assert.isObject(item.media); 71 | assert.isObject(item.media.thumbnail); 72 | } 73 | }, 74 | 'additional atom tests': { 75 | topic: function() { 76 | parser.parseURL('http://code.visualstudio.com/feed.xml', {}, this.callback); 77 | }, 78 | 'response is not null': function(err, docs) { 79 | assert.isNull(err); 80 | assert.isNotNull(docs); 81 | }, 82 | 'response is properly formatted': function(err, docs) { 83 | assert.equal(docs.type, 'atom'); 84 | assert.isObject(docs.metadata); 85 | assert.isString(docs.metadata.title); 86 | assert.isString(docs.metadata.url); 87 | assert.isString(docs.metadata.id); 88 | assert.isString(docs.metadata.update); 89 | }, 90 | 'response contains items': function(err, docs) { 91 | assert.isArray(docs.items); 92 | assert.ok(docs.items.length > 0); 93 | var item = docs.items[0]; 94 | assert.isString(item.id); 95 | assert.isString(item.title); 96 | assert.isString(item.desc); 97 | assert.isArray(item.category); 98 | assert.isString(item.link); 99 | assert.isNumber(item.updated); 100 | } 101 | }, 102 | 'feedburner tests': { 103 | topic: function() { 104 | parser.parseURL('http://feeds.feedburner.com/TechCrunch', this.callback); 105 | }, 106 | 'response is not null': function(err, docs) { 107 | assert.isNull(err); 108 | assert.isNotNull(docs); 109 | }, 110 | 'response is formatted as rss': function(err, docs) { 111 | assert.equal(docs.type, 'rss'); 112 | assert.isObject(docs.metadata); 113 | assert.isArray(docs.items); 114 | }, 115 | 'response contains items': function(err, docs) { 116 | assert.isArray(docs.items); 117 | assert.ok(docs.items.length > 0); 118 | }, 119 | 'response contains images': function(err, docs) { 120 | assert.ok(docs.metadata.image); 121 | } 122 | }, 123 | 'oddities': { 124 | 'empty xml': { 125 | topic: function() { 126 | parser.parseString('', {}, this.callback); 127 | }, 128 | 'returns an error': function(err, docs) { 129 | assert.instanceOf(err, Error); 130 | assert.isUndefined(docs); 131 | }, 132 | }, 133 | 'bad status code': { 134 | topic: function() { 135 | parser.parseURL('http://google.com/notafile', this.callback); 136 | }, 137 | 'returns an error': function(err, docs) { 138 | assert.instanceOf(err, Error); 139 | assert.isUndefined(docs); 140 | }, 141 | }, 142 | 'non xml url': { 143 | topic: function() { 144 | parser.parseURL('http://google.com', {}, this.callback); 145 | }, 146 | 'returns an error': function(err, docs) { 147 | assert.instanceOf(err, Error); 148 | assert.isUndefined(docs); 149 | } 150 | } 151 | }, 152 | 'craigslist': { 153 | topic: function() { 154 | parser.parseURL('http://portland.craigslist.org/sof/index.rss', this.callback); 155 | }, 156 | 'response is formatted as rss': function(err, docs) { 157 | assert.equal(docs.type, 'rss'); 158 | assert.isObject(docs.metadata); 159 | assert.isArray(docs.items); 160 | }, 161 | 'response contains items': function(err, docs) { 162 | assert.isArray(docs.items); 163 | assert.ok(docs.items.length > 0); 164 | }, 165 | 'response items have titles': function(err, docs) { 166 | assert.isArray(docs.items); 167 | assert.ok(docs.items.length > 0); 168 | assert.isNotNull(docs.items[0].title); 169 | }, 170 | 'response items have links': function(err, docs) { 171 | assert.isArray(docs.items); 172 | assert.ok(docs.items.length > 0); 173 | assert.isNotNull(docs.items[0].link); 174 | }, 175 | 'response items have desc': function(err, docs) { 176 | assert.isArray(docs.items); 177 | assert.ok(docs.items.length > 0); 178 | assert.isNotNull(docs.items[0].desc); 179 | }, 180 | 'response items have date': function(err, docs) { 181 | assert.isArray(docs.items); 182 | assert.ok(docs.items.length > 0); 183 | assert.isNotNull(docs.items[0].date); 184 | } 185 | } 186 | }).export(module); 187 | --------------------------------------------------------------------------------