Information Security Projects - Stock Price Checker

Tell us what’s happening:

Hi everyone, I’ve completed the Stock Price Checker project and believe I’ve met all the requirements. I implemented CSP using Helmet and followed all the given steps. I also created a .env file and configured NODE_ENV=test as instructed.

I’ve already spent several days trying to troubleshoot this, but the test results always show the same messages and won’t update, even after testing locally and on Replit. I’m not sure what I’m missing. Has anyone faced this issue or found a workaround?

Here are the repeated messages I keep seeing:

  • You should set the content security policies to only allow loading of scripts and CSS from your server.

  • You can send a GET request to /api/stock-prices, passing a NASDAQ stock symbol to a stock query parameter. The returned object will contain a property named stockData.

  • The stockData property includes the stock symbol as a string, the price as a number, and likes as a number.

  • If you pass along 2 stocks, the returned value will be an array with information about both stocks. Instead of likes, it will display rel_likes (the difference between the likes on both stocks) for both stockData objects.

  • All 5 functional tests are complete and passing.

Thanks in advance!

Your code so far

Your browser information:

User Agent is: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0

Challenge Information:

Information Security Projects - Stock Price Checker

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()
          });
        });

});