├── .gitignore ├── bin ├── setup └── mkapplink.js ├── db ├── models │ ├── cart.test.js │ ├── guest.js │ ├── review.js │ ├── order.js │ ├── cart.js │ ├── order.test.js │ ├── product.js │ ├── index.js │ ├── user.js │ ├── user.test.js │ ├── oauth.js │ ├── review.test.js │ └── product.test.js ├── index.js └── seed.js ├── server ├── orders.tests.js ├── auth.filters.js ├── orders.js ├── users.js ├── api.js ├── review.js ├── users.test.js ├── start.js ├── products.js ├── auth.tst.js ├── cart.js ├── auth.js ├── reviews.test.js └── products.test.js ├── .profile ├── .DS_Store ├── app ├── .DS_Store ├── components │ ├── .DS_Store │ ├── Checkout.js │ ├── Footer.js │ ├── WhoAmI.jsx │ ├── App.js │ ├── Orders.js │ ├── Login.jsx │ ├── Product.js │ ├── Reviews.js │ ├── Header.js │ ├── Signup.js │ ├── Products.js │ ├── Jokes.test.jsx │ ├── Sidebar.js │ ├── WhoAmI.test.jsx │ ├── Login.test.jsx │ ├── Cart.js │ └── Jokes.jsx ├── containers │ ├── SidebarContainer.js │ ├── HeaderContainer.js │ ├── CheckoutContainer.js │ ├── OrdersContainer.js │ ├── ReviewsContainer.js │ ├── ProductsContainer.js │ ├── CartContainer.js │ └── ProductContainer.js ├── action-creators │ ├── orders.js │ ├── reviews.js │ ├── header.js │ ├── products.js │ └── cart.js ├── store.jsx ├── constants.js ├── reducers │ ├── header-reducer.js │ ├── orders-reducer.js │ ├── index.jsx │ ├── cart-reducer.js │ ├── reviews-reducer.js │ ├── products-reducer.js │ └── auth.jsx └── main.jsx ├── public ├── .DS_Store ├── favicon.ico ├── index.html ├── monitor.svg └── style.css ├── .babelrc ├── webpack.config.js ├── index.js ├── README.md ├── monitor.svg ├── package.json └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | mkapplink.js -------------------------------------------------------------------------------- /db/models/cart.test.js: -------------------------------------------------------------------------------- 1 | cart.test.js -------------------------------------------------------------------------------- /server/orders.tests.js: -------------------------------------------------------------------------------- 1 | orders.tests.js -------------------------------------------------------------------------------- /.profile: -------------------------------------------------------------------------------- 1 | # npm install --dev 2 | npm run build 3 | npm run seed -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosowsk/build-a-box/HEAD/.DS_Store -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosowsk/build-a-box/HEAD/app/.DS_Store -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosowsk/build-a-box/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosowsk/build-a-box/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosowsk/build-a-box/HEAD/app/components/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-2" 6 | ] 7 | } -------------------------------------------------------------------------------- /app/containers/SidebarContainer.js: -------------------------------------------------------------------------------- 1 | import Sidebar from '../components/Sidebar'; 2 | import { connect } from 'react-redux'; 3 | 4 | export default connect()(Sidebar); -------------------------------------------------------------------------------- /db/models/guest.js: -------------------------------------------------------------------------------- 1 | 2 | const Sequelize = require('sequelize') 3 | const db = require('APP/db') 4 | 5 | 6 | const Guest = db.define('guests', { 7 | 8 | }); 9 | 10 | module.exports = Guest -------------------------------------------------------------------------------- /app/action-creators/orders.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ORDERS } from '../constants'; 2 | import axios from 'axios'; 3 | 4 | export const receiveOrders = orders => ({ 5 | type: RECEIVE_ORDERS, 6 | orders 7 | }); -------------------------------------------------------------------------------- /app/containers/HeaderContainer.js: -------------------------------------------------------------------------------- 1 | import Header from '../components/Header'; 2 | import { connect } from 'react-redux'; 3 | 4 | const mapStateToProps = (state) => { 5 | return { 6 | user: state 7 | } 8 | } 9 | 10 | export default connect(mapStateToProps)(Header); -------------------------------------------------------------------------------- /app/components/Checkout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function (props) { 4 | 5 | const cart = props.selectedCart; 6 | console.log(cart) 7 | return ( 8 |
9 |
10 |

{ cart[0].name }

11 |
12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /app/containers/CheckoutContainer.js: -------------------------------------------------------------------------------- 1 | import Checkout from '../components/Checkout'; 2 | import { connect } from 'react-redux'; 3 | 4 | const mapStateToProps = (state) => { 5 | return { 6 | selectedCart: state.cart.list 7 | }; 8 | }; 9 | 10 | 11 | 12 | export default connect(mapStateToProps)(Checkout); -------------------------------------------------------------------------------- /app/containers/OrdersContainer.js: -------------------------------------------------------------------------------- 1 | import Orders from '../components/Orders'; 2 | import { connect } from 'react-redux'; 3 | 4 | const mapStateToProps = (state) => { 5 | return { 6 | orders: state.orders.list 7 | }; 8 | }; 9 | 10 | 11 | 12 | export default connect( 13 | mapStateToProps 14 | )(Orders); -------------------------------------------------------------------------------- /app/containers/ReviewsContainer.js: -------------------------------------------------------------------------------- 1 | 2 | import Reviews from '../components/Reviews'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | reviews: state.reviews.list 8 | }; 9 | }; 10 | 11 | 12 | 13 | export default connect( 14 | mapStateToProps 15 | )(Reviews); -------------------------------------------------------------------------------- /app/containers/ProductsContainer.js: -------------------------------------------------------------------------------- 1 | 2 | import Products from '../components/Products'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | products: state.products.list 8 | }; 9 | }; 10 | 11 | 12 | 13 | export default connect( 14 | mapStateToProps 15 | )(Products); -------------------------------------------------------------------------------- /app/store.jsx: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import rootReducer from './reducers' 3 | import createLogger from 'redux-logger' 4 | import thunkMiddleware from 'redux-thunk' 5 | 6 | import {whoami} from './reducers/auth' 7 | 8 | const store = createStore(rootReducer, applyMiddleware(createLogger(), thunkMiddleware)) 9 | 10 | export default store 11 | 12 | // Set the auth info at start 13 | store.dispatch(whoami()) 14 | -------------------------------------------------------------------------------- /app/constants.js: -------------------------------------------------------------------------------- 1 | // Products 2 | 3 | export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'; 4 | 5 | // Product 6 | 7 | export const RECEIVE_PRODUCT = 'RECEIVE_PRODUCT'; 8 | 9 | // Cart 10 | 11 | export const RECEIVE_CART = 'RECEIVE_CART'; 12 | 13 | // Reviews 14 | 15 | export const RECEIVE_REVIEWS = 'RECEIVE_REVIEWS'; 16 | 17 | 18 | export const RECEIVE_ORDERS = 'RECEIVE_ORDERS'; 19 | 20 | // Header 21 | 22 | export const RECEIVE_USER = 'RECEIVE_USER' 23 | 24 | -------------------------------------------------------------------------------- /app/containers/CartContainer.js: -------------------------------------------------------------------------------- 1 | import Cart from '../components/Cart'; 2 | import { connect } from 'react-redux'; 3 | import {createOrder} from '../action-creators/cart' 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | selectedCart: state.cart.list 8 | }; 9 | }; 10 | 11 | // const mapDispatch = (createOrder) => dispatch => { 12 | // return { 13 | // createOrder 14 | // } 15 | // } 16 | 17 | 18 | export default connect(mapStateToProps)(Cart); 19 | -------------------------------------------------------------------------------- /app/action-creators/reviews.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_REVIEWS } from '../constants'; 2 | import axios from 'axios'; 3 | 4 | export const receiveReviews = reviews => ({ 5 | type: RECEIVE_REVIEWS, 6 | reviews 7 | }); 8 | // Reviews 9 | export const getReviewsByProductId = productId => { 10 | return dispatch => { 11 | axios.get(`/api/products/${productId}/reviews`) 12 | .then(response => { 13 | dispatch(receiveReviews(response.data)); 14 | }); 15 | }; 16 | }; -------------------------------------------------------------------------------- /db/models/review.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Sequelize = require('sequelize') 4 | const db = require('APP/db') 5 | const Product = require('./product') 6 | 7 | module.exports = db.define('reviews', { 8 | title: { 9 | type: Sequelize.TEXT, 10 | allowNull: false 11 | }, 12 | content: { 13 | type: Sequelize.STRING, 14 | allowNull: false 15 | }, 16 | stars: { 17 | type: Sequelize.INTEGER, 18 | validate: { 19 | isInt: true, 20 | min: 0, 21 | max: 5 22 | } 23 | } 24 | }); -------------------------------------------------------------------------------- /app/action-creators/header.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_USER } from '../constants'; 2 | import axios from 'axios'; 3 | 4 | export const receiveUser = user => ({ 5 | type: RECEIVE_USER, 6 | user 7 | }); 8 | 9 | export const createUser = content => { 10 | console.log("CONTENT",content); 11 | axios.post('api/users', {content}) 12 | .then(() => { 13 | console.log('Success for user') 14 | }).catch((err) => { 15 | console.log(err) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 | 13 | ); -------------------------------------------------------------------------------- /app/reducers/header-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_USER 3 | } from '../constants'; 4 | 5 | 6 | const initialUserState = { 7 | list: [], 8 | 9 | }; 10 | 11 | export default function (state = initialUserState, action) { 12 | 13 | const newState = Object.assign({}, state); 14 | 15 | switch (action.type) { 16 | 17 | case RECEIVE_USER: 18 | newState.list = [action.user]; 19 | 20 | 21 | break; 22 | 23 | 24 | default: 25 | return state; 26 | 27 | } 28 | 29 | return newState; 30 | 31 | } -------------------------------------------------------------------------------- /server/auth.filters.js: -------------------------------------------------------------------------------- 1 | const mustBeLoggedIn = (req, res, next) => { 2 | if (!req.user) { 3 | return res.status(401).send('You must be logged in') 4 | } 5 | next() 6 | } 7 | 8 | const selfOnly = action => (req, res, next) => { 9 | if (req.params.id !== req.user.id) { 10 | return res.status(403).send(`You can only ${action} yourself.`) 11 | } 12 | next() 13 | } 14 | 15 | const forbidden = message => (req, res, next) => { 16 | res.status(403).send(message) 17 | } 18 | 19 | module.exports = {mustBeLoggedIn, selfOnly, forbidden,} 20 | -------------------------------------------------------------------------------- /app/reducers/orders-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ORDERS 3 | } from '../constants'; 4 | 5 | // import {convertAlbum, convertAlbums} from '../utils'; 6 | 7 | const initialOrdersState = { 8 | list: [] 9 | }; 10 | 11 | export default function (state = initialOrdersState, action) { 12 | 13 | const newState = Object.assign({}, state); 14 | 15 | switch (action.type) { 16 | 17 | case RECEIVE_ORDERS: 18 | newState.list = action.orders; 19 | break; 20 | 21 | default: 22 | return state; 23 | 24 | } 25 | 26 | return newState; 27 | 28 | } -------------------------------------------------------------------------------- /app/reducers/index.jsx: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import productsReducer from './products-reducer' 3 | import cartReducer from './cart-reducer' 4 | import reviewsReducer from './reviews-reducer' 5 | import ordersReducer from './orders-reducer' 6 | import headerReducer from './header-reducer' 7 | 8 | 9 | const rootReducer = combineReducers({ 10 | auth: require('./auth').default, 11 | products: productsReducer, 12 | cart: cartReducer, 13 | reviews: reviewsReducer, 14 | orders: ordersReducer, 15 | header: headerReducer 16 | }) 17 | 18 | export default rootReducer 19 | -------------------------------------------------------------------------------- /app/components/WhoAmI.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const WhoAmI = ({ user, logout }) => ( 4 |
5 | {user && user.name} 6 | 7 |
8 | ) 9 | 10 | import {logout} from 'APP/app/reducers/auth' 11 | import {connect} from 'react-redux' 12 | 13 | export default connect ( 14 | ({ auth }) => ({ user: auth }), 15 | {logout}, 16 | ) (WhoAmI) -------------------------------------------------------------------------------- /app/reducers/cart-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_CART 3 | } from '../constants'; 4 | 5 | // import {convertAlbum, convertAlbums} from '../utils'; 6 | 7 | const initialCartState = { 8 | list: [], 9 | 10 | }; 11 | 12 | export default function (state = initialCartState, action) { 13 | 14 | const newState = Object.assign({}, state); 15 | 16 | switch (action.type) { 17 | 18 | case RECEIVE_CART: 19 | newState.list = [...newState.list,action.cart]; 20 | 21 | 22 | break; 23 | 24 | 25 | default: 26 | return state; 27 | 28 | } 29 | 30 | return newState; 31 | 32 | } -------------------------------------------------------------------------------- /app/containers/ProductContainer.js: -------------------------------------------------------------------------------- 1 | import Product from '../components/Product'; 2 | import { connect } from 'react-redux'; 3 | import {receiveCart, addProductToCart} from '../action-creators/cart'; 4 | import store from '../store'; 5 | 6 | 7 | 8 | const mapStateToProps = (state) => { 9 | return { 10 | selectedProduct: state.products.selected 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | 16 | return{ 17 | 18 | addProductToCart (product){ 19 | store.dispatch(addProductToCart(product)); 20 | } 21 | } 22 | } 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(Product); 25 | -------------------------------------------------------------------------------- /db/models/order.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const db = require('APP/db') 3 | 4 | const Order = db.define('orders', { 5 | name: { 6 | type: Sequelize.STRING 7 | }, 8 | billAddress: { 9 | type: Sequelize.STRING, 10 | 11 | }, 12 | shipAddress: { 13 | type: Sequelize.STRING, 14 | 15 | }, 16 | ccInfo: { 17 | type: Sequelize.TEXT, 18 | 19 | }, 20 | expiration: { 21 | type: Sequelize.STRING, 22 | 23 | }, 24 | totalPrice: { 25 | type: Sequelize.STRING, 26 | 27 | } 28 | }) 29 | 30 | module.exports = Order -------------------------------------------------------------------------------- /server/orders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const Order = db.model('orders') 5 | 6 | module.exports = require('express').Router() 7 | .get('/', (req, res, next) => 8 | Order.findAll() 9 | .then(orders => res.json(orders)) 10 | .catch(next)) 11 | .post('/', (req, res, next) => { 12 | console.log("BODY", req.body.content) 13 | let order = Order.build(req.body.content) 14 | order.save() 15 | .then(order => { 16 | console.log("ORDER", order) 17 | res.status(201).json({ 18 | message: 'Created successfully', 19 | order: order, 20 | }) 21 | }) 22 | .catch(next) 23 | }) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: './app/main.jsx', 7 | output: { 8 | path: __dirname, 9 | filename: './public/bundle.js' 10 | }, 11 | context: __dirname, 12 | devtool: 'source-map', 13 | resolve: { 14 | extensions: ['', '.js', '.jsx'] 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /jsx?$/, 20 | exclude: /(node_modules|bower_components)/, 21 | loader: 'babel', 22 | query: { 23 | presets: ['react', 'es2015', 'stage-2'] 24 | } 25 | } 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /server/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const User = db.model('users') 5 | 6 | const {mustBeLoggedIn, forbidden} = require('./auth.filters') 7 | 8 | module.exports = require('express').Router() 9 | .get('/', forbidden('only admins can list users'), (req, res, next) => 10 | User.findAll() 11 | .then(users => res.json(users)) 12 | .catch(next)) 13 | .post('/', (req, res, next) => 14 | User.create(req.body.content) 15 | .then(user => res.status(201).json(user)) 16 | .catch(next)) 17 | .get('/:id', mustBeLoggedIn, (req, res, next) => 18 | User.findById(req.params.id) 19 | .then(user => res.json(user)) 20 | .catch(next)) -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const api = module.exports = require('express').Router() 5 | 6 | api 7 | .get('/heartbeat', (req, res) => res.send({ok: true,})) 8 | .use('/auth', require('./auth')) 9 | .use('/users', require('./users')) 10 | .use('/products', require('./products')) 11 | .use('/', require('./review')) 12 | .use('/cart', require('./cart')) 13 | .use('/orders', require('./orders')); 14 | 15 | 16 | 17 | // Send along any errors 18 | api.use((err, req, res, next) => { 19 | console.log(err); 20 | res.status(500).send(err) 21 | 22 | }) 23 | 24 | // No routes matched? 404. 25 | api.use((req, res) => res.status(404).end()) -------------------------------------------------------------------------------- /app/reducers/reviews-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_REVIEWS 3 | } from '../constants'; 4 | 5 | // import {convertAlbum, convertAlbums} from '../utils'; 6 | 7 | const initialReviewsState = { 8 | // selected:{}, 9 | list: [] 10 | }; 11 | 12 | export default function (state = initialReviewsState, action) { 13 | 14 | const newState = Object.assign({}, state); 15 | 16 | switch (action.type) { 17 | 18 | case RECEIVE_REVIEWS: 19 | newState.list = action.reviews; 20 | break; 21 | // case RECEIVE_PRODUCT: 22 | // newState.selected = action.product; 23 | // break; 24 | 25 | default: 26 | return state; 27 | 28 | } 29 | 30 | return newState; 31 | 32 | } -------------------------------------------------------------------------------- /app/reducers/products-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_PRODUCTS, RECEIVE_PRODUCT 3 | } from '../constants'; 4 | 5 | // import {convertAlbum, convertAlbums} from '../utils'; 6 | 7 | const initialProductsState = { 8 | selected:{}, 9 | list: [] 10 | }; 11 | 12 | export default function (state = initialProductsState, action) { 13 | 14 | const newState = Object.assign({}, state); 15 | 16 | switch (action.type) { 17 | 18 | case RECEIVE_PRODUCTS: 19 | newState.list = action.products; 20 | break; 21 | case RECEIVE_PRODUCT: 22 | newState.selected = action.product; 23 | break; 24 | 25 | default: 26 | return state; 27 | 28 | } 29 | 30 | return newState; 31 | 32 | } -------------------------------------------------------------------------------- /app/components/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import HeaderContainer from '../containers/HeaderContainer'; 4 | import SidebarContainer from '../containers/SidebarContainer'; 5 | import Footer from './Footer' 6 | 7 | export default function (props) { 8 | return ( 9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 | { 17 | props.children && React.cloneElement(props.children, props) 18 | } 19 |
21 |
22 |
23 | ); 24 | } -------------------------------------------------------------------------------- /app/components/Orders.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | export default function (props) { 5 | const orders = props.orders; 6 | // const products = props.products; 7 | 8 | return ( 9 |
10 |
11 |

Orders

12 | { 13 | orders && orders.map(order => ( 14 |
15 |
16 |

{ order.name }

17 |
{ order.id }
18 |
{ order.shipAddress }
19 |
${ order.totalPrice }
20 |
21 |
22 | )) 23 | } 24 |
25 |
26 | ); 27 | } -------------------------------------------------------------------------------- /app/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {router} from 'react-router' 3 | 4 | export const Login = ({ login }) => ( 5 |
{ 6 | evt.preventDefault() 7 | login(evt.target.username.value, evt.target.password.value) 8 | browserHistory.push('/') 9 | } } style={{marginLeft: 5}}> 10 |
Email:
11 |
Password:
12 |
13 | 14 |
15 |
16 | ) 17 | 18 | import {login} from 'APP/app/reducers/auth' 19 | import {connect} from 'react-redux' 20 | 21 | export default connect ( 22 | state => ({}), 23 | {login}, 24 | ) (Login) -------------------------------------------------------------------------------- /db/models/cart.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Product = require('./product') 4 | 5 | /*eslint no-undef:'off'*/ 6 | 7 | const db = require('APP/db') 8 | const Sequelize = require('sequelize') 9 | 10 | module.exports = db.define('carts', { 11 | products: { 12 | type: Sequelize.ARRAY(Sequelize.INTEGER) 13 | // Array of product_id's 14 | }, 15 | totalPrice: { 16 | type: Sequelize.INTEGER, 17 | defaultValue: 0 18 | } 19 | }, 20 | { 21 | hooks: { 22 | afterUpdate: 23 | function setTotalPrice (cart) { 24 | var total = 0 25 | cart.products.forEach((product) => { 26 | Product.findOne({ 27 | where: { id: product} 28 | }) 29 | .then((foundProduct) => { 30 | total += foundProduct.price 31 | }) 32 | }) 33 | cart.totalPrice = total 34 | } 35 | } 36 | }) 37 | 38 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/action-creators/products.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_PRODUCTS, RECEIVE_PRODUCT } from '../constants'; 2 | import axios from 'axios'; 3 | 4 | export const receiveProducts = products => ({ 5 | type: RECEIVE_PRODUCTS, 6 | products 7 | }); 8 | export const receiveProduct = product => ({ 9 | type: RECEIVE_PRODUCT, 10 | product 11 | }); 12 | 13 | export const getProductById = productId => { 14 | return dispatch => { 15 | axios.get(`/api/products/${productId}`) 16 | .then(response => { 17 | dispatch(receiveProduct(response.data)); 18 | }); 19 | }; 20 | }; 21 | 22 | export const getProductsByCategory = categoryId => { 23 | return dispatch => { 24 | axios.get(`/api/products/category/${categoryId}`) 25 | .then(response => { 26 | dispatch(receiveProducts(response.data)); 27 | }); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /db/models/order.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint no-undef: 'off', no-console: 'off' */ 4 | 5 | const db = require('APP/db') 6 | const Order = require('./order') 7 | const { 8 | expect 9 | } = require('chai') 10 | 11 | describe('Order', () => { 12 | before('wait for the db', () => { 13 | db.didSync 14 | }) 15 | 16 | let order; 17 | 18 | beforeEach(function() { 19 | order = Order.create({ 20 | creditCard: 1111222233334444, 21 | billAddress: '42 Wallaby Way, Syndey', 22 | shipAddress: '42 Wallaby Way, Sydney', 23 | email: 'email@gmail.com', 24 | name: 'Email', 25 | status: 'Ordered', 26 | date: '1/1/2017', 27 | products: [{ 28 | name: 'Asus780', 29 | description: "This is an Asus product", 30 | price: 100, 31 | photoUrl: 'fillmurray.com/400/400', 32 | category: 'CPU', 33 | stock: 4, 34 | quantity: 1 35 | }] 36 | }) 37 | }) 38 | 39 | }) -------------------------------------------------------------------------------- /app/components/Product.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | export default function (props) { 5 | 6 | const product = props.selectedProduct; 7 | const addProductToCart = props.addProductToCart; 8 | console.log("PRO ID:",product.id) 9 | 10 | return ( 11 |
12 |
13 |

{ product.name }

14 | 15 |

{ product.description }

16 |

$ { product.price }

17 | 18 | 19 | 20 |

{ product.stars }

21 | 22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /server/review.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const Review = db.model('reviews') 5 | 6 | module.exports = require('express').Router() 7 | .get('/products/:productId/reviews', (req, res, next) => { 8 | console.log("ID:",req.params.productId) 9 | Review.findAll({ 10 | where: {product_id: req.params.productId} 11 | }) 12 | .then(reviews => res.json(reviews)) 13 | .catch(next) 14 | }) 15 | 16 | .post('/products/:id/reviews', (req, res, next) => 17 | Review.create(req.body) 18 | .then(review => res.status(201).json({ 19 | message: 'Created successfully', 20 | review: review, 21 | })) 22 | .catch(next)) 23 | .get('/products/:id/reviews/:reviewId', (req, res, next) => 24 | Review.findById(req.params.reviewId) 25 | .then(review => { 26 | if (!review) res.sendStatus(404); 27 | else res.json(review) 28 | }) 29 | .catch(next)) 30 | -------------------------------------------------------------------------------- /app/components/Reviews.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import {Link} from 'react-router'; 4 | 5 | export default function (props) { 6 | const reviews = props.reviews; 7 | 8 | return ( 9 |
10 |
11 |

Reviews

12 | { 13 | reviews && reviews.map(review => ( 14 |
15 | 16 |
17 |
18 | { review.title } 19 |
20 | { review.content } 21 | Rating: { review.stars } 22 |
23 |
24 | )) 25 | } 26 |
27 |
28 | ); 29 | }; 30 | 31 | // Add photo of product 32 | // 33 | // 34 | // -------------------------------------------------------------------------------- /app/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import { whoami } from '../reducers/auth' 4 | import axios from 'axios' 5 | 6 | export default function (props) { 7 | let user; 8 | if (props.user.auth) user = props.user.auth.name 9 | else user = 'GUEST' 10 | console.log('STATE:', user) 11 | 12 | // console.log(props) 13 | 14 | return ( 15 |
16 |

Build-A-Box

17 | 18 |
19 | {user.toUpperCase()} 20 | 21 | MY CART 22 | 23 | 24 | LOGIN 25 | 26 | 27 | LOGOUT 28 | 29 | 30 | SIGN UP 31 | 32 |
33 |
34 |
35 | ); 36 | } -------------------------------------------------------------------------------- /db/models/product.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const bcrypt = require('bcrypt') 5 | const Sequelize = require('sequelize') 6 | const Review = require('./review.js'); 7 | 8 | module.exports = db.define('products', { 9 | name: { 10 | type: Sequelize.STRING, 11 | allowNull: false 12 | }, 13 | photoUrl: { 14 | type: Sequelize.STRING, 15 | allowNull: false 16 | }, 17 | description: { 18 | type: Sequelize.STRING, 19 | allowNull: false 20 | }, 21 | price: { 22 | type: Sequelize.INTEGER, 23 | validate: { 24 | isNumeric: true, 25 | }, 26 | allowNull: false 27 | }, 28 | category: { 29 | type: Sequelize.STRING, 30 | validate: { 31 | isIn: [ 32 | ['CPU', 'Motherboard', 'Case', 'GPU', 'RAM', 'Power Supply', 'Sound Card', 'HDD', 'SSD', 'Monitor', 'Keyboard', 'Mouse', 'Speakers', 'Headphones', 'Mousepad'] 33 | ] 34 | } 35 | }, 36 | stock: { 37 | type: Sequelize.INTEGER, 38 | validate: { 39 | isNumeric: true, 40 | 41 | }, 42 | allowNull: false 43 | } 44 | 45 | }) -------------------------------------------------------------------------------- /db/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Require our models. Running each module registers the model into sequelize 4 | // so any other part of the application could call sequelize.model('User') 5 | // to get access to the User model. 6 | const Guest = require('./guest') 7 | const User = require('./user') 8 | const Product = require('./product') 9 | const Review = require('./review') 10 | const Cart = require('./cart') 11 | // const Order = require('./order') 12 | 13 | const Order = require('./order') 14 | User.hasMany(Review); 15 | Product.hasMany(Review, { 16 | as: 'Reviews' 17 | }); 18 | Review.belongsTo(Product); 19 | 20 | User.belongsToMany(Product, {through: 'cart'}); 21 | Product.belongsToMany(User, {through: 'cart'}); 22 | 23 | Guest.belongsToMany(Product, {through: 'sessionCart'}); 24 | Product.belongsToMany(Guest, {through: 'sessionCart'}); 25 | 26 | Order.belongsTo(User); 27 | User.hasMany(Order, { as: 'Orders'}); 28 | Product.hasMany(Order, { as: 'Orders'}); 29 | 30 | 31 | module.exports = { 32 | User, 33 | Product, 34 | Review, 35 | Cart, 36 | Guest, 37 | // Order 38 | Order 39 | } -------------------------------------------------------------------------------- /app/components/Signup.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {router} from 'react-router' 3 | import { browserHistory } from 'react-router' 4 | import { createUser } from '../action-creators/header' 5 | 6 | function handleSubmit(e){ 7 | e.preventDefault() 8 | 9 | let formData = { 10 | name: e.target.name.value, 11 | email: e.target.email.value, 12 | password: e.target.password.value, 13 | } 14 | 15 | createUser(formData) 16 | browserHistory.push('/Login') 17 | alert('Account created! Please log in.') 18 | } 19 | 20 | export const Signup = ({ signup }) => ( 21 | 22 |
(handleSubmit(e))}> 23 |
Name:
24 |
Email:
25 |
Password:
26 |
27 | 28 |
29 |
30 | ) 31 | 32 | import {signup} from 'APP/app/reducers/auth' 33 | import {connect} from 'react-redux' 34 | 35 | export default connect ( 36 | state => ({}), 37 | {Signup}, 38 | ) (Signup) -------------------------------------------------------------------------------- /app/reducers/auth.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {browserHistory} from 'react-router' 3 | 4 | const reducer = (state=null, action) => { 5 | switch(action.type) { 6 | case AUTHENTICATED: 7 | return action.user 8 | } 9 | return state 10 | } 11 | 12 | const AUTHENTICATED = 'AUTHENTICATED' 13 | export const authenticated = user => ({ 14 | type: AUTHENTICATED, user 15 | }) 16 | 17 | export const login = (username, password) => 18 | dispatch => 19 | axios.post('/api/auth/local/login', 20 | {username, password}) 21 | .then(() => dispatch(whoami())) 22 | .then(() => browserHistory.push('/')) 23 | .catch(() => dispatch(whoami())) 24 | 25 | export const logout = () => 26 | dispatch => 27 | axios.post('/api/auth/logout') 28 | .then(() => dispatch(whoami())) 29 | .catch(() => dispatch(whoami())) 30 | 31 | export const whoami = () => 32 | dispatch => 33 | axios.get('/api/auth/whoami') 34 | .then(response => { 35 | const user = response.data 36 | dispatch(authenticated(user)) 37 | }) 38 | .catch(failed => dispatch(authenticated(null))) 39 | 40 | export default reducer -------------------------------------------------------------------------------- /app/components/Products.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import {Link} from 'react-router'; 4 | 5 | export default function (props) { 6 | const products = props.products; 7 | function shorten(string) { 8 | if (string.length > 15) { 9 | string = string.slice(0,15) + '...' 10 | } 11 | return string 12 | } 13 | 14 | return ( 15 |
16 |
17 |

Products

18 | { 19 | products && products.map(product => ( 20 |
21 | 22 | 23 | 24 |
25 |

26 | { shorten(product.name) } 27 |

28 |
${ product.price }.00
29 |
Category: { product.category }
30 |
{ product.stock } in stock
31 |
32 |
33 | )) 34 | } 35 |
36 |
37 | ); 38 | }; 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/action-creators/cart.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CART } from '../constants'; 2 | import axios from 'axios'; 3 | 4 | export const receiveCart = cart => ({ 5 | type: RECEIVE_CART, 6 | cart 7 | }); 8 | 9 | 10 | export const addProductToCart = product => { 11 | return dispatch => { 12 | axios.post('/api/cart/', {product}) 13 | .then(() => { 14 | dispatch(receiveCart(product)); 15 | }); 16 | }; 17 | }; 18 | 19 | export const removeProductFromCart = product => { 20 | return dispatch => { 21 | axios.post(`/api/cart/`, {product}) 22 | .then(response => { 23 | dispatch(receiveCart(response.data)); 24 | }); 25 | }; 26 | }; 27 | 28 | export const getProductsOfUser = productId => { 29 | return dispatch => { 30 | axios.get(`/api/products/${productId}`) 31 | .then(response => { 32 | dispatch(receiveProduct(response.data)); 33 | }); 34 | }; 35 | }; 36 | 37 | export const createOrder = content => { 38 | console.log("CONTENT",content); 39 | axios.post('api/orders', {content}) 40 | .then(() => { 41 | console.log('Success?') 42 | }).catch((err) => { 43 | console.log(err) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /app/components/Jokes.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | 6 | import Jokes from './Jokes' 7 | 8 | describe('', () => { 9 | const joke = { 10 | q: 'Why did the skeleton write tests?', 11 | a: 'To see if she did anything bone-headed.', 12 | } 13 | 14 | let root 15 | beforeEach('render the root', () => 16 | root = shallow() 17 | ) 18 | 19 | it('shows a joke', () => { 20 | root.setState({ joke, answered: false }) 21 | expect(root.find('h1')).to.have.length(1) 22 | expect(root.find('h1').text()).equal(joke.q) 23 | }) 24 | 25 | it("doesn't show the answer when state.answered=false", () => { 26 | root.setState({ joke, answered: false }) 27 | expect(root.find('h2')).to.have.length(0) 28 | }) 29 | 30 | it('shows the answer when state.answered=true', () => { 31 | root.setState({ joke, answered: true }) 32 | expect(root.find('h2')).to.have.length(1) 33 | expect(root.find('h2').text()).to.equal(joke.a) 34 | }) 35 | 36 | it('when tapped, sets state.answered=true', () => { 37 | root.setState({ joke, answered: false }) 38 | root.simulate('click') 39 | expect(root.state().answered).to.be.true 40 | }) 41 | }) -------------------------------------------------------------------------------- /bin/mkapplink.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const chalk = require('chalk') 4 | const fs = require('fs') 5 | const {resolve} = require('path') 6 | 7 | const appLink = resolve(__dirname, '..', 'node_modules', 'APP') 8 | 9 | const symlinkError = error => 10 | `******************************************************************* 11 | ${appLink} must point to '..' 12 | 13 | This symlink lets you require('APP/some/path') rather than 14 | ../../../some/path 15 | 16 | I tried to create it, but got this error: 17 | ${error.message} 18 | 19 | You might try this: 20 | 21 | rm ${appLink} 22 | 23 | Then run me again. 24 | 25 | ~ xoxo, bones 26 | ********************************************************************` 27 | 28 | function makeAppSymlink() { 29 | console.log(`Linking '${appLink}' to '..'`) 30 | try { 31 | try { fs.unlinkSync(appLink) } catch(swallowed) { } 32 | fs.symlinkSync('..', appLink) 33 | } catch (error) { 34 | console.error(chalk.red(symlinkError(error))) 35 | process.exit(1) 36 | } 37 | console.log(`Ok, created ${appLink}`) 38 | } 39 | 40 | function ensureAppSymlink() { 41 | try { 42 | const currently = fs.readlinkSync(appLink) 43 | if (currently !== '..') { 44 | throw new Error(`${appLink} is pointing to '${currently}' rather than '..'`) 45 | } 46 | } catch (error) { 47 | makeAppSymlink() 48 | } 49 | } 50 | 51 | if (module === require.main) { 52 | ensureAppSymlink() 53 | } -------------------------------------------------------------------------------- /app/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | export default function (props) { 5 | 6 | const image = "monitor.svg" 7 | 8 | return ( 9 | 10 | 11 | 12 | 13 |
14 |

15 | Motherboards 16 |

17 |
18 |
19 |

20 | CPUs 21 |

22 |
23 |
24 |

25 | GPUs 26 |

27 |
28 |
29 |

30 | RAM 31 |

32 |
33 |
34 |

35 | HDDs 36 |

37 |
38 |
39 |

40 | SSDs 41 |

42 |
43 |
44 |

45 | Cases 46 |

47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/components/WhoAmI.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | import {createStore} from 'redux' 8 | 9 | import WhoAmIContainer, {WhoAmI} from './WhoAmI' 10 | 11 | describe('', () => { 12 | const user = { 13 | name: 'Dr. Bones', 14 | } 15 | const logout = spy() 16 | let root 17 | beforeEach('render the root', () => 18 | root = shallow() 19 | ) 20 | 21 | it('greets the user', () => { 22 | expect(root.text()).to.contain(user.name) 23 | }) 24 | 25 | it('has a logout button', () => { 26 | expect(root.find('button.logout')).to.have.length(1) 27 | }) 28 | 29 | it('calls props.logout when logout is tapped', () => { 30 | root.find('button.logout').simulate('click') 31 | expect(logout).to.have.been.called 32 | }) 33 | }) 34 | 35 | describe("'s connection", () => { 36 | const state = { 37 | auth: {name: 'Dr. Bones'} 38 | } 39 | 40 | let root, store, dispatch 41 | beforeEach('create store and render the root', () => { 42 | store = createStore(state => state, state) 43 | dispatch = spy(store, 'dispatch') 44 | root = shallow() 45 | }) 46 | 47 | it('gets prop.user from state.auth', () => { 48 | expect(root.find(WhoAmI)).to.have.prop('user').eql(state.auth) 49 | }) 50 | }) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {resolve} = require('path') 4 | const chalk = require('chalk') 5 | const pkg = require('./package.json') 6 | const debug = require('debug')(`${pkg.name}:boot`) 7 | 8 | const nameError = 9 | `******************************************************************* 10 | You need to give your app a proper name. 11 | 12 | The package name 13 | 14 | ${pkg.name} 15 | 16 | isn't valid. If you don't change it, things won't work right. 17 | 18 | Please change it in ${__dirname}/package.json 19 | ~ xoxo, bones 20 | ********************************************************************` 21 | 22 | const reasonableName = /^[a-z0-9\-_]+$/ 23 | if (!reasonableName.test(pkg.name)) { 24 | console.error(chalk.red(nameError)) 25 | } 26 | 27 | // This will load a secrets file from 28 | // 29 | // ~/.your_app_name.env.js 30 | // or ~/.your_app_name.env.json 31 | // 32 | // and add it to the environment. 33 | const env = Object.create(process.env) 34 | , secretsFile = resolve(env.HOME, `.${pkg.name}.env`) 35 | try { 36 | Object.assign(env, require(secretsFile)) 37 | } catch (error) { 38 | debug('%s: %s', secretsFile, error.message) 39 | debug('%s: env file not found or invalid, moving on', secretsFile) 40 | } 41 | 42 | module.exports = { 43 | get name() { return pkg.name }, 44 | get isTesting() { return !!global.it }, 45 | get isProduction() { 46 | return process.env.NODE_ENV === 'production' 47 | }, 48 | get baseUrl() { 49 | return env.BASE_URL || `http://localhost:${PORT}` 50 | }, 51 | get port() { 52 | return env.PORT || 1337 53 | }, 54 | package: pkg, 55 | env, 56 | } 57 | -------------------------------------------------------------------------------- /db/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const bcrypt = require('bcrypt') 4 | const Sequelize = require('sequelize') 5 | const db = require('APP/db') 6 | 7 | 8 | const User = db.define('users', { 9 | name: { 10 | type: Sequelize.STRING, 11 | // allowNull: false 12 | }, 13 | email: { 14 | type: Sequelize.STRING, 15 | validate: { 16 | isEmail: true, 17 | notEmpty: true, 18 | } 19 | }, 20 | billAddress: Sequelize.STRING, 21 | shipAddress: Sequelize.STRING, 22 | isAdmin: { 23 | type: Sequelize.BOOLEAN, 24 | allowNull: false, 25 | defaultValue: false 26 | }, 27 | 28 | // We support oauth, so users may or may not have passwords. 29 | password_digest: Sequelize.STRING, 30 | password: Sequelize.VIRTUAL, 31 | }, { 32 | indexes: [{ 33 | fields: ['email'], 34 | unique: true, 35 | }], 36 | hooks: { 37 | beforeCreate: setEmailAndPassword, 38 | beforeUpdate: setEmailAndPassword, 39 | }, 40 | instanceMethods: { 41 | authenticate(plaintext) { 42 | return new Promise((resolve, reject) => 43 | bcrypt.compare(plaintext, this.password_digest, 44 | (err, result) => 45 | err ? reject(err) : resolve(result)) 46 | ) 47 | } 48 | } 49 | }) 50 | 51 | function setEmailAndPassword(user) { 52 | user.email = user.email && user.email.toLowerCase() 53 | if (!user.password) return Promise.resolve(user) 54 | 55 | return new Promise((resolve, reject) => 56 | bcrypt.hash(user.get('password'), 10, (err, hash) => { 57 | if (err) reject(err) 58 | user.set('password_digest', hash) 59 | resolve(user) 60 | }) 61 | ) 62 | } 63 | 64 | module.exports = User -------------------------------------------------------------------------------- /server/users.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const {expect} = require('chai') 3 | const db = require('APP/db') 4 | const User = require('APP/db/models/user') 5 | const app = require('./start') 6 | 7 | describe('/api/users', () => { 8 | describe('when not logged in', () => { 9 | it('GET /:id fails 401 (Unauthorized)', () => 10 | request(app) 11 | .get(`/api/users/1`) 12 | .expect(401) 13 | ) 14 | 15 | it('POST creates a user', () => 16 | request(app) 17 | .post('/api/users') 18 | .send({ 19 | name: 'beth', 20 | email: 'beth@secrasdfaets.org', 21 | password: '12345', 22 | }) 23 | .expect(201) 24 | ) 25 | 26 | it('POST fails to create a user when email is not given', () => 27 | request(app) 28 | .post('/api/users') 29 | .send({ 30 | email: '', 31 | password: '12345', 32 | }) 33 | .expect(500) 34 | ) 35 | 36 | it('POST fails to create a user when email is not valid', () => 37 | request(app) 38 | .post('/api/users') 39 | .send({ 40 | email: 'invalidemail', 41 | password: '12345', 42 | }) 43 | .expect(500) 44 | ) 45 | 46 | 47 | it('POST redirects to the user it just made', () => 48 | request(app) 49 | .post('/api/users') 50 | .send({ 51 | name: 'eve', 52 | email: 'eve@interasdfloper.com', 53 | password: '23456', 54 | }) 55 | .redirects(1) 56 | .then(res => expect(res.body).to.contain({ 57 | email: 'eve@interasdfloper.com' 58 | })) 59 | ) 60 | }) 61 | 62 | }) 63 | 64 | -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | const bodyParser = require('body-parser') 5 | const {resolve} = require('path') 6 | const passport = require('passport') 7 | const db = require('APP/db') 8 | 9 | // Bones has a symlink from node_modules/APP to the root of the app. 10 | // That means that we can require paths relative to the app root by 11 | // saying require('APP/whatever'). 12 | // 13 | // This next line requires our root index.js: 14 | const pkg = require('APP') 15 | 16 | const app = express() 17 | 18 | if (!pkg.isProduction && !pkg.isTesting) { 19 | // Logging middleware (dev only) 20 | app.use(require('volleyball')) 21 | } 22 | 23 | module.exports = app 24 | // We'll store the whole session in a cookie 25 | .use(require('cookie-session') ({ 26 | name: 'session', 27 | keys: [process.env.SESSION_SECRET || 'an insecure secret key'], 28 | })) 29 | 30 | // Body parsing middleware 31 | .use(bodyParser.urlencoded({ extended: true })) 32 | .use(bodyParser.json()) 33 | 34 | // Authentication middleware 35 | .use(passport.initialize()) 36 | .use(passport.session()) 37 | 38 | // Serve static files from ../public 39 | .use(express.static(resolve(__dirname, '..', 'public'))) 40 | 41 | // Serve our api 42 | .use('/api', require('./api')) 43 | 44 | // Send index.html for anything else. 45 | .get('/*', (_, res) => res.sendFile(resolve(__dirname, '..', 'public', 'index.html'))) 46 | 47 | if (module === require.main) { 48 | // Start listening only if we're the main module. 49 | // 50 | // https://nodejs.org/api/modules.html#modules_accessing_the_main_module 51 | const server = app.listen( 52 | process.env.PORT || 1337, 53 | () => { 54 | console.log(`--- Started HTTP Server for ${pkg.name} ---`) 55 | console.log(`Listening on ${JSON.stringify(server.address())}`) 56 | } 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/components/Login.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | 8 | import {Login} from './Login' 9 | 10 | describe('', () => { 11 | let root 12 | beforeEach('render the root', () => 13 | root = shallow() 14 | ) 15 | 16 | it('shows a login form', () => { 17 | expect(root.find('input[name="username"]')).to.have.length(1) 18 | expect(root.find('input[name="password"]')).to.have.length(1) 19 | }) 20 | 21 | it('shows a password field', () => { 22 | const pw = root.find('input[name="password"]') 23 | expect(pw).to.have.length(1) 24 | expect(pw.at(0)).to.have.attr('type').equals('password') 25 | }) 26 | 27 | it('has a login button', () => { 28 | const submit = root.find('input[type="submit"]') 29 | expect(submit).to.have.length(1) 30 | }) 31 | 32 | describe('when submitted', () => { 33 | const login = spy() 34 | const root = shallow() 35 | const submitEvent = { 36 | preventDefault: spy(), 37 | target: { 38 | username: {value: 'bones@example.com'}, 39 | password: {value: '12345'}, 40 | } 41 | } 42 | 43 | beforeEach('submit', () => { 44 | login.reset() 45 | submitEvent.preventDefault.reset() 46 | root.simulate('submit', submitEvent) 47 | }) 48 | 49 | it('calls props.login with credentials', () => { 50 | expect(login).to.have.been.calledWith( 51 | submitEvent.target.username.value, 52 | submitEvent.target.password.value, 53 | ) 54 | }) 55 | 56 | it('calls preventDefault', () => { 57 | expect(submitEvent.preventDefault).to.have.been.called 58 | }) 59 | }) 60 | }) -------------------------------------------------------------------------------- /db/models/user.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const User = require('./user') 5 | const {expect} = require('chai') 6 | const Review = require('./review'); 7 | 8 | describe('User', () => { 9 | before('wait for the db', () => db.didSync) 10 | 11 | var user; 12 | 13 | describe('authenticate(plaintext: String) ~> Boolean', () => { 14 | it('resolves true if the password matches', () => 15 | User.create({ 16 | name: 'Jeff', 17 | email: 'email@gmail.com', 18 | isAdmin: true, 19 | password: 'ok' 20 | }) 21 | .then(user => user.authenticate('ok')) 22 | .then(result => expect(result).to.be.true)) 23 | 24 | it("resolves false if the password doesn't match", () => 25 | User.create({ 26 | name: 'Jeff', 27 | email: 'lame@gmail.com', 28 | isAdmin: true, 29 | password: 'ok' 30 | }) 31 | .then(user => user.authenticate('not ok')) 32 | .then(result => expect(result).to.be.false)) 33 | }) 34 | 35 | describe('Associations', () => { 36 | it('Make sure user can have many reviews', () => { 37 | 38 | var reviewA = Review.create({ 39 | title: 'GOOD', 40 | content: 'this is my content', 41 | stars: 3 42 | }) 43 | var reviewB = Review.create({ 44 | title: 'Terrible', 45 | content: 'Next content', 46 | stars: 1 47 | }) 48 | var userA = User.create({ 49 | name: 'Jeff', 50 | email: 'cool@gmail.com', 51 | isAdmin: true, 52 | password: 'ok' 53 | }) 54 | 55 | return Promise.all([reviewA, reviewB, userA]) 56 | .then(function([reviewA, reviewB, userA]) { 57 | return userA.setReviews([reviewA, reviewB]); 58 | }).then((userA) => { 59 | return userA.getReviews() 60 | }).then(reviews => { 61 | expect(reviews[0].title).to.exist; 62 | }) 63 | 64 | }) 65 | 66 | }) 67 | }) -------------------------------------------------------------------------------- /server/products.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const Product = db.model('products') 5 | 6 | module.exports = require('express').Router() 7 | .get('/', (req, res, next) => 8 | Product.findAll() 9 | .then(products => res.json(products)) 10 | .catch(next)) 11 | .post('/', (req, res, next) => 12 | Product.create(req.body) 13 | .then(product => res.status(201).json({ 14 | message: 'Created successfully', 15 | product: product, 16 | })) 17 | .catch(next)) 18 | .get('/category/:categoryId', (req, res, next) => 19 | Product.findAll({ 20 | where: { 21 | category: req.params.categoryId, 22 | } 23 | }) 24 | .then(products => { 25 | if (!products) res.sendStatus(404); 26 | else res.json(products) 27 | }) 28 | .catch(next)) 29 | .get('/:id', (req, res, next) => 30 | Product.findById(req.params.id) 31 | .then(foundProduct => { 32 | if (!foundProduct) res.sendStatus(404); 33 | else res.json(foundProduct) 34 | }) 35 | .catch(next)) 36 | .put('/:id', (req, res, next) => { 37 | console.log("any string hittin the route") 38 | Product.update(req.body, { 39 | where: {id: req.params.id}, 40 | returning: true 41 | }) 42 | .then(function (results) { 43 | var updated = results[1][0] 44 | // console.log("hello", updated) 45 | res.json({ 46 | message: 'Updated successfully', 47 | product: updated 48 | }) 49 | }) 50 | .catch(function(err){ 51 | console.log(err) 52 | }) 53 | }) 54 | // Product.findById(req.params.id) 55 | // .then(function (found) { 56 | // if (!found) { 57 | // var err = new Error('not found'); 58 | // err.status = 404; 59 | // throw err; 60 | // } 61 | // return found.update(req.body); 62 | // }) 63 | // .then(function (updated) { 64 | // res.json({ 65 | // message: 'Updated successfully', 66 | // product: updated 67 | // }); 68 | // }) 69 | // .catch(next)) 70 | 71 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const debug = require('debug')('sql') 3 | const chalk = require('chalk') 4 | const Sequelize = require('sequelize') 5 | const app = require('APP') 6 | 7 | const name = (process.env.DATABASE_NAME || app.name) + 8 | (app.isTesting ? '_test' : '') 9 | 10 | const url = process.env.DATABASE_URL || `postgres://localhost:5432/${name}` 11 | 12 | console.log(chalk.yellow(`Opening database connection to ${url}`)); 13 | 14 | // create the database instance 15 | const db = module.exports = new Sequelize(url, { 16 | logging: debug, // export DEBUG=sql in the environment to get SQL queries 17 | native: true, // lets Sequelize know we can use pg-native for ~30% more speed 18 | define: { 19 | underscored: true, // use snake_case rather than camelCase column names 20 | freezeTableName: true, // don't change table names from the one specified 21 | timestamps: true, // automatically include timestamp columns 22 | } 23 | }) 24 | 25 | // pull in our models 26 | require('./models') 27 | 28 | // sync the db, creating it if necessary 29 | function sync(force = app.isTesting, retries = 0, maxRetries = 5) { 30 | return db.sync({ 31 | force 32 | }) 33 | .then(ok => console.log(`Synced models to db ${url}`)) 34 | .catch(fail => { 35 | // Don't do this auto-create nonsense in prod, or 36 | // if we've retried too many times. 37 | if (app.isProduction || retries > maxRetries) { 38 | console.error(chalk.red(`********** database error ***********`)) 39 | console.error(chalk.red(` Couldn't connect to ${url}`)) 40 | console.error() 41 | console.error(chalk.red(fail)) 42 | console.error(chalk.red(`*************************************`)) 43 | return 44 | } 45 | // Otherwise, do this autocreate nonsense 46 | console.log(`${retries ? `[retry ${retries}]` : ''} Creating database ${name}...`) 47 | return new Promise((resolve, reject) => 48 | require('child_process').exec(`createdb "${name}"`, resolve) 49 | ).then(() => sync(true, retries + 1)) 50 | }) 51 | } 52 | 53 | db.didSync = sync() -------------------------------------------------------------------------------- /db/models/oauth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('oauth') 4 | const Sequelize = require('sequelize') 5 | const db = require('APP/db') 6 | 7 | const OAuth = db.define('oauths', { 8 | uid: Sequelize.STRING, 9 | provider: Sequelize.STRING, 10 | 11 | // OAuth v2 fields 12 | accessToken: Sequelize.STRING, 13 | refreshToken: Sequelize.STRING, 14 | 15 | // OAuth v1 fields 16 | token: Sequelize.STRING, 17 | tokenSecret: Sequelize.STRING, 18 | 19 | // The whole profile as JSON 20 | profileJson: Sequelize.JSON, 21 | }, { 22 | indexes: [{ 23 | fields: ['uid'], 24 | unique: true, 25 | }], 26 | }) 27 | 28 | OAuth.V2 = (accessToken, refreshToken, profile, done) => 29 | this.findOrCreate({ 30 | where: { 31 | provider: profile.provider, 32 | uid: profile.id, 33 | } 34 | }) 35 | .then(oauth => { 36 | debug('provider:%s will log in user:{name=%s uid=%s}', 37 | profile.provider, 38 | profile.displayName, 39 | token.uid) 40 | oauth.profileJson = profile 41 | return db.Promise.props({ 42 | oauth, 43 | user: token.getUser(), 44 | _saveProfile: oauth.save(), 45 | }) 46 | }) 47 | .then(({ 48 | oauth, 49 | user 50 | }) => user || 51 | User.create({ 52 | name: profile.displayName, 53 | }).then(user => db.Promise.props({ 54 | user, 55 | _setOauthUser: oauth.setUser(user) 56 | })) 57 | ) 58 | .then(({ 59 | user 60 | }) => done(null, user)) 61 | .catch(done) 62 | 63 | 64 | OAuth.setupStrategy = 65 | ({ 66 | provider, 67 | strategy, 68 | config, 69 | oauth = OAuth.V2, 70 | passport 71 | }) => { 72 | const undefinedKeys = Object.keys(config) 73 | .map(k => config[k]) 74 | .filter(value => typeof value === 'undefined') 75 | if (undefinedKeys.length) { 76 | undefinedKeys.forEach(key => 77 | debug('provider:%s: needs environment var %s', provider, key)) 78 | debug('provider:%s will not initialize', provider) 79 | return 80 | } 81 | 82 | debug('initializing provider:%s', provider) 83 | passport.use(new strategy(config, oauth)) 84 | } 85 | 86 | module.exports = OAuth -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hi, I'm bones 2 | 3 | I'm a happy little skeleton. You can clone me to use as a starter on your projects! 4 | I have React, Redux, Sequelize, and Express all just rattling around in here ready 5 | to go. 6 | 7 | ## I need node >= 6.7.0 8 | 9 | If you don't have it, I'll complain and tell you how to install it. 10 | 11 | ## 1. Make me into something! 12 | 13 | Create a git repo however you want to. You can fork me on Github, but you can only do 14 | that once (so weird!). You can also create a Github repo and clone it, or just do 15 | `git init` in an empty directory on your machine. 16 | 17 | After you have a repo on your machine: 18 | 19 | ``` 20 | git remote add bones https://github.com/queerviolet/bones.git 21 | git fetch bones 22 | git merge bones/master 23 | ``` 24 | 25 | And then you'll have me! If I change—which I probably will—you can get the most recent 26 | version by doing this again: 27 | 28 | ``` 29 | git fetch bones 30 | git merge bones/master 31 | ``` 32 | 33 | ## 2. I need a name. 34 | 35 | I don't have a name. I think I used to have one, but it turned to dust right along with my 36 | heart and liver and pituitary gland and all that stuff. 37 | 38 | Anyway, I'll need one. Give me a name in `package.json`. 39 | 40 | ## 3. Start my dusty heart 41 | 42 | Short and sweet: 43 | 44 | ``` 45 | npm install 46 | npm run build-watch 47 | npm start 48 | ``` 49 | 50 | `npm start` doesn't build, so watch out for that. The reason it doesn't build is because you 51 | probably want to watch the build and run me in separate terminals. Otherwise, build errors get 52 | all mixed in with HTTP request logging. 53 | 54 | ## My anatomy 55 | 56 | `/app` has the React/Redux setup. `main.jsx` is the entry point. 57 | 58 | `/db` has the Sequelize models and database setup. It'll create the database for you if it doesn't exist, 59 | assuming you're using postgres. 60 | 61 | `/server` has the Express server and routes. `start.js` is the entry point. 62 | 63 | `/bin` has scripts. (Right now it has *one* script that creates a useful symlink.) 64 | 65 | ## Conventions 66 | 67 | I use `require` and `module.exports` in `.js` files. 68 | 69 | I use `import` and `export` in `.jsx` files, unless `require` makes for cleaner code. 70 | 71 | I use two spaces, no semi-colons, and trailing commas where possible. I'll 72 | have a linter someday soon. 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /db/models/review.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const Product = require('./product') 5 | const Review = require('./review') 6 | const {expect, assert} = require('chai') 7 | 8 | 9 | 10 | describe('Review', function() { 11 | before('wait for the db', () => db.didSync) 12 | 13 | var review; 14 | 15 | beforeEach(function() { 16 | review = Review.build({ 17 | title: 'This is a good product', 18 | content: "I dont see why this is necessary", 19 | stars: 5 20 | }); 21 | 22 | review.save(); 23 | }); 24 | 25 | // afterEach(function () { 26 | // return Product.truncate({ cascade: true }); 27 | // }); 28 | 29 | describe('Validation of fields', () => { 30 | 31 | it('Has all fields populated', function() { 32 | 33 | expect(review.title).to.be.a('string'); 34 | expect(review.content).to.be.a('string'); 35 | assert.isNumber(review.stars, 'the stars'); 36 | 37 | 38 | }) 39 | 40 | it('throws error if stars above 5', function() { 41 | review.stars = 10; 42 | 43 | return review.validate() 44 | .then((result) => { 45 | expect(result).to.be.an.instanceOf(Error); 46 | expect(result.message).to.contain('Validation error'); 47 | }) 48 | }); 49 | 50 | it('throws error if stars below 0', function() { 51 | review.stars = -2; 52 | 53 | return review.validate() 54 | .then((result) => { 55 | expect(result).to.be.an.instanceOf(Error); 56 | expect(result.message).to.contain('Validation error'); 57 | }) 58 | }) 59 | }) 60 | 61 | describe('associations', () => { 62 | 63 | // Testing for Review.belongsTo(Product) 64 | 65 | it('Checks that a review belongs to a product', function() { 66 | 67 | return Product.create({ 68 | name: 'Asus780', 69 | description: "This is an Asus product", 70 | price: 100, 71 | photoUrl: 'fillmurray.com/400/400', 72 | category: 'CPU', 73 | stock: 4 74 | }) 75 | .then((product) => { 76 | review.setProduct(product); 77 | }) 78 | .then(() => { 79 | return review.getProduct() 80 | }) 81 | .then((product) => { 82 | expect(product.name).to.equal('Asus780'); 83 | }) 84 | 85 | }) 86 | 87 | 88 | }) 89 | }) -------------------------------------------------------------------------------- /db/models/product.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const Product = require('./product') 5 | const Review = require('./review'); 6 | const {expect, assert} = require('chai') 7 | 8 | 9 | describe('Product', () => { 10 | before('wait for the db', () => db.didSync) 11 | 12 | var photoUrl = 'fillmurray.com/400/400' 13 | var fullText = "This is an Asus product"; 14 | var product; 15 | 16 | beforeEach(function() { 17 | product = Product.build({ 18 | name: 'Asus780', 19 | description: fullText, 20 | price: 100, 21 | photoUrl: photoUrl, 22 | category: 'CPU', 23 | stock: 4 24 | }); 25 | 26 | product.save(); 27 | }); 28 | 29 | // afterEach(function () { 30 | // return Product.truncate({ cascade: true }); 31 | // }); 32 | 33 | describe('Validation of fields', () => { 34 | 35 | it('Has all fields populated', function() { 36 | 37 | 38 | expect(product.name).to.be.a('string'); 39 | expect(product.description).to.be.a('string'); 40 | assert.isNumber(product.price, 'the price'); 41 | expect(product.photoUrl).to.be.a('string'); 42 | expect(product.category).to.be.a('string'); 43 | assert.isNumber(product.stock, 'the stock'); 44 | 45 | }) 46 | 47 | // Use isIn validator 48 | 49 | it('throws error if category is not from predefined categories', function() { 50 | product.category = 'dog'; 51 | 52 | return product.validate() 53 | .then((result) => { 54 | expect(result).to.be.an.instanceOf(Error); 55 | expect(result.message).to.contain('Validation error'); 56 | }) 57 | }) 58 | }) 59 | 60 | describe('associations', () => { 61 | 62 | // Testing for product.hasMany(Review, {as: 'Reviews'}) 63 | 64 | it('Checks that a review belongs to a product and a products has many reviews', function() { 65 | 66 | var reviewA = Review.create({ 67 | title: 'GOOD', 68 | content: 'this is my content', 69 | stars: 3 70 | }) 71 | var reviewB = Review.create({ 72 | title: 'Terrible', 73 | content: 'Next content', 74 | stars: 1 75 | }) 76 | 77 | return Promise.all([reviewA, reviewB]) 78 | .then(function([reviewA, reviewB]) { 79 | return product.setReviews([reviewA, reviewB]); 80 | }) 81 | .then(() => { 82 | return product.getReviews(); 83 | }) 84 | .then((reviews) => { 85 | expect(reviews[0].title).to.equal('GOOD'); 86 | }) 87 | }) 88 | }) 89 | 90 | }) -------------------------------------------------------------------------------- /server/auth.tst.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const {expect} = require('chai') 3 | const db = require('APP/db') 4 | const User = require('APP/db/models/user') 5 | const app = require('./start') 6 | 7 | const alice = { 8 | username: 'alice@secrets.org', 9 | password: '12345' 10 | } 11 | 12 | describe('/api/auth', () => { 13 | before('create a user', () => 14 | db.didSync 15 | .then(() => 16 | User.create( 17 | {email: alice.username, 18 | password: alice.password 19 | }) 20 | ) 21 | ) 22 | 23 | describe('POST /local/login (username, password)', () => { 24 | it('succeeds with a valid username and password', () => 25 | request(app) 26 | .post('/api/auth/local/login') 27 | .send(alice) 28 | .expect(302) 29 | .expect('Set-Cookie', /session=.*/) 30 | .expect('Location', '/') 31 | ) 32 | 33 | it('fails with an invalid username and password', () => 34 | request(app) 35 | .post('/api/auth/local/login') 36 | .send({username: alice.username, password: 'wrong'}) 37 | .expect(401) 38 | ) 39 | }) 40 | 41 | describe('GET /whoami', () => { 42 | describe('when logged in,', () => { 43 | const agent = request.agent(app) 44 | before('log in', () => agent 45 | .post('/api/auth/local/login') 46 | .send(alice)) 47 | 48 | it('responds with the currently logged in user', () => 49 | agent.get('/api/auth/whoami') 50 | .set('Accept', 'application/json') 51 | .expect(200) 52 | .then(res => expect(res.body).to.contain({ 53 | email: alice.username 54 | })) 55 | ) 56 | }) 57 | 58 | it('when not logged in, responds with an empty object', () => 59 | request(app).get('/api/auth/whoami') 60 | .expect(200) 61 | .then(res => expect(res.body).to.eql({})) 62 | ) 63 | }) 64 | 65 | describe('POST /logout when logged in', () => { 66 | const agent = request.agent(app) 67 | 68 | before('log in', () => agent 69 | .post('/api/auth/local/login') 70 | .send(alice)) 71 | 72 | it('logs you out and redirects to whoami', () => agent 73 | .post('/api/auth/logout') 74 | .expect(302) 75 | .expect('Location', '/api/auth/whoami') 76 | .then(() => 77 | agent.get('/api/auth/whoami') 78 | .expect(200) 79 | .then(rsp => expect(rsp.body).eql({})) 80 | ) 81 | ) 82 | }) 83 | 84 | // fails when address is not given 85 | 86 | // fails when address is not valid 87 | 88 | 89 | }) -------------------------------------------------------------------------------- /monitor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build_a_box", 3 | "version": "0.0.1", 4 | "description": "A happy little skeleton.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "check-node-version --node '>= 6.7.0' && bin/setup && mocha --compilers js:babel-register app/**/*.test.js app/**/*.test.jsx db/**/*.test.js server/**/*.test.js", 8 | "test-watch": "check-node-version --node '>= 6.7.0' && bin/setup && mocha --compilers js:babel-register --watch app/**/*.test.js app/**/*.test.jsx db/**/*.test.js server/**/*.test.js", 9 | "build": "check-node-version --node '>= 6.7.0' && bin/setup && webpack", 10 | "build-watch": "check-node-version --node '>= 6.7.0' && bin/setup && webpack -w", 11 | "start": "check-node-version --node '>= 6.7.0' && bin/setup && nodemon server/start.js && npm run seed", 12 | "seed": "node db/seed.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/queerviolet/bones.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "redux", 21 | "skeleton" 22 | ], 23 | "author": "Ashi Krishnan ", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/queerviolet/bones/issues" 27 | }, 28 | "homepage": "https://github.com/queerviolet/bones#readme", 29 | "dependencies": { 30 | "axios": "^0.15.2", 31 | "babel": "^6.5.2", 32 | "babel-core": "^6.18.0", 33 | "babel-loader": "^6.2.7", 34 | "babel-preset-es2015": "^6.18.0", 35 | "babel-preset-react": "^6.16.0", 36 | "babel-preset-stage-2": "^6.18.0", 37 | "bcrypt": "^0.8.7", 38 | "body-parser": "^1.15.2", 39 | "chai-enzyme": "^0.5.2", 40 | "chalk": "^1.1.3", 41 | "check-node-version": "^1.1.2", 42 | "cookie-session": "^2.0.0-alpha.1", 43 | "enzyme": "^2.5.1", 44 | "express": "^4.14.0", 45 | "font-awesome": "^4.7.0", 46 | "nodemon": "^1.11.0", 47 | "passport": "^0.3.2", 48 | "passport-facebook": "^2.1.1", 49 | "passport-github2": "^0.1.10", 50 | "passport-google-oauth": "^1.0.0", 51 | "passport-local": "^1.0.0", 52 | "pg": "^6.1.0", 53 | "pg-native": "^1.10.0", 54 | "react": "^15.3.2", 55 | "react-dom": "^15.3.2", 56 | "react-redux": "^4.4.5", 57 | "react-router": "^3.0.0", 58 | "redux": "^3.6.0", 59 | "redux-logger": "^2.7.0", 60 | "redux-thunk": "^2.1.0", 61 | "sequelize": "^3.24.6", 62 | "sinon": "^1.17.6", 63 | "sinon-chai": "^2.8.0", 64 | "webpack": "^1.13.3" 65 | }, 66 | "devDependencies": { 67 | "chai": "^3.5.0", 68 | "mocha": "^3.1.2", 69 | "supertest": "^2.0.1", 70 | "supertest-as-promised": "^4.0.1", 71 | "volleyball": "^1.4.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/monitor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /server/cart.js: -------------------------------------------------------------------------------- 1 | const db = require('APP/db') 2 | const Product = db.model('products') 3 | const Guest = db.model('guests') 4 | 5 | 6 | 7 | module.exports = require('express').Router() 8 | // .get('/:cartId', (req, res, next) => 9 | // Product.findById() 10 | // .then(cart => res.json(products)) 11 | // .catch(next)) 12 | 13 | .post('/', function(req, res, next) { 14 | var product = req.body; 15 | // if(req.user){ 16 | 17 | // req.session.product = product; 18 | 19 | 20 | let tempGuest; 21 | if(!req.session.id){ 22 | Guest.create() 23 | .then(function(guest){ 24 | req.session.id = guest.id; 25 | tempGuest = guest; 26 | return Product.findById(req.body.product.id) 27 | }) 28 | .then(function(product){ 29 | console.log('PRODUCT', product) 30 | return tempGuest.addProduct(product); 31 | }).then(() => { 32 | res.send('Finished') 33 | }) 34 | .catch(next); 35 | } else { 36 | console.log(req.session.id) 37 | Guest.findById(req.session.id) 38 | .then(function(foundGuest) { 39 | console.log(foundGuest) 40 | tempGuest = foundGuest; 41 | return Product.findById(req.body.product.id) 42 | }) 43 | .then(function(product) { 44 | return tempGuest.addProduct(product); 45 | }) 46 | .then(() => { 47 | res.send('Finished') 48 | }) 49 | .catch(next); 50 | } 51 | }) 52 | 53 | 54 | // router.get('/:id', (req, res, next) => 55 | // Product.findById(req.params.id) 56 | // .then(foundProduct => { 57 | // if (!foundProduct) res.sendStatus(404); 58 | // else res.json(foundProduct) 59 | // }) 60 | // .catch(next)) 61 | 62 | // router.get('/:category', (req, res, next) => 63 | // Product.findByAll({ 64 | // where: { 65 | // category: req.params.category, 66 | // } 67 | // }) 68 | // .then(products => { 69 | // if (!products) res.sendStatus(404); 70 | // else res.json(products) 71 | // }) 72 | // .catch(next)) 73 | 74 | // router.put('/:id', (req, res, next) => { 75 | // console.log("any string hittin the route") 76 | // Product.update(req.body, { 77 | // where: {id: req.params.id}, 78 | // returning: true 79 | // }) 80 | // .then(function (results) { 81 | // var updated = results[1][0] 82 | // // console.log("hello", updated) 83 | // res.json({ 84 | // message: 'Updated successfully', 85 | // product: updated 86 | // }) 87 | // }) 88 | // .catch(function(err){ 89 | // console.log(err) 90 | // }) 91 | //}) 92 | 93 | -------------------------------------------------------------------------------- /app/components/Cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link, browserHistory} from 'react-router' 3 | import {createOrder} from '../action-creators/cart' 4 | 5 | export default function (props) { 6 | 7 | const cart = props.selectedCart; 8 | let total = 0; 9 | cart.forEach(product => { 10 | total += product.price 11 | }) 12 | 13 | function showCheckout() { 14 | var elem = document.getElementById('checkout') 15 | if (elem.style.display === 'none') elem.style.display = 'block' 16 | } 17 | 18 | // function makeOrder(e) { 19 | // e.preventDefault() 20 | // var elem = document.getElementById('checkout') 21 | // createOrder() 22 | 23 | // {billAddress: elem.elements.billAddress.value} 24 | // } 25 | 26 | function handleSubmit(e){ 27 | e.preventDefault() 28 | 29 | 30 | let formData = { 31 | name: e.target.name.value, 32 | shipAddress: e.target.shipAddress.value, 33 | billAddress: e.target.billAddress.value, 34 | ccInfo: e.target.ccNumber.value, 35 | expiration: e.target.ccExpDate.value, 36 | totalPrice: e.target.totalPrice.value, 37 | } 38 | 39 | 40 | createOrder(formData) 41 | browserHistory.push('/orders') 42 | } 43 | 44 | return ( 45 |
46 |
47 |

Cart

48 | 49 | { 50 | cart && cart.map(product => ( 51 |
52 |

{ product.name }

53 |
${ product.price }.00
54 |
Category: { product.category }
55 |
56 | )) 57 | } 58 | 59 |
60 |

Total: ${total}.00

61 |
62 | 63 |
64 | 65 |
66 | 67 |
(handleSubmit(e))}> 68 |
Name:
69 |
Shipping Address:
70 |
Billing Address:
71 |
Credit Card Number:
72 |
Expiration Date:
73 |
Total Price:
74 | 75 |
76 | 77 |
78 |
79 | ); 80 | } -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; } 3 | 4 | body { 5 | background: white; 6 | font-family: orbitron; 7 | font-size: 14px; 8 | padding-bottom: 56px; 9 | overflow-x: hidden; 10 | } 11 | 12 | #main { 13 | padding: 0; } 14 | 15 | .col-xs-10 { 16 | padding-left: 10px 17 | } 18 | 19 | h1, h2, h3 { 20 | font-weight: 700; 21 | } 22 | 23 | h1 { 24 | font-size: 2em; 25 | } 26 | 27 | h2 { 28 | font-size: 1.8em; 29 | } 30 | 31 | header h1{ 32 | font-size: 28px; 33 | } 34 | 35 | .logo { 36 | padding-top: 10px; 37 | font-size: 3em; 38 | } 39 | 40 | sidebar { 41 | font-color: #6799A3; 42 | position: fixed; 43 | left: 0; 44 | top: 0; 45 | background-color: #E7F3FF; 46 | padding: 0 !important; 47 | height: 100%; 48 | width: inherit; 49 | overflow-y: auto; 50 | border-right: 2px solid #555; } 51 | sidebar .playlist-item { 52 | margin-left: 10px; 53 | padding-left:10px; } 54 | sidebar::-webkit-scrollbar{ 55 | width: 10px; 56 | background-color: #222; } 57 | sidebar::-webkit-scrollbar-thumb{ 58 | background-color: black; 59 | -webkit-border-radius: 1ex; } 60 | sidebar .logo { 61 | width: 100%; 62 | padding: 15px; } 63 | sidebar section > * { 64 | padding: 0 15px; } 65 | sidebar .menu-item { 66 | transition: border-left, 0.2s; } 67 | sidebar .menu-item a { 68 | color: #6799A3; } 69 | sidebar .menu-item a:hover { 70 | color: white; } 71 | sidebar .menu-item.active, sidebar .menu-item:hover { 72 | border-left: 3px solid #00bc8c; } 73 | 74 | button { 75 | text-align: center; 76 | } 77 | 78 | header { 79 | width:100%; 80 | height: 100%; 81 | /*border-style: solid; 82 | border-width: 1px;*/ 83 | /*padding-bottom: 5px;*/ 84 | background-color: #E7F3FF; 85 | display: block; 86 | text-align: center; 87 | /*display: flex; 88 | justify-content: center;*/ 89 | } 90 | .logo { 91 | margin: 0; 92 | text-align: center; 93 | } 94 | .clear { 95 | clear: both; 96 | } 97 | 98 | header .menu { 99 | /*display: flex; 100 | justify-content: flex-end; 101 | align-items: flex-end; 102 | height: 100%;*/ 103 | /* position: absolute;*/ 104 | float: right; 105 | } 106 | 107 | header a { 108 | margin: 0 10px; 109 | } 110 | 111 | .headerButton { 112 | background-color: #6799A3; 113 | text-align: center; 114 | } 115 | 116 | .loginRegister { 117 | text-align: right; 118 | padding-right: 5px; 119 | } 120 | 121 | footer { 122 | font-size: 1.25em; 123 | position: fixed; 124 | background-color: #6799A3; 125 | bottom: 0; 126 | left: 0; 127 | width: 100%; 128 | height: 56px; 129 | /*border-top: 1px solid #222;*/ 130 | border-bottom: 1px solid #222; 131 | padding: 8px; } 132 | footer .bar { 133 | overflow: hidden; 134 | padding: 9px; } 135 | footer .progress { 136 | cursor: pointer; } 137 | footer .progress-bar { 138 | transition: none !important; } 139 | 140 | .productImg { 141 | max-height: 50%; 142 | max-width: 50%; 143 | } 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /app/main.jsx: -------------------------------------------------------------------------------- 1 | // 'use strict' 2 | import React from 'react' 3 | import {Router, Route, IndexRedirect, browserHistory} from 'react-router' 4 | import {render} from 'react-dom' 5 | import {connect, Provider} from 'react-redux' 6 | import axios from 'axios' 7 | 8 | import App from './components/App' 9 | import store from './store' 10 | 11 | import OrdersContainer from './containers/OrdersContainer' 12 | import {receiveOrders} from './action-creators/orders' 13 | import CheckoutContainer from './containers/CheckoutContainer' 14 | 15 | import Jokes from './components/Jokes' 16 | import Login from './components/Login' 17 | import WhoAmI from './components/WhoAmI' 18 | import Signup from './components/Signup' 19 | import ProductsContainer from './containers/ProductsContainer' 20 | import ProductContainer from './containers/ProductContainer' 21 | import ReviewsContainer from './containers/ReviewsContainer' 22 | import CartContainer from './containers/CartContainer'; 23 | import {receiveProducts, getProductById, getProductsByCategory} from './action-creators/products' 24 | import {receiveReviews, getReviewsByProductId} from './action-creators/reviews' 25 | 26 | const onProductsEnter = () => { 27 | console.log('RUNNING') 28 | // const products = axios.get('/products'); 29 | axios.get('/api/products') 30 | .then(response => response.data) 31 | .then(products => { 32 | store.dispatch(receiveProducts(products)); 33 | }); 34 | }; 35 | 36 | const onProductEnter = (nextRouterState) => { 37 | 38 | const productId = nextRouterState.params.productId; 39 | store.dispatch(getProductById(productId)); 40 | 41 | } 42 | 43 | 44 | 45 | const onReviewsEnter = (nextRouterState) => { 46 | 47 | const productId = nextRouterState.params.productId; 48 | store.dispatch(getReviewsByProductId(productId)); 49 | 50 | } 51 | 52 | const onCartEnter = (nextRouterState) => { 53 | 54 | // const cartId = nextRouterState.params.cartId; 55 | 56 | axios.get('/api/cart') 57 | .then(response => response.data) 58 | .then(products => { 59 | store.dispatch(receiveProducts(products)); 60 | }); 61 | store.dispatch(getProductById(cartId)); 62 | 63 | } 64 | 65 | const onCategoryEnter = (nextRouterState) => { 66 | 67 | const categoryId = nextRouterState.params.categoryId; 68 | store.dispatch(getProductsByCategory(categoryId)); 69 | 70 | } 71 | 72 | const onOrdersEnter = (nextRouterState) => { 73 | 74 | axios.get('/api/orders') 75 | .then(response => response.data) 76 | .then(orders => { 77 | store.dispatch(receiveOrders(orders)); 78 | }); 79 | } 80 | 81 | 82 | // const ExampleApp = connect( 83 | // ({ auth }) => ({ user: auth }) 84 | // ) ( 85 | // ({ user, children }) => 86 | //
87 | // 90 | // {children} 91 | //
92 | // ) 93 | 94 | render ( 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {/* */} 108 | 109 | 110 | 111 | 112 | 113 | , 114 | document.getElementById('main') 115 | ) -------------------------------------------------------------------------------- /server/auth.js: -------------------------------------------------------------------------------- 1 | const app = require('APP'), {env} = app 2 | const debug = require('debug')(`${app.name}:auth`) 3 | const passport = require('passport') 4 | 5 | const User = require('APP/db/models/user') 6 | const OAuth = require('APP/db/models/oauth') 7 | const auth = require('express').Router() 8 | 9 | 10 | /************************* 11 | * Auth strategies 12 | * 13 | * The OAuth model knows how to configure Passport middleware. 14 | * To enable an auth strategy, ensure that the appropriate 15 | * environment variables are set. 16 | * 17 | * You can do it on the command line: 18 | * 19 | * FACEBOOK_CLIENT_ID=abcd FACEBOOK_CLIENT_SECRET=1234 npm start 20 | * 21 | * Or, better, you can create a ~/.$your_app_name.env.json file in 22 | * your home directory, and set them in there: 23 | * 24 | * { 25 | * FACEBOOK_CLIENT_ID: 'abcd', 26 | * FACEBOOK_CLIENT_SECRET: '1234', 27 | * } 28 | * 29 | * Concentrating your secrets this way will make it less likely that you 30 | * accidentally push them to Github, for example. 31 | * 32 | * When you deploy to production, you'll need to set up these environment 33 | * variables with your hosting provider. 34 | **/ 35 | 36 | // Facebook needs the FACEBOOK_CLIENT_ID and FACEBOOK_CLIENT_SECRET 37 | // environment variables. 38 | OAuth.setupStrategy({ 39 | provider: 'facebook', 40 | strategy: require('passport-facebook').Strategy, 41 | config: { 42 | clientID: env.FACEBOOK_CLIENT_ID, 43 | clientSecret: env.FACEBOOK_CLIENT_SECRET, 44 | callbackURL: `${app.rootUrl}/api/auth/login/facebook`, 45 | }, 46 | passport 47 | }) 48 | 49 | // Google needs the GOOGLE_CONSUMER_SECRET AND GOOGLE_CONSUMER_KEY 50 | // environment variables. 51 | OAuth.setupStrategy({ 52 | provider: 'google', 53 | strategy: require('passport-google-oauth').Strategy, 54 | config: { 55 | consumerKey: env.GOOGLE_CONSUMER_KEY, 56 | consumerSecret: env.GOOGLE_CONSUMER_SECRET, 57 | callbackURL: `${app.rootUrl}/api/auth/login/google`, 58 | }, 59 | passport 60 | }) 61 | 62 | // Github needs the GITHUB_CLIENT_ID AND GITHUB_CLIENT_SECRET 63 | // environment variables. 64 | OAuth.setupStrategy({ 65 | provider: 'github', 66 | strategy: require('passport-github2').Strategy, 67 | config: { 68 | clientID: env.GITHUB_CLIENT_ID, 69 | clientSecrets: env.GITHUB_CLIENT_SECRET, 70 | callbackURL: `${app.rootUrl}/api/auth/login/github`, 71 | }, 72 | passport 73 | }) 74 | 75 | // Other passport configuration: 76 | 77 | passport.serializeUser((user, done) => { 78 | debug('will serialize user.id=%d', user.id) 79 | done(null, user.id) 80 | debug('did serialize user.id=%d', user.id) 81 | }) 82 | 83 | passport.deserializeUser( 84 | (id, done) => { 85 | debug('will deserialize user.id=%d', id) 86 | User.findById(id) 87 | .then(user => { 88 | debug('deserialize did ok user.id=%d', user.id) 89 | done(null, user) 90 | }) 91 | .catch(err => { 92 | debug('deserialize did fail err=%s', err) 93 | done(err) 94 | }) 95 | } 96 | ) 97 | 98 | passport.use(new (require('passport-local').Strategy) ( 99 | (email, password, done) => { 100 | debug('will authenticate user(email: "%s")', email) 101 | User.findOne({where: {email}}) 102 | .then(user => { 103 | if (!user) { 104 | debug('authenticate user(email: "%s") did fail: no such user', email) 105 | return done(null, false, { message: 'Login incorrect' }) 106 | } 107 | return user.authenticate(password) 108 | .then(ok => { 109 | if (!ok) { 110 | debug('authenticate user(email: "%s") did fail: bad password') 111 | return done(null, false, { message: 'Login incorrect' }) 112 | } 113 | debug('authenticate user(email: "%s") did ok: user.id=%d', user.id) 114 | done(null, user) 115 | }) 116 | }) 117 | .catch(done) 118 | } 119 | )) 120 | 121 | auth.get('/whoami', (req, res) => res.send(req.user)) 122 | 123 | auth.post('/:strategy/login', (req, res, next) => 124 | passport.authenticate(req.params.strategy, { 125 | successRedirect: '/' 126 | })(req, res, next) 127 | ) 128 | 129 | auth.post('/logout', (req, res, next) => { 130 | req.logout() 131 | res.redirect('/api/auth/whoami') 132 | }) 133 | 134 | module.exports = auth 135 | 136 | -------------------------------------------------------------------------------- /app/components/Jokes.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class BonesJokes extends Component { 4 | componentDidMount() { 5 | this.nextJoke() 6 | } 7 | 8 | nextJoke = () => 9 | this.setState({ 10 | joke: randomJoke(), 11 | answered: false, 12 | }) 13 | 14 | answer = () => 15 | this.setState({answered: true}) 16 | 17 | render() { 18 | if (!this.state) { return null } 19 | 20 | const {joke, answered} = this.state 21 | return ( 22 |
23 |

{joke.q}

24 | {answered &&

{joke.a}

} 25 | ~xoxo, bones 26 |
27 | ) 28 | } 29 | } 30 | 31 | function randomJoke() { 32 | return jokes[Math.floor(Math.random() * jokes.length)] 33 | } 34 | 35 | const jokes = `Q: Who won the skeleton beauty contest? 36 | A: No body 37 | Q: What do skeletons say before they begin dining? 38 | A: Bone appetit ! 39 | Q: When does a skeleton laugh? 40 | A: When something tickles his funny bone. 41 | Q: Why didn't the skeleton dance at the Halloween party? 42 | A: It had no body to dance with. 43 | Q: What type of art do skeletons like? 44 | A: Skull tures 45 | Q: What did the skeleton say when his brother told a lie? 46 | A: You can't fool me, I can see right through you. 47 | Q: What did the skeleton say while riding his Harley Davidson motorcycle? 48 | A: I'm bone to be wild! 49 | Q: Why didn't the skeleton dance at the party? 50 | A: He had no body to dance with. 51 | Q: What do you give a skeleton for valentine's day? 52 | A: Bone-bones in a heart shaped box. 53 | Q: Who was the most famous skeleton detective? 54 | A: Sherlock Bones. 55 | Q: Who was the most famous French skeleton? 56 | A: Napoleon bone-apart 57 | Q: What instrument do skeletons play? 58 | A: Trom-BONE. 59 | Q: What does a skeleton orders at a restaurant? 60 | A: Spare ribs!!! 61 | Q: When does a skeleton laugh? 62 | A: When something tickles his funny bone. 63 | Q: Why didn't the skeleton eat the cafeteria food? 64 | A: Because he didn't have the stomach for it! 65 | Q: Why couldn't the skeleton cross the road? 66 | A: He didn't have the guts. 67 | Q: Why are skeletons usually so calm ? 68 | A: Nothing gets under their skin ! 69 | Q: Why do skeletons hate winter? 70 | A: Beacuse the cold goes right through them ! 71 | Q: Why are graveyards so noisy ? 72 | A: Beacause of all the coffin ! 73 | Q: Why didn't the skeleton go to the party ? 74 | A: He had no body to go with ! 75 | Q: What happened when the skeletons rode pogo sticks ? 76 | A: They had a rattling good time ! 77 | Q: Why did the skeleton go to hospital ? 78 | A: To have his ghoul stones removed ! 79 | Q: How did the skeleton know it was going to rain ? 80 | A: He could feel it in his bones ! 81 | Q: What's a skeleton's favourite musical instrument ? 82 | A: A trom-bone ! 83 | Q: How do skeletons call their friends ? 84 | A: On the telebone ! 85 | Q: What do you call a skeleton who won't get up in the mornings ? 86 | A: Lazy bones ! 87 | Q: What do boney people use to get into their homes ? 88 | A: Skeleton keys ! 89 | Q: What do you call a skeleton who acts in Westerns ? 90 | A: Skint Eastwood ! 91 | Q: What happened to the boat that sank in the sea full of piranha fish ? 92 | A: It came back with a skeleton crew ! 93 | Q: What do you call a skeleton snake ? 94 | A: A rattler ! 95 | Q: What is a skeletons like to drink milk ? 96 | A: Milk - it's so good for the bones ! 97 | Q: Why did the skeleton stay out in the snow all night ? 98 | A: He was a numbskull ! 99 | Q: What do you call a stupid skeleton ? 100 | A: Bonehead ! 101 | Q: What happened to the skeleton who stayed by the fire too long ? 102 | A: He became bone dry ! 103 | Q: What happened to the lazy skeleton ? 104 | A: He was bone idle ! 105 | Q: Why did the skeleton pupil stay late at school ? 106 | A: He was boning up for his exams ! 107 | Q: What sort of soup do skeletons like ? 108 | A: One with plenty of body in it ! 109 | Q: Why did the skeleton run up a tree ? 110 | A: Because a dog was after his bones ! 111 | Q: What did the skeleton say to his girlfriend ? 112 | A: I love every bone in your body ! 113 | Q: Why wasn't the naughty skeleton afraid of the police ? 114 | A: Because he knew they couldn't pin anything on him ! 115 | Q: How do skeletons get their mail ? 116 | A: By bony express ! 117 | Q: Why don't skeletons play music in church ? 118 | A: They have no organs ! 119 | Q: What kind of plate does a skeleton eat off ? 120 | A: Bone china ! 121 | Q: Why do skeletons hate winter ? 122 | A: Because the wind just goes straight through them ! 123 | Q: What's a skeleton's favourite pop group ? 124 | A: Boney M ! 125 | Q: What do you do if you see a skeleton running across a road ? 126 | A: Jump out of your skin and join him ! 127 | Q: What did the old skeleton complain of ? 128 | A: Aching bones ! 129 | Q: What is a skeleton ? 130 | A: Somebody on a diet who forgot to say "when" ! 131 | Q: What happened to the skeleton that was attacked by a dog ? 132 | A: He ran off with some bones and didn't leave him with a leg to stand on ! 133 | Q: Why are skeletons so calm ? 134 | A: Because nothing gets under their skin ! 135 | Q: What do you call a skeleton that is always telling lies ? 136 | A: A boney phoney ! 137 | Q: Why didn't the skeleton want to play football ? 138 | A: Because his heart wasn't in it ! 139 | Q: What happened to the skeleton who went to a party ? 140 | A: All the others used him as a coat rack ! 141 | Q: What do you call a skeleton who presses the door bell ? 142 | A: A dead ringer ! 143 | Q: When does a skeleton laugh? 144 | A: When something tickles his funny bone. 145 | Q: How did skeletons send their letters in the old days? 146 | A: By bony express! 147 | Q: How do you make a skeleton laugh? 148 | A: Tickle his funny bone!` 149 | .split('\n') 150 | .reduce((all, row, i) => 151 | i % 2 === 0 152 | ? [...all, {q: row}] 153 | : [...all.slice(0, all.length - 1), Object.assign({a: row}, all[all.length - 1])], 154 | []) -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | 5 | var expect = require('chai').expect; 6 | var request = require('supertest-as-promised'); 7 | 8 | var app = require('../app'); 9 | var agent = request.agent(app); 10 | 11 | var db = require('../models/database'); 12 | var Article = require('../models/article'); 13 | var User = require('../models/user'); 14 | 15 | /** 16 | * 17 | * Article Route Tests 18 | * 19 | * Do these after you finish the Article Model tests 20 | * 21 | */ 22 | describe('Articles Route:', function () { 23 | 24 | /** 25 | * First we clear the database before beginning each run 26 | */ 27 | before(function () { 28 | return db.sync({force: true}); 29 | }); 30 | 31 | /** 32 | * Also, we empty the tables after each spec 33 | */ 34 | afterEach(function () { 35 | return Promise.all([ 36 | Article.truncate({ cascade: true }), 37 | User.truncate({ cascade: true }) 38 | ]); 39 | }); 40 | 41 | describe('GET /articles', function () { 42 | /** 43 | * Problem 1 44 | * We'll run a GET request to /articles 45 | * 46 | * 1. It should return JSON (i.e., use res.json) 47 | * 2. Because there isn't anything in the DB, it should be an empty array 48 | * 49 | * **Extra Credit**: Consider using app.param to automatically load 50 | * in the Article whenever a param :id is detected 51 | */ 52 | it('responds with an array via JSON', function () { 53 | 54 | return agent 55 | .get('/articles') 56 | .expect('Content-Type', /json/) 57 | .expect(200) 58 | .expect(function (res) { 59 | // res.body is the JSON return object 60 | expect(res.body).to.be.an.instanceOf(Array); 61 | expect(res.body).to.have.length(0); 62 | }); 63 | 64 | }); 65 | 66 | /** 67 | * Problem 2 68 | * Save an article in the database using our model and then retrieve it 69 | * using the GET /articles route 70 | * 71 | */ 72 | it('returns an article if there is one in the DB', function () { 73 | 74 | var article = Article.build({ 75 | title: 'Test Article', 76 | content: 'Test body' 77 | }); 78 | 79 | return article.save().then(function () { 80 | 81 | return agent 82 | .get('/articles') 83 | .expect(200) 84 | .expect(function (res) { 85 | expect(res.body).to.be.an.instanceOf(Array); 86 | expect(res.body[0].content).to.equal('Test body'); 87 | }); 88 | 89 | }); 90 | 91 | }); 92 | 93 | /** 94 | * Problem 3 95 | * Save a second article in the database using our model, then retrieve it 96 | * using the GET /articles route 97 | * 98 | */ 99 | it('returns another article if there is one in the DB', function () { 100 | 101 | var article1 = Article.build({ 102 | title: 'Test Article', 103 | content: 'Test body' 104 | }); 105 | 106 | var article2 = Article.build({ 107 | title: 'Another Test Article', 108 | content: 'Another test body' 109 | }); 110 | 111 | return article1.save() 112 | .then(function () { return article2.save() }) 113 | .then(function () { 114 | 115 | return agent 116 | .get('/articles') 117 | .expect(200) 118 | .expect(function (res) { 119 | expect(res.body).to.be.an.instanceOf(Array); 120 | expect(res.body[0].content).to.equal('Test body'); 121 | expect(res.body[1].content).to.equal('Another test body'); 122 | }); 123 | 124 | }); 125 | 126 | }); 127 | 128 | }); 129 | 130 | /** 131 | * Search for articles by ID 132 | */ 133 | describe('GET /articles/:id', function () { 134 | 135 | var coolArticle; 136 | 137 | beforeEach(function () { 138 | 139 | var creatingArticles = [{ 140 | title: 'Boring article', 141 | content: 'This article is boring' 142 | }, { 143 | title: 'Cool Article', 144 | content: 'This article is cool' 145 | }, { 146 | title: 'Riveting Article', 147 | content: 'This article is riveting' 148 | }] 149 | .map(data => Article.create(data)); 150 | 151 | return Promise.all(creatingArticles) 152 | .then(createdArticles => { 153 | coolArticle = createdArticles[1]; 154 | }); 155 | 156 | }); 157 | 158 | /** 159 | * This is a proper GET /articles/ID request 160 | * where we search by the ID of the article created above 161 | */ 162 | it('returns the JSON of the article based on the id', function () { 163 | 164 | return agent 165 | .get('/articles/' + coolArticle.id) 166 | .expect(200) 167 | .expect(function (res) { 168 | if (typeof res.body === 'string') { 169 | res.body = JSON.parse(res.body); 170 | } 171 | expect(res.body.title).to.equal('Cool Article'); 172 | }); 173 | 174 | }); 175 | 176 | /** 177 | * Here we pass in a bad ID to the URL, we should get a 404 error 178 | */ 179 | it('returns a 404 error if the ID is not correct', function () { 180 | 181 | return agent 182 | .get('/articles/76142896') 183 | .expect(404); 184 | 185 | }); 186 | 187 | }); 188 | 189 | /** 190 | * Series of tests to test creation of new Articles using a POST 191 | * request to /articles 192 | */ 193 | describe('POST /articles', function () { 194 | 195 | /** 196 | * Test the creation of an article 197 | * Here we don't get back just the article, we get back a Object 198 | * of this type, which you construct manually: 199 | * 200 | * { 201 | * message: 'Created successfully' 202 | * article: { 203 | * id: ... 204 | * title: ... 205 | * content: ... 206 | * } 207 | * } 208 | */ 209 | it('creates a new article', function () { 210 | 211 | return agent 212 | .post('/articles') 213 | .send({ 214 | title: 'Awesome POST-Created Article', 215 | content: 'Can you believe I did this in a test?' 216 | }) 217 | .expect(200) 218 | .expect(function (res) { 219 | expect(res.body.message).to.equal('Created successfully'); 220 | expect(res.body.article.id).to.not.be.an('undefined'); 221 | expect(res.body.article.title).to.equal('Awesome POST-Created Article'); 222 | }); 223 | 224 | }); 225 | 226 | // This one should fail with a 500 because we don't set the article.content 227 | it('does not create a new article without content', function () { 228 | 229 | return agent 230 | .post('/articles') 231 | .send({ 232 | title: 'This Article Should Not Be Allowed' 233 | }) 234 | .expect(500); 235 | 236 | }); 237 | 238 | // Check if the articles were actually saved to the database 239 | it('saves the article to the DB', function () { 240 | 241 | return agent 242 | .post('/articles') 243 | .send({ 244 | title: 'Awesome POST-Created Article', 245 | content: 'Can you believe I did this in a test?' 246 | }) 247 | .expect(200) 248 | .then(function () { 249 | return Article.findOne({ 250 | where: { title: 'Awesome POST-Created Article' } 251 | }); 252 | }) 253 | .then(function (foundArticle) { 254 | expect(foundArticle).to.exist; // eslint-disable-line no-unused-expressions 255 | expect(foundArticle.content).to.equal('Can you believe I did this in a test?'); 256 | }); 257 | 258 | }); 259 | 260 | // Do not assume async operations (like db writes) will work; always check 261 | it('sends back JSON of the actual created article, not just the POSTed data', function () { 262 | 263 | return agent 264 | .post('/articles') 265 | .send({ 266 | title: 'Coconuts', 267 | content: 'A full-sized coconut weighs about 1.44 kg (3.2 lb).', 268 | extraneous: 'Sequelize will quietly ignore this non-schema property' 269 | }) 270 | .expect(200) 271 | .expect(function (res) { 272 | expect(res.body.article.extraneous).to.be.an('undefined'); 273 | expect(res.body.article.createdAt).to.exist; // eslint-disable-line no-unused-expressions 274 | }); 275 | 276 | }); 277 | 278 | }); 279 | 280 | /** 281 | * Series of specs to test updating of Articles using a PUT 282 | * request to /articles/:id 283 | */ 284 | describe('PUT /articles/:id', function () { 285 | 286 | var article; 287 | 288 | beforeEach(function () { 289 | 290 | return Article.create({ 291 | title: 'Final Article', 292 | content: 'You can do it!' 293 | }) 294 | .then(function (createdArticle) { 295 | article = createdArticle; 296 | }); 297 | 298 | }); 299 | 300 | /** 301 | * Test the updating of an article 302 | * Here we don't get back just the article, we get back a Object 303 | * of this type, which you construct manually: 304 | * 305 | * { 306 | * message: 'Updated successfully' 307 | * article: { 308 | * id: ... 309 | * title: ... 310 | * content: ... 311 | * } 312 | * } 313 | */ 314 | it('updates an article', function () { 315 | 316 | return agent 317 | .put('/articles/' + article.id) 318 | .send({ 319 | title: 'Awesome PUT-Updated Article' 320 | }) 321 | .expect(200) 322 | .expect(function (res) { 323 | expect(res.body.message).to.equal('Updated successfully'); 324 | expect(res.body.article.id).to.not.be.an('undefined'); 325 | expect(res.body.article.title).to.equal('Awesome PUT-Updated Article'); 326 | expect(res.body.article.content).to.equal('You can do it!'); 327 | }); 328 | 329 | }); 330 | 331 | it('saves updates to the DB', function () { 332 | 333 | return agent 334 | .put('/articles/' + article.id) 335 | .send({ 336 | title: 'Awesome PUT-Updated Article' 337 | }) 338 | .then(function () { 339 | return Article.findById(article.id); 340 | }) 341 | .then(function (foundArticle) { 342 | expect(foundArticle).to.exist; // eslint-disable-line no-unused-expressions 343 | expect(foundArticle.title).to.equal('Awesome PUT-Updated Article'); 344 | }); 345 | 346 | }); 347 | 348 | it('gets 500 for invalid update', function () { 349 | 350 | return agent 351 | .put('/articles/' + article.id) 352 | .send({ title: '' }) 353 | .expect(500); 354 | 355 | }); 356 | 357 | }); 358 | 359 | }); -------------------------------------------------------------------------------- /server/reviews.test.js: -------------------------------------------------------------------------------- 1 | //todo add looking at reviews for a given user 2 | 3 | const request = require('supertest-as-promised') 4 | const {expect} = require('chai') 5 | const db = require('APP/db') 6 | const Review = require('APP/db/models/review') 7 | const Product = require('APP/db/models/product') 8 | const User = require('APP/db/models/user') 9 | 10 | const app = require('./start') 11 | 12 | describe('Reviews Route:', function () { 13 | 14 | /** 15 | * First we clear the database before beginning each run 16 | */ 17 | before(function () { 18 | return db.sync({force: true}); 19 | }); 20 | 21 | /** 22 | * Also, we empty the tables after each spec 23 | */ 24 | afterEach(function () { 25 | return Promise.all([ 26 | Product.truncate({ cascade: true }), 27 | // User.truncate({ cascade: true }) 28 | ]); 29 | }); 30 | 31 | describe('GET /products/:id/reviews', function () { 32 | /** 33 | * Problem 1 34 | * We'll run a GET request to /products/:id/reviews 35 | * 36 | * 1. It should return JSON (i.e., use res.json) 37 | * 2. Because there isn't anything in the DB, it should be an empty array 38 | * 39 | * **Extra Credit**: Consider using app.param to automatically load 40 | * in the product whenever a param :id is detected 41 | */ 42 | it('responds with an array via JSON', function () { 43 | 44 | request(app) 45 | .get('/products/:id/reviews') 46 | .expect('Content-Type', /json/) 47 | .expect(200) 48 | .expect(function (res) { 49 | // res.body is the JSON return object 50 | expect(res.body).to.be.an.instanceOf(Array); 51 | expect(res.body).to.have.length(0); 52 | }); 53 | 54 | }); 55 | 56 | /** 57 | * Problem 2 58 | * Save an products in the database using our model and then retrieve it 59 | * using the GET /products/:id/reviews route 60 | * 61 | */ 62 | it('returns a review for a specific product', function () { 63 | 64 | var product = Product.build({ 65 | title: 'Asus motherboard', 66 | description: 'board', 67 | price: 5.99, 68 | quantity: 1, 69 | category: 'motherboard', 70 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 71 | }) 72 | 73 | var user = User.build({ 74 | email: 'beth@secrets.org', 75 | password: '12345', 76 | }) 77 | 78 | var review = Review.build({ 79 | title: 'Great Product!', 80 | content: 'This is the content for the review', 81 | rating: 5, 82 | userId: user.id, 83 | productId: product.id, 84 | }) 85 | 86 | return product.save().then(function () { 87 | return user.save().then(function () { 88 | return review.save().then(function () { 89 | request(app) 90 | .get('/products/:id/reviews') 91 | .expect(function (res) { 92 | expect(res.body).to.be.an.instanceOf(Array) 93 | expect(res.body[0].title).to.equal('Great Product!') 94 | expect(res.body[0].content).to.equal('This is the content for the review') 95 | expect(res.body[0].rating).to.equal(5) 96 | }) 97 | }) 98 | }) 99 | }) 100 | }) 101 | 102 | /** 103 | * Problem 3 104 | * Save a second review in the database using our model, then retrieve it 105 | * using the GET /products/:id/reviews route 106 | * 107 | */ 108 | it('returns a review if there is one in the DB for a given product', function () { 109 | 110 | 111 | var product = Product.build({ 112 | title: 'Asus motherboard', 113 | description: 'board', 114 | price: 5.99, 115 | quantity: 1, 116 | category: 'motherboard', 117 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 118 | }) 119 | 120 | var user1 = User.build({ 121 | email: 'beth@secrets.org', 122 | password: '12345', 123 | }) 124 | 125 | var user2 = User.build({ 126 | email: 'dan@secrets.org', 127 | password: '56789', 128 | }) 129 | 130 | var review1 = Review.build({ 131 | title: 'Great Product!', 132 | content: 'This is the content for the review', 133 | rating: 5, 134 | userId: user1.id, 135 | productId: product.id, 136 | }) 137 | 138 | var review2 = Review.build({ 139 | title: 'Bad Product!', 140 | content: 'This is the content for the review', 141 | rating: 2, 142 | userId: user2.id, 143 | productId: product.id, 144 | }) 145 | 146 | return product.save().then(function () { 147 | return user1.save().then(function () { 148 | return user2.save().then(function () { 149 | return review1.save().then(function () { 150 | return review2.save().then(function () { 151 | request(app) 152 | .get('/products/:id/reviews') 153 | .expect(function (res) { 154 | expect(res.body).to.be.an.instanceOf(Array) 155 | expect(res.body[0].title).to.equal('Great Product!') 156 | expect(res.body[1].title).to.equal('Bad Product!') 157 | }) 158 | }) 159 | }) 160 | }) 161 | }) 162 | }) 163 | }) 164 | 165 | // re add the following tests once we have models built out 166 | 167 | /** 168 | * Series of tests to test creation of new reviews using a POST 169 | * request to /products/:id/reviews 170 | */ 171 | 172 | describe('POST /products/:id/reviews', function () { 173 | 174 | var coolProduct; 175 | 176 | beforeEach(function () { 177 | 178 | Product.create({ 179 | title: 'Asus motherboard', 180 | description: 'board', 181 | price: 5.99, 182 | quantity: 1, 183 | category: 'motherboard', 184 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 185 | }) 186 | .then(createdProduct => { 187 | coolProduct = createdProduct; 188 | }); 189 | 190 | 191 | it('creates a new review', function () { 192 | request(app) 193 | .post('/products/' + coolProduct.id + '/reviews') 194 | .send({ 195 | title: 'Great Product!', 196 | content: 'This is the content for the review', 197 | rating: 5, 198 | userId: 1, 199 | productId: 1, 200 | }) 201 | .expect(200) 202 | .expect(function (res) { 203 | expect(res.body.message).to.equal('Created successfully'); 204 | expect(res.body.review.id).to.not.be.an('undefined'); 205 | expect(res.body.review.title).to.equal('Great Product!'); 206 | expect(res.body.review.content).to.equal('This is the content for the review'); 207 | expect(res.body.review.rating).to.equal(5); 208 | 209 | }); 210 | 211 | }); 212 | 213 | // This one should fail with a 500 because we don't set the review.content 214 | it('does not create a new review without content', function () { 215 | 216 | request(app) 217 | .post('/products/' + coolProduct.id + '/reviews') 218 | .send({ 219 | title: 'This Product Review Should Not Be Allowed' 220 | }) 221 | .expect(500); 222 | 223 | }); 224 | 225 | // Check if the review were actually saved to the database 226 | it('saves the review to the DB', function () { 227 | 228 | request(app) 229 | .post('/products/' + coolProduct.id + '/reviews') 230 | .send({ 231 | title: 'Great Product!', 232 | content: 'This is the content for the review', 233 | rating: 5, 234 | userId: 1, 235 | productId: 1, 236 | }) 237 | .expect(200) 238 | .then(function () { 239 | return Review.findOne({ 240 | where: { title: 'Great Product!' } 241 | }); 242 | }) 243 | .then(function (foundReview) { 244 | expect(foundReview).to.exist; // eslint-disable-line no-unused-expressions 245 | expect(foundReview.title).to.equal('Great Product!'); 246 | }); 247 | 248 | }); 249 | 250 | // Do not assume async operations (like db writes) will work; always check 251 | it('sends back JSON of the actual created product, not just the POSTed data', function () { 252 | 253 | request(app) 254 | .post('/products/' + coolProduct.id + '/reviews') 255 | .send({ 256 | title: 'Great Product!', 257 | content: 'This is the content for the review', 258 | rating: 5, 259 | userId: 1, 260 | productId: 1, 261 | extraneous: 'Sequelize will quietly ignore this non-schema property' 262 | }) 263 | .expect(200) 264 | .expect(function (res) { 265 | expect(res.body.review.extraneous).to.be.an('undefined'); 266 | expect(res.body.review.createdAt).to.exist; // eslint-disable-line no-unused-expressions 267 | }); 268 | 269 | }); 270 | 271 | }); 272 | 273 | /** 274 | * Series of specs to test updating of products using a PUT 275 | * request to /product/:id 276 | */ 277 | // describe('PUT /product/:id', function () { 278 | 279 | // var product; 280 | 281 | // beforeEach(function () { 282 | 283 | // Product.create({ 284 | // title: 'Asus motherboard', 285 | // description: 'board', 286 | // price: 5.99, 287 | // quantity: 1, 288 | // category: 'motherboard', 289 | // photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 290 | // }) 291 | // .then(createdProduct => { 292 | // product = createdProduct; 293 | // }); 294 | // }); 295 | 296 | // /** 297 | // * Test the updating of an product 298 | // * Here we don't get back just the prodcut, we get back a Object 299 | // * of this type, which you construct manually: 300 | // * 301 | // * 302 | // * } 303 | // */ 304 | // it('updates a product', function () { 305 | 306 | // request(app) 307 | // .put('/products/' + product.id) 308 | // .send({ 309 | // title: 'Asus mobo2' 310 | // }) 311 | // .expect(200) 312 | // .expect(function (res) { 313 | // expect(res.body.message).to.equal('Updated successfully'); 314 | // expect(res.body.product.id).to.not.be.an('undefined'); 315 | // expect(res.body.product.title).to.equal('Asus mobo2'); 316 | // expect(res.body.product.photoUrl).to.equal('http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2'); 317 | // }); 318 | 319 | // }); 320 | 321 | // it('saves updates to the DB', function () { 322 | 323 | // request(app) 324 | // .put('/products/' + product.id) 325 | // .send({ 326 | // title: 'Asus mobo2' 327 | // }) 328 | // .then(function () { 329 | // return Product.findById(product.id); 330 | // }) 331 | // .then(function (foundProduct) { 332 | // expect(foundProduct).to.exist; // eslint-disable-line no-unused-expressions 333 | // expect(foundProduct.title).to.equal('Asus mobo2'); 334 | // }); 335 | 336 | // }); 337 | 338 | // it('gets 500 for invalid update', function () { 339 | 340 | // request(app) 341 | // .put('/products/' + product.id) 342 | // .send({ title: '' }) 343 | // .expect(500); 344 | 345 | }); 346 | 347 | }); 348 | 349 | }); -------------------------------------------------------------------------------- /server/products.test.js: -------------------------------------------------------------------------------- 1 | // todo add search 2 | // find by price 3 | 4 | const db = require('APP/db') 5 | const request = require('supertest-as-promised') 6 | const {expect} = require('chai') 7 | const Product = require('APP/db/models/product') 8 | const app = require('./start') 9 | 10 | describe('Products Route:', function () { 11 | // Clear the database before beginning each run 12 | // before(function () { 13 | // return db.sync({force: true}); 14 | // }) 15 | // // Empty the tables after each spec 16 | // afterEach(function () { 17 | // return Promise.all([ 18 | // Product.truncate({ cascade: true }), 19 | // // User.truncate({ cascade: true }) 20 | // ]) 21 | // }) 22 | 23 | describe('GET /products', function () { 24 | // There isn't anything in the DB, should send an empty array 25 | it('responds with an array via JSON', function () { 26 | 27 | request(app) 28 | .get('/products') 29 | .expect('Content-Type', /json/) 30 | .expect(200) 31 | .expect(function (res) { 32 | // res.body is the JSON return object 33 | expect(res.body).to.be.an.instanceOf(Array); 34 | expect(res.body).to.have.length(0); 35 | }); 36 | 37 | }); 38 | // Save products in the database using our model and then retrieve it 39 | // using the GET /products route 40 | 41 | it('returns an product if there is one in the DB', function () { 42 | 43 | let product = Product.build({ 44 | name: 'Asus Motherboard', 45 | description: 'board', 46 | price: 599, 47 | stock: 1, 48 | category: 'Motherboard', 49 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 50 | }); 51 | 52 | return product.save().then(function () { 53 | 54 | request(app) 55 | .get('/products') 56 | .expect(200) 57 | .expect(function (res) { 58 | expect(res.body).to.be.an.instanceOf(Array); 59 | expect(res.body[0].name).to.equal('Asus Motherboard'); 60 | expect(res.body[0].description).to.equal('board'); 61 | expect(res.body[0].price).to.equal(599); 62 | expect(res.body[0].stock).to.equal(1); 63 | expect(res.body[0].category).to.equal('Motherboard'); 64 | expect(res.body[0].photoUrl).to.equal('http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2'); 65 | }); 66 | 67 | }); 68 | 69 | }); 70 | 71 | it('returns another product if there is one in the DB', function () { 72 | 73 | var product1 = Product.build({ 74 | name: 'Asus Motherboard', 75 | description: 'board', 76 | price: 599, 77 | stock: 1, 78 | category: 'Motherboard', 79 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 80 | }); 81 | 82 | var product2 = Product.build({ 83 | name: 'Amd Motherboard', 84 | description: 'board', 85 | price: 799, 86 | stock: 2, 87 | category: 'Motherboard', 88 | photoUrl: 'http://images10.newegg.com/NeweggImage/ProductImageCompressAll1280/13-130-970-V01.jpg?w=660&h=500&ex=2' 89 | }); 90 | 91 | return product1.save() 92 | .then(function () { return product2.save() }) 93 | .then(function () { 94 | 95 | request(app) 96 | .get('/products') 97 | .expect(200) 98 | .expect(function (res) { 99 | expect(res.body).to.be.an.instanceOf(Array); 100 | expect(res.body[0].name).to.equal('Asus Motherboard'); 101 | expect(res.body[1].name).to.equal('Amd Motherboard'); 102 | }) 103 | 104 | }) 105 | 106 | }) 107 | 108 | }) 109 | 110 | describe('GET /products/:id', function () { 111 | 112 | let coolProduct; 113 | 114 | beforeEach(function () { 115 | 116 | let creatingProducts = [{ 117 | name: 'Asus Motherboard', 118 | description: 'board', 119 | price: 599, 120 | stock: 1, 121 | category: 'Motherboard', 122 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 123 | }, { 124 | name: 'Amd Motherboard', 125 | description: 'board', 126 | price: 799, 127 | stock: 2, 128 | category: 'Motherboard', 129 | photoUrl: 'http://images10.newegg.com/NeweggImage/ProductImageCompressAll1280/13-130-970-V01.jpg?w=660&h=500&ex=2' 130 | }] 131 | .map(data => Product.create(data)); 132 | 133 | return Promise.all(creatingProducts) 134 | .then(createdProducts => { 135 | coolProduct = createdProducts[1]; 136 | }) 137 | 138 | }) 139 | 140 | it('returns the JSON of the product based on the id', function () { 141 | 142 | request(app) 143 | .get('/products/' + coolProduct.id) 144 | .expect(200) 145 | .expect(function (res) { 146 | if (typeof res.body === 'string') { 147 | res.body = JSON.parse(res.body); 148 | } 149 | expect(res.body.name).to.equal('Amd Motherboard'); 150 | }) 151 | 152 | }) 153 | 154 | it('returns a 404 error if the ID is not correct', function () { 155 | 156 | request(app) 157 | .get('/products/76142896') 158 | .expect(404); 159 | 160 | }) 161 | 162 | }) 163 | 164 | describe('GET /products/:category', function () { 165 | 166 | let coolProduct; 167 | 168 | beforeEach(function () { 169 | 170 | var creatingProducts = [{ 171 | name: 'Asus Motherboard', 172 | description: 'board', 173 | price: 599, 174 | stock: 1, 175 | category: 'Motherboard', 176 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 177 | }, { 178 | name: 'Amd Motherboard', 179 | description: 'board', 180 | price: 799, 181 | stock: 2, 182 | category: 'Motherboard', 183 | photoUrl: 'http://images10.newegg.com/NeweggImage/ProductImageCompressAll1280/13-130-970-V01.jpg?w=660&h=500&ex=2' 184 | }, { 185 | name: 'ATI GPU', 186 | description: 'graphics', 187 | price: 150, 188 | stock: 1, 189 | category: 'GPU', 190 | photoUrl: 'http://images10.newegg.com/productimage/A0ZX_1_20150215234604920.jpg?ex=2' 191 | }] 192 | .map(data => Product.create(data)); 193 | 194 | return Promise.all(creatingProducts) 195 | .then(createdProducts => { 196 | coolProduct = createdProducts[1]; 197 | }) 198 | 199 | }) 200 | 201 | it('returns the JSON of the product based on the category', function () { 202 | 203 | request(app) 204 | .get('/products/Motherboard') 205 | .expect(200) 206 | .expect(function (res) { 207 | if (typeof res.body === 'string') { 208 | res.body = JSON.parse(res.body); 209 | } 210 | expect(res.body[0].name).to.equal('Asus Motherboard'); 211 | expect(res.body[1].name).to.equal('Amd Motherboard'); 212 | }) 213 | 214 | }) 215 | 216 | 217 | it('returns a 404 error if the ID is not correct', function () { 218 | 219 | request(app) 220 | .get('/products/76142896') 221 | .expect(404) 222 | 223 | }) 224 | 225 | }) 226 | 227 | describe('POST /products', function () { 228 | 229 | it('creates a new product', function () { 230 | 231 | request(app) 232 | .post('/products') 233 | .send({ 234 | name: 'Asus Motherboard', 235 | description: 'board', 236 | price: 599, 237 | stock: 1, 238 | category: 'Motherboard', 239 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 240 | }) 241 | .expect(200) 242 | .expect(function (res) { 243 | expect(res.body.message).to.equal('Created successfully'); 244 | expect(res.body.product.id).to.not.be.an('undefined'); 245 | expect(res.body.product.name).to.equal('Asus Motherboard'); 246 | }); 247 | 248 | }); 249 | 250 | it('does not create a new product without content', function () { 251 | 252 | request(app) 253 | .post('/products') 254 | .send({ 255 | name: 'This product Should Not Be Allowed' 256 | }) 257 | .expect(500); 258 | 259 | }); 260 | 261 | it('saves the product to the DB', function () { 262 | var test = request(app) 263 | .post('/products') 264 | .send({ 265 | name: 'Asus Motherboard', 266 | description: 'board', 267 | price: 599, 268 | stock: 1, 269 | category: 'Motherboard', 270 | photoUrl: 'http://images10.newegg.comctImageCompressAll1280/13-132-927-V01=660&h=500&ex=2' 271 | }) 272 | // console.log(test.status) 273 | request(app) 274 | .post('/products') 275 | .send({ 276 | name: 'Asus Motherboard', 277 | description: 'board', 278 | price: 599, 279 | stock: 1, 280 | category: 'Motherboard', 281 | photoUrl: 'http://images10.newegg.comctImageCompressAll1280/13-132-927-V01=660&h=500&ex=2' 282 | }) 283 | // .expect(200) 284 | .then(function () { 285 | return Product.findOne({ 286 | where: { name: 'Asus Motherboard' } 287 | }); 288 | }) 289 | .then(function (foundProducts) { 290 | expect(foundProducts).to.exist; // eslint-disable-line no-unused-expressions 291 | expect(foundProducts.name).to.equal('Asus Motherboard'); 292 | }) 293 | 294 | }) 295 | 296 | // Do not assume async operations (like db writes) will work; always check 297 | it('sends back JSON of the actual created product, not just the POSTed data', function () { 298 | 299 | request(app) 300 | .post('/products') 301 | .send({ 302 | name: 'Asus Motherboard', 303 | description: 'board', 304 | price: 599, 305 | stock: 1, 306 | category: 'Motherboard', 307 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2', 308 | extraneous: 'Sequelize will quietly ignore this non-schema property' 309 | }) 310 | .expect(200) 311 | .expect(function (res) { 312 | expect(res.body.product.extraneous).to.be.an('undefined'); 313 | expect(res.body.product.createdAt).to.exist; // eslint-disable-line no-unused-expressions 314 | }) 315 | 316 | }) 317 | 318 | }) 319 | 320 | describe('PUT /products/:id', function () { 321 | 322 | let product; 323 | 324 | before(function () { 325 | // console.log(Product) 326 | return Product.create({ 327 | name: 'Asus Motherboard', 328 | description: 'board', 329 | price: 599, 330 | stock: 1, 331 | category: 'Motherboard', 332 | photoUrl: 'http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2' 333 | }) 334 | .then(function (createdProduct) { 335 | product = createdProduct; 336 | }) 337 | 338 | }) 339 | 340 | it('updates a product', function () { 341 | // console.log(product.id) 342 | request(app) 343 | .put('/products/' + product.id) 344 | .send({ 345 | name: 'Asus mobo2' 346 | }) 347 | .expect(200) 348 | .expect(function (res) { 349 | expect(res.body.message).to.equal('Updated successfully'); 350 | expect(res.body.product.id).to.not.be.an('undefined'); 351 | expect(res.body.product.name).to.equal('Asus mobo2'); 352 | expect(res.body.product.photoUrl).to.equal('http://images10.newegg.com/ProductImageCompressAll1280/13-132-927-V01.jpg?w=660&h=500&ex=2'); 353 | }) 354 | 355 | }) 356 | 357 | // it.only('saves updates to the DB', function () { 358 | // console.log(product.id) 359 | // return request(app) 360 | // .put('/products/' + product.id) 361 | // .send({ 362 | // name: 'Asus mobo2' 363 | // }) 364 | // .then(function () { 365 | // return Product.findById(product.id); 366 | // }) 367 | // .then(function (foundProduct) { 368 | // // console.log(foundProduct) 369 | // expect(foundProduct).to.exist; // eslint-disable-line no-unused-expressions 370 | // expect(foundProduct.name).to.equal('Asus mobo2'); 371 | // }) 372 | 373 | // }) 374 | 375 | it('gets 500 for invalid update', function () { 376 | 377 | request(app) 378 | .put('/products/' + product.id) 379 | .send({ name: '' }) 380 | .expect(500); 381 | 382 | }) 383 | 384 | }) 385 | 386 | }) -------------------------------------------------------------------------------- /db/seed.js: -------------------------------------------------------------------------------- 1 | const db = require('APP/db') 2 | 3 | const seedUsers = () => db.Promise.map( 4 | [{ 5 | id:1, 6 | name: "Jonathan Guy", 7 | email: "jguy@gmail.com", 8 | billAddress: "5 Hanover Square, New York, NY", 9 | shipAddress: "5 Hanover Square, New York, NY", 10 | isAdmin: true, 11 | password: 'monkey' 12 | }, { 13 | id:2, 14 | name: "Buddy Pal", 15 | email: "palbud@gmail.com", 16 | billAddress: "123 Tree Road, Dundersphere, NH", 17 | shipAddress: "6 City Place, Industritown, NJ", 18 | isAdmin: false, 19 | password: 'iambuddy' 20 | }, { 21 | id:3, 22 | name: "Emma Watson", 23 | email: "emmawatson@gmail.com", 24 | billAddress: "120 Wall Street, New York City, NY", 25 | shipAddress: "120 Wall Street, New York City, NY", 26 | isAdmin: true, 27 | password: 'hermione' 28 | }, { 29 | id:4, 30 | name: "Harry Potter", 31 | email: "harrypotter@gmail.com", 32 | billAddress: "25 Smith Street, Salt Lake City, UT", 33 | shipAddress: "120 Wall Street, New York City, NYC", 34 | isAdmin: true, 35 | password: 'iamlordvoldemort' 36 | }, { 37 | id:5, 38 | name: "Ron Weaseley", 39 | email: "ronweaseley@gmail.com", 40 | billAddress: "55 Main Street, Houston, TX", 41 | shipAddress: "55 Main Street, Houston, TX", 42 | isAdmin: false, 43 | password: 'magiccarpet' 44 | }, { 45 | id:6, 46 | name: "Barack Obama", 47 | email: "bobama@yahoo.com", 48 | billAddress: "1600 Pennsylvania Ave., Washington, DC", 49 | shipAddress: "1600 Pennsylvania Ave., Washington, DC", 50 | isAdmin: true, 51 | password: 'prez4life' 52 | }, { 53 | id:7, 54 | name: "Joe Biden", 55 | email: "jbiden@hotmail.com", 56 | billAddress: "1600 Pennsylvania Ave., Washington, DC", 57 | shipAddress: "1600 Pennsylvania Ave., Washington, DC", 58 | isAdmin: false, 59 | password: 'obamasbestfriend' 60 | }, { 61 | id:8, 62 | name: "Michelle Obama", 63 | email: "mobama@gmail.com", 64 | billAddress: "1600 Pennsylvania Ave., Washington, DC", 65 | shipAddress: "1600 Pennsylvania Ave., Washington, DC", 66 | isAdmin: true, 67 | password: 'eatvegetables' 68 | }, { 69 | id:9, 70 | name: "Hermione Granger", 71 | email: "hgranger@gmail.com", 72 | billAddress: "55 Rodeo Drive, Las Vegas, NV", 73 | shipAddress: "55 Main Street, Houston, TX", 74 | isAdmin: false, 75 | password: 'firstInClass' 76 | }, { 77 | id:10, 78 | name: "Hillary Clinton", 79 | email: "hclinton@gmail.com", 80 | billAddress: "1678 5th Street, New York City, NY", 81 | shipAddress: "1678 5th Street, New York City, NY", 82 | isAdmin: false, 83 | password: '2016president' 84 | }], user => db.model('users').create(user)) 85 | 86 | const seedReviews = () => db.Promise.map( 87 | [{ 88 | id:1, 89 | title: "Amazing CPU", 90 | content: "This is an incredible \"bang for the buck\" CPU.", 91 | stars: 4, 92 | user_id:5, 93 | product_id:1, 94 | }, { 95 | id:2, 96 | title: "Good but slightly too expensive", 97 | content: "Handled everything I threw at it, from benchmarks to games. Price is a bit too high", 98 | stars: 3, 99 | user_id:2, 100 | product_id:2, 101 | }, { 102 | id:3, 103 | title: "Perfect budget board", 104 | content: "It met all my requirements for a lower-end gaming build. Enough space for all my drives and devices, with room to grow.", 105 | stars: 5, 106 | user_id:4, 107 | product_id:4, 108 | }, { 109 | id:4, 110 | title: "Not flashy but gets the job done", 111 | content: "Threw together a PC for my father, only going to be used for web browsing and streaming videos. Perfect RAM for the price point. ", 112 | stars: 5, 113 | user_id:8, 114 | product_id:10, 115 | }, { 116 | id:5, 117 | title: "Terrible Experience", 118 | content: "The K means unlocked for overclocking. First time with very slight overclock, it blew itself up. Could be luck of the draw but this experience left a bad taste in my mouth. Moving to AMD for this generation of CPUs.", 119 | stars: 1, 120 | user_id:3, 121 | product_id:6, 122 | }, { 123 | id:6, 124 | title: "Awesome!", 125 | content: "Blazingly fast GPU!", 126 | stars: 5, 127 | user_id:7, 128 | product_id:14, 129 | }, { 130 | id:7, 131 | title: "Not worth the price", 132 | content: "Simply too expensive for what amounts to a small performance gain.", 133 | stars: 1, 134 | user_id:5, 135 | product_id:14, 136 | }, { 137 | id:8, 138 | title: "This SSD is amazing", 139 | content: "This SSD makes computing pure joy.", 140 | stars: 5, 141 | user_id:6, 142 | product_id:20, 143 | }, { 144 | id:9, 145 | title: "Great case!", 146 | content: "A bit pricey but worth it", 147 | stars: 4, 148 | user_id:3, 149 | product_id:11, 150 | }, { 151 | id:10, 152 | title: "Solid HDD", 153 | content: "Good hard disk at a good price", 154 | stars: 4, 155 | user_id:8, 156 | product_id:17, 157 | }], review => db.model('reviews').create(review)) 158 | 159 | const seedProducts = () => db.Promise.map( 160 | [{ 161 | id:1, 162 | name: "AMD A4-6300", 163 | photoUrl: "https://images10.newegg.com/NeweggImage/ProductImage/19-113-349-02.jpg", 164 | description: "Richland Dual-Core 3.7 GHz with AMD Radeon HD 8370D onboard graphics, with an FM2 Socket, that uses 65W.", 165 | price: 34, 166 | category: "CPU", 167 | stock: 52, 168 | }, { 169 | id:2, 170 | name: "Intel Core i7-4690X", 171 | photoUrl: "https://images10.newegg.com/NeweggImage/ProductImage/19-116-938-04.jpg", 172 | description: "Ivy Bridge-E 6-Core 3.6 GHz (Turbo 4 GHz) with an LGA 2011 socket, that uses 130W.", 173 | price: 2810, 174 | category: "CPU", 175 | stock: 3, 176 | }, { 177 | id:3, 178 | name: "ASUS ROG STRIX Z270E", 179 | photoUrl: "https://images10.newegg.com/ProductImage/13-132-928-V01.jpg", 180 | description: "A gaming ATX motherboard. It has an LGA1151 Socket, is DDR4 RAM and USB 3.1 ready, with onboard Wi-Fi.", 181 | price: 200, 182 | category: "Motherboard", 183 | stock: 22, 184 | }, { 185 | id:4, 186 | name: "GIGABYTE GA-B150M-DS3H", 187 | photoUrl: "https://images10.newegg.com/ProductImage/13-128-881-07.jpg", 188 | description: "A Micro ATX Intel Motherboard. It has an LGA1151 Socket, is DDR4 RAM and USB 3.0 ready, with HDMI.", 189 | price: 60, 190 | category: "Motherboard", 191 | stock: 43, 192 | }, { 193 | id:5, 194 | name: "MSI X99A GodLike", 195 | photoUrl: "https://images10.newegg.com/ProductImage/13-130-921-08.jpg", 196 | description: "A gaming extended ATX motherboard, with carbon skin armor. It has an LGA2011-v3 socket, is DDR4 RAM and USB 3.1 ready.", 197 | price: 580, 198 | category: "Motherboard", 199 | stock: 9, 200 | }, { 201 | id:6, 202 | name: "Intel Core i7-7700K", 203 | photoUrl: "https://images10.newegg.com/ProductImage/19-117-726-Z01.jpg", 204 | description: "Kaby Lake Quad-Core 4.2 GHz with Intel HD Graphics 630, an LGA 1151 socket, that uses 91W.", 205 | price: 350, 206 | category: "CPU", 207 | stock: 34, 208 | }, { 209 | id:7, 210 | name: "HyperX Fury 8GB", 211 | photoUrl: "https://images10.newegg.com/ProductImage/20-104-444-05.jpg", 212 | description: "Single stick of 240-PIN 8GB DDR3 1600.", 213 | price: 66, 214 | category: "RAM", 215 | stock: 76, 216 | }, { 217 | id:8, 218 | name: "G.SKILL TridentZ Series 128GB", 219 | photoUrl: "https://images10.newegg.com/ProductImage/20-232-334-01.jpg", 220 | description: "Eight sticks of powerful 288-PIN 16GB DDR4 3200.", 221 | price: 1100, 222 | category: "RAM", 223 | stock: 14, 224 | }, { 225 | id:9, 226 | name: "Ballistix Sport 16GB", 227 | photoUrl: "https://images10.newegg.com/NeweggImage/ProductImage/20-148-545-02.jpg", 228 | description: "Two sticks of 240-PIN 8GB DDR3 1600.", 229 | price: 102, 230 | category: "RAM", 231 | stock: 24, 232 | }, { 233 | id:10, 234 | name: "G.SKILL Value 4GB", 235 | photoUrl: "https://images10.newegg.com/NeweggImage/ProductImage/20-231-423-02.jpg", 236 | description: "Single Stick of 240-PIN 4GB DDR3 1333.", 237 | price: 24, 238 | category: "RAM", 239 | stock: 87, 240 | }, { 241 | id:11, 242 | name: "COOLER MASTER HAF X High Air Flow Full Tower Computer Case with Windowed Side Panel and USB 3.0 Ports", 243 | photoUrl: "http://images10.newegg.com/NeweggImage/ProductImageCompressAll1280/11-119-225-14.jpg?w=660&h=500", 244 | description: "The case that is held up as the benchmark for full towers, HAF X has remained the flagship of the highly popular High Air Flow Series (HAF).", 245 | price: 190, 246 | category: "Case", 247 | stock: 4, 248 | }, { 249 | id:12, 250 | name: "Corsair Carbide Series 200R Black Steel / Plastic Compact ATX Mid Tower Case", 251 | photoUrl: "http://images10.newegg.com/NeweggImage/ProductImageCompressAll1280/11-139-018-02.jpg?w=660&h=500", 252 | description: "Less work. More play. Build with the Carbide Series 200R and the only time you’ll need to pick up a screwdriver is to install the motherboard.", 253 | price: 60, 254 | category: "Case", 255 | stock: 55, 256 | }, { 257 | id:13, 258 | name: "DEEPCOOL GENOME II The Upgraded worldwide first unique gaming case with integrated 360mm liquid cooling system White case with Blue helix", 259 | photoUrl: "http://images10.newegg.com/ProductImageCompressAll1280/11-853-040-01.jpg?w=660&h=500", 260 | description: "As the successor to the widely acclaimed DeepCool Genome ATX PC Case, the Genome II is meticulously designed to push PC user experience a step further with a number of refinements.", 261 | price: 230, 262 | category: "Case", 263 | stock: 1 264 | }, { 265 | id:14, 266 | name: "EVGA GeForce GTX 1080 SC GAMING ACX 3.0, 08G-P4-6183-KR, 8GB GDDR5X, LED, DX12 OSD Support (PXOC)", 267 | photoUrl: "http://images10.newegg.com/ProductImageCompressAll1280/14-487-244_R01.jpg?w=660&h=500", 268 | description: "NVIDIA's flagship GeForce GTX 1080 is the most advanced gaming GPU ever created, powered by the new NVIDIA Pascal architecture.", 269 | price: 590, 270 | category: "GPU", 271 | stock: 17, 272 | }, { 273 | id:15, 274 | name: "EVGA GeForce GTX 1060 6GB SSC GAMING ACX 3.0, 6GB GDDR5, LED, DX12 OSD Support (PXOC), 06G-P4-6267-KR", 275 | photoUrl: "http://images10.newegg.com/ProductImageCompressAll1280/14-487-275-01.jpg?w=660&h=500", 276 | description: "Quality card at an affordable price", 277 | price: 270, 278 | category: "GPU", 279 | stock: 10, 280 | }, { 281 | id:16, 282 | name: "XFX Radeon R7 240 R7-240A-2TS2 2GB 128-Bit DDR3 PCI Express 3.0 Video Cards", 283 | photoUrl: "http://images10.newegg.com/ProductImageCompressAll1280/14-150-783_R01.jpg?w=660&h=500", 284 | description: "Starter GPU for an enthusiast beginning the hobby", 285 | price: 56, 286 | category: "GPU", 287 | stock: 88, 288 | }, { 289 | id:17, 290 | name: "Seagate Desktop HDD ST1000DM003 1TB 64MB Cache SATA 6.0Gb/s 3.5 Internal Hard Drive Bare Drive", 291 | photoUrl: "http://images10.newegg.com/productimage/22-148-840-10.jpg", 292 | description: "The Seagate Desktop HDD is the one drive for every desktop system need, supported by 30 years of trusted performance, reliability and simplicity", 293 | price: 50, 294 | category: "HDD", 295 | stock: 106, 296 | }, { 297 | id:18, 298 | name: "TOSHIBA P300 HDWD130XZSTA 3TB 7200 RPM 64MB Cache SATA 6.0Gb/s 3.5 Desktop Internal Hard Drive Retail Packaging", 299 | photoUrl: "http://images10.newegg.com/ProductImageCompressAll1280/22-149-633_R01.jpg?w=660&h=500", 300 | description: "The TOSHIBA P300 is a great drive upgrade for desktop computers. It features high specifications for high performance, and rich hard driver technologies for great reliability and low power consumption.", 301 | price: 98, 302 | category: "HDD", 303 | stock: 29, 304 | }, { 305 | id:19, 306 | name: "WD Blue 1TB Desktop Hard Disk Drive - 7200 RPM SATA 6Gb/s 64MB Cache 3.5 Inch - WD10EZEX", 307 | photoUrl: "http://images10.newegg.com/ProductImageCompressAll1280/22-236-339-V01.jpg?w=660&h=500", 308 | description: "Next best thing to an SSD", 309 | price: 50, 310 | category: "HDD", 311 | stock: 17, 312 | }, { 313 | id:20, 314 | name: "SAMSUNG 850 EVO 2.5 500GB SATA III 3-D Vertical Internal Solid State Drive (SSD) MZ-75E500B/AM", 315 | photoUrl: "http://images10.newegg.com/ProductImageCompressAll1280/20-147-373_R01.jpg?w=660&h=500", 316 | description: "Samsung's 850 EVO series SSD is the industry's #1 best-selling SSD and is perfect for everyday computing.", 317 | price: 183, 318 | category: "SSD", 319 | stock: 55 320 | }], product => db.model('products').create(product)) 321 | 322 | // const seedCart = () => db.Promise.map( 323 | // [{ 324 | // id: 1, 325 | // products: [1, 2], 326 | // totalPrice: 0 327 | // }], cart => db.model('carts').create(cart)) 328 | 329 | 330 | db.didSync 331 | .then(() => db.sync({ 332 | force: true 333 | })) 334 | .then(seedUsers) 335 | .then((users) => console.log(`Seeded ${users.length} users OK`)) 336 | .then(seedProducts) 337 | .then((products) => console.log(`Seeded ${products.length} products OK`)) 338 | .then(seedReviews) 339 | .then((reviews) => console.log(`Seeded ${reviews.length} reviews OK`)) 340 | // .then(seedCart) 341 | // .then((cart) => console.log(`Seeded cart with ${cart[0].totalPrice} products OK`)) 342 | .catch(error => console.error(error)) 343 | .finally(() => db.close()) 344 | 345 | 346 | 347 | --------------------------------------------------------------------------------