From 84c8c10dddd6eb0c521d3a80b3149b0a7adff49c Mon Sep 17 00:00:00 2001 From: Nora Fossenier Date: Thu, 30 Apr 2020 17:16:46 -0700 Subject: [PATCH 1/8] add subscribers index --- subscribers.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/subscribers.js b/subscribers.js index fd5bd4b..8984172 100644 --- a/subscribers.js +++ b/subscribers.js @@ -19,6 +19,21 @@ */ class Subscribers { + /* + * Returns a list of subscribers to the callback method + */ + static index(req, dynamoDb, callback) { + const params = {TableName: process.env.SUBSCRIBERS_TABLE}; + dynamoDb.scan(params, (error, result) => { + if (error) { + console.error(error); + callback('render', 'error', {error: error}); + } else { + callback('render', 'subscribers/index', {subscribers: result.Items}); + } + }) + } + /* * Renders a form to create a new subscriber. */ From f79f7400e856de6eec899b0a205c3fc0dc71b7cb Mon Sep 17 00:00:00 2001 From: norafoss Date: Wed, 28 Oct 2020 18:18:45 -0700 Subject: [PATCH 2/8] add cartoons tab javascript --- cartoons.js | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 cartoons.js diff --git a/cartoons.js b/cartoons.js new file mode 100644 index 0000000..b07861f --- /dev/null +++ b/cartoons.js @@ -0,0 +1,185 @@ +/* + * Copyright 2020 Zane Littrell, Nora Fossenier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const Login = require('./login'); + +/* + * Controller class for the cartoons tab. + */ +class Cartoons { + + /* + * Renders an index page with links to all the cartoon jpgs. + */ + static index(req, dynamoDb, callback) { + const params = { + TableName: process.env.ISSUE_TABLE, + }; + dynamoDb.scan(params).promise() + .then(data => { + callback('render', 'cartoons/index', {issues: data.Items}); + }) + .catch(error => { + console.error(error); + callback('render', 'error', {error: error}); + }); + } + + /* + * Displays the given cartoon. + */ + static show(req, dynamoDb, callback) { + const params = { + TableName: process.env.ISSUE_TABLE, + Key: { + issueId: Number(req.params.issueId) + } + }; + dynamoDb.get(params).promise() + .then(data => { + callback('render', 'cartoons/show', {issue: data.Item}); + }) + .catch(error => { + console.error(error); + callback('render', 'error', {error: error}); + }); + } + + /* + * Renders a form to create a new cartoon link. + * NOTE: + * User must be logged in. + */ + static new_issue(req, dynamoDb, callback) { + if (Login.authenticate(req)) { + callback('render', 'cartoons/new'); + } else { + callback('redirect', '/login'); + } + } + + /* + * Creates a new issue link in the database. Redirects to the issue index on + * successful creation. + * NOTE: + * User must be logged in. + */ + static create(req, dynamoDb, callback) { + if (Login.authenticate(req)) { + const params = { + TableName: process.env.ISSUE_TABLE, + Item: { + issueId: Number(req.body.issueId), + link: req.file.location, + } + }; + dynamoDb.put(params).promise() + .then(() => { + callback('redirect', '/cartoons'); + }) + .catch(error => { + console.error(error); + callback('render', 'error', {error: error}); + }); + } else { + callback('redirect', '/login'); + } + } + + /* + * Renders a page to edit a cartoon. + * NOTE: + * User must be logged in. + */ + static edit(req, dynamoDb, callback) { + if (Login.authenticate(req)) { + const params = { + TableName: process.env.ISSUE_TABLE, + Key: { + issueId: req.params.issueId + } + }; + dynamoDb.get(params).promise() + .then(data => { + callback('render', 'cartoons/edit', {issue: data.Item}); + }) + .catch(error => { + console.error(error); + callback('render', 'error', {error: error}); + }); + } else { + callback('redirect', '/login'); + } + } + + /* + * Updates the data for the current cartoon. After successfully + * updating, the user is redirected to the cartoon index. + * NOTE: + * User must be logged in. + */ + static update(req, dynamoDb, callback) { + if (Login.authenticate(req)) { + const params = { + TableName: process.env.ISSUE_TABLE, + Key: { + issueId: req.body.issueId, + }, + UpdateExpression: 'SET link = :link', + ExpressionAttributeValues: { + ':link': req.file.location, + } + }; + console.log(params); + dynamoDb.update(params).promise() + .then(() => { + callback('redirect', '/cartoons'); + }) + .catch(error => { + console.error(error); + callback('render', 'error', {error: error}); + }); + } else { + callback('redirect', '/login'); + } + } + + /* + * Deletes the given cartoon and redirects to the cartoon index. + * NOTE: + * User must be logged in. + */ + static destroy(req, dynamoDb, callback) { + if (Login.authenticate(req)) { + const params = { + TableName: process.env.ISSUE_TABLE, + Key: { + issueId: req.params.issueId + } + }; + dynamoDb.delete(params).promise() + .then(() => { + callback('redirect', '/cartoons'); + }) + .catch(error => { + console.error(error); + callback('render', 'error', {error: error}); + }); + } else { + callback('redirect', '/login'); + } + } +} +module.exports = Issues; From 1ef1b9f32ff04ccc22bc574e1f474e87eba8f192 Mon Sep 17 00:00:00 2001 From: norafoss Date: Mon, 2 Nov 2020 21:14:25 -0800 Subject: [PATCH 3/8] minor fix --- cartoons.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cartoons.js b/cartoons.js index b07861f..3df6848 100644 --- a/cartoons.js +++ b/cartoons.js @@ -62,7 +62,7 @@ class Cartoons { * NOTE: * User must be logged in. */ - static new_issue(req, dynamoDb, callback) { + static new_cartoon(req, dynamoDb, callback) { if (Login.authenticate(req)) { callback('render', 'cartoons/new'); } else { From 89fed34c74daa91bd9f1bd03f56ffe1485fba889 Mon Sep 17 00:00:00 2001 From: norafoss Date: Mon, 2 Nov 2020 21:14:46 -0800 Subject: [PATCH 4/8] add test for cartoons tab --- test/cartoons.js | 391 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 test/cartoons.js diff --git a/test/cartoons.js b/test/cartoons.js new file mode 100644 index 0000000..79e0200 --- /dev/null +++ b/test/cartoons.js @@ -0,0 +1,391 @@ +/* + * Copyright 2020 Zane Littrell, Nora Fossenier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const should = require('chai').should(); +const sinon = require('sinon'); +const dotenv = require('dotenv'); +const faker = require('faker'); +const Cartoons = require('../issues'); + +const result = dotenv.config( + { path: process.cwd() + '/test/.env' }); +if (result.error) { + throw result.error; +} + +let req = { + signedCookies: [], + params: [], + file: [] +}; + +let db = { + scan: function(params, callback) { + throw new Error('Use stub instead'); + }, + get: function(params, callback) { + throw new Error('Use stub instead'); + }, + put: function(params, callback) { + callback(null); // no error + }, + update: function(params, callback) { + callback(null); // no error + }, + delete: function(params, callback) { + callback(null); // no error + } +}; + +describe('Cartoons', () => { + beforeEach(() => { + req.signedCookies = []; + req.params = []; + req.body = []; + req.query = []; + }); + afterEach(() => { + // Restore the default sandbox here + sinon.restore(); + }); + describe('#index()', () => { + it('should render the index page', (done) => { + const result = { + TableName: process.env.ISSUE_TABLE, + Items: [1, 2, 3, 4] + }; + const scanPromise = { + promise: function() { + return new Promise((resolve, reject) => { + resolve(result); + }); + } + }; + sinon.stub(db, 'scan').returns(scanPromise); + + Cartoons.index(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('cartoons/index'); + obj.issues.should.equal(result.Items); + done(); + }); + }); + it('should render an error on database error', (done) => { + const error = new Error('some failure'); + const scanPromise = { + promise: function() { + return new Promise((resolve, reject) => { + reject(error); + }); + } + }; + sinon.stub(db, 'scan').returns(scanPromise); + + Cartoons.index(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('error'); + obj.error.should.equal(error); + done(); + }); + }); + }); + describe('#show()', () => { + it('should display a specific cartoon', (done) => { + const result = { + TableName: process.env.ISSUE_TABLE, + Item: { + issueId: 'test-id', + link: 'blank' + } + }; + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + resolve(result); + }); + } + }; + req.params.issueId = result.Item.issueId; + sinon.stub(db, 'get').returns(promise); + + Cartoons.show(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('Cartoons/show'); + obj.should.have.property('issue'); + obj.issue.issueId.should.equal(result.Item.issueId); + obj.issue.link.should.equal(result.Item.link); + done(); + }); + }); + it('should render an error on some error', (done) => { + const error = new Error('some error'); + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + reject(error); + }); + } + }; + req.params.issueId = 'test-id'; + sinon.stub(db, 'get').returns(promise); + + Cartoons.show(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('error'); + obj.error.should.equal(error); + done(); + }); + }); + }); + describe('#new_cartoon()', () => { + it('should require a login', (done) => { + Cartoons.new_cartoon(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/login'); + should.not.exist(obj); + done(); + }); + }); + it('should render a form for a new submission', (done) => { + req.signedCookies.id_token = 1; + + Cartoons.new_cartoon(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('cartoons/new'); + should.not.exist(obj); + done(); + }); + }); + }); + describe('#create()', () => { + it('should require a login', (done) => { + Cartoons.create(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/login'); + should.not.exist(obj); + done(); + }); + }); + it('should redirect on success', (done) => { + const putPromise = { + promise: function() { + return new Promise((resolve, reject) => { + resolve(); + }); + } + }; + sinon.stub(db, 'put').returns(putPromise); + + req.file.location = '/dev/null/'; + req.signedCookies.id_token = 1; + + Cartoons.create(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/cartoons'); + should.not.exist(obj); + done(); + }); + }); + it('should render an error when an error occurs', (done) => { + const error = new Error('some failure'); + const putPromise = { + promise: function() { + return new Promise((resolve, reject) => { + reject(error); + }); + } + }; + sinon.stub(db, 'put').returns(putPromise); + + req.file.location = '/dev/null/'; + req.signedCookies.id_token = 1; + + Cartoons.create(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('error'); + obj.error.should.equal(error); + done(); + }); + }); + }); + describe('#edit()', () => { + it('should render the edit form', (done) => { + req.signedCookies.id_token = 1; + + const result = { + TableName: process.env.ISSUE_TABLE, + Item: { + issueId: 'test-id', + link: '/dev/null' + } + }; + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + resolve(result); + }); + } + }; + sinon.stub(db, 'get').returns(promise); + + Cartoons.edit(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('cartoons/edit'); + obj.issue.should.equal(result.Item); + done(); + }); + }); + it('should render an error', (done) => { + req.signedCookies.id_token = 1; + + const error = new Error('Some error'); + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + reject(error); + }); + } + }; + sinon.stub(db, 'get').returns(promise); + + Cartoons.edit(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('error'); + obj.error.should.equal(error); + done(); + }); + }); + it('should require a login', (done) => { + Cartoons.edit(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/login'); + should.not.exist(obj); + done(); + }); + }); + }); + describe('#update()', () => { + it('should update the issue', (done) => { + req.signedCookies.id_token = 1; + req.body.issueId = 'fake-id'; + req.file.location = '/dev/null/'; + + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + resolve(); + }); + } + }; + let spy = sinon.stub(db, 'update').returns(promise); + + const expectedUpdate = { + TableName: process.env.ISSUE_TABLE, + Key: { + issueId: req.body.issueId, + }, + UpdateExpression: 'SET link = :link', + ExpressionAttributeValues: { + ':link': req.file.location, + } + }; + + Cartoons.update(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/cartoons'); + should.not.exist(obj); + spy.calledOnceWithExactly(sinon.match(expectedUpdate)).should.be.true; + done(); + }); + }); + it('should render an error', (done) => { + req.signedCookies.id_token = 1; + + const error = new Error('Some error'); + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + reject(error); + }); + } + }; + sinon.stub(db, 'update').returns(promise); + + Cartoons.update(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('error'); + obj.error.should.equal(error); + done(); + }); + }); + it('should require a login', (done) => { + Cartoons.update(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/login'); + should.not.exist(obj); + done(); + }); + }); + }); + describe('#destroy()', () => { + it('should require a login', (done) => { + Cartoons.destroy(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/login'); + should.not.exist(obj); + done(); + }); + }); + it('should redirect on success', (done) => { + req.signedCookies.id_token = 1; + + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + resolve(); + }); + } + }; + sinon.stub(db, 'delete').returns(promise); + + Cartoons.destroy(req, db, (action, page, obj) => { + action.should.equal('redirect'); + page.should.equal('/cartoons'); + should.not.exist(obj); + done(); + }); + }); + it('should render an error', (done) => { + req.signedCookies.id_token = 1; + + const error = new Error('Some error'); + const promise = { + promise: function() { + return new Promise((resolve, reject) => { + reject(error); + }); + } + }; + sinon.stub(db, 'delete').returns(promise); + + Cartoons.destroy(req, db, (action, page, obj) => { + action.should.equal('render'); + page.should.equal('error'); + obj.error.should.equal(error); + done(); + }); + }); + }); +}); From 612b922795826ca5f4ffe88d523794620d8f42d4 Mon Sep 17 00:00:00 2001 From: norafoss Date: Tue, 3 Nov 2020 12:42:30 -0800 Subject: [PATCH 5/8] resolve errors in base cartoon code --- cartoons.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cartoons.js b/cartoons.js index 3df6848..5fe4fb9 100644 --- a/cartoons.js +++ b/cartoons.js @@ -71,7 +71,7 @@ class Cartoons { } /* - * Creates a new issue link in the database. Redirects to the issue index on + * Creates a new cartoon link in the database. Redirects to the cartoons index on * successful creation. * NOTE: * User must be logged in. @@ -182,4 +182,4 @@ class Cartoons { } } } -module.exports = Issues; +module.exports = Cartoons; From 11ef54885b3beff5089115a6c0c48db1cdb0b086 Mon Sep 17 00:00:00 2001 From: norafoss Date: Tue, 3 Nov 2020 12:42:51 -0800 Subject: [PATCH 6/8] resolve errors in cartoon testing code --- test/cartoons.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cartoons.js b/test/cartoons.js index 79e0200..c4c86bc 100644 --- a/test/cartoons.js +++ b/test/cartoons.js @@ -17,7 +17,7 @@ const should = require('chai').should(); const sinon = require('sinon'); const dotenv = require('dotenv'); const faker = require('faker'); -const Cartoons = require('../issues'); +const Cartoons = require('../cartoons'); const result = dotenv.config( { path: process.cwd() + '/test/.env' }); @@ -277,7 +277,7 @@ describe('Cartoons', () => { }); }); describe('#update()', () => { - it('should update the issue', (done) => { + it('should update the cartoon', (done) => { req.signedCookies.id_token = 1; req.body.issueId = 'fake-id'; req.file.location = '/dev/null/'; From 72355df4d369bd1dfe54e657ac91ab3280724f04 Mon Sep 17 00:00:00 2001 From: norafoss Date: Thu, 12 Nov 2020 17:12:58 -0800 Subject: [PATCH 7/8] fix testing error --- test/cartoons.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cartoons.js b/test/cartoons.js index c4c86bc..cf07a22 100644 --- a/test/cartoons.js +++ b/test/cartoons.js @@ -106,7 +106,7 @@ describe('Cartoons', () => { const result = { TableName: process.env.ISSUE_TABLE, Item: { - issueId: 'test-id', + issueId: 1, link: 'blank' } }; @@ -122,7 +122,7 @@ describe('Cartoons', () => { Cartoons.show(req, db, (action, page, obj) => { action.should.equal('render'); - page.should.equal('Cartoons/show'); + page.should.equal('cartoons/show'); obj.should.have.property('issue'); obj.issue.issueId.should.equal(result.Item.issueId); obj.issue.link.should.equal(result.Item.link); From 01b2a9020f51a37707238597e170dcdd76eeabc0 Mon Sep 17 00:00:00 2001 From: norafoss Date: Tue, 13 Apr 2021 11:56:46 -0700 Subject: [PATCH 8/8] add views for cartoon tab --- views/cartoons/edit.hbs | 5 +++++ views/cartoons/index.hbs | 13 +++++++++++++ views/cartoons/new.hbs | 5 +++++ views/cartoons/show.hbs | 17 +++++++++++++++++ views/partials/cartoon_form.hbs | 13 +++++++++++++ 5 files changed, 53 insertions(+) create mode 100644 views/cartoons/edit.hbs create mode 100644 views/cartoons/index.hbs create mode 100644 views/cartoons/new.hbs create mode 100644 views/cartoons/show.hbs create mode 100644 views/partials/cartoon_form.hbs diff --git a/views/cartoons/edit.hbs b/views/cartoons/edit.hbs new file mode 100644 index 0000000..5950981 --- /dev/null +++ b/views/cartoons/edit.hbs @@ -0,0 +1,5 @@ +{{> header}} +

Edit

+{{> cartoon_form method='PUT'}} +Back +{{> footer}} diff --git a/views/cartoons/index.hbs b/views/cartoons/index.hbs new file mode 100644 index 0000000..01ec5a4 --- /dev/null +++ b/views/cartoons/index.hbs @@ -0,0 +1,13 @@ +{{> header}} +

Visuals

+{{#if req.signedCookies.id_token}} +

New Cartoon/Faux Ad

+{{/if}} +

+ {{#each issues}} +
+

+
+ {{/each}} +
+{{> footer}} diff --git a/views/cartoons/new.hbs b/views/cartoons/new.hbs new file mode 100644 index 0000000..6eba854 --- /dev/null +++ b/views/cartoons/new.hbs @@ -0,0 +1,5 @@ +{{> header}} +

New

+{{> cartoon_form method='POST'}} +Back +{{> footer}} diff --git a/views/cartoons/show.hbs b/views/cartoons/show.hbs new file mode 100644 index 0000000..c5c0147 --- /dev/null +++ b/views/cartoons/show.hbs @@ -0,0 +1,17 @@ +{{> header}} +
+
+

{{issue.title}}

+

Created by {{issue.artist}}

+
+
+ {{issue.title}} +
+
+{{#if req.signedCookies.id_token}} +
+ Edit + Delete +
+{{/if}} +{{> footer}} diff --git a/views/partials/cartoon_form.hbs b/views/partials/cartoon_form.hbs new file mode 100644 index 0000000..8d190e1 --- /dev/null +++ b/views/partials/cartoon_form.hbs @@ -0,0 +1,13 @@ +
+ +
+ +
+ +
+ +
+ + + +