Here are some relevant code snippets. If anyone spots something I might be missing or has suggestions, I’d really appreciate it!
server.js
'use strict';
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const apiRoutes = require('./routes/api.js');
const fccTestingRoutes = require('./routes/fcctesting.js');
const runner = require('./test-runner');
const helmet = require('helmet');
const app = express();
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
}
}));
app.use('/public', express.static(process.cwd() + '/public'));
app.use(cors({origin: '*'})); //For FCC testing purposes only
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
//Index page (static HTML)
app.route('/')
.get(function (req, res) {
res.sendFile(process.cwd() + '/views/index.html');
});
//For FCC testing purposes
fccTestingRoutes(app);
//Routing for API
apiRoutes(app);
//404 Not Found Middleware
app.use(function(req, res, next) {
res.status(404)
.type('text')
.send('Not Found');
});
//Start our server and tests!
const listener = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on port ' + listener.address().port);
if(process.env.NODE_ENV==='test') {
console.log('Running Tests...');
setTimeout(function () {
try {
runner.run();
} catch(e) {
console.log('Tests are not valid:');
console.error(e);
}
}, 3500);
}
});
module.exports = app; //for testing
.env
PORT=3000
NODE_ENV=test
api.js
'use strict';
const axios = require('axios');
const crypto = require('crypto');
const likesStore = {};
module.exports = function (app) {
app.route('/api/stock-prices')
.get(async function (req, res){
try {
const result = await processStocks(req);
res.json(result);
} catch (error) {
if (error.message === 'Only 1 Like per IP Allowed') {
return res.status(400).json({ error: 'Only 1 Like per IP Allowed' });
}
if (error.message === 'Invalid stock query') {
return res.status(400).json({ error: 'Invalid stock query' });
}
return res.status(500).json({ error: 'Failed to retrieve stock data' });
}
});
async function getStockData(stockName, like, ip) {
const key = stockName.toUpperCase();
if (!likesStore[key]) likesStore[key] = new Set();
const clientIp = await anonymizeIp(ip.replace('::ffff:', ''));
const requestUrl = `https://stock-price-checker-proxy.freecodecamp.rocks/v1/stock/${stockName}/quote`;
try {
const response = await axios.get(requestUrl, { timeout: 5000 });
const { symbol, latestPrice } = response.data;
if (like) {
if (likesStore[key].has(clientIp)) {
throw new Error('Only 1 Like per IP Allowed');
}
likesStore[key].add(clientIp);
}
return { stock: symbol, price: latestPrice, likes: likesStore[key].size };
} catch (error) {
if (error.message === 'Only 1 Like per IP Allowed') throw error;
if (error.response) throw new Error('Stock not found or invalid response from API');
if (error.request) throw new Error('No response from stock price API');
throw new Error('Request setup failed');
}
}
async function processStocks(req) {
if (typeof req.query.stock === 'string') {
const stockInfo = await getStockData(req.query.stock, req.query.like === 'true', req.ip);
return {
stockData: {
stock: stockInfo.stock,
price: Number(stockInfo.price.toFixed(2)),
likes: stockInfo.likes
}
};
} else if (Array.isArray(req.query.stock) && req.query.stock.length === 2) {
const [stockOne, stockTwo] = await Promise.all([
getStockData(req.query.stock[0], req.query.like === 'true', req.ip),
getStockData(req.query.stock[1], req.query.like === 'true', req.ip)
]);
stockOne.rel_likes = stockOne.likes - stockTwo.likes;
stockTwo.rel_likes = stockTwo.likes - stockOne.likes;
return {
stockData: [
{ stock: stockOne.stock, price: Number(stockOne.price.toFixed(2)), rel_likes: stockOne.rel_likes },
{ stock: stockTwo.stock, price: Number(stockTwo.price.toFixed(2)), rel_likes: stockTwo.rel_likes }
]
};
} else {
throw new Error('Invalid stock query');
}
}
async function anonymizeIp(ip) {
return crypto.createHash('sha256').update(ip).digest('hex');
}
};
2_functional-tests.js
const chaiHttp = require('chai-http');
const chai = require('chai');
const assert = chai.assert;
const server = require('../server');
chai.use(chaiHttp);
suite('Functional Tests', function() {
test('Viewing one stock', function(done) {
chai.request(server)
.get('/api/stock-prices')
.query({stock: 'goog'})
.end(function(err, res){
assert.equal(res.body['stockData']['stock'].toUpperCase(), 'GOOG')
assert.isNotNull(res.body['stockData']['price'])
assert.isNotNull(res.body['stockData']['likes'])
done();
});
});
test('Viewing one stock and liking it', function(done) {
chai.request(server)
.get('/api/stock-prices')
.query({stock: 'goog', like: true})
.end(function(err, res){
assert.equal(res.body['stockData']['stock'], 'GOOG')
assert.equal(res.body['stockData']['likes'], 1)
done();
});
});
test('Viewing the same stock and liking it again', function(done) {
chai.request(server)
.get('/api/stock-prices')
.query({stock: 'goog', like: true})
.end(function(err, res){
assert.equal(res.body.error, 'Only 1 Like per IP Allowed');
done()
});
});
test('Viewing two stocks', function(done) {
chai.request(server)
.get('/api/stock-prices')
.query({ stock: ['msft', 'goog'] })
.end(function(err, res) {
assert.isNull(err);
assert.equal(res.status, 200);
const stockData = res.body.stockData;
assert.isArray(stockData);
assert.equal(stockData.length, 2);
const msftData = stockData.find(item => item.stock === 'MSFT');
const googData = stockData.find(item => item.stock === 'GOOG');
assert.isObject(msftData);
assert.equal(msftData.stock.toUpperCase(), 'MSFT')
assert.exists(msftData.price);
assert.exists(msftData.rel_likes);
assert.isObject(googData);
assert.equal(googData.stock.toUpperCase(), 'GOOG')
assert.exists(googData.price);
assert.exists(googData.rel_likes);
done();
});
});
test('Viewing two stocks and liking them', function(done) {
chai.request(server)
.get('/api/stock-prices')
.query({stock: ['spot', 'amzn'], like: true})
.end(function(err, res){
assert.isNull(err);
assert.equal(res.status, 200);
const stockData = res.body.stockData;
assert.isArray(stockData);
assert.equal(stockData.length, 2);
const spotData = stockData.find(item => item.stock === 'SPOT');
const amznData = stockData.find(item => item.stock === 'AMZN');
assert.isObject(spotData);
assert.equal(spotData.stock.toUpperCase(), 'SPOT')
assert.exists(spotData.price);
assert.equal(spotData.rel_likes, 0);
assert.isObject(amznData);
assert.equal(amznData.stock.toUpperCase(), 'AMZN')
assert.exists(amznData.price);
assert.equal(amznData.rel_likes, 0);
done()
});
});
});