From d2a6180cf6f1b0c141ffc286c4417acff9cd1334 Mon Sep 17 00:00:00 2001 From: Mark Tyers Date: Fri, 13 Sep 2019 21:48:57 +0100 Subject: [PATCH] created todo app with database --- .gitignore | 1 + exercises/07_unit_testing/database/index.js | 61 +++++++++ .../07_unit_testing/database/modules/todo.js | 45 +++++++ .../07_unit_testing/database/package.json | 19 +++ .../07_unit_testing/database/public/style.css | 11 ++ .../07_unit_testing/database/sqlite-async.js | 33 +++++ .../07_unit_testing/database/views/home.hbs | 33 +++++ exercises/07_unit_testing/todoold/index.js | 90 ------------- exercises/07_unit_testing/todoold/lists.js | 120 ------------------ .../07_unit_testing/todoold/package.json | 25 ---- .../07_unit_testing/todoold/test/todo-spec.js | 60 --------- 11 files changed, 203 insertions(+), 295 deletions(-) create mode 100644 exercises/07_unit_testing/database/index.js create mode 100644 exercises/07_unit_testing/database/modules/todo.js create mode 100644 exercises/07_unit_testing/database/package.json create mode 100644 exercises/07_unit_testing/database/public/style.css create mode 100644 exercises/07_unit_testing/database/sqlite-async.js create mode 100644 exercises/07_unit_testing/database/views/home.hbs delete mode 100755 exercises/07_unit_testing/todoold/index.js delete mode 100755 exercises/07_unit_testing/todoold/lists.js delete mode 100644 exercises/07_unit_testing/todoold/package.json delete mode 100755 exercises/07_unit_testing/todoold/test/todo-spec.js diff --git a/.gitignore b/.gitignore index ae2fd49a..008f8e67 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ website.db *.c9 messages.txt +todo.db diff --git a/exercises/07_unit_testing/database/index.js b/exercises/07_unit_testing/database/index.js new file mode 100644 index 00000000..c0bcccbd --- /dev/null +++ b/exercises/07_unit_testing/database/index.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +'use strict' + +const Koa = require('koa') +const Router = require('koa-router') +const stat = require('koa-static') +const bodyParser = require('koa-bodyparser') +const handlebars = require('koa-hbs-renderer') + +const app = new Koa() +const router = new Router() +app.use(stat('public')) +app.use(bodyParser()) +app.use(handlebars({ paths: { views: `${__dirname}/views` } })) +app.use(router.routes()) + +const port = 8080 +const dbName = 'todo.db' + +const ToDo = require('./modules/todo') + +router.get('/', async ctx => { + try { + const todo = await new ToDo(dbName) + const data = {} + if(ctx.query.msg) data.msg = ctx.query.msg + data.items = await todo.getAll() + console.log('all records') + console.log(data) + ctx.render('home', data) + } catch(err) { + console.log(err.message) + ctx.render('home', {msg: err.message}) + } +}) + +router.post('/', async ctx => { + try { + const todo = await new ToDo(dbName) + const body = ctx.request.body + todo.add(body.item, body.qty) + ctx.redirect('/') + } catch(err) { + console.log(err.message) + ctx.redirect(`/?msg=${err.message}`) + } +}) + +router.get('/delete/:key', ctx => { + try { + console.log(`key: ${ctx.params.key}`) + todo.delete(ctx.params.key) + ctx.redirect('/msg=item deleted') + } catch(err) { + console.log(err.message) + ctx.redirect(`/${err.message}`) + } +}) + +module.exports = app.listen(port, () => console.log(`listening on port ${port}`)) diff --git a/exercises/07_unit_testing/database/modules/todo.js b/exercises/07_unit_testing/database/modules/todo.js new file mode 100644 index 00000000..d2121b52 --- /dev/null +++ b/exercises/07_unit_testing/database/modules/todo.js @@ -0,0 +1,45 @@ + +'use strict' + +const sqlite = require('sqlite-async') + +module.exports = class ToDo { + + constructor(dbName = ':memory:') { + return (async() => { + this.db = await sqlite.open(dbName) + const sql = 'CREATE TABLE IF NOT EXISTS items(id INTEGER PRIMARY KEY AUTOINCREMENT, item TEXT, qty NUMERIC)' + await this.db.run(sql) + return this + })() + } + + async add(item, qty) { + qty = Number(qty) + if(isNaN(qty)) throw new Error('the quantity must be a number') + const sql = `INSERT INTO items(item, qty) VALUES("${item}", ${qty})` + await this.db.run(sql) + } + + async getAll() { + const sql = 'SELECT * FROM items' + const data = await this.db.all(sql) + console.log(data) + return data + } + + async delete(id) { + const sql = `DELETE FROM items WHERE id=${id}` + await this.db.run(sql) + } + + async countItems() { + const sql = 'SELECT COUNT(*) as items FROM items' + const data = await this.db.get(sql) + return data + } + +} + +// https://stackoverflow.com/questions/43431550/async-await-class-constructor +// https://www.sqlite.org/inmemorydb.html diff --git a/exercises/07_unit_testing/database/package.json b/exercises/07_unit_testing/database/package.json new file mode 100644 index 00000000..ba2b115b --- /dev/null +++ b/exercises/07_unit_testing/database/package.json @@ -0,0 +1,19 @@ +{ + "name": "database", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "koa": "^2.8.1", + "koa-bodyparser": "^4.2.1", + "koa-hbs-renderer": "^1.2.0", + "koa-router": "^7.4.0", + "koa-static": "^5.0.0", + "sqlite-async": "^1.0.12" + } +} diff --git a/exercises/07_unit_testing/database/public/style.css b/exercises/07_unit_testing/database/public/style.css new file mode 100644 index 00000000..8a5442c3 --- /dev/null +++ b/exercises/07_unit_testing/database/public/style.css @@ -0,0 +1,11 @@ + +body { + font-family: Arial, Helvetica, sans-serif; +} + +.msg { + border: 1px solid red; + font-weight: bold; + color: red; + padding: 1em; +} diff --git a/exercises/07_unit_testing/database/sqlite-async.js b/exercises/07_unit_testing/database/sqlite-async.js new file mode 100644 index 00000000..e2c2384f --- /dev/null +++ b/exercises/07_unit_testing/database/sqlite-async.js @@ -0,0 +1,33 @@ + +'use strict' +/* +const sqlite = require('sqlite-async') +const db = await sqlite.open('./website.db') +await db.run('xxx') +const records = await db.get('xxx') +await db.close() +*/ + +module.exports.open = async name => { + // opens a connection + console.log(name) + return new DB() +} + +class DB { + async run(query) { + // runs a query + console.log('run') + console.log(query) + } + async get(query) { + // runs a query and returns data + console.log('get') + console.log(query) + return ['bread', 'butter', 'cheese'] + } + async close() { + // closes the connection + console.log('close') + } +} diff --git a/exercises/07_unit_testing/database/views/home.hbs b/exercises/07_unit_testing/database/views/home.hbs new file mode 100644 index 00000000..1a9495f0 --- /dev/null +++ b/exercises/07_unit_testing/database/views/home.hbs @@ -0,0 +1,33 @@ + + + + + ToDo List + + + +

ToDo List

+

My List

+ {{#if msg}} +

{{msg}}

+ {{/if}} + {{#if items.length}} + + + + {{#each items}} + + {{/each}} +
Shopping List
ItemQtyAction
{{this.item}}{{this.qty}}delete
+ {{else}} +

Your list is empty, lets add some items...

+ {{/if}} +
+ Item: + + Qty: + + +
+ + diff --git a/exercises/07_unit_testing/todoold/index.js b/exercises/07_unit_testing/todoold/index.js deleted file mode 100755 index 4d2f9d24..00000000 --- a/exercises/07_unit_testing/todoold/index.js +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node -/* eslint no-magic-numbers: 0 */ - -'use strict' - -/* import the 'restify' module and create an instance. */ -const restify = require('restify') -const server = restify.createServer() - -/* import the required plugins to parse the body and auth header. */ -server.use(restify.fullResponse()) -server.use(restify.bodyParser()) -server.use(restify.authorizationParser()) - -/* import our custom module. */ -const lists = require('./lists.js') - -/* if we receive a GET request for the base URL redirect to /lists */ -server.get('/', (req, res, next) => { - res.redirect('/lists', next) -}) - -/* this route provides a URL for the 'lists' collection. It demonstrates how a single resource/collection can have multiple representations. */ -server.get('/lists', (req, res) => { - console.log('getting a list of all the lists') - /* we will be including URLs to link to other routes so we need the name of the host. Notice also that we are using an 'immutable variable' (constant) to store the host string since the value won't change once assigned. The 'const' keyword is new to ECMA6 and is supported in NodeJS. */ - const host = req.headers.host - console.log(host) - /* creating some empty variables */ - let data //type - /* is the client requesting xml data? The req.header object stores any headers passed in the request. The 'Accept' header lets the client express a preference for the format of the representation. Note you should always provide a sensible default. */ - if (req.header('Accept') === 'application/xml') { - data = lists.getAllXML(host) - } else { - data = lists.getAll(host) - } - /* we need to set the content-type to match the data we are sending. We then send the response code and body. Finally we signal the end of the response. */ - res.setHeader('content-type', data.contentType) - res.send(data.code, data.response) - res.end() -}) - -/* This route provides a URL for each list resource. It includes a parameter (indicated by a :). The string entered here is stored in the req.params object and can be used by the script. */ -server.get('/lists/:listID', (req, res) => { - console.log('getting a list based on its ID') - /* Here we store the id we want to retrieve in an 'immutable variable'. */ - const listID = req.params.listID - /* Notice that all the business logic is kept in the 'lists' module. This stops the route file from becoming cluttered and allows us to implement 'unit testing' (we cover this in a later topic) */ - const data = lists.getByID(listID) - res.setHeader('content-type', 'application/json') - res.send(data.code, data.response) - res.end() -}) - -/* This route points to the 'lists' collection. The POST method indicates that we indend to add a new resource to the collection. Any resource added to a collection using POST should be assigned a unique id by the server. This id should be returned in the response body. */ -server.post('/lists', (req, res) => { - console.log('adding a new list') - /* The req object contains all the data associated with the request received from the client. The 'body' property contains the request body as a string. */ - const body = req.body - /* Since we are using the authorization parser plugin we gain an additional object which contains the information from the 'Authorization' header extracted into useful information. Here we are displaying it in the console so you can understand its structure. */ - const auth = req.authorization - console.log(auth) - const data = lists.addNew(auth, body) - res.setHeader('content-type', data.contentType) - res.send(data.code, data.response) - res.end() -}) - -/* The PUT method is used to 'update' a named resource. This is not only used to update a named resource that already exists but is also used to create a NEW RESOURCE at the named URL. It's important that you understand how this differs from a POST request. */ -server.put('/lists/:listID', (req, res) => { - res.setHeader('content-type', 'application/json') - //res.send(data.code, {status: data.status, message: 'this should update the specified resource'}) - res.end() -}) - -/* The DELETE method removes the resource at the specified URL. */ -server.del('/lists/:listID', (req, res) => { - res.setHeader('content-type', 'application/json') - //res.send(data.code, {status: data.status, message: 'this should delete the specified resource'}) - res.end() -}) - -const port = process.env.PORT || 8080 -server.listen(port, err => { - if (err) { - console.error(err) - } else { - console.log('App is ready at : ' + port) - } -}) diff --git a/exercises/07_unit_testing/todoold/lists.js b/exercises/07_unit_testing/todoold/lists.js deleted file mode 100755 index ae00a951..00000000 --- a/exercises/07_unit_testing/todoold/lists.js +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env node -/* eslint no-magic-numbers: 0 */ - -'use strict' - -/* We import the packages needed for this module. These should be listed in the 'package.json' file and can then be imported automatically using 'npm install' */ -const rand = require('csprng') -const builder = require('xmlbuilder') - -/* This array is used to store the lists. In a production system you would need to implement some form of data persistence such as a database. */ -const lists = [] - -/* this is a private function that can only be accessed from inside the module. It checks that the json data supplied in the request body comprises a single array of strings. The parameter contains the string passed in the request body. */ -function validateJson(json) { - /* */ - if (typeof json.name !== 'string') { - console.log('name not a string') - return false - } - /* returns false if the list key is not an Array */ - if (!Array.isArray(json.list)) { - console.log('json.list is not an array') - return false - } - /* */ - for(const i=0; i ({name: item.name, link: `http://${host}/lists/${item.id}`})) - return {code: 200, contentType: 'application/json', response: {status: 'success', message: lists.length+' lists found', data: notes}} -} - -/* This is a sister property to 'getAll' but returns an XML representation instead. */ -exports.getAllXML = function(host) { - console.log('getAllXML') - /* Here the xmlbuilder module is used to construct an XML document. */ - const xml = builder.create('root', {version: '1.0', encoding: 'UTF-8', standalone: true}) - /* If there are no lists in the array we build a single message node. */ - if (lists.length === 0) { - xml.ele('message', {status: 'error'}, 'no lists found') - } else { - /* If there are lists we build a different message node. */ - xml.ele('message', {status: 'success'}, 'lists found') - /* Then we create a 'lists' node which will be used to contain each list node. */ - const xmlLists = xml.ele('lists', {count: lists.length}) - /* We now loop through the lists, create a list node for each and add the details. */ - for(const i=0; i < lists.length; i++) { - const list = xmlLists.ele('list', {id: lists[i].id}) - list.ele('name', lists[i].name) - list.ele('link', {href: `http://${host}/lists/${lists[i].id}`}) - } - } - /* Once the XML document is complete we call 'end()' to turn it into an XML string. */ - xml.end({pretty: true}) - /* There is a logical error in this code, can you spot it? */ - return {code: 200, contentType: 'application/xml', response: xml} -} - -/* This public property contains a function to add a new list to the module. */ -exports.addNew = function(auth, body) { - console.log('addNew') - /* The first parameter should contain the authorization data. We check that it contains an object called 'basic' */ - if (auth.basic === undefined) { - console.log('missing basic auth') - return {code: 401, contentType: 'application/json', response: { status: 'error', message: 'missing basic auth' }} - } - console.log('A') - /* In this simple example we have hard-coded the username and password. You should be storing this somewhere are looking it up. */ - if (auth.basic.username !== 'testuser' || auth.basic.password !== 'p455w0rd') { - console.log('invalid credentials') - return {code: 401, contentType: 'application/json', response: { status: 'error', message: 'invalid credentials' }} - } - console.log('B') - console.log(body) - /* The second parameter contains the request body as a 'string'. We need to turn this into a JavaScript object then pass it to a function to check its structure. */ - const json = body - console.log('C') - console.log(json) - const valid = validateJson(json) - /* If the 'validateJson()' function returns 'false' we need to return an error. */ - if (valid === false) { - return {code: 400 ,contentType: 'application/json', response: { status: 'error', message: 'JSON data missing in request body' }} - } - /* In this example we use the csprng module to generate a long random string. In a production system this might be generated by the chosen data store. */ - const newId = rand(160, 36) - /* We now create an object and push it onto the array. */ - const newList = {id: newId, name: json.name, list: json.list} - lists.push(newList) - /* And return a success code. */ - return {code: 201, contentType: 'application/json', response: { status: 'success', message: 'new list added', data: newList }} -} diff --git a/exercises/07_unit_testing/todoold/package.json b/exercises/07_unit_testing/todoold/package.json deleted file mode 100644 index 90aac883..00000000 --- a/exercises/07_unit_testing/todoold/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "todo", - "version": "1.0.0", - "description": "Simple API to maintain to do lists.", - "main": "index.js", - "scripts": { - "start": "node index.js" - }, - "keywords": [ - "api", - "restify", - "tutorial" - ], - "author": "Mark J Tyers", - "license": "ISC", - "dependencies": { - "csprng": "^0.1.1", - "restify": "^4.0.3", - "xmlbuilder": "^3.1.0" - }, - "devDependencies": { - "frisby": "^0.8.5", - "jasmine-node": "^1.14.5" - } -} diff --git a/exercises/07_unit_testing/todoold/test/todo-spec.js b/exercises/07_unit_testing/todoold/test/todo-spec.js deleted file mode 100755 index c8f1281d..00000000 --- a/exercises/07_unit_testing/todoold/test/todo-spec.js +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env node - -'use strict' - -const frisby = require('frisby') - -const status = { - 'ok': 200, - 'created': 201, - 'notModified': 304, - 'badRequest': 400, - 'unauthorised': 401, - 'notFound': 404 -} - -const firstIndex = 0 -const single = 1 - -/* // globalSetup defines any settigs used for ALL requests */ -frisby.globalSetup({ - request: { - headers: {'Authorization': 'Basic dGVzdHVzZXI6cDQ1NXcwcmQ=','Content-Type': 'application/json'} - } -}) - -/* here is a simple automated API call making a GET request. We check the response code, one of the response headers and the content of the response body. After completing the test we call 'toss()' which moves the script to the next test. */ -frisby.create('get empty list') - .get('http://localhost:8080/lists') - .expectStatus(status.notFound) - .expectHeaderContains('Content-Type', 'application/json') - .expectJSON({message: 'no lists found'}) - .toss() - -/* in this second POST example we don't know precisely what values will be returned but we can check for the correct data types. Notice that the request body is passed as the second parameter and we need to pass a third parameter to indicate we are passing the data in json format. */ -frisby.create('add a new list') - .post('http://localhost:8080/lists', {'name': 'shopping', 'list': ['Cheese', 'Bread', 'Butter']}, {json: true}) - .expectStatus(status.created) - .afterJSON( json => { - expect(json.message).toEqual('new list added') - expect(json.data.name).toEqual('shopping') - expect(json.data.list.length).toEqual(3) - }) - .toss() - -/* Since Frisby is built on the Jasmine library we can use any of the standard matchers by enclosing them in an anonymous function passed to the 'afterJSON()' method. */ -frisby.create('check number of lists') - .get('http://localhost:8080/lists') - .expectStatus(status.ok) - .afterJSON( json => { - // you can retrieve args using json.args.x - /* these are standard Jasmine matchers as covered in the first worksheet. */ - expect(json.message).toContain('1') - expect(json.data.length).toEqual(single) - /* We can even use the data returned to make additional API calls. Remember the JS scoping rules? */ - frisby.create('Second test, run after first is completed') - .get(json.data[firstIndex].link) - .expectStatus(status.ok) - .toss() - }) - .toss()