Browse Source

Add linkPreview route, tests and implementation

sbkwgh 7 years ago
parent
commit
1356d55090

+ 15 - 106
frontend/src/assets/js/linkExpander.js

@@ -7,52 +7,6 @@ export default {
 			let parsed = document.createElement('div');
 			parsed.innerHTML = HTML;
 
-			let expandPatterns = {
-				'wikipedia': {
-					hostnameRegExp: /[a-z]+\.wikipedia\.org/,
-					pathnameRegExp: /\/wiki\/.+/,
-					getURL (link) {
-						let page = link.pathname.split('/').slice(-1)[0];
-						let countryVersion = link.hostname.split('.')[0];
-						
-						return `https://${countryVersion}.wikipedia.org/api/rest_v1/page/summary/${page}?redirect=true`;
-					},
-					getContent (link, data) {
-						let content = data.extract.slice(0, 500).trim();
-
-						return {
-							title: data.titles.display,
-							URL: data.content_urls.desktop.page,
-							content: content.length > 500 ? content + '...' : content
-						}
-					}
-				},
-				'github': {
-					hostnameRegExp: /github\.com/,
-					pathnameRegExp: /\/.+\/.+/,
-					getURL (link) {
-						return 'https://api.github.com/repos' + link.pathname;
-					},
-					getContent (link, data) {
-						return {
-							title: data.full_name,
-							URL: data.html_url,
-							content: data.description
-						}
-					}
-				},
-				'twitter': {
-					hostnameRegExp: /twitter\.com/,
-					pathnameRegExp: /\/.+\/status\/\d+/,
-					getURL (link) {
-						return '/api/v1/link_expansion/twitter?url=' + link.href;
-					},
-					getContent (link, data) {
-						return data.html;
-					}
-				}
-			};
-
 			let links = Array
 				.from(parsed.querySelectorAll('p a[href]'))
 				.filter(a => {
@@ -63,69 +17,24 @@ export default {
 					)
 				});
 
-			let expandableLinks = {};
-
 			links.forEach(link => {
-				for(let expandName in expandPatterns) {
-					let expand = expandPatterns[expandName];
-
-					if(
-						expand.hostnameRegExp.test(link.hostname) &&
-						expand.pathnameRegExp.test(link.pathname)
-					) {
-						if(!expandableLinks[expandName]) {
-							expandableLinks[expandName] = [];
+				Vue.axios
+					.get('/api/v1/link_preview?url=' + link.href)
+					.then(res => {
+						if(res.data.length) {
+							let div = document.createElement('div');
+							div.innerHTML = res.data;
+
+							link.parentNode.replaceChild(
+								div.children[0],
+								link
+							);
 						}
 
-						expandableLinks[expandName].push(link);
-
-						break;
-					}
-				}
-			});
-
-			for(let expandName in expandableLinks) {
-				let expandPattern = expandPatterns[expandName];
-
-				expandableLinks[expandName].forEach(link => {
-					let URL = expandPattern.getURL(link);
-
-					Vue.axios
-						.get(URL)
-						.then(res => {
-							let content = expandPattern.getContent(link, res.data);
-							let h = document.createElement.bind(document);
-							let div = h('div');
-
-							if(typeof content === 'string') {
-								div.innerHTML = content;
-							} else {
-								let h2 = h('h2');
-								let a = h('a');
-								let span = h('span');
-								let textNode = document.createTextNode(content.content);
-
-								a.textContent = content.title;
-								a.href = content.URL;
-								a.setAttribute('target', '_blank');
-								a.setAttribute('rel', 'noopener noreferrer');
-								span.textContent = 'from ' + link.hostname;
-
-								h2.appendChild(a);
-								h2.appendChild(span);
-								div.appendChild(h2)
-								div.appendChild(textNode)
-
-								div.classList.add('expanded_link');
-							}
-
-							link.parentNode.replaceChild(div, link);
-
-							completedAPICall();
-						})
-						.catch(completedAPICall);
-				});
-			}
+						completedAPICall();
+					})
+					.catch(completedAPICall);
+			})
 
 			let completed = 0;
 			let completedAPICall = () => {

+ 18 - 8
frontend/src/assets/scss/elementStyles.scss

@@ -241,22 +241,32 @@ b, strong {
 	}
 }
 
-.expanded_link {
+.link_preview {
 	border: thick solid $color__gray--primary;
-	padding: 0.5rem;
+	padding: 1rem;
 
-	h2 {
+	h1, h2, p {
 		margin: 0;
-		margin-bottom: 0.5rem;
-		font-size: 1.25rem;
 	}
 
-	span {
-		margin-left: 0.25rem;
+	h1 {
+		font-size: 1.25rem;
+	}
+	h2 {
 		font-size: 1rem;
-		font-weight: 400;
+		font-weight: normal;
 		color: $color__darkgray--primary;
 	}
+	p {
+		font-weight: 300;
+		display: flex;
+		margin-top: 0.5rem;
+	}
+	img {
+		max-width: 100px;
+		max-height: 100px;
+		margin-right: 0.5rem;
+	}
 }
 
 blockquote.twitter-tweet {

+ 42 - 0
lib/linkPreview/getOGPreviewData.js

@@ -0,0 +1,42 @@
+let cheerio = require('cheerio');
+let axios = require('axios');
+
+module.exports = async function getOGPreviewData (url) {
+	try {
+		let res = await axios.get(url);
+		let $ = cheerio.load(res.data);
+
+		let OG = {
+			title: $('meta[property="og:title"]'),
+			url: $('meta[property="og:url"]'),
+			image: $('meta[property="og:image"]'),
+			description: $('meta[property="og:description"]')
+		};
+
+		let alternative = {
+			title: $('title'),
+			description: $('meta[name="description"]')
+		};
+
+		let data = {};
+
+		if(OG.title.length && OG.url.length) {
+			data.title = OG.title.attr('content'); 
+			data.url = OG.url.attr('content');
+
+			if(OG.image) data.image = OG.image.attr('content');
+			if(OG.description) data.description = OG.description.attr('content');
+
+			return data;
+		} else if(alternative.title.length && alternative.description.length) {
+			data.title = alternative.title.text();
+			data.description = alternative.description.attr('content');
+		} else {
+			return null;
+		}
+
+		return data;
+	} catch (e) {
+		return null;
+	}
+}

+ 30 - 0
lib/linkPreview/getPreviewHTML.js

@@ -0,0 +1,30 @@
+let url = require('url');
+let ejs = require('ejs');
+
+module.exports = function getPreviewHTML (data) {
+	let template = `
+		<div class='link_preview'>
+			<h1>
+				<a href='<%= url %>' target='_blank' rel='noopener noreferrer'>
+					<%= title %>
+				</a>
+			</h1>
+			<h2>
+				from <%= hostname %>
+			</h2>
+			<% if(locals.image || locals.description) { %>
+				<p>
+					<% if(locals.image) { %>
+						<img src='<%= image %>'>
+					<% } %>
+					<% if(locals.description) { %>
+						<%= description %>
+					<% } %>
+				</p>
+			<% } %>
+		</div>
+	`;
+
+	data.hostname = url.parse(data.url).hostname;
+	return ejs.render(template, data);
+}

+ 40 - 0
lib/linkPreview/index.js

@@ -0,0 +1,40 @@
+let fs = require('fs');
+let path = require('path')
+
+let getOGPreviewData = require('./getOGPreviewData');
+let getPreviewHTML = require('./getPreviewHTML');
+let previewPatterns = [];
+
+fs.readdirSync(path.join(__dirname, 'patterns'), (err, files) => {
+	if(!err) {
+		previewPatterns = files.map(file => {
+			return require(path.join(__dirname, file));
+		});
+	}
+});
+
+module.exports =  async function linkPreview(url) {
+	let previewData;
+
+	for(let pattern of previewPatterns) {
+		if(pattern.matches(url)) {
+			previewData = await pattern.getPreviewData(url);
+			break;
+		}
+	}
+
+	//If the url doesn't match a pattern for a specific
+	//site, try getting a possible preview using OG tags
+	if(!previewData) previewData = await getOGPreviewData(url);
+
+	//If there is some data scraped from the site for a
+	//preview, generate a HTML string
+	//Otherwise return an empty string
+	if(typeof previewData === 'object' && previewData !== null) {
+		return getPreviewHTML(previewData);
+	} else if(typeof previewData === 'string') {
+		return previewData;
+	} else {
+		return '';
+	}
+}

+ 23 - 0
lib/linkPreview/patterns/github.js

@@ -0,0 +1,23 @@
+let url = require('url');
+let axios = require('axios');
+
+module.exports = {
+	matches (url) {
+		return url.match(/^https?:\/\/(www\.)?github\.com\/.+\/.+/);
+	},
+	async getPreviewData (link_url) {
+		try {
+			let pathname = url.parse(link_url).pathname;
+			let res = await axios.get('https://api.github.com/repos' + pathname);
+
+			return {
+				title: res.data.full_name,
+				url: res.data.html_url,
+				description: res.data.description
+			};
+		} catch (e) {
+			console.log(e)
+			return null;
+		}
+	}
+};

+ 16 - 0
lib/linkPreview/patterns/twitter.js

@@ -0,0 +1,16 @@
+let axios = require('axios');
+
+module.exports = {
+	matches (url) {
+		return url.match(/^https?:\/\/(www\.)?twitter\.com\/.+\/status\/\d+/);
+	},
+	async getPreviewData (url) {
+		try {
+			let res = await axios.get('https://publish.twitter.com/oembed?url=' + url);
+			return res.data.html;
+		} catch (e) {
+			console.log(e)
+			return null;
+		}
+	}
+};

+ 26 - 0
lib/linkPreview/patterns/wikipedia.js

@@ -0,0 +1,26 @@
+let url = require('url');
+let axios = require('axios');
+
+module.exports = {
+	matches (url) {
+		return url.match(/^https?:\/\/[a-z]+\.wikipedia\.org\/wiki\/.+/);
+	},
+	async getPreviewData (link_url) {
+		try {
+			let parsedUrl = url.parse(link_url);
+			let page = parsedUrl.pathname.split('/').slice(-1)[0];
+			let countryVersion = parsedUrl.hostname.split('.')[0];
+						
+			let res = await axios.get(`https://${countryVersion}.wikipedia.org/api/rest_v1/page/summary/${page}?redirect=true`);
+			let content = res.data.extract.slice(0, 500).trim();
+
+			return {
+				title: res.data.titles.display,
+				url: res.data.content_urls.desktop.page,
+				description: content.length < 500 ? content : content + '...'
+			}
+		} catch (e) {
+			return null;
+		}
+	}
+};

+ 150 - 0
package-lock.json

@@ -9,6 +9,11 @@
       "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.3.tgz",
       "integrity": "sha512-unWrGQhXRrNTk/MabfBLCM/rWT6zxR1OSK0GIPxsP8NX8mJcsNWkERPp4z0pTyHLiANy+Nwczf8Q2I16Pth1FA=="
     },
+    "@types/node": {
+      "version": "9.4.7",
+      "resolved": "http://registry.npmjs.org/@types/node/-/node-9.4.7.tgz",
+      "integrity": "sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw=="
+    },
     "abbrev": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz",
@@ -113,6 +118,15 @@
       "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
       "dev": true
     },
+    "axios": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
+      "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
+      "requires": {
+        "follow-redirects": "1.4.1",
+        "is-buffer": "1.1.5"
+      }
+    },
     "backo2": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@@ -189,6 +203,11 @@
         "type-is": "1.6.15"
       }
     },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
+    },
     "brace-expansion": {
       "version": "1.1.8",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
@@ -333,6 +352,26 @@
         "supports-color": "2.0.0"
       }
     },
+    "cheerio": {
+      "version": "1.0.0-rc.2",
+      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
+      "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=",
+      "requires": {
+        "css-select": "1.2.0",
+        "dom-serializer": "0.1.0",
+        "entities": "1.1.1",
+        "htmlparser2": "3.9.2",
+        "lodash": "4.17.5",
+        "parse5": "3.0.3"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="
+        }
+      }
+    },
     "chownr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
@@ -591,6 +630,22 @@
         "which": "1.3.0"
       }
     },
+    "css-select": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
+      "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
+      "requires": {
+        "boolbase": "1.0.0",
+        "css-what": "2.1.0",
+        "domutils": "1.5.1",
+        "nth-check": "1.0.1"
+      }
+    },
+    "css-what": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
+      "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0="
+    },
     "d": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
@@ -747,6 +802,44 @@
       "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz",
       "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI="
     },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+      "requires": {
+        "domelementtype": "1.1.3",
+        "entities": "1.1.1"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+          "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
+        }
+      }
+    },
+    "domelementtype": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+      "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI="
+    },
+    "domhandler": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz",
+      "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=",
+      "requires": {
+        "domelementtype": "1.3.0"
+      }
+    },
+    "domutils": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+      "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
+      "requires": {
+        "dom-serializer": "0.1.0",
+        "domelementtype": "1.3.0"
+      }
+    },
     "dont-sniff-mimetype": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz",
@@ -815,6 +908,11 @@
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
       "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
     },
+    "ejs": {
+      "version": "2.5.7",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.7.tgz",
+      "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo="
+    },
     "encodeurl": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
@@ -926,6 +1024,11 @@
         "wtf-8": "1.0.0"
       }
     },
+    "entities": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+      "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
+    },
     "error-ex": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
@@ -1230,6 +1333,24 @@
       "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz",
       "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU="
     },
+    "follow-redirects": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz",
+      "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==",
+      "requires": {
+        "debug": "3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
     "for-in": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -1725,6 +1846,19 @@
       "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.1.0.tgz",
       "integrity": "sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA=="
     },
+    "htmlparser2": {
+      "version": "3.9.2",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
+      "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
+      "requires": {
+        "domelementtype": "1.3.0",
+        "domhandler": "2.4.1",
+        "domutils": "1.5.1",
+        "entities": "1.1.1",
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
     "http-errors": {
       "version": "1.6.2",
       "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
@@ -2678,6 +2812,14 @@
         "path-key": "2.0.1"
       }
     },
+    "nth-check": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
+      "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=",
+      "requires": {
+        "boolbase": "1.0.0"
+      }
+    },
     "number-is-nan": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
@@ -2851,6 +2993,14 @@
       "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
       "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
     },
+    "parse5": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
+      "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
+      "requires": {
+        "@types/node": "9.4.7"
+      }
+    },
     "parsejson": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",

+ 3 - 0
package.json

@@ -14,11 +14,14 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
+    "axios": "^0.18.0",
     "bcryptjs": "^2.4.3",
     "body-parser": "^1.16.0",
+    "cheerio": "^1.0.0-rc.2",
     "compression": "^1.7.0",
     "connect-session-sequelize": "^5.1.0",
     "cross-env": "^3.1.4",
+    "ejs": "^2.5.7",
     "express": "^4.14.1",
     "express-session": "^1.15.1",
     "helmet": "^3.9.0",

+ 0 - 38
routes/link_expansion.js

@@ -1,38 +0,0 @@
-let https = require('https');
-let express = require('express');
-let router = express.Router();
-
-const Errors = require('../lib/errors.js');
-
-function getJsonHTTPS (url, cb) {
-	https.get(url, res => {
-		if(res.statusCode === 200) {
-			let chunks = [];
-
-			res.on('data', chunk => chunks.push(chunk));
-			res.on('end', () => {
-				let data = Buffer.concat(chunks).toString();
-				let json = JSON.parse(data);
-
-				cb(null, json);
-			})
-		} else {
-			cb(
-				new Error(`Request Failed.\nStatus Code: ${res.statusCode}`)
-			);
-		}
-	})
-}
-
-router.get('/twitter', async (req, res, next) => {
-	let url = 'https://publish.twitter.com/oembed?url=' + req.query.url
-	getJsonHTTPS(url, (err, data) => {
-		if(err) {
-			next(Errors.unknown);
-		} else {
-			res.json(data);
-		}
-	});
-});
-
-module.exports = router;

+ 16 - 0
routes/link_preview.js

@@ -0,0 +1,16 @@
+let linkPreview = require('../lib/linkPreview');
+let express = require('express');
+let router = express.Router();
+
+const Errors = require('../lib/errors.js');
+
+router.get('/', async (req, res, next) => {
+	try {
+		let HTML = await linkPreview(req.query.url);
+		res.send(HTML);
+	} catch (e) {
+		next(e);
+	}
+});
+
+module.exports = router;

+ 1 - 1
server.js

@@ -49,7 +49,7 @@ app.use('/api/v1/ban', require('./routes/ban'))
 app.use('/api/v1/search', require('./routes/search'))
 app.use('/api/v1/log', require('./routes/log'))
 app.use('/api/v1/poll', require('./routes/poll'))
-app.use('/api/v1/link_expansion', require('./routes/link_expansion'))
+app.use('/api/v1/link_preview', require('./routes/link_preview'))
 
 app.use('/static', express.static(path.join(__dirname, 'frontend', 'dist', 'static')))
 app.get('*', (req, res) => {

+ 144 - 0
test/link_preview.js

@@ -0,0 +1,144 @@
+process.env.NODE_ENV = 'test';
+
+let chai = require('chai');
+let server = require('../server');
+let should = chai.should();
+let expect = chai.expect;
+
+let getOGPreviewData = require('../lib/linkPreview/getOGPreviewData');
+let getPreviewHTML = require('../lib/linkPreview/getPreviewHTML');
+let linkPreview = require('../lib/linkPreview');
+
+let github = require('../lib/linkPreview/patterns/github');
+let wikipedia = require('../lib/linkPreview/patterns/wikipedia');
+let twitter = require('../lib/linkPreview/patterns/twitter');
+
+const Errors = require('../lib/errors.js');
+
+chai.use(require('chai-http'));
+chai.use(require('chai-things'));
+
+
+describe('link_expansion', () => {
+	//Wait for app to start before commencing
+	before((done) => {
+		if(server.locals.appStarted) done();
+		server.on('appStarted', done);
+	});
+
+	describe('getOGPreviewData', () => {
+		it('should return an object containing relevant OG data', async () => {
+			let data = await getOGPreviewData('https://www.theguardian.com/news/2018/mar/17/cambridge-analytica-facebook-influence-us-election')
+
+			data.should.have.property(
+				'title',
+				'Revealed: 50 million Facebook profiles harvested for Cambridge Analytica in major data breach'
+			);
+			data.should.have.property(
+				'description',
+				'Whistleblower describes how firm linked to former Trump adviser Steve Bannon compiled user data to target American voters• How Cambridge Analytica’s algorithms turned ‘likes’ into a political tool'
+			);
+			data.should.have.property(
+				'url',
+				'http://www.theguardian.com/news/2018/mar/17/cambridge-analytica-facebook-influence-us-election'
+			);
+			data.should.have.property(
+				'image',
+				'https://i.guim.co.uk/img/media/97532076a6935a1e79eba294437ed91f3eb4df6b/0_626_4480_2688/master/4480.jpg?w=1200&h=630&q=55&auto=format&usm=12&fit=crop&crop=faces%2Centropy&bm=normal&ba=bottom%2Cleft&blend64=aHR0cHM6Ly91cGxvYWRzLmd1aW0uY28udWsvMjAxOC8wMS8zMS9mYWNlYm9va19kZWZhdWx0LnBuZw&s=365825fe053733ae12f9b050f5374594'
+			);
+		});
+		it('should use other meta or title tags if there is no OG tags availible', async () => {
+			let data = await getOGPreviewData('http://ejs.co');
+			data.should.have.property('title', 'EJS -- Embedded JavaScript templates');
+			data.should.have.property(
+				'description',
+				"'E' is for 'effective'. EJS is a simple templating language that lets you generate HTML markup with plain JavaScript. No religiousness about how to organize things. No reinvention of iteration and control-flow. It's just plain JavaScript."
+			);
+		});
+		it('should return null if there is no OG tags availible', async () => {
+			let data = await getOGPreviewData('http://blank.org');
+			expect(data).to.be.null;
+		});
+	});
+
+	describe('getPreviewHTML', () => {
+		it('should return an HTML string for given object', () => {
+			let HTML = getPreviewHTML({
+				url: 'http://www.example.com',
+				description: 'description',
+				title: 'title',
+				image: 'image'
+			});
+
+			(typeof HTML).should.equal('string');
+		})
+		it('should correctly deal with the conditional', () => {
+			let HTML = getPreviewHTML({
+				url: 'http://www.example.com',
+				description: 'description',
+				title: 'title'
+			});
+			(typeof HTML).should.equal('string');
+		})
+	});
+
+	describe('linkPreview', () => {
+		it('should get a HTML string from an OG link', async () => {
+			let HTML = await linkPreview('https://www.theguardian.com/news/2018/mar/17/cambridge-analytica-facebook-influence-us-election');
+
+			(typeof HTML).should.equal('string');
+		});
+		it('should return an empty string from an invalid site', async () => {
+			let HTML = await linkPreview('http://blank.org');
+
+			(typeof HTML).should.equal('string');
+		});
+	});
+
+	describe('GitHub', () => {
+		it('should match a valid GitHub url', () => {
+			github.matches('https://github.com/sbkwgh/forum').should.not.be.null;
+			github.matches('http://github.com/sbkwgh/forum').should.not.be.null;
+			
+			expect(github.matches('http://notgithub.com/sbkwgh/forum')).to.be.null;
+		});
+		it('should return a data object', async () => {
+			let data = await github.getPreviewData('https://github.com/sbkwgh/forum');
+
+			data.should.have.property('title', 'sbkwgh/forum')
+			data.should.have.property('url', 'https://github.com/sbkwgh/forum')
+			data.should.have.property('description', 'Forum software created using Express, Vue, and Sequelize')
+		});
+	});
+
+	describe('Wikipedia', () => {
+		it('should match a valid Wikipedia url', () => {
+			wikipedia.matches('https://en.wikipedia.org/wiki/google').should.not.be.null;
+			wikipedia.matches('http://fr.wikipedia.org/wiki/google').should.not.be.null;
+			
+			expect(wikipedia.matches('http://en.wikipedia.org/notapage')).to.be.null;
+		});
+		it('should return a data object', async () => {
+			let data = await wikipedia.getPreviewData('https://en.wikipedia.org/wiki/google');
+
+			data.should.have.property('title', 'Google')
+			data.should.have.property('url', 'https://en.wikipedia.org/wiki/Google')
+			data.description.should.have.length(503)
+		});
+	});
+
+	describe('Twitter', () => {
+		it('should match a valid Wikipedia url', () => {
+			twitter.matches('https://twitter.com/user/status/12345').should.not.be.null;
+			
+			expect(twitter.matches('http://twitter.com/notapage/123456')).to.be.null;
+			expect(twitter.matches('http://twitter.com/notapage/status/qwertyu')).to.be.null;
+		});
+		it('should return a data object', async () => {
+			let HTML = await twitter.getPreviewData('https://twitter.com/Interior/status/463440424141459456');
+
+			(typeof HTML).should.equal('string');
+			HTML.should.have.length.above(0);
+		});
+	});
+})