So today I was awoken to the fact that my database had mysteriously lost data after putting out a new feature.
This a MongoDB database whereby, via the driver, you can replace documents.
After some digging I found that despite calling a function “updateEmployer” GitHub Co-Pilot had actually written the body of a function called “replaceEmployer” with only the fields in the C# schema file. I mean fair I didn’t know the database fields not defined in coding but that’s irrelevant, you would never use ReplaceOneAsync in an update function.
Since I accidentally trusted the AI without doing a proper review and it passed unit tests this went out and wiped the database…
Vibe coding is a fallacy and one you should avoid. This isn’t my first run in with AI incompetence it is just my last tbh
https://github.com/facebook/react-native/issues/29614#issuecomment-3069870022 this worked best for me:
const [behaviour, setBehaviour] = useState(‘height’);
useEffect(() => {
const showListener = Keyboard.addListener(‘keyboardDidShow’, () => {
setBehaviour(‘height’);
});
const hideListener = Keyboard.addListener(‘keyboardDidHide’, () => {
setBehaviour(undefined);
});
return () => {
showListener.remove();
hideListener.remove();
};
}, []);
….
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === ‘android’ ? behaviour : undefined}
….
const [behaviour, setBehaviour] = useState(‘height’);
useEffect(() => {
const showListener = Keyboard.addListener(‘keyboardDidShow’, () => {
setBehaviour(‘height’);
});
const hideListener = Keyboard.addListener(‘keyboardDidHide’, () => {
setBehaviour(undefined);
});
return () => {
showListener.remove();
hideListener.remove();
};
}, []);
….
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === ‘android’ ? behaviour : undefined}
….
'use strict';
const MongoClient = require('mongodb').MongoClient;
const _ = require('lodash');
/**
* @typedef IMongoStoreDocument
* @extends import('mongodb').Document
* @property {import('mongodb').ObjectId} _id
* @property {number} counter
* @property {Date} expirationDate
* @property {string} [key]
*/
class MongoStore {
/** @type {import('mongodb').MongoClient} */
client;
/** @type {{uri: string, user: string, password: string, authSource: string, collectionName: string}} */
dbOptions;
/** @type {import('mongodb').Collection} */
collection;
/** @type {number} */
expireTimeMs;
/** @type {boolean} */
resetExpireDateOnChange;
/** @type {Function} */
errorHandler;
/** @type {boolean} */
createTtlIndex;
/** @type {Object} */
connectionOptions;
/** @type {string} */
prefix;
/**
* @constructor. Only required if the user needs to pass
* some store specific parameters. For example, in a Mongo Store, the user will
* need to pass the URI, username and password for the Mongo database.
*
* Accepting a custom `prefix` here is also recommended.
*
* @param options {{
* collectionName: string,
* uri: string,
* user: string,
* password: string,
* authSource: string,
* expireTimeMs: Number,
* resetExpireDateOnChange: Boolean,
* errorHandler: Function,
* createTtlIndex: Boolean,
* collection: import('mongodb').Collection,
* connectionOptions: import('mongodb').MongoClientOptions,
* authSource: string,
* prefix: string,
* }}
*/
constructor(options) {
const allOptions = _.defaults(options, {
collectionName: 'expressRateRecords',
expireTimeMs: 60000,
resetExpireDateOnChange: false,
errorHandler: _.noop,
createTtlIndex: true,
connectionOptions: {},
prefix: '',
});
if (!allOptions.collection && (!allOptions.collectionName || !allOptions.uri)) {
throw new Error('collection or collectionName and uri should be set');
}
this.dbOptions = {
uri: allOptions.uri,
user: allOptions.user,
password: allOptions.password,
authSource: allOptions.authSource,
collectionName: allOptions.collectionName,
};
this.collection = allOptions.collection;
this.expireTimeMs = allOptions.expireTimeMs;
this.resetExpireDateOnChange = allOptions.resetExpireDateOnChange;
this.errorHandler = allOptions.errorHandler;
this.createTtlIndex = allOptions.createTtlIndex;
this.connectionOptions = allOptions.connectionOptions;
this.prefix = allOptions.prefix;
}
/**
* @returns {Collection<Document>}
* @private
*/
async _getCollection() {
if (!this.client) {
const connectionOptions = this.connectionOptions;
if (this.dbOptions.user && this.dbOptions.password) {
const dbName = _.last(this.dbOptions.uri.split('/'));
connectionOptions.authSource = this.dbOptions.authSource || dbName;
connectionOptions.auth = {
user: this.dbOptions.user,
password: this.dbOptions.password,
};
}
this.client = await MongoClient.connect(this.dbOptions.uri, connectionOptions);
}
const collection = this.client.db().collection(this.dbOptions.collectionName);
if (this.createTtlIndex) {
await collection.createIndex({ expirationDate: 1 }, { expireAfterSeconds: 0 });
}
return collection;
}
/**
* Method that actually initializes the store. Must be synchronous.
*
* This method is optional, it will be called only if it exists.
*
* @param options {Partial<import('express-rate-limit').Options>} - The options used to setup express-rate-limit.
*
* @public
*/
init(options) {
this.expireTimeMs = options.windowMs;
}
/**
* Method to prefix the keys with the given text.
*
* Call this from get, increment, decrement, resetKey, etc.
*
* @param key {string} - The key.
*
* @returns {string} - The text + the key.
*/
prefixKey(key) {
return `${this.prefix}${key}`;
}
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {{totalHits: number, resetTime: Date}} - The number of hits and reset time for that client.
*
* @public
*/
async get(key) {
const collection = await this._getCollection();
const expressRateRecord = await collection.findOne({ _id: key });
return {
totalHits: expressRateRecord ? expressRateRecord.counter : 0,
resetTime: expressRateRecord ? expressRateRecord.expirationDate : new Date(),
};
}
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {{totalHits: number, resetTime: Date}} - The number of hits and reset time for that client.
*
* @public
*/
async increment(key) {
try {
const collection = await this._getCollection();
const modifier = {
$inc: { counter: 1 },
};
const expirationDate = new Date(Date.now() + this.expireTimeMs);
if (this.resetExpireDateOnChange) {
modifier.$set = { expirationDate: expirationDate };
} else {
modifier.$setOnInsert = { expirationDate: expirationDate };
}
const /** @type IMongoStoreDocument */ expressRateRecord = await collection.findOneAndUpdate(
{ _id: key },
modifier,
{
upsert: true,
returnDocument: 'after',
},
);
console.log('expressRateRecord', expressRateRecord);
return {
totalHits: expressRateRecord.counter, // A positive integer
resetTime: expressRateRecord.expirationDate, // A JS `Date` object
};
} catch (err) {
// call function again in case of duplicate key error
if (err && err.code === 11000) {
return this.increment(key);
}
this.errorHandler(err);
throw err;
}
}
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async decrement(key) {
try {
const collection = await this._getCollection();
const modifier = {
$inc: { counter: -1 },
};
const expirationDate = new Date(Date.now() + this.expireTimeMs);
if (this.resetExpireDateOnChange) {
modifier.$set = { expirationDate: expirationDate };
} else {
modifier.$setOnInsert = { expirationDate: expirationDate };
}
await collection.findOneAndUpdate({ _id: key }, modifier, {
upsert: true,
returnDocument: 'after',
});
} catch (err) {
this.errorHandler(err);
throw err;
}
}
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async resetKey(key) {
try {
const collection = await this._getCollection();
await collection.deleteOne({ _id: key });
} catch (err) {
this.errorHandler(err);
throw err;
}
}
}
module.exports = MongoStore;
Using this https://bojnansky.com/running-angular-tests-with-github-actions/ as a guide I made GitHub actions for angular testing, I used this action:
name: Run Tests
on:
push:
branches:
- beta
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Angular CLI and sass
run: npm install -g @angular/cli@^14.0.0 sass
- name: Install modules
run: npm install --force
- name: Run tests
run: npm run test-ci
With the npm command in the link:
"test-ci": "ng test --browsers=ChromeHeadless --watch=false"
function handler(event) {
const request = event.request;
const response = event.response;
const headers = response.headers;
let original_uri = '';
if (request.headers.hasOwnProperty('x-original-uri')) {
original_uri = request.headers['x-original-uri'].value;
}
let referer = '';
if (request.headers.hasOwnProperty('referer')) {
referer = request.headers['referer'].value;
}
if (
(
// Super important! when PWA queries for new cache it actually does not send the URI, figured this out after lots of debugging
!referer || !referer.includes('ngsw-worker.js')
) &&
request.uri.includes('index.html') &&
original_uri &&
!original_uri.includes('/embedded')
) {
response.headers['x-frame-options'] = {value: 'SAMEORIGIN'};
}
return response;
}
A good env:
A good env setup:
jest.config.js
module.exports = {
preset: '@shelf/jest-mongodb',
transform: { "^.+\\.[tj]s?(x)?$": ["ts-jest", {"rootDir": "."}] },
testMatch: [ "**/tests/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" ],
setupFilesAfterEnv: ['<rootDir>/setup-jest.js'],
};
jest-mongodb-config.js
module.exports = {
mongodbMemoryServerOptions: {
binary: {
version: '5.0.0',
skipMD5: true,
},
autoStart: false,
instance: {},
},
};
A global file for running predictable tests:
setup-jest.js
import {afterAll, beforeAll} from "@jest/globals";
import {BatchInterceptor} from "@mswjs/interceptors";
import MongoDB from "./services/MongoDB";
import './config/app.js';
import {faker} from "@faker-js/faker";
import nodeInterceptors from '@mswjs/interceptors/presets/node'
import mongoose from "mongoose";
beforeAll(async () => {
config.APP_ENV = 'JEST';
config.MONGO_URL = global.__MONGO_URI__;
config.EMAIL_DEFAULT_WELCOME_FROM_EMAIL = faker.internet.email();
config.LOGS_ENABLE_SENTRY = false;
config.LOGS_ENABLE_CLOUDWATCH = false;
config.LOGS_ENABLE_CONSOLE = false;
config.EMAIL_SES_HOST = 'https://email.eu-west-1.amazonaws.com/';
await MongoDB.connect(config);
});
beforeEach(async () => {
global.interceptor = new BatchInterceptor({
name: 'my-interceptor',
interceptors: nodeInterceptors,
});
interceptor.apply();
interceptor.on('request', ({request, requestId}) => {
// This allows us to fake sending emails via SES
// TODO add one for SMTP
if (request.url === config.EMAIL_SES_HOST) {
request.respondWith(new Response(
'<SendEmailResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">' +
'<SendEmailResult>' +
'<MessageId>0102018e1161bc00-332b3f83-4295-4b94-912b-f3f983a8a1f9-000000</MessageId>' +
'</SendEmailResult>' +
'<ResponseMetadata>' +
'<RequestId>24239c14-b187-4bea-b458-31a7c7d02a26</RequestId>' +
'</ResponseMetadata>' +
'</SendEmailResponse>',
{
status: 200,
}
));
}
});
const collections = await mongoose.connection.db.collections();
for (const collection of collections) {
await mongoose.connection.db.dropCollection(collection.collectionName);
}
});
afterAll(async () => {
await MongoDB.close();
});
I also tend to add some helper components, namely a base record creator like so:
BaseFixture.js
import mongoose from "mongoose";
export default class {
static collectionName;
static getBaseRecord() {
return {};
}
static async createBaseRecords(records) {
const recordsInserted = [];
for (const record of records) {
let recordInserted = {...this.getBaseRecord(), ...record};
const res = await mongoose.connection.db.collection(this.collectionName)
.insertOne(recordInserted);
recordInserted._id = res.insertedId;
recordsInserted.push(recordInserted);
}
return recordsInserted;
}
}
UserFixture.js
import User from "../../models/User";
import {faker} from "@faker-js/faker";
import BaseFixture from "./BaseFixture";
export default class extends BaseFixture {
static collectionName = User.collectionName();
static getBaseRecord() {
return {
first_name: faker.person.firstName(),
last_name: faker.person.lastName(),
email: faker.internet.email().toLowerCase(),
status: 'ACTIVATE',
user_roles: ['grp_coach'],
// password123
password: '$2a$10$koNBPtKwiVjKkQbnnVBrAOdIjflMnC/OzYzy/OhXjOLPr9RSjwe4K',
password_salt: '$2a$20$piAYssU0VGLwhWbZRhCT5e'
};
}
}
So an example unit test making use of these would be:
import {afterAll, beforeAll, describe, expect, it, test} from '@jest/globals';
import * as LoginController from '../../../controllers/LoginController';
import RequestMock from "../../mocks/RequestMock";
import ResponseMock from "../../mocks/ResponseMock";
import UserFixture from "../../fixtures/UserFixture";
import UserSubscriptionFixture from "../../fixtures/UserSubscriptionFixture";
import UnauthorizedError from "../../../exceptions/UnauthorizedError";
describe('Login Controller', () => {
it('Should allow a user to login', async () => {
const users = await UserFixture.createBaseRecords([{}]);
const subs = await UserSubscriptionFixture.createBaseRecords([{
user_id: users[0]._id,
}])
let response = ResponseMock.create();
await LoginController.signIn(RequestMock.create({
body: {
Authenticate_type: 'FORM',
email: users[0].email,
password: 'password123',
},
portal: {
type: 'COACH',
}
}), response);
expect(response.statusCode).toEqual(200);
});
it ('Should not allow invalid credentials to login', async() => {
const users = await UserFixture.createBaseRecords([{}]);
const subs = await UserSubscriptionFixture.createBaseRecords([{
user_id: users[0]._id,
}])
let exceptionThrown = null;
const response = ResponseMock.create();
await LoginController.signIn(RequestMock.create({
body: {
Authenticate_type: 'FORM',
email: users[0].email,
password: 'wrongpw',
},
portal: {
type: 'COACH',
}
}), response, (e) => {
exceptionThrown = e;
});
expect(exceptionThrown).toBeInstanceOf(UnauthorizedError);
expect(exceptionThrown.message).toEqual('It seems like your password is incorrect.');
});
});
ng test –include=’**/sign-in.component.spec.ts’
This is because NACL is used for internal routing as well so you need to add the ephemeral ports, i.e. https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html#nacl-ephemeral-ports most commonly 1024-65535, all AWS services use these ports to communicate
You can do this with response-time plugin but I wanted something a bit more flexible and can easily be used on select routes as middleware, so I made:
import onHeaders from 'on-headers';
import net from "net";
import moment from "moment/moment";
import ApiLog from "../models/RequestLog";
// Slow is 10 seconds
const defaultSlowTime = 10000;
function _getTimeDiff(hrtime) {
var diff = process.hrtime(hrtime);
return diff[0] * 1e3 + diff[1] * 1e-6
}
function _ipIsPrivate(ip) {
if (ip.substring(0,7) === "::ffff:")
ip = ip.substring(7);
if (net.isIPv4(ip)) {
// 10.0.0.0 - 10.255.255.255 || 172.16.0.0 - 172.31.255.255 || 192.168.0.0 - 192.168.255.255
return /^(10)\.(.*)\.(.*)\.(.*)$/.test(ip) || /^(172)\.(1[6-9]|2[0-9]|3[0-1])\.(.*)\.(.*)$/.test(ip) || /^(192)\.(168)\.(.*)\.(.*)$/.test(ip)
}
// else: ip is IPv6
const firstWord = ip.split(":").find(el => !!el); //get first not empty word
// The original IPv6 Site Local addresses (fec0::/10) are deprecated. Range: fec0 - feff
if (/^fe[c-f][0-f]$/.test(firstWord))
return true;
// These days Unique Local Addresses (ULA) are used in place of Site Local.
// Range: fc00 - fcff
else if (/^fc[0-f]{2}$/.test(firstWord))
return true;
// Range: fd00 - fcff
else if (/^fd[0-f]{2}$/.test(firstWord))
return true;
// Link local addresses (prefixed with fe80) are not routable
else if (firstWord === "fe80")
return true;
// Discard Prefix
else if (firstWord === "100")
return true;
// Any other IP address is not Unique Local Address (ULA)
return false;
}
export default class {
static log(onlySlow ) {
const slowTime = onlySlow === true ? defaultSlowTime : (onlySlow === false ? 0 : onlySlow);
return (req, res, next) => {
onHeaders(res, function onHeaders() {
const {
log: {
userId = null,
} = {},
ip,
originalUrl,
startedAt,
hrtime,
} = req;
const time = _getTimeDiff(hrtime);
// TODO do we want to add configuration to omit private IPs such as dev computers?
const isPrivateIp = _ipIsPrivate(ip);
if (slowTime < time) {
// async so we don't stop request
ApiLog.createLog({
userId,
startedAt: startedAt.toISOString(),
endedAt: moment().toISOString(),
ip: ip,
url: originalUrl,
duration: time,
});
}
});
next();
}
}
}
And added
app.use((req, res, next) => {
// This is our own personal version of response-time that we can attach to specific routes
req.hrtime = process.hrtime();
req.startedAt = moment();
});
in my base expressjs route file, now I can easily log certain routes by doing:
router.get('/v2/events/all/:year/:month',
RequestLogMiddleware.log(true),
celebrate(calendarEventSchema.retrieveAllEventsByMonth),
CalendarEventController.retrieveAllEventsByMonth
);
