Skip to content

Commit

Permalink
fix: handle bad response format (#232)
Browse files Browse the repository at this point in the history
* fix(client): handle bad response formats

test(client): add unit tests for bad repsonse format handling

chore(client): created mock expressjs app for testing timeouts and bad
response formats

* chore(compose): remove the express-app from docker compose

* chore(precommit): disable eslint temporarily
  • Loading branch information
guilhem-fry authored Mar 6, 2024
1 parent 9ea833f commit f0a0876
Show file tree
Hide file tree
Showing 14 changed files with 1,367 additions and 45 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ repos:
exclude: consent_api/templates/
types_or: ["javascript", "css"]

- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.54.0
hooks:
- id: eslint
# - repo: https://github.com/pre-commit/mirrors-eslint
# rev: v8.54.0
# hooks:
# - id: eslint
24 changes: 24 additions & 0 deletions client/src/singleconsent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isCrossOrigin,
getOriginFromLink,
getCookie,
validateConsentObject
} from './utils'

const MOCK_API_BASE_URL = 'https://test-url.com'
Expand Down Expand Up @@ -447,3 +448,26 @@ describe('origin', function () {
})
})
})

describe("validateConsentObject", () => {
it("should return true if the object is valid", () => {
const response = { essential: true, settings: false, usage: false, campaigns: false }
expect(validateConsentObject(response)).toBe(true)
})
it("should return false if the object is not an object", () => {
const response = 'not an object'
expect(validateConsentObject(response)).toBe(false)
})
it("should return false if the object is null", () => {
const response = null
expect(validateConsentObject(response)).toBe(false)
})
it("should return false if the object is missing keys", () => {
const response = { essential: true, usage: false, campaigns: false }
expect(validateConsentObject(response)).toBe(false)
})
it("should return false if the object has extra keys", () => {
const response = { essential: true, settings: false, usage: false, campaigns: false, extra: true }
expect(validateConsentObject(response)).toBe(false)
})
})
127 changes: 87 additions & 40 deletions client/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,67 @@
const DEFAULT_TIMEOUT = 10000

export function request(url, options, onSuccess, onError) {
var req = new XMLHttpRequest()
var isTimeout = false
options = options || {}

req.onreadystatechange = function () {
if (req.readyState === req.DONE) {
if (req.status >= 200 && req.status < 400) {
onSuccess(JSON.parse(req.responseText))
} else if (req.status === 0 && req.timeout > 0) {
// Possible timeout, waiting for ontimeout event
// Timeout will throw a status = 0 request
// onreadystatechange preempts ontimeout
// And we can't know for sure at this stage if it's a timeout
setTimeout(function () {
if (isTimeout) {
return
try {
var req = new XMLHttpRequest()
var isTimeout = false
options = options || {}

req.onreadystatechange = function () {
if (req.readyState === req.DONE) {
if (req.status >= 200 && req.status < 400) {
let jsonResponse;
try {
jsonResponse = JSON.parse(req.responseText)
} catch (error) {
return onError(error)
}
onError(
new Error(
'Request to ' + url + ' failed with status: ' + req.status
onSuccess(JSON.parse(req.responseText))
} else if (req.status === 0 && req.timeout > 0) {
// Possible timeout, waiting for ontimeout event
// Timeout will throw a status = 0 request
// onreadystatechange preempts ontimeout
// And we can't know for sure at this stage if it's a timeout
setTimeout(function () {
if (isTimeout) {
return
}
return onError(
new Error(
'Request to ' + url + ' failed with status: ' + req.status
)
)
}, 500)
} else {
return onError(
new Error('Request to ' + url + ' failed with status: ' + req.status)
)
}, 500)
} else {
onError(
new Error('Request to ' + url + ' failed with status: ' + req.status)
)
}
}
}
}

req.open(options.method || 'GET', url, true)
req.open(options.method || 'GET', url, true)

if (options.timeout) {
req.timeout = options.timeout
} else {
req.timeout = DEFAULT_TIMEOUT
}

if (options.timeout) {
req.timeout = options.timeout
} else {
req.timeout = DEFAULT_TIMEOUT
}
req.ontimeout = function () {
isTimeout = true
return onError(new Error('Request to ' + url + ' timed out'))
}

req.ontimeout = function () {
isTimeout = true
onError(new Error('Request to ' + url + ' timed out'))
}

var headers = options.headers || {}
for (var name in headers) {
req.setRequestHeader(name, headers[name])
}
var headers = options.headers || {}
for (var name in headers) {
req.setRequestHeader(name, headers[name])
}

req.send(options.body || null)
req.send(options.body || null)
} catch (error) {
return onError(error)
}
}

export function addUrlParameter(url, name, value) {
Expand Down Expand Up @@ -158,3 +168,40 @@ export function getCookie(name: string, defaultValue?: string) {

return defaultValue || null
}

export function validateConsentObject(response) {
try {
if (typeof response !== 'object' || response === null) {
return false;
}

var expectedKeys = ['essential', 'settings', 'usage', 'campaigns'];
var allKeysPresent = true;
var responseKeysCount = 0;

for (var i = 0; i < expectedKeys.length; i++) {
if (!(expectedKeys[i] in response)) {
allKeysPresent = false;
break;
}
}

var allValuesBoolean = true;
for (var key in response) {
if (response.hasOwnProperty(key)) {
responseKeysCount++;
if (typeof response[key] !== 'boolean') {
allValuesBoolean = false;
break;
}
}
}

var correctNumberOfKeys = responseKeysCount === expectedKeys.length;

} catch (err) {
return false;
}

return allKeysPresent && allValuesBoolean && correctNumberOfKeys;
}
20 changes: 19 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ services:
timeout: 2s
retries: 10
ports:
- "5432:5432"
- "5433:5432"
environment:
POSTGRES_PASSWORD: postgres

Expand Down Expand Up @@ -55,7 +55,10 @@ services:
entrypoint: ["make", "run"]
environment:
PORT: "80"
# CONSENT_API_ORIGIN: "http://consent-api"
CONSENT_API_ORIGIN: "http://localhost:8000"
# CONSENT_API_ORIGIN: "http://localhost:3000"
# CONSENT_API_ORIGIN: "https://expired.badssl.com"
OTHER_SERVICE_ORIGIN_DOCKER: "http://dummy-service-2"
OTHER_SERVICE_ORIGIN_HOST: "http://localhost:8002"
ENV: ${ENV}
Expand All @@ -78,6 +81,21 @@ services:
OTHER_SERVICE_ORIGIN_HOST: "http://localhost:8001"
ENV: ${ENV}


# express-app:
# container_name: express-app
# networks:
# - e2e
# build:
# context: ./express-app
# dockerfile: .Dockerfile
# ports:
# - "3000:3000"
# volumes:
# - "./express-app:/usr/src/app"



selenoid-ui:
profiles:
- "testing"
Expand Down
20 changes: 20 additions & 0 deletions testing-app/.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Use the official Node.js 16 image as a parent image
FROM node:16

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json (if available) to the working directory
COPY package*.json ./

# Install any dependencies
RUN npm install

# Bundle your app's source code inside the Docker image
COPY . .

# Your app binds to port 3000 so you'll use the EXPOSE instruction to have it mapped by the docker daemon
EXPOSE 3000

# Define the command to run your app using CMD which defines your runtime
CMD [ "npm", "start" ]
1 change: 1 addition & 0 deletions testing-app/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
1 change: 1 addition & 0 deletions testing-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
db.json
7 changes: 7 additions & 0 deletions testing-app/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Testing app

This an ExpressJS app that was designed specifically for testing how the client reacts with bad response formats and request timeouts.

You can run it locally.

In a sense, it's a test-mock of the SCON API.
82 changes: 82 additions & 0 deletions testing-app/apiMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const { v4 } = require("uuid");
const db = require("./db");

const DELAYS = {
// setConsents: true,
// getConsents: true,
// timeout: true,
// origins: true,
};

const DELAY_MS = 11000;

module.exports = function (app) {
app.get("/", (req, res) => {
res.send("<h1>Hello world</h1>");
});

app.get("/timeout", (req, res) => {
if (DELAYS.timeout) {
setTimeout(() => {
res.send("<h1>Timeout</h1>");
}, DELAY_MS);
} else {
res.send("<h1>Timeout</h1>");
}
});

app.get("/api/v1/origins", (req, res) => {
if (DELAYS.origins) {
setTimeout(() => {
res.send(["http://localhost:3000"]);
}, DELAY_MS);
} else {
res.send(["http://localhost:3000"]);
}
});

app.get("/api/v1/consent/:uid", (req, res) => {
const { uid } = req.params;
const consents = db.getConsents(uid);

if (DELAYS.getConsents) {
setTimeout(() => {
res.send({ consents });
}, DELAY_MS);
} else {
res.send({ consents });
}
});

app.post("/api/v1/consent/:uid", (req, res) => {
const { uid: paramId } = req.params;
const consents = JSON.parse(req.body.status);

const uid = paramId || v4();

db.updateConsents(uid, consents);

if (DELAYS.setConsents) {
setTimeout(() => {
res.send({ uid, consents });
}, DELAY_MS);
} else {
res.send({ uid, consents });
}
});

app.post("/api/v1/consent", (req, res) => {
const consents = JSON.parse(req.body.status);
const uid = v4();

db.createConsents(uid, consents);

if (DELAYS.setConsents) {
setTimeout(() => {
res.send({ uid, consents });
}, DELAY_MS);
} else {
res.send({ uid, consents });
}
});
};
36 changes: 36 additions & 0 deletions testing-app/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const fs = require("fs");

const DB_FILE = "db.json";

const getDb = () => {
if (fs.existsSync(DB_FILE)) {
return JSON.parse(fs.readFileSync(DB_FILE, "utf-8"));
} else {
// create the db
fs.writeFileSync(DB_FILE, "{}");
}
};

const createConsents = (uid, consents) => {
const db = getDb();
console.log("db", db);
db[uid] = consents;
fs.writeFileSync(DB_FILE, JSON.stringify(db));
};

const updateConsents = (uid, consents) => {
const db = getDb();
db[uid] = consents;
fs.writeFileSync(DB_FILE, JSON.stringify(db));
};

const getConsents = (uid) => {
const db = getDb();
return db[uid] || null;
};

module.exports = {
createConsents,
updateConsents,
getConsents,
};
Loading

0 comments on commit f0a0876

Please sign in to comment.